dashboard wip

This commit is contained in:
Lucas Tan 2025-07-20 15:37:43 +01:00
parent 35dd46e799
commit 6c994e970c
5 changed files with 166 additions and 14 deletions

28
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "🔍 Debug Streamlit",
"type": "debugpy",
"request": "launch",
// Tell VS Code to use `python -m streamlit run ...`
"module": "streamlit",
// Replace `app.py` (or dashboard.py) with your entry-point
"args": [
"run",
"dashboard.py",
// (optional but highly recommended) disable the auto-reloader
"--server.runOnSave=false"
],
// so you can interact with the app and see logs
"console": "integratedTerminal",
// only step into *your* code, not the Streamlit internals
"justMyCode": true
}
]
}

View File

@ -37,7 +37,8 @@ def discharge_bess(bess, site_name, dt, discharge_power):
# maintain SoC if not assigned to the site # maintain SoC if not assigned to the site
new_soc = unit["SoC"] new_soc = unit["SoC"]
# update SoC # update SoC and current load
bess["units"][index]["current_load_kW"] = discharge_power
bess["units"][index]["SoC"] = new_soc bess["units"][index]["SoC"] = new_soc
return bess return bess
@ -72,7 +73,7 @@ def predict_swap_time(bess_soc_for_cycle):
return swap_times return swap_times
def update_cycle_SoC(bess_data, bess_soc_for_cycle, timestamps): def update_cycle_SoC(bess_data, bess_soc_for_cycle, timestamp):
init_df = pd.DataFrame(columns=["Timestamp", "SoC"]) init_df = pd.DataFrame(columns=["Timestamp", "SoC"])
# assign SoC for cycle # assign SoC for cycle
for unit in bess_data["units"]: for unit in bess_data["units"]:
@ -85,13 +86,15 @@ def update_cycle_SoC(bess_data, bess_soc_for_cycle, timestamps):
[ [
bess_soc_for_cycle[unit_name], bess_soc_for_cycle[unit_name],
pd.DataFrame( pd.DataFrame(
[[timestamps[i], unit["SoC"]]], [[timestamp, unit["SoC"]]],
columns=["Timestamp", "SoC"], columns=["Timestamp", "SoC"],
), ),
], ],
axis=0, axis=0,
) )
return bess_soc_for_cycle
def arrange_swap(bess_data, c): def arrange_swap(bess_data, c):
for unit in bess_data["units"]: for unit in bess_data["units"]:

54
Utilities/DataVis.py Normal file
View File

@ -0,0 +1,54 @@
import pandas as pd
def format_dataframe(
bess_soc_for_cycle, bess_data, load_profiles_since_start, swap_time
):
"""Formats the DataFrame for display in the dashboard."""
# Create a DataFrame for sites
# columns = ["Site Name", "MBESS Unit", "Current Load (kW)", "SoC (%)", "Predicted Swap Time"]
status_df = pd.DataFrame(
columns=[
"Site Name",
"MBESS Unit",
"Current Load (kW)",
"SoC (%)",
"Predicted Swap Time",
"Cycle Discharge Profile",
"Load Profile Since Start",
]
)
for site in load_profiles_since_start.keys():
index = next(i for i, d in enumerate(bess_data["units"]) if d["site"] == site)
soc = bess_data["units"][index]["SoC"]
current_load = bess_data["units"][index]["current_load_kW"]
unit_name = bess_data["units"][index]["name"]
predicted_swap_time = swap_time.get(unit_name, "N/A")
status_df = pd.concat(
[
status_df,
pd.DataFrame(
[
{
"Site Name": site,
"MBESS Unit": unit_name,
"Current Load (kW)": current_load,
"SoC (%)": soc * 100, # Convert to percentage
"Predicted Swap Time": predicted_swap_time,
"Cycle Discharge Profile": bess_soc_for_cycle[unit_name][
"SoC"
].tolist(),
"Load Profile Since Start": load_profiles_since_start[
site
].tolist(),
}
]
),
],
ignore_index=True,
)
return status_df

View File

