diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2b06ace --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} diff --git a/Utilities/BESS.py b/Utilities/BESS.py index 76ffb35..25c33ac 100644 --- a/Utilities/BESS.py +++ b/Utilities/BESS.py @@ -37,7 +37,8 @@ def discharge_bess(bess, site_name, dt, discharge_power): # maintain SoC if not assigned to the site 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 return bess @@ -72,7 +73,7 @@ def predict_swap_time(bess_soc_for_cycle): 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"]) # assign SoC for cycle 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], pd.DataFrame( - [[timestamps[i], unit["SoC"]]], + [[timestamp, unit["SoC"]]], columns=["Timestamp", "SoC"], ), ], axis=0, ) + return bess_soc_for_cycle + def arrange_swap(bess_data, c): for unit in bess_data["units"]: diff --git a/Utilities/DataVis.py b/Utilities/DataVis.py new file mode 100644 index 0000000..9ed6012 --- /dev/null +++ b/Utilities/DataVis.py @@ -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 diff --git a/dashboard.py b/dashboard.py index 9fcb3cc..a028ced 100644 --- a/dashboard.py +++ b/dashboard.py @@ -1,7 +1,17 @@ # dashboard.py import streamlit as st -import matplotlib.pyplot as plt -from main import start_sim, stop_sim, reset_sim, bess_soc_since_start +import matplotlib.pyplot as pl +import pandas as pd +import main +from main import ( + start_sim, + stop_sim, + reset_sim, +) +import time + + +st.set_page_config(layout="wide") # Header st.logo("https://rooftop.my/logo.svg", size="large") @@ -31,3 +41,54 @@ with col3: if st.button("Reset", use_container_width=True): reset_sim() 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.") diff --git a/main.py b/main.py index c1db0be..5b225db 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from Utilities.BESS import ( predict_swap_time, update_cycle_SoC, ) +from Utilities.DataVis import format_dataframe import matplotlib.pyplot as pl import pandas as pd 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 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 +load_profiles_since_start = None +status_df = None # load site info c["site_info"] = yaml.safe_load(open(c["paths"]["site_info"])) + ### <<< CONTROL ADDED >>> Initialize simulation state globals sim_i = 0 running = False @@ -42,7 +46,8 @@ sim_lock = threading.Lock() # initialise BESS 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 = initial_site_assignment(c, bd) bess_data = bd @@ -64,7 +69,7 @@ _init_state() def simulation_loop(): """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: while True: with sim_lock: @@ -100,7 +105,7 @@ def simulation_loop(): # update cycle SoC and predict swaps 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) @@ -113,15 +118,16 @@ def simulation_loop(): print(len(cumulative_load_profiles), "profiles generated") 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 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 def start_sim():