Spaces:
Running
Running
""" | |
Scenario comparison visualization module for HVAC Load Calculator. | |
This module provides visualization tools for comparing different scenarios. | |
""" | |
import streamlit as st | |
import pandas as pd | |
import numpy as np | |
import plotly.graph_objects as go | |
import plotly.express as px | |
from typing import Dict, List, Any, Optional, Tuple | |
import math | |
# Import calculation modules | |
from utils.cooling_load import CoolingLoadCalculator | |
from utils.heating_load import HeatingLoadCalculator | |
class ScenarioComparisonVisualization: | |
"""Class for scenario comparison visualization.""" | |
def create_scenario_summary_table(scenarios: Dict[str, Dict[str, Any]]) -> pd.DataFrame: | |
""" | |
Create a summary table of different scenarios. | |
Args: | |
scenarios: Dictionary with scenario data | |
Returns: | |
DataFrame with scenario summary | |
""" | |
# Initialize data | |
data = [] | |
# Process scenarios | |
for scenario_name, scenario_data in scenarios.items(): | |
# Extract cooling and heating loads | |
cooling_loads = scenario_data.get("cooling_loads", {}) | |
heating_loads = scenario_data.get("heating_loads", {}) | |
# Create summary row | |
row = { | |
"Scenario": scenario_name, | |
"Cooling Load (W)": cooling_loads.get("total", 0), | |
"Sensible Heat Ratio": cooling_loads.get("sensible_heat_ratio", 0), | |
"Heating Load (W)": heating_loads.get("total", 0) | |
} | |
# Add to data | |
data.append(row) | |
# Create DataFrame | |
df = pd.DataFrame(data) | |
return df | |
def create_load_comparison_chart(scenarios: Dict[str, Dict[str, Any]], load_type: str = "cooling") -> go.Figure: | |
""" | |
Create a bar chart comparing loads across scenarios. | |
Args: | |
scenarios: Dictionary with scenario data | |
load_type: Type of load to compare ("cooling" or "heating") | |
Returns: | |
Plotly figure with load comparison | |
""" | |
# Initialize data | |
scenario_names = [] | |
total_loads = [] | |
component_loads = {} | |
# Process scenarios | |
for scenario_name, scenario_data in scenarios.items(): | |
# Extract loads based on load type | |
if load_type == "cooling": | |
loads = scenario_data.get("cooling_loads", {}) | |
components = ["walls", "roofs", "floors", "windows_conduction", "windows_solar", | |
"doors", "infiltration_sensible", "infiltration_latent", | |
"people_sensible", "people_latent", "lights", "equipment_sensible", "equipment_latent"] | |
else: # heating | |
loads = scenario_data.get("heating_loads", {}) | |
components = ["walls", "roofs", "floors", "windows", "doors", | |
"infiltration_sensible", "infiltration_latent", | |
"ventilation_sensible", "ventilation_latent"] | |
# Add scenario name | |
scenario_names.append(scenario_name) | |
# Add total load | |
total_loads.append(loads.get("total", 0)) | |
# Add component loads | |
for component in components: | |
if component not in component_loads: | |
component_loads[component] = [] | |
component_loads[component].append(loads.get(component, 0)) | |
# Create figure | |
fig = go.Figure() | |
# Add total load bars | |
fig.add_trace(go.Bar( | |
x=scenario_names, | |
y=total_loads, | |
name="Total Load", | |
marker_color="rgba(55, 83, 109, 0.7)", | |
opacity=0.7 | |
)) | |
# Add component load bars | |
for component, loads in component_loads.items(): | |
# Skip components with zero loads | |
if sum(loads) == 0: | |
continue | |
# Format component name for display | |
display_name = component.replace("_", " ").title() | |
fig.add_trace(go.Bar( | |
x=scenario_names, | |
y=loads, | |
name=display_name, | |
visible="legendonly" | |
)) | |
# Update layout | |
title = f"{load_type.title()} Load Comparison" | |
y_title = f"{load_type.title()} Load (W)" | |
fig.update_layout( | |
title=title, | |
xaxis_title="Scenario", | |
yaxis_title=y_title, | |
barmode="group", | |
height=500, | |
legend=dict( | |
orientation="h", | |
yanchor="bottom", | |
y=1.02, | |
xanchor="right", | |
x=1 | |
) | |
) | |
return fig | |
def create_percentage_difference_chart(scenarios: Dict[str, Dict[str, Any]], | |
baseline_scenario: str, | |
load_type: str = "cooling") -> go.Figure: | |
""" | |
Create a bar chart showing percentage differences from a baseline scenario. | |
Args: | |
scenarios: Dictionary with scenario data | |
baseline_scenario: Name of the baseline scenario | |
load_type: Type of load to compare ("cooling" or "heating") | |
Returns: | |
Plotly figure with percentage difference chart | |
""" | |
# Check if baseline scenario exists | |
if baseline_scenario not in scenarios: | |
raise ValueError(f"Baseline scenario '{baseline_scenario}' not found in scenarios") | |
# Get baseline loads | |
if load_type == "cooling": | |
baseline_loads = scenarios[baseline_scenario].get("cooling_loads", {}) | |
components = ["walls", "roofs", "floors", "windows_conduction", "windows_solar", | |
"doors", "infiltration_sensible", "infiltration_latent", | |
"people_sensible", "people_latent", "lights", "equipment_sensible", "equipment_latent"] | |
else: # heating | |
baseline_loads = scenarios[baseline_scenario].get("heating_loads", {}) | |
components = ["walls", "roofs", "floors", "windows", "doors", | |
"infiltration_sensible", "infiltration_latent", | |
"ventilation_sensible", "ventilation_latent"] | |
baseline_total = baseline_loads.get("total", 0) | |
# Initialize data | |
scenario_names = [] | |
percentage_diffs = [] | |
component_diffs = {} | |
# Process scenarios (excluding baseline) | |
for scenario_name, scenario_data in scenarios.items(): | |
if scenario_name == baseline_scenario: | |
continue | |
# Extract loads based on load type | |
if load_type == "cooling": | |
loads = scenario_data.get("cooling_loads", {}) | |
else: # heating | |
loads = scenario_data.get("heating_loads", {}) | |
# Add scenario name | |
scenario_names.append(scenario_name) | |
# Calculate percentage difference for total load | |
scenario_total = loads.get("total", 0) | |
if baseline_total != 0: | |
percentage_diff = (scenario_total - baseline_total) / baseline_total * 100 | |
else: | |
percentage_diff = 0 | |
percentage_diffs.append(percentage_diff) | |
# Calculate percentage differences for components | |
for component in components: | |
if component not in component_diffs: | |
component_diffs[component] = [] | |
baseline_component = baseline_loads.get(component, 0) | |
scenario_component = loads.get(component, 0) | |
if baseline_component != 0: | |
component_diff = (scenario_component - baseline_component) / baseline_component * 100 | |
else: | |
component_diff = 0 | |
component_diffs[component].append(component_diff) | |
# Create figure | |
fig = go.Figure() | |
# Add total percentage difference bars | |
fig.add_trace(go.Bar( | |
x=scenario_names, | |
y=percentage_diffs, | |
name="Total Load", | |
marker_color="rgba(55, 83, 109, 0.7)", | |
opacity=0.7 | |
)) | |
# Add component percentage difference bars | |
for component, diffs in component_diffs.items(): | |
# Skip components with zero differences | |
if sum([abs(diff) for diff in diffs]) == 0: | |
continue | |
# Format component name for display | |
display_name = component.replace("_", " ").title() | |
fig.add_trace(go.Bar( | |
x=scenario_names, | |
y=diffs, | |
name=display_name, | |
visible="legendonly" | |
)) | |
# Update layout | |
title = f"{load_type.title()} Load Percentage Difference from {baseline_scenario}" | |
y_title = "Percentage Difference (%)" | |
fig.update_layout( | |
title=title, | |
xaxis_title="Scenario", | |
yaxis_title=y_title, | |
barmode="group", | |
height=500, | |
legend=dict( | |
orientation="h", | |
yanchor="bottom", | |
y=1.02, | |
xanchor="right", | |
x=1 | |
) | |
) | |
# Add zero line | |
fig.add_shape( | |
type="line", | |
x0=-0.5, | |
x1=len(scenario_names) - 0.5, | |
y0=0, | |
y1=0, | |
line=dict( | |
color="black", | |
width=1, | |
dash="dash" | |
) | |
) | |
return fig | |
def create_radar_chart(scenarios: Dict[str, Dict[str, Any]], load_type: str = "cooling") -> go.Figure: | |
""" | |
Create a radar chart comparing key metrics across scenarios. | |
Args: | |
scenarios: Dictionary with scenario data | |
load_type: Type of load to compare ("cooling" or "heating") | |
Returns: | |
Plotly figure with radar chart | |
""" | |
# Define metrics based on load type | |
if load_type == "cooling": | |
metrics = [ | |
"total", | |
"total_sensible", | |
"total_latent", | |
"walls", | |
"roofs", | |
"windows_conduction", | |
"windows_solar", | |
"infiltration_sensible", | |
"people_sensible", | |
"lights", | |
"equipment_sensible" | |
] | |
metric_names = [ | |
"Total Load", | |
"Sensible Load", | |
"Latent Load", | |
"Walls", | |
"Roofs", | |
"Windows (Conduction)", | |
"Windows (Solar)", | |
"Infiltration", | |
"People", | |
"Lights", | |
"Equipment" | |
] | |
else: # heating | |
metrics = [ | |
"total", | |
"walls", | |
"roofs", | |
"floors", | |
"windows", | |
"doors", | |
"infiltration_sensible", | |
"ventilation_sensible" | |
] | |
metric_names = [ | |
"Total Load", | |
"Walls", | |
"Roofs", | |
"Floors", | |
"Windows", | |
"Doors", | |
"Infiltration", | |
"Ventilation" | |
] | |
# Initialize figure | |
fig = go.Figure() | |
# Process scenarios | |
for scenario_name, scenario_data in scenarios.items(): | |
# Extract loads based on load type | |
if load_type == "cooling": | |
loads = scenario_data.get("cooling_loads", {}) | |
else: # heating | |
loads = scenario_data.get("heating_loads", {}) | |
# Extract metric values | |
values = [loads.get(metric, 0) for metric in metrics] | |
# Add trace | |
fig.add_trace(go.Scatterpolar( | |
r=values, | |
theta=metric_names, | |
fill="toself", | |
name=scenario_name | |
)) | |
# Update layout | |
title = f"{load_type.title()} Load Comparison (Radar Chart)" | |
fig.update_layout( | |
title=title, | |
polar=dict( | |
radialaxis=dict( | |
visible=True, | |
range=[0, max([max([scenarios[s].get(f"{load_type}_loads", {}).get(m, 0) for m in metrics]) for s in scenarios]) * 1.1] | |
) | |
), | |
height=600, | |
showlegend=True | |
) | |
return fig | |
def create_parallel_coordinates_chart(scenarios: Dict[str, Dict[str, Any]]) -> go.Figure: | |
""" | |
Create a parallel coordinates chart comparing scenarios. | |
Args: | |
scenarios: Dictionary with scenario data | |
Returns: | |
Plotly figure with parallel coordinates chart | |
""" | |
# Initialize data | |
data = [] | |
# Process scenarios | |
for scenario_name, scenario_data in scenarios.items(): | |
# Extract cooling and heating loads | |
cooling_loads = scenario_data.get("cooling_loads", {}) | |
heating_loads = scenario_data.get("heating_loads", {}) | |
# Create data point | |
point = { | |
"Scenario": scenario_name, | |
"Cooling Load (W)": cooling_loads.get("total", 0), | |
"Heating Load (W)": heating_loads.get("total", 0), | |
"Sensible Heat Ratio": cooling_loads.get("sensible_heat_ratio", 0), | |
"Walls (Cooling)": cooling_loads.get("walls", 0), | |
"Windows (Cooling)": cooling_loads.get("windows_conduction", 0) + cooling_loads.get("windows_solar", 0), | |
"Internal Gains (Cooling)": cooling_loads.get("people_sensible", 0) + cooling_loads.get("lights", 0) + cooling_loads.get("equipment_sensible", 0), | |
"Walls (Heating)": heating_loads.get("walls", 0), | |
"Windows (Heating)": heating_loads.get("windows", 0), | |
"Infiltration (Heating)": heating_loads.get("infiltration_sensible", 0) | |
} | |
# Add to data | |
data.append(point) | |
# Create DataFrame | |
df = pd.DataFrame(data) | |
# Create figure | |
fig = px.parallel_coordinates( | |
df, | |
color="Cooling Load (W)", | |
labels={ | |
"Scenario": "Scenario", | |
"Cooling Load (W)": "Cooling Load (W)", | |
"Heating Load (W)": "Heating Load (W)", | |
"Sensible Heat Ratio": "Sensible Heat Ratio", | |
"Walls (Cooling)": "Walls (Cooling)", | |
"Windows (Cooling)": "Windows (Cooling)", | |
"Internal Gains (Cooling)": "Internal Gains (Cooling)", | |
"Walls (Heating)": "Walls (Heating)", | |
"Windows (Heating)": "Windows (Heating)", | |
"Infiltration (Heating)": "Infiltration (Heating)" | |
}, | |
color_continuous_scale=px.colors.sequential.Viridis | |
) | |
# Update layout | |
fig.update_layout( | |
title="Scenario Comparison (Parallel Coordinates)", | |
height=600 | |
) | |
return fig | |
def display_scenario_comparison(scenarios: Dict[str, Dict[str, Any]]) -> None: | |
""" | |
Display scenario comparison visualization in Streamlit. | |
Args: | |
scenarios: Dictionary with scenario data | |
""" | |
st.header("Scenario Comparison Visualization") | |
# Check if scenarios exist | |
if not scenarios: | |
st.warning("No scenarios available for comparison.") | |
return | |
# Create tabs for different visualizations | |
tab1, tab2, tab3, tab4, tab5 = st.tabs([ | |
"Scenario Summary", | |
"Load Comparison", | |
"Percentage Difference", | |
"Radar Chart", | |
"Parallel Coordinates" | |
]) | |
with tab1: | |
st.subheader("Scenario Summary") | |
df = ScenarioComparisonVisualization.create_scenario_summary_table(scenarios) | |
st.dataframe(df, use_container_width=True) | |
# Add download button for CSV | |
csv = df.to_csv(index=False).encode('utf-8') | |
st.download_button( | |
label="Download Scenario Summary as CSV", | |
data=csv, | |
file_name="scenario_summary.csv", | |
mime="text/csv" | |
) | |
with tab2: | |
st.subheader("Load Comparison") | |
# Add load type selector | |
load_type = st.radio( | |
"Select Load Type", | |
["cooling", "heating"], | |
horizontal=True, | |
key="load_comparison_type" | |
) | |
# Create and display chart | |
fig = ScenarioComparisonVisualization.create_load_comparison_chart(scenarios, load_type) | |
st.plotly_chart(fig, use_container_width=True) | |
with tab3: | |
st.subheader("Percentage Difference") | |
# Add baseline scenario selector | |
baseline_scenario = st.selectbox( | |
"Select Baseline Scenario", | |
list(scenarios.keys()), | |
key="baseline_scenario" | |
) | |
# Add load type selector | |
load_type = st.radio( | |
"Select Load Type", | |
["cooling", "heating"], | |
horizontal=True, | |
key="percentage_diff_type" | |
) | |
# Create and display chart | |
try: | |
fig = ScenarioComparisonVisualization.create_percentage_difference_chart( | |
scenarios, baseline_scenario, load_type | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
except ValueError as e: | |
st.error(str(e)) | |
with tab4: | |
st.subheader("Radar Chart") | |
# Add load type selector | |
load_type = st.radio( | |
"Select Load Type", | |
["cooling", "heating"], | |
horizontal=True, | |
key="radar_chart_type" | |
) | |
# Create and display chart | |
fig = ScenarioComparisonVisualization.create_radar_chart(scenarios, load_type) | |
st.plotly_chart(fig, use_container_width=True) | |
with tab5: | |
st.subheader("Parallel Coordinates") | |
# Create and display chart | |
fig = ScenarioComparisonVisualization.create_parallel_coordinates_chart(scenarios) | |
st.plotly_chart(fig, use_container_width=True) | |
# Create a singleton instance | |
scenario_comparison = ScenarioComparisonVisualization() | |
# Example usage | |
if __name__ == "__main__": | |
import streamlit as st | |
# Create sample scenarios | |
scenarios = { | |
"Base Case": { | |
"cooling_loads": { | |
"total": 5000, | |
"total_sensible": 4000, | |
"total_latent": 1000, | |
"sensible_heat_ratio": 0.8, | |
"walls": 1000, | |
"roofs": 800, | |
"floors": 200, | |
"windows_conduction": 500, | |
"windows_solar": 800, | |
"doors": 100, | |
"infiltration_sensible": 300, | |
"infiltration_latent": 200, | |
"people_sensible": 300, | |
"people_latent": 200, | |
"lights": 400, | |
"equipment_sensible": 400, | |
"equipment_latent": 600 | |
}, | |
"heating_loads": { | |
"total": 6000, | |
"walls": 1500, | |
"roofs": 1000, | |
"floors": 500, | |
"windows": 1200, | |
"doors": 200, | |
"infiltration_sensible": 800, | |
"infiltration_latent": 0, | |
"ventilation_sensible": 800, | |
"ventilation_latent": 0, | |
"internal_gains_offset": 1000 | |
} | |
}, | |
"Improved Insulation": { | |
"cooling_loads": { | |
"total": 4200, | |
"total_sensible": 3500, | |
"total_latent": 700, | |
"sensible_heat_ratio": 0.83, | |
"walls": 600, | |
"roofs": 500, | |
"floors": 150, | |
"windows_conduction": 500, | |
"windows_solar": 800, | |
"doors": 100, | |
"infiltration_sensible": 300, | |
"infiltration_latent": 200, | |
"people_sensible": 300, | |
"people_latent": 200, | |
"lights": 400, | |
"equipment_sensible": 400, | |
"equipment_latent": 300 | |
}, | |
"heating_loads": { | |
"total": 4500, | |
"walls": 900, | |
"roofs": 600, | |
"floors": 300, | |
"windows": 1200, | |
"doors": 200, | |
"infiltration_sensible": 800, | |
"infiltration_latent": 0, | |
"ventilation_sensible": 800, | |
"ventilation_latent": 0, | |
"internal_gains_offset": 1000 | |
} | |
}, | |
"Better Windows": { | |
"cooling_loads": { | |
"total": 4000, | |
"total_sensible": 3300, | |
"total_latent": 700, | |
"sensible_heat_ratio": 0.83, | |
"walls": 1000, | |
"roofs": 800, | |
"floors": 200, | |
"windows_conduction": 250, | |
"windows_solar": 400, | |
"doors": 100, | |
"infiltration_sensible": 300, | |
"infiltration_latent": 200, | |
"people_sensible": 300, | |
"people_latent": 200, | |
"lights": 400, | |
"equipment_sensible": 400, | |
"equipment_latent": 300 | |
}, | |
"heating_loads": { | |
"total": 5000, | |
"walls": 1500, | |
"roofs": 1000, | |
"floors": 500, | |
"windows": 600, | |
"doors": 200, | |
"infiltration_sensible": 800, | |
"infiltration_latent": 0, | |
"ventilation_sensible": 800, | |
"ventilation_latent": 0, | |
"internal_gains_offset": 1000 | |
} | |
} | |
} | |
# Display scenario comparison | |
scenario_comparison.display_scenario_comparison(scenarios) | |