diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..bc1c306 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,34 @@ +[server] +enableStaticServing = true + +[[theme.fontFaces]] +family = "Exo2" +url = "app/static/EXO2-VARIABLEFONT_WGHT.TTF" +style = "normal" +weight = 400 + + +[[theme.fontFaces]] +family = "Exo2" +url = "app/static/EXO2-BOLD.TTF" +style = "bold" +weight = 700 + +[[theme.fontFaces]] +family = "Exo2" +url = "app/static/EXO2-ITALIC.TTF" +style = "italic" +weight = 400 + +[[theme.fontFaces]] +family = "Exo2" +url = "app/static/EXO2-BOLDITALIC.TTF" +style = "bold italic" +weight = 7 + +[theme] +base="dark" +primaryColor="#fcd913" +font="Exo2" +codeFont="Exo2" + diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..9fcb3cc --- /dev/null +++ b/dashboard.py @@ -0,0 +1,33 @@ +# dashboard.py +import streamlit as st +import matplotlib.pyplot as plt +from main import start_sim, stop_sim, reset_sim, bess_soc_since_start + +# Header +st.logo("https://rooftop.my/logo.svg", size="large") +st.title("MEOS Control Dashboard") +st.subheader("Mobile Energy Operations Simulation (MEOS)") +st.text("Run MEOS Simulation and Monitor MBESS Status") + +# some instructions + +# --- SESSION STATE SETUP --- +if "running" not in st.session_state: + st.session_state.running = False +if "plot_area" not in st.session_state: + st.session_state.plot_area = st.empty() + +# --- CONTROL BUTTONS --- +col1, col2, col3 = st.columns(3) +with col1: + if st.button("Start", use_container_width=True): + start_sim() + st.session_state.running = True +with col2: + if st.button("Stop", use_container_width=True): + stop_sim() + st.session_state.running = False +with col3: + if st.button("Reset", use_container_width=True): + reset_sim() + st.session_state.running = False diff --git a/main.py b/main.py index e3a2bb6..c1db0be 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,8 @@ import matplotlib.pyplot as pl import pandas as pd from concurrent.futures import ThreadPoolExecutor +import threading ### <<< CONTROL ADDED >>> +import time ### <<< CONTROL ADDED >>> # read config file c = yaml.safe_load(open("YAMLs/config.yml")) @@ -21,105 +23,125 @@ c = yaml.safe_load(open("YAMLs/config.yml")) bess_data = yaml.safe_load(open(c["paths"]["bess"])) ## simulation time setup -# get current time c["sim_start_time"] = get_start_time() -# get time step in minutes, then convert to seconds dt = c["sim_time"]["time_step_minutes"] * 60 -# compute end time based on duration in days 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) - -# batch process hours in seconds c["sim_time"]["batch_process_seconds"] = c["sim_time"]["batch_process_hours"] * 60 * 60 # 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 +is_running_in_async = False +sim_lock = threading.Lock() -def generate_and_cache_profiles(c, dt): - """Generates load profiles for all sites and caches them.""" - return get_load_profiles( + +# initialise BESS +def _init_state(): + global bess_data, bess_soc_since_start, bess_soc_for_cycle, cumulative_load_profiles + bd = initialise_SoC(bess_data.copy()) + bd = initial_site_assignment(c, bd) + bess_data = bd + + bess_soc_since_start = pd.DataFrame( + columns=[unit["name"] for unit in bess_data["units"]] + ) + init_df = pd.DataFrame(columns=["Timestamp", "SoC"]) + bess_soc_for_cycle = {unit["name"]: init_df for unit in bess_data["units"]} + + cumulative_load_profiles = get_load_profiles( c, dt, c["sim_start_time"], c["sim_time"]["batch_process_seconds"] ) -# initialise BESS -bess_data = initialise_SoC(bess_data) -bess_data = initial_site_assignment(c, bess_data) -# bess SoC dataframe -bess_soc_since_start = pd.DataFrame( - columns=[unit["name"] for unit in bess_data["units"]] -) -# bess SoC dictionary, meant to track SoC progress over each cycle. -# resets after each charging cycle. This is for predicting swap times. -init_df = pd.DataFrame(columns=["Timestamp", "SoC"]) -bess_soc_for_cycle = {unit["name"]: init_df for unit in bess_data["units"]} +# do initial setup +_init_state() -# get initial load profiles -cumulative_load_profiles = get_load_profiles( - c, dt, c["sim_start_time"], c["sim_time"]["batch_process_seconds"] -) -# async function is running -is_running_in_async = False +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 + with ThreadPoolExecutor() as executor: + while True: + with sim_lock: + if not running or sim_i >= len(timestamps): + break -# loop through -with ThreadPoolExecutor() as executor: - for i in range(0, len(timestamps)): - # start generating load profiles 200 seconds before data required - if len(cumulative_load_profiles) <= len(timestamps): - if is_running_in_async is False: - # generate load profiles - future = executor.submit(generate_and_cache_profiles, c, dt) - is_running_in_async = True - else: - is_running_in_async = False + i = sim_i + sim_i += 1 - # check if any BESS units are below threshold (buffer as defined in config) - - # discharge BESS for each site - for site in c["site_info"]["sites"]: - site_name = site["name"] - discharge_power = cumulative_load_profiles[site_name].iloc[i] - bess_data = discharge_bess(bess_data, site_name, dt, discharge_power) - temp_soc = [unit["SoC"] for unit in bess_data["units"]] - - # append SoC to dataframe - bess_soc_since_start = pd.concat( - [ - bess_soc_since_start, - pd.DataFrame( - [temp_soc], - columns=bess_soc_since_start.columns, - index=[timestamps[i]], - ), - ], - axis=0, - ) - - # update cycle SoC - # this is for predicting swap times - bess_soc_for_cycle = update_cycle_SoC(bess_data, bess_soc_for_cycle, timestamps) - - # predict swap times - swap_times = predict_swap_time(bess_soc_for_cycle) - - # add to cumulative load profiles - # check if future exists and is done - if is_running_in_async: - if future.done(): - load_profiles = future.result() - cumulative_load_profiles = pd.concat( - [ - cumulative_load_profiles, - load_profiles, - ], - axis=0, - ) - print(len(cumulative_load_profiles), "load profiles generated") + # pre-fetch next batch if needed + if len(cumulative_load_profiles) <= len(timestamps): + if not is_running_in_async: + future = executor.submit( + get_load_profiles, + c, + dt, + c["sim_start_time"], + c["sim_time"]["batch_process_seconds"], + ) + is_running_in_async = True + else: is_running_in_async = False -pl.plot(cumulative_load_profiles) -pl.show() -pl.plot(bess_soc_since_start) + # discharge BESS for each site + for site in c["site_info"]["sites"]: + name = site["name"] + p = cumulative_load_profiles[name].iloc[i] + bess_data = discharge_bess(bess_data, name, dt, p) + + # record SoC + temp_soc = [u["SoC"] for u in bess_data["units"]] + bess_soc_since_start.loc[timestamps[i]] = temp_soc + + # update cycle SoC and predict swaps + bess_soc_for_cycle = update_cycle_SoC( + bess_data, bess_soc_for_cycle, timestamps + ) + swap_times = predict_swap_time(bess_soc_for_cycle) + + # integrate newly fetched profiles + if is_running_in_async and future.done(): + load_profiles = future.result() + cumulative_load_profiles = pd.concat( + [cumulative_load_profiles, load_profiles], axis=0 + ) + print(len(cumulative_load_profiles), "profiles generated") + is_running_in_async = False + + # 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(): + """Starts the simulation in a background thread.""" + global running, sim_thread + if not running: + running = True + sim_thread = threading.Thread(target=simulation_loop, daemon=True) + sim_thread.start() + + +def stop_sim(): + """Stops the simulation loop.""" + global running + running = False + + +def reset_sim(): + """Stops and re-initializes the simulation state.""" + global running, sim_i + running = False + sim_i = 0 + _init_state() diff --git a/static/EXO2-BLACK.TTF b/static/EXO2-BLACK.TTF new file mode 100644 index 0000000..03fd8be Binary files /dev/null and b/static/EXO2-BLACK.TTF differ diff --git a/static/EXO2-BLACKITALIC.TTF b/static/EXO2-BLACKITALIC.TTF new file mode 100644 index 0000000..27b2349 Binary files /dev/null and b/static/EXO2-BLACKITALIC.TTF differ diff --git a/static/EXO2-BOLD.TTF b/static/EXO2-BOLD.TTF new file mode 100644 index 0000000..3dc74df Binary files /dev/null and b/static/EXO2-BOLD.TTF differ diff --git a/static/EXO2-BOLDITALIC.TTF b/static/EXO2-BOLDITALIC.TTF new file mode 100644 index 0000000..f4317e9 Binary files /dev/null and b/static/EXO2-BOLDITALIC.TTF differ diff --git a/static/EXO2-EXTRABOLD.TTF b/static/EXO2-EXTRABOLD.TTF new file mode 100644 index 0000000..0997ff6 Binary files /dev/null and b/static/EXO2-EXTRABOLD.TTF differ diff --git a/static/EXO2-EXTRABOLDITALIC.TTF b/static/EXO2-EXTRABOLDITALIC.TTF new file mode 100644 index 0000000..701917d Binary files /dev/null and b/static/EXO2-EXTRABOLDITALIC.TTF differ diff --git a/static/EXO2-EXTRALIGHT.TTF b/static/EXO2-EXTRALIGHT.TTF new file mode 100644 index 0000000..77db84f Binary files /dev/null and b/static/EXO2-EXTRALIGHT.TTF differ diff --git a/static/EXO2-EXTRALIGHTITALIC.TTF b/static/EXO2-EXTRALIGHTITALIC.TTF new file mode 100644 index 0000000..1858b7a Binary files /dev/null and b/static/EXO2-EXTRALIGHTITALIC.TTF differ diff --git a/static/EXO2-LIGHT.TTF b/static/EXO2-LIGHT.TTF new file mode 100644 index 0000000..9089bba Binary files /dev/null and b/static/EXO2-LIGHT.TTF differ diff --git a/static/EXO2-LIGHTITALIC.TTF b/static/EXO2-LIGHTITALIC.TTF new file mode 100644 index 0000000..9445334 Binary files /dev/null and b/static/EXO2-LIGHTITALIC.TTF differ diff --git a/static/EXO2-MEDIUM.TTF b/static/EXO2-MEDIUM.TTF new file mode 100644 index 0000000..99cd074 Binary files /dev/null and b/static/EXO2-MEDIUM.TTF differ diff --git a/static/EXO2-MEDIUMITALIC.TTF b/static/EXO2-MEDIUMITALIC.TTF new file mode 100644 index 0000000..a034b78 Binary files /dev/null and b/static/EXO2-MEDIUMITALIC.TTF differ diff --git a/static/EXO2-SEMIBOLD.TTF b/static/EXO2-SEMIBOLD.TTF new file mode 100644 index 0000000..c89ab43 Binary files /dev/null and b/static/EXO2-SEMIBOLD.TTF differ diff --git a/static/EXO2-SEMIBOLDITALIC.TTF b/static/EXO2-SEMIBOLDITALIC.TTF new file mode 100644 index 0000000..f540fab Binary files /dev/null and b/static/EXO2-SEMIBOLDITALIC.TTF differ diff --git a/static/EXO2-THIN.TTF b/static/EXO2-THIN.TTF new file mode 100644 index 0000000..bbc551a Binary files /dev/null and b/static/EXO2-THIN.TTF differ diff --git a/static/EXO2-THINITALIC.TTF b/static/EXO2-THINITALIC.TTF new file mode 100644 index 0000000..8dec8f5 Binary files /dev/null and b/static/EXO2-THINITALIC.TTF differ diff --git a/static/EXO2-VARIABLEFONT_WGHT.TTF b/static/EXO2-VARIABLEFONT_WGHT.TTF new file mode 100644 index 0000000..f654bc5 Binary files /dev/null and b/static/EXO2-VARIABLEFONT_WGHT.TTF differ