|
|
""" |
|
|
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 |
|
|
|
|
|
|
|
|
from utils.cooling_load import CoolingLoad |
|
|
from utils.heating_load import HeatingLoad |
|
|
|
|
|
|
|
|
class ScenarioComparison: |
|
|
"""Class for scenario comparison visualization.""" |
|
|
|
|
|
@staticmethod |
|
|
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 |
|
|
""" |
|
|
|
|
|
data = [] |
|
|
|
|
|
|
|
|
for scenario_name, scenario_data in scenarios.items(): |
|
|
|
|
|
cooling_loads = scenario_data.get("cooling_loads", {}) |
|
|
heating_loads = scenario_data.get("heating_loads", {}) |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
|
|
|
|
|
|
data.append(row) |
|
|
|
|
|
|
|
|
df = pd.DataFrame(data) |
|
|
|
|
|
return df |
|
|
|
|
|
@staticmethod |
|
|
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 |
|
|
""" |
|
|
|
|
|
scenario_names = [] |
|
|
total_loads = [] |
|
|
component_loads = {} |
|
|
|
|
|
|
|
|
for scenario_name, scenario_data in scenarios.items(): |
|
|
|
|
|
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: |
|
|
loads = scenario_data.get("heating_loads", {}) |
|
|
components = ["walls", "roofs", "floors", "windows", "doors", |
|
|
"infiltration_sensible", "infiltration_latent", |
|
|
"ventilation_sensible", "ventilation_latent"] |
|
|
|
|
|
|
|
|
scenario_names.append(scenario_name) |
|
|
|
|
|
|
|
|
total_loads.append(loads.get("total", 0)) |
|
|
|
|
|
|
|
|
for component in components: |
|
|
if component not in component_loads: |
|
|
component_loads[component] = [] |
|
|
|
|
|
component_loads[component].append(loads.get(component, 0)) |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
|
|
|
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 |
|
|
)) |
|
|
|
|
|
|
|
|
for component, loads in component_loads.items(): |
|
|
|
|
|
if sum(loads) == 0: |
|
|
continue |
|
|
|
|
|
|
|
|
display_name = component.replace("_", " ").title() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
x=scenario_names, |
|
|
y=loads, |
|
|
name=display_name, |
|
|
visible="legendonly" |
|
|
)) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
@staticmethod |
|
|
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 |
|
|
""" |
|
|
|
|
|
if baseline_scenario not in scenarios: |
|
|
raise ValueError(f"Baseline scenario '{baseline_scenario}' not found in scenarios") |
|
|
|
|
|
|
|
|
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: |
|
|
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) |
|
|
|
|
|
|
|
|
scenario_names = [] |
|
|
percentage_diffs = [] |
|
|
component_diffs = {} |
|
|
|
|
|
|
|
|
for scenario_name, scenario_data in scenarios.items(): |
|
|
if scenario_name == baseline_scenario: |
|
|
continue |
|
|
|
|
|
|
|
|
if load_type == "cooling": |
|
|
loads = scenario_data.get("cooling_loads", {}) |
|
|
else: |
|
|
loads = scenario_data.get("heating_loads", {}) |
|
|
|
|
|
|
|
|
scenario_names.append(scenario_name) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
|
|
|
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 |
|
|
)) |
|
|
|
|
|
|
|
|
for component, diffs in component_diffs.items(): |
|
|
|
|
|
if sum([abs(diff) for diff in diffs]) == 0: |
|
|
continue |
|
|
|
|
|
|
|
|
display_name = component.replace("_", " ").title() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
x=scenario_names, |
|
|
y=diffs, |
|
|
name=display_name, |
|
|
visible="legendonly" |
|
|
)) |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
@staticmethod |
|
|
def create_radar_chart(scenarios: Dict[str, Dict[str, Any]], |
|
|
metrics: List[str], |
|
|
load_type: str = "cooling") -> go.Figure: |
|
|
""" |
|
|
Create a radar chart comparing multiple metrics across scenarios. |
|
|
|
|
|
Args: |
|
|
scenarios: Dictionary with scenario data |
|
|
metrics: List of metrics to compare |
|
|
load_type: Type of load to compare ("cooling" or "heating") |
|
|
|
|
|
Returns: |
|
|
Plotly figure with radar chart |
|
|
""" |
|
|
|
|
|
data = [] |
|
|
|
|
|
|
|
|
for scenario_name, scenario_data in scenarios.items(): |
|
|
|
|
|
if load_type == "cooling": |
|
|
loads = scenario_data.get("cooling_loads", {}) |
|
|
else: |
|
|
loads = scenario_data.get("heating_loads", {}) |
|
|
|
|
|
|
|
|
values = [] |
|
|
for metric in metrics: |
|
|
values.append(loads.get(metric, 0)) |
|
|
|
|
|
|
|
|
data.append(go.Scatterpolar( |
|
|
r=values, |
|
|
theta=metrics, |
|
|
fill="toself", |
|
|
name=scenario_name |
|
|
)) |
|
|
|
|
|
|
|
|
fig = go.Figure(data=data) |
|
|
|
|
|
|
|
|
title = f"{load_type.title()} Load Metrics Comparison" |
|
|
|
|
|
fig.update_layout( |
|
|
title=title, |
|
|
polar=dict( |
|
|
radialaxis=dict( |
|
|
visible=True, |
|
|
range=[0, max([max(trace["r"]) for trace in data]) * 1.1] |
|
|
) |
|
|
), |
|
|
showlegend=True, |
|
|
height=600 |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
@staticmethod |
|
|
def create_parallel_coordinates_chart(scenarios: Dict[str, Dict[str, Any]], |
|
|
metrics: List[str], |
|
|
load_type: str = "cooling") -> go.Figure: |
|
|
""" |
|
|
Create a parallel coordinates chart comparing multiple metrics across scenarios. |
|
|
|
|
|
Args: |
|
|
scenarios: Dictionary with scenario data |
|
|
metrics: List of metrics to compare |
|
|
load_type: Type of load to compare ("cooling" or "heating") |
|
|
|
|
|
Returns: |
|
|
Plotly figure with parallel coordinates chart |
|
|
""" |
|
|
|
|
|
data = [] |
|
|
|
|
|
|
|
|
for scenario_name, scenario_data in scenarios.items(): |
|
|
|
|
|
if load_type == "cooling": |
|
|
loads = scenario_data.get("cooling_loads", {}) |
|
|
else: |
|
|
loads = scenario_data.get("heating_loads", {}) |
|
|
|
|
|
|
|
|
row = {"Scenario": scenario_name} |
|
|
|
|
|
|
|
|
for metric in metrics: |
|
|
|
|
|
display_name = metric.replace("_", " ").title() |
|
|
row[display_name] = loads.get(metric, 0) |
|
|
|
|
|
|
|
|
data.append(row) |
|
|
|
|
|
|
|
|
df = pd.DataFrame(data) |
|
|
|
|
|
|
|
|
fig = px.parallel_coordinates( |
|
|
df, |
|
|
color="Scenario", |
|
|
dimensions=[col for col in df.columns if col != "Scenario"], |
|
|
title=f"{load_type.title()} Load Metrics Comparison" |
|
|
) |
|
|
|
|
|
|
|
|
fig.update_layout( |
|
|
height=600 |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
@staticmethod |
|
|
def calculate_scenario_differences(scenarios: Dict[str, Dict[str, Any]], |
|
|
baseline_scenario: str, |
|
|
load_type: str = "cooling") -> pd.DataFrame: |
|
|
""" |
|
|
Calculate differences between scenarios and 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: |
|
|
DataFrame with scenario differences |
|
|
""" |
|
|
|
|
|
if baseline_scenario not in scenarios: |
|
|
raise ValueError(f"Baseline scenario '{baseline_scenario}' not found in scenarios") |
|
|
|
|
|
|
|
|
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: |
|
|
baseline_loads = scenarios[baseline_scenario].get("heating_loads", {}) |
|
|
components = ["walls", "roofs", "floors", "windows", "doors", |
|
|
"infiltration_sensible", "infiltration_latent", |
|
|
"ventilation_sensible", "ventilation_latent"] |
|
|
|
|
|
|
|
|
data = [] |
|
|
|
|
|
|
|
|
for scenario_name, scenario_data in scenarios.items(): |
|
|
if scenario_name == baseline_scenario: |
|
|
continue |
|
|
|
|
|
|
|
|
if load_type == "cooling": |
|
|
loads = scenario_data.get("cooling_loads", {}) |
|
|
else: |
|
|
loads = scenario_data.get("heating_loads", {}) |
|
|
|
|
|
|
|
|
row = {"Scenario": scenario_name} |
|
|
|
|
|
|
|
|
for component in components: |
|
|
baseline_value = baseline_loads.get(component, 0) |
|
|
scenario_value = loads.get(component, 0) |
|
|
|
|
|
|
|
|
display_name = component.replace("_", " ").title() |
|
|
|
|
|
|
|
|
row[f"{display_name} (W)"] = scenario_value - baseline_value |
|
|
|
|
|
|
|
|
if baseline_value != 0: |
|
|
row[f"{display_name} (%)"] = (scenario_value - baseline_value) / baseline_value * 100 |
|
|
else: |
|
|
row[f"{display_name} (%)"] = 0 |
|
|
|
|
|
|
|
|
baseline_total = baseline_loads.get("total", 0) |
|
|
scenario_total = loads.get("total", 0) |
|
|
|
|
|
row["Total (W)"] = scenario_total - baseline_total |
|
|
|
|
|
if baseline_total != 0: |
|
|
row["Total (%)"] = (scenario_total - baseline_total) / baseline_total * 100 |
|
|
else: |
|
|
row["Total (%)"] = 0 |
|
|
|
|
|
|
|
|
data.append(row) |
|
|
|
|
|
|
|
|
df = pd.DataFrame(data) |
|
|
|
|
|
return df |
|
|
|