Source code for fleetmanager.dashboard.page_goal
import datetime
import dash_bootstrap_components as dbc
import dash_daq as daq
import pandas as pd
import plotly.graph_objects as go
from dash import ALL, MATCH, Input, Output, State, callback, dcc, html
from dash.exceptions import PreventUpdate
from sqlalchemy import func, or_, and_
from sqlalchemy.orm import Query
import fleetmanager.data_access.db_engine as db
from ..data_access import Cars
from .utils import (
accordian_table,
card,
get_emission,
goal_bar_plot,
unix_time_millis,
unix_to_datetime,
)
from fleetmanager.model.tabu import TabuSearch
from fleetmanager.model.vehicle_optimisation import FleetOptimisation
fig = go.Figure()
from .view import app
Session = db.session_factory(db.engine_creator())
description_header = dbc.Toast(
[
html.P(
"På denne side kan man som bruger anmode AI modulet om at komme med forslag til nye flådesammensætninger",
className="mb-0",
)
],
header="Flådesammensætning",
className="description-header",
)
layout = html.Div(
[
description_header,
# dbc.Label(id="longcallback"),
dbc.Row(
[
dbc.Col(
[
html.H3("Optimeringsindstillinger"),
dbc.Card(
[
dbc.CardBody(
[
dbc.Form(
[
dbc.Row(
[
dbc.Label(
"Ekstra årligt beløb",
width=4,
),
dbc.Col(
[
dbc.Input(
type="number",
id="ekstra_omkostning",
),
dbc.FormText(
id="eksisterende_omkostninger"
),
],
width=8,
),
],
className="mb-3",
),
dbc.Row(
[
dbc.Label(
"Besparelse i CO2e",
width=4,
),
dbc.Col(
[
dbc.Input(
type="number",
id="reduktion_co2e",
),
dbc.FormText(
"Den ønskede procentvise CO2e besparelse"
),
],
width=8,
),
],
className="mb-3",
),
dbc.Row(
[
dbc.Label(
"Prioritér CO2e besparelser",
width=4,
),
dbc.Col(
[
dcc.Slider(
min=0,
max=10,
step=1,
value=5,
marks=None,
tooltip={
"placement": "bottom",
"always_visible": True,
},
id="co2e_prioritization",
),
dbc.FormText(
"Antal kroner du er villig til at betale for at spare X ton CO2e"
),
],
width=8,
),
],
className="mb-3",
),
dbc.Row(
[
dbc.Label(
"Vælg leasingperiode",
width=4,
),
dbc.Col(
[
html.Div(
id="leasingperiode"
),
dbc.FormText(
id="leasing_text"
),
],
width=8,
),
],
className="mb-4",
),
]
),
html.Div(
[
daq.BooleanSwitch(
on=False,
label={
"label": "Intelligent Allokering",
"style": {"font-size": "16px"},
},
color="#32CD32",
labelPosition="top",
id="intelligent_simulation_op",
),
daq.BooleanSwitch(
on=False,
label={
"label": "Begræns km/år",
"style": {"font-size": "16px"},
},
color="#32CD32",
labelPosition="top",
id="km_aar_op",
),
],
className="left-column-elements",
),
html.Div(
[
dbc.Button(
"Optimér",
color="success",
className="right-btn",
id="optimer",
# disabled=True,
)
],
className="right-column-elements",
),
html.Div([], id="vehicle-list"),
],
)
]
),
]
),
dbc.Col(
[
html.H3("Fremtidig flådesammensætning"),
dbc.Row(
[
dbc.Col(
dbc.Card(
[
dbc.CardBody(
[
html.H4("Total omkostning pr. år"),
html.P(id="solution_omkostning"),
],
className="metric-card",
)
]
)
),
dbc.Col(
dbc.Card(
[
dbc.CardBody(
[
html.H4("CO2e-udledning pr. år"),
html.P(id="solution_udledning"),
],
className="metric-card",
)
]
)
),
],
style={"margin-bottom": "10px"},
),
dbc.Card(
[
dbc.CardBody(
[
dbc.Row(
[
dbc.Col(
card(
dcc.Graph(
figure=fig,
id={
"type": "goal_fig",
"index": 1,
},
),
),
),
dbc.Col(
dbc.Card(
[
dbc.CardBody(
dcc.Graph(
figure=fig,
id={
"type": "goal_fig",
"index": 2,
},
)
)
]
),
),
],
),
html.Div(id="accordinan_solution"),
dcc.Store(id="long_callback_optim"),
dbc.Spinner(
fullscreen=True,
spinner_class_name="large_spinner",
fullscreen_class_name="fullscreen_spinner",
fullscreen_style={"visibility": "hidden"},
id="optim_spinner",
type="grow",
),
]
),
]
),
]
),
]
),
],
)
[docs]@callback(Output("intelligent_simulation_op", "disabled"), Input("km_aar_op", "on"))
def enable_int_op(on):
if on:
return True
return False
[docs]@callback(Output("km_aar_op", "disabled"), Input("intelligent_simulation_op", "on"))
def enable_km_op(on):
if on:
return True
return False
[docs]@callback(
Output({"type": "goal_fig_store", "index": MATCH}, "data"),
Input({"type": "goal_fig", "index": MATCH}, "figure"),
)
def persistent_goal_figure(fig):
return fig
[docs]@callback(
Output("long_callback_optim", "data"),
Input("optimer", "n_clicks"),
Input("vehicle_sel_store", "data"),
State("vehicle-list", "data"),
State("location_store", "data"),
State("date_store", "data"),
State({"type": "goal_fig_store", "index": ALL}, "data"),
State("solution_omkostning", "children"),
State("solution_udledning", "children"),
State("ekstra_omkostning", "value"),
State("reduktion_co2e", "value"),
State("co2e_prioritization", "value"),
State("intelligent_simulation_op", "on"),
State("km_aar_op", "on"),
)
def long_callback_optim(*args):
if args[0] is None:
return None
return args
@app.long_callback(
Output({"type": "goal_fig", "index": 1}, "figure"),
Output({"type": "goal_fig", "index": 2}, "figure"),
Output("accordinan_solution", "children"),
Output("solution_udledning", "children"),
Output("solution_udledning", "style"),
Output("solution_omkostning", "children"),
Output("solution_omkostning", "style"),
Input("long_callback_optim", "data"),
running=[
(
Output("optim_spinner", "fullscreen_style"),
{"visibility": "visible"},
{"visibility": "hidden"},
)
],
interval=3000,
prevent_initial_call=True,
)
def optim(args):
if args is None:
return [fig, fig, [], 0, None, 0, None]
(
n_clicks,
selected_vehicles,
frame,
location_id,
dates,
figs,
omkostning,
udledning,
ekstra_omkostning,
reduktion_co2e,
co2e_prioritization,
intelligent_simulation,
km_aar,
) = args
# todo persist figures
# todo persist accordions
# todo persist values
if n_clicks is None or frame is None:
omk_value = 0 if pd.isna(omkostning) else omkostning
udl_value = 0 if pd.isna(udledning) else udledning
return [*figs, [], *udl_value, *omk_value]
active_vehicles = {}
for vehicle in frame:
if vehicle["id"] not in active_vehicles:
active_vehicles[vehicle["id"]] = 0
active_vehicles[vehicle["id"]] += vehicle["count"]
settings = {
"location": location_id,
"dates": dates,
"active_vehicles": active_vehicles,
}
fo = FleetOptimisation(settings, km_aar=km_aar)
tb = TabuSearch(
fo,
location_id,
dates,
co2e_goal=0 if pd.isna(reduktion_co2e) else reduktion_co2e,
expense_goal=0 if pd.isna(ekstra_omkostning) else ekstra_omkostning,
weight=co2e_prioritization,
intelligent=intelligent_simulation,
km_aar=km_aar,
)
if len(tb.dummy_trips.trips) == 0:
tb.report = []
else:
tb.run()
if len(tb.report) == 0:
omk_value = 0 if pd.isna(omkostning) else omkostning
udl_value = 0 if pd.isna(udledning) else udledning
return [*figs, [], *udl_value, *omk_value]
omkostning = []
co2e = []
co2e_reduktioner = []
omkostning_besparelse = []
accordians = []
keys = []
header = html.Thead(
html.Tr(
[
html.Th("Biltype"),
html.Th("Årlig omk. (DKK)"),
html.Th("Drivmiddel forbrug"),
html.Th("Antal"),
]
)
)
# walk through the solutions
for k, solution in enumerate(tb.report):
keys.append(f"Løsning {k + 1}")
# save the solution details and generate accordian title
omk = solution["omkostning"]
omkostning.append(omk)
udl = solution["co2e"]
co2e.append(udl)
title = f"Løsning {k + 1}: Årlig omkostning: {str(round(omk)).replace('.', ',')} DKK, Årlig ton CO2e: {str(round(udl, 3)).replace('.', ',')}"
if solution["omkostning"] > tb.expense_goal or solution["co2e"] > tb.co2e_goal:
if (
solution["omkostning"] > tb.expense_goal
and solution["co2e"] > tb.co2e_goal
):
title += ". Overskredet omkostnings - og CO2e mål."
else:
title += (
". Overskedet omkostningsmål."
if solution["omkostning"] > tb.expense_goal
else ". Overskredet CO2e mål."
)
fleet = []
# generate accordian vehicle table
for solution_vehicle in solution["flåde"]:
fleet.append(
html.Tr(
[
html.Td(f'{solution_vehicle["class_name"]}'),
html.Td(solution_vehicle["omkostning_aar"]),
html.Td(solution_vehicle["stringified_emission"]),
html.Td(solution_vehicle["count"]),
]
)
)
# details for the "Simuleringsdetaljer" table
besparelse = round(tb.cur_result[0]) - round(solution["omkostning"])
omkostning_besparelse.append(besparelse)
reduktion = round(tb.cur_result[1] - solution["co2e"], 3)
co2e_reduktioner.append(reduktion)
reduktion = f"{str(reduktion).replace('.', ',')}"
solution_table = accordian_table(
header,
fleet,
tb.cur_result[0],
solution["omkostning"],
besparelse,
tb.cur_result[1],
solution["co2e"],
reduktion,
)
accordians.append(dbc.AccordionItem(solution_table, title=title))
accordian_card = dbc.Accordion(accordians, always_open=True)
omkostning_bar = goal_bar_plot(
keys,
omkostning,
title="Årlig omkostning i kr.",
xlabel="Løsninger",
ylabel="kr. pr. år",
)
co2e_bar = goal_bar_plot(
keys,
co2e,
title="Årlig udledning ton CO2e",
xlabel="Løsninger",
ylabel="Ton CO2e pr. år",
)
return [
omkostning_bar,
co2e_bar,
accordian_card,
f"{str(round(co2e[0], 3)).replace('.', ',')} Ton CO2e",
{"color": "red" if co2e_reduktioner[0] < 0 else "green"},
f"{round(omkostning[0])} DKK",
{"color": "red" if omkostning_besparelse[0] < 0 else "green"},
]
[docs]@callback(
Output("leasingperiode", "children"),
Output("eksisterende_omkostninger", "children"),
State("date_store", "data"),
State("location_store", "data"),
Input("url", "pathname"),
Input("vehicle_sel_store", "data"),
)
def make_vehicle_list(dates, location, url, selected_vehicles):
# todo ensure to pull the bikes
if url != "/page_goal" or selected_vehicles is None:
raise PreventUpdate
omkostninger = "Det ekstra årlige beløb du vil betale."
start = datetime.datetime.now()
current_expense = 0
with Session() as session:
expense_query = session.query(
func.sum(Cars.omkostning_aar).label("total_expense")
).filter(
Cars.location == location,
func.coalesce(Cars.end_leasing, dates[-1]) >= dates[-1],
)
for a in expense_query:
current_expense += a.total_expense
omkostninger += f" Nuværende faste omkostninger: {round(current_expense)} DKK, eksl. brændstofforbrug"
rows = session.query(
func.coalesce(Cars.end_leasing, start).label("end_leasing")
).filter(Cars.id.in_(selected_vehicles))
dates = []
for row in rows:
dates.append(row.end_leasing)
if dates is None or len(dates) == 0:
all_dates = [
datetime.datetime.now(),
datetime.datetime.now() + datetime.timedelta(days=365),
]
else:
sorted_dates = sorted(dates)
all_dates = (
[datetime.datetime.now()]
+ [
datetime.datetime.fromisoformat(date) if type(date) is str else date
for date in sorted(sorted_dates)
]
+ [
datetime.datetime.fromisoformat(sorted_dates[-1])
if type(sorted_dates[-1]) is str
else sorted_dates[-1]
]
)
all_dates[-1] += datetime.timedelta(days=30)
value = [unix_time_millis(date) for date in all_dates]
marks = {
a: str(date.date())
for a, date in zip([value[0], value[-1]], [all_dates[0], all_dates[-1]])
}
mind = value[0]
maxd = value[-1]
value = [value[0], value[-1]]
date_slider = (
dcc.RangeSlider(
min=mind,
max=maxd,
value=value,
marks=marks,
tooltip={
"always_visible": False,
},
id="slids",
),
)
return [date_slider, omkostninger]
[docs]@callback(
Output("vehicle-list", "children"),
Output("vehicle-list", "data"),
Output("leasing_text", "children"),
Input("slids", "value"),
Input("vehicle_sel_store", "data"),
)
def update_view(unix_times, selected_vehicles):
start, end = [unix_to_datetime(va) for va in unix_times]
leasing_text = f"Juster leasingperioden hvis du ønsker at frigøre køretøjer til udskiftning. Valgt periode: {start.date()} til {end.date()}"
with Session() as session:
fake_end = datetime.datetime(year=1000, month=1, day=1)
subq = (
session.query(
Cars.make,
func.coalesce(Cars.model, "").label("model"),
func.coalesce(Cars.omkostning_aar, 0).label("omkostning_aar"),
func.coalesce(Cars.co2_pr_km, 0).label("co2_pr_km"),
func.coalesce(Cars.km_aar, 0).label("km_aar"),
func.coalesce(Cars.sleep, 0).label("sleep"),
func.coalesce(Cars.range, 0).label("range"),
func.coalesce(Cars.wltp_el, float(0)).label("wltp_el"),
func.coalesce(Cars.wltp_fossil, float(0)).label("wltp_fossil"),
func.coalesce(Cars.capacity_decrease, float(0)).label(
"capacity_decrease"
),
func.count(Cars.make).label("count"),
func.coalesce(Cars.end_leasing, fake_end).label("end_leasing"),
)
.filter(
Cars.id.in_(selected_vehicles),
or_(
Cars.end_leasing == fake_end,
and_(
func.coalesce(Cars.end_leasing, end) <= end,
func.coalesce(Cars.end_leasing, end) >= start,
),
),
)
.group_by(
Cars.make,
Cars.model,
Cars.omkostning_aar,
Cars.co2_pr_km,
Cars.km_aar,
Cars.sleep,
Cars.range,
Cars.wltp_el,
Cars.wltp_fossil,
Cars.capacity_decrease,
Cars.end_leasing,
)
).subquery()
cte = (
session.query(Cars.id, subq)
.filter(Cars.id.in_(selected_vehicles))
.join(
Cars,
(Cars.make == subq.c.make)
& (func.coalesce(Cars.model, "") == subq.c.model)
& (func.coalesce(Cars.omkostning_aar, 0) == subq.c.omkostning_aar)
& (func.coalesce(Cars.end_leasing, fake_end) == subq.c.end_leasing)
& (func.coalesce(Cars.co2_pr_km, 0) == subq.c.co2_pr_km)
& (func.coalesce(Cars.km_aar, 0) == subq.c.km_aar)
& (func.coalesce(Cars.sleep, 0) == subq.c.sleep)
& (func.coalesce(Cars.range, 0) == subq.c.range)
& (func.coalesce(Cars.wltp_el, float(0)) == subq.c.wltp_el)
& (func.coalesce(Cars.wltp_fossil, float(0)) == subq.c.wltp_fossil)
& (
func.coalesce(Cars.capacity_decrease, float(0))
== subq.c.capacity_decrease
),
)
.cte()
)
first_id = (
session.query(func.min(cte.c.id))
.filter(cte.c.id.in_(selected_vehicles))
.group_by(
cte.c.make,
cte.c.model,
cte.c.omkostning_aar,
cte.c.co2_pr_km,
cte.c.km_aar,
cte.c.sleep,
cte.c.range,
cte.c.wltp_el,
cte.c.wltp_fossil,
cte.c.capacity_decrease,
cte.c.end_leasing,
)
)
rows = session.query(cte).filter(cte.c.id.in_(first_id))
vehicle_data = []
body = []
for row in rows:
vehicle_data.append(
{
"id": row.id,
"make": row.make,
"model": row.model,
"omkostning_aar": row.omkostning_aar,
"co2_pr_km": row.co2_pr_km,
"km_aar": row.km_aar,
"sleep": row.sleep,
"range": row.range,
"wltp_el": row.wltp_el,
"wltp_fossil": row.wltp_fossil,
"capacity_decrease": row.capacity_decrease,
"count": row.count,
"end_leasing": row.end_leasing,
}
)
body.append(
html.Tr(
[
html.Td("{} {}".format(row.make, row.model)),
html.Td(round(row.omkostning_aar)),
html.Td(get_emission(row)),
html.Td(row.count),
html.Td(
"Ejet"
if str(row.end_leasing) == str(fake_end)
else str(row.end_leasing).split()[0]
),
]
)
)
header = html.Thead(
html.Tr(
[
html.Th("Biltype"),
html.Th("Årlig omk. (DKK)"),
html.Th("Drivmiddel forbrug"),
html.Th("Antal i beholdning"),
html.Th("Slut leasing"),
]
)
)
return [
dbc.Table(
[header, html.Tbody(body)],
),
vehicle_data,
leasing_text,
]