dashboard wip
This commit is contained in:
parent
35dd46e799
commit
6c994e970c
28
.vscode/launch.json
vendored
Normal file
28
.vscode/launch.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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
54
Utilities/DataVis.py
Normal 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
|
||||||
65
dashboard.py
65
dashboard.py
@ -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
24
main.py
@ -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():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user