@ -1,7 +1,17 @@
# dashboard.py # dashboard.py
import streamlit as st import streamlit as st
import matplotlib.pyplot as plt import matplotlib.pyplot as pl
from main import start_sim, stop_sim, reset_sim, bess_soc_since_start import pandas as pd
import main
from main import (
start_sim,
stop_sim,
reset_sim,
)
import time
st.set_page_config(layout="wide")
# Header # Header
st.logo("https://rooftop.my/logo.svg", size="large") st.logo("https://rooftop.my/logo.svg", size="large")
@ -31,3 +41,54 @@ with col3:
if st.button("Reset", use_container_width=True): if st.button("Reset", use_container_width=True):
reset_sim() reset_sim()
st.session_state.running = False st.session_state.running = False
placeholder = st.empty()
def show_table():
df = main.status_df
if df is None or df.empty:
placeholder.text("Waiting for first simulation step…")
else:
placeholder.dataframe(
df,
column_config={
"Site Name": st.column_config.TextColumn("Site Name"),
"MBESS Unit": st.column_config.TextColumn(
"MBESS Unit", help="Name of the MBESS unit at the site"
),
"Current Load (kW)": st.column_config.NumberColumn(
"Current Load (kW)", help="Current BESS discharge load in kW"
),
"SoC (%)": st.column_config.ProgressColumn(
"State of Charge",
help="State of Charge of the BESS unit",
format="%d%%",
min_value=0,
max_value=100,
),
"Predicted Swap Time": st.column_config.TextColumn(
"Predicted Swap Time", help="Predicted time for BESS swap"
),
"Cycle Discharge Profile": st.column_config.LineChartColumn(
"Cycle Discharge Profile",
help="Cycle discharge profile of the BESS unit",
),
"Load Profile Since Start": st.column_config.LineChartColumn(
"Load Profile Since Start",
help="Load profile since the start of the simulation",
),
},
use_container_width=True,
)
if st.session_state.running:
st.subheader("BESS State of Charge (SoC) and Energy Consumption")
# display BESS data, SoC, Load Consumption
show_table()
time.sleep(1)
st.rerun()
else:
show_table()
st.info("Simulation paused.")

24
main.py
View File

@ -9,6 +9,7 @@ from Utilities.BESS import (
predict_swap_time, predict_swap_time,
update_cycle_SoC, update_cycle_SoC,
) )
from Utilities.DataVis import format_dataframe
import matplotlib.pyplot as pl import matplotlib.pyplot as pl
import pandas as pd import pandas as pd
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -29,10 +30,13 @@ duration = c["sim_time"]["duration_days"] * 24 * 60 * 60
c["sim_end_time"] = c["sim_start_time"] + duration c["sim_end_time"] = c["sim_start_time"] + duration
timestamps = np.arange(c["sim_start_time"], c["sim_end_time"] + 1, dt) timestamps = np.arange(c["sim_start_time"], c["sim_end_time"] + 1, dt)
c["sim_time"]["batch_process_seconds"] = c["sim_time"]["batch_process_hours"] * 60 * 60 c["sim_time"]["batch_process_seconds"] = c["sim_time"]["batch_process_hours"] * 60 * 60
load_profiles_since_start = None
status_df = None
# load site info # load site info
c["site_info"] = yaml.safe_load(open(c["paths"]["site_info"])) c["site_info"] = yaml.safe_load(open(c["paths"]["site_info"]))
### <<< CONTROL ADDED >>> Initialize simulation state globals ### <<< CONTROL ADDED >>> Initialize simulation state globals
sim_i = 0 sim_i = 0
running = False running = False
@ -42,7 +46,8 @@ sim_lock = threading.Lock()
# initialise BESS # initialise BESS
def _init_state(): def _init_state():
global bess_data, bess_soc_since_start, bess_soc_for_cycle, cumulative_load_profiles global bess_data, bess_soc_since_start, bess_soc_for_cycle, cumulative_load_profiles, load_profiles_since_start
bd = initialise_SoC(bess_data.copy()) bd = initialise_SoC(bess_data.copy())
bd = initial_site_assignment(c, bd) bd = initial_site_assignment(c, bd)
bess_data = bd bess_data = bd
@ -64,7 +69,7 @@ _init_state()
def simulation_loop(): def simulation_loop():
"""Runs the loop, stepping through timestamps until stopped or finished.""" """Runs the loop, stepping through timestamps until stopped or finished."""
global sim_i, running, is_running_in_async, cumulative_load_profiles, bess_data global sim_i, running, is_running_in_async, cumulative_load_profiles, bess_data, bess_soc_for_cycle, load_profiles_since_start, status_df
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
while True: while True:
with sim_lock: with sim_lock:
@ -100,7 +105,7 @@ def simulation_loop():
# update cycle SoC and predict swaps # update cycle SoC and predict swaps
bess_soc_for_cycle = update_cycle_SoC( bess_soc_for_cycle = update_cycle_SoC(
bess_data, bess_soc_for_cycle, timestamps bess_data, bess_soc_for_cycle, timestamps[i]
) )
swap_times = predict_swap_time(bess_soc_for_cycle) swap_times = predict_swap_time(bess_soc_for_cycle)
@ -113,15 +118,16 @@ def simulation_loop():
print(len(cumulative_load_profiles), "profiles generated") print(len(cumulative_load_profiles), "profiles generated")
is_running_in_async = False is_running_in_async = False
load_profiles_since_start = cumulative_load_profiles.iloc[: i + 1]
# format data for display
status_df = format_dataframe(
bess_soc_for_cycle, bess_data, load_profiles_since_start, swap_times
)
# small sleep to allow dashboard to refresh / release GIL # small sleep to allow dashboard to refresh / release GIL
time.sleep(0.01) time.sleep(0.01)
# once loop ends, you can plot or notify completion here
pl.plot(cumulative_load_profiles)
pl.show()
pl.plot(bess_soc_since_start)
pl.show()
### <<< CONTROL ADDED >>> Control functions ### <<< CONTROL ADDED >>> Control functions
def start_sim(): def start_sim():