diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..feefea2 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,8 @@ +# this is needed for local development with docker +[server] +# if you don't want to start the default browser: +headless = true +# you will need this for local development: +runOnSave = true +# you will need this if running docker on windows host: +fileWatcherType = "poll" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d0d0eb..904a08b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,16 +7,6 @@ } }, "isort.args": ["--profile", "black"], - "terminal.integrated.env.linux": { - "PYTHONPATH": "${workspaceFolder}/campus-decarb" - }, - "terminal.integrated.env.osx": { - "PYTHONPATH": "${workspaceFolder}/campus-decarb" - }, - "terminal.integrated.env.windows": { - "PYTHONPATH": "${workspaceFolder}/campus-decarb" - }, - "python.analysis.extraPaths": ["campus-decarb"], - "jupyter.notebookFileRoot": "${workspaceFolder}/campus-decarb", + "jupyter.notebookFileRoot": "${workspaceFolder}", "python.envFile": "${workspaceFolder}/.env" } diff --git a/campus-decarb/__init__.py b/campus-decarb/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..70f73bc --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,6 @@ +version: '3.4' +services: + frontend: + volumes: + - ./frontend:/app/frontend + - ./lib:/app/lib \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8fa0bb2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.4' +services: + frontend: # offloads api callbacks to a scalable service + build: + context: . + dockerfile: frontend/Dockerfile + env_file: + - .env + ports: + - "8501:8501" diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..786e865 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements/ requirements/ + +RUN pip install --no-cache-dir -r requirements/base-requirements.txt -r requirements/fe-requirements.txt + +COPY .streamlit/ .streamlit/ +COPY frontend/ frontend/ +COPY lib/ lib/ + +ENV PYTHONPATH=/app + +CMD ["streamlit", "run", "frontend/app.py", "--server.port", "8501", "--server.address", "0.0.0.0"] + diff --git a/frontend/__init__.py b/frontend/__init__.py index e69de29..fe18203 100644 --- a/frontend/__init__.py +++ b/frontend/__init__.py @@ -0,0 +1,19 @@ +from typing import Literal, Optional + +from pydantic import Field +from pydantic_settings import BaseSettings + +FrontendEnvs = Literal["dev", "prod"] + + +class FrontendSettings(BaseSettings): + + env: str = Field("dev", env="ENV") + password: str = Field(..., env="PASSWORD") + + class Config: + env_prefix = "FRONTEND_" + env_file = ".env" + + +frontend_settings = FrontendSettings() diff --git a/frontend/app.py b/frontend/app.py index f3ef07b..9decb3d 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -1,33 +1,239 @@ +import json +import os + import pandas as pd +import plotly.express as px import streamlit as st + +from frontend import frontend_settings as settings from lib.client import client from lib.supa import Building st.set_page_config(layout="wide", page_title="MIT Decarbonization") + @st.cache_data def get_all_buildings(): - df = pd.DataFrame(client.table("Building").select("*").execute().data).set_index('id') + df = pd.DataFrame(client.table("Building").select("*").execute().data).set_index( + "id" + ) # create csv bytes csv = df.to_csv().encode() return df, csv + +@st.cache_data +def get_building_scenarios(building_id: int): + ids = ( + client.table("DemandScenarioBuilding") + .select("id, demand_scenario_id") + .eq("building_id", building_id) + .execute() + .data + ) + scenario_ids = [d["demand_scenario_id"] for d in ids] + results_ids = [d["id"] for d in ids] + return scenario_ids, results_ids + + +@st.cache_data +def get_scenarios(): + return client.table("DemandScenario").select("*").execute().data + + +@st.cache_data +def get_scenario_results(scenario_id: int): + ids = ( + client.table("DemandScenarioBuilding") + .select("id") + .eq("demand_scenario_id", scenario_id) + .execute() + .data + ) + results_ids = [d["id"] for d in ids] + results = ( + client.table("BuildingSimulationResult") + .select("*") + .in_("id", results_ids) + .execute() + .data + ) + dfs = [] + for result in results: + result["heating"] = json.loads(result["heating"]) + result["cooling"] = json.loads(result["cooling"]) + result["lighting"] = json.loads(result["lighting"]) + result["equipment"] = json.loads(result["equipment"]) + df = pd.DataFrame( + { + "heating": result["heating"], + "cooling": result["cooling"], + "lighting": result["lighting"], + "equipment": result["equipment"], + } + ) + df["Timestamp"] = pd.date_range(start="2024-01-01", periods=len(df), freq="h") + df = df.set_index("Timestamp") + df["result_id"] = result["id"] + df = df.set_index("result_id", append=True) + dfs.append(df) + df_buildings = pd.concat(dfs) + df = df_buildings.groupby("Timestamp").sum() + df.columns = [x.capitalize() for x in df.columns] + df = df.reset_index("Timestamp") + df_melted = df.melt( + id_vars=["Timestamp"], var_name="End Use", value_name="Energy [J]" + ) + return df, df_melted, df_buildings + + +@st.cache_data +def get_scenario_building_result(scenario_id: int, building_id: int): + ids = ( + client.table("DemandScenarioBuilding") + .select("id") + .eq("demand_scenario_id", scenario_id) + .eq("building_id", building_id) + .execute() + .data + ) + results_ids = [d["id"] for d in ids] + results = ( + client.table("BuildingSimulationResult") + .select("*") + .in_("id", results_ids) + .execute() + .data + ) + df = pd.DataFrame( + { + "heating": json.loads(results[0]["heating"]), + "cooling": json.loads(results[0]["cooling"]), + "lighting": json.loads(results[0]["lighting"]), + "equipment": json.loads(results[0]["equipment"]), + } + ) + df["Timestamp"] = pd.date_range(start="2024-01-01", periods=len(df), freq="h") + df.columns = [x.capitalize() for x in df.columns] + df_melted = df.melt( + id_vars=["Timestamp"], var_name="End Use", value_name="Energy [J]" + ) + + return df, df_melted + + +ENDUSE_PASTEL_COLORS = { + "Heating": "#FF7671", + "Cooling": "#6D68E6", + "Lighting": "#FFD700", + "Equipment": "#90EE90", +} + + def render_title(): st.title("MIT Decarbonization") + def render_buildings(): - st.header("Buildings") all_buildings, all_buildings_csv = get_all_buildings() - st.download_button("Download all buildings", all_buildings_csv, "buildings_metadata.csv", "Download all buildings", use_container_width=True, type="primary") - building_id = st.selectbox('Building', all_buildings.index, format_func=lambda x: all_buildings.loc[x, 'name'], help='Select a building to view its data') + st.download_button( + "Download all building metadata", + all_buildings_csv, + "buildings_metadata.csv", + "Download all buildings", + use_container_width=True, + type="primary", + ) + building_id = st.selectbox( + "Building", + all_buildings.index, + format_func=lambda x: all_buildings.loc[x, "name"], + help="Select a building to view its data", + ) building = all_buildings.loc[building_id] - st.dataframe(building) + scenario_ids, results_ids = get_building_scenarios(building_id) + all_scenarios = get_scenarios() + filtered_scenarios = [s for s in all_scenarios if s["id"] in scenario_ids] + l, r = st.columns(2) + with l: + scenario_id = st.selectbox( + "Building Demand Scenario", + scenario_ids, + format_func=lambda x: [ + s["name"] for s in filtered_scenarios if s["id"] == x + ][0], + help="Select a demand scenario to view its data", + ) + result, result_melted = get_scenario_building_result(scenario_id, building_id) + fig = px.line( + result_melted, + x="Timestamp", + y="Energy [J]", + color="End Use", + title=f"Building {building['name']} Energy Use", + color_discrete_map=ENDUSE_PASTEL_COLORS, + ) + st.plotly_chart(fig, use_container_width=True) + with r: + st.dataframe(building) -render_title() -st.divider() -render_buildings() +def render_building_scenarios(): + all_scenarios = get_scenarios() + scenario = st.selectbox( + "Demand Scenario", + all_scenarios, + format_func=lambda x: x["name"], + help="Select a demand scenario to view its data", + ) + df, df_melted, df_buildings = get_scenario_results(scenario["id"]) + l, r = st.columns(2) + with l: + st.download_button( + "Download scenario results", + df.to_csv().encode(), + f"{scenario['name']}_results.csv", + "Download scenario results", + use_container_width=True, + type="primary", + ) + with r: + st.download_button( + "Download scenario buildings results", + df_buildings.to_csv().encode(), + f"{scenario['name']}_buildings_results.csv", + "Download scenario buildings results", + use_container_width=True, + type="primary", + ) + fig = px.line( + df_melted, + x="Timestamp", + y="Energy [J]", + color="End Use", + title=f"{scenario['name']} Demand Scenario", + color_discrete_map=ENDUSE_PASTEL_COLORS, + ) + st.plotly_chart(fig, use_container_width=True) +def password_protect(): + if "password" not in st.session_state: + st.session_state.password = None + if st.session_state.password == settings.password: + return True + else: + password = st.text_input("Password", type="password") + st.session_state.password = password + return st.session_state.password == settings.password + +render_title() +logged_in = password_protect() +if logged_in: + buildings_tab, scenarios_tab = st.tabs(["Buildings", "Scenarios"]) + with buildings_tab: + render_buildings() + with scenarios_tab: + render_building_scenarios() diff --git a/requirements/fe-requirements.txt b/requirements/fe-requirements.txt index e251330..1e6ae59 100644 --- a/requirements/fe-requirements.txt +++ b/requirements/fe-requirements.txt @@ -1 +1,2 @@ -streamlit \ No newline at end of file +streamlit +plotly \ No newline at end of file