BuildingBench commited on
Commit
d7f7deb
Β·
verified Β·
1 Parent(s): d80611a

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +1395 -0
streamlit_app.py ADDED
@@ -0,0 +1,1395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def create_dataset_statistics(buildings_df: pd.DataFrame, weather_df: pd.DataFrame, combinations_df: pd.DataFrame):
2
+ """Create comprehensive dataset statistics section"""
3
+ st.subheader("πŸ“Š Dataset Statistics & Information")
4
+
5
+ # Dataset description
6
+ st.markdown("""
7
+ <div style="background-color: #1e1e1e; border-radius: 10px; padding: 20px; margin: 20px 0;
8
+ border-left: 4px solid #2196f3; color: white;">
9
+ <h4 style="color: #2196f3; margin-bottom: 15px;">πŸ—οΈ About This Dataset</h4>
10
+ <p style="font-size: 1.1em; line-height: 1.6; margin-bottom: 10px;">
11
+ This comprehensive building energy dataset contains energy models for various building types across different climate zones.
12
+ The dataset is designed for energy simulation research, building performance analysis, and climate impact studies.
13
+ </p>
14
+ <p style="font-size: 1.1em; line-height: 1.6; margin-bottom: 10px;">
15
+ Each building model includes detailed geometric properties, construction materials, HVAC systems, and occupancy schedules.
16
+ Multiple variations are generated from base models to study the impact of different parameters on energy performance.
17
+ </p>
18
+ <p style="font-size: 1.1em; line-height: 1.6;">
19
+ Weather data is sourced from global meteorological stations and covers multiple climate zones as defined by ASHRAE standards,
20
+ enabling comprehensive climate-specific energy analysis.
21
+ </p>
22
+ </div>
23
+ """, unsafe_allow_html=True)
24
+
25
+ # Detailed statistics
26
+ col1, col2 = st.columns(2)
27
+
28
+ with col1:
29
+ st.subheader("🏒 Building Dataset Details")
30
+
31
+ if not buildings_df.empty:
32
+ # Building type breakdown
33
+ building_types = buildings_df['building_type'].value_counts()
34
+ st.markdown("**Building Types Distribution:**")
35
+ for btype, count in building_types.items():
36
+ percentage = (count / len(buildings_df)) * 100
37
+ st.write(f"β€’ **{btype.title()}**: {count} models ({percentage:.1f}%)")
38
+
39
+ st.markdown("---")
40
+
41
+ # Variation breakdown
42
+ variation_types = buildings_df['variation_type'].value_counts()
43
+ st.markdown("**Variation Types:**")
44
+ for var_type, count in variation_types.items():
45
+ percentage = (count / len(buildings_df)) * 100
46
+ st.write(f"β€’ **{var_type.title()}**: {count} models ({percentage:.1f}%)")
47
+
48
+ st.markdown("---")
49
+
50
+ # Climate zone coverage
51
+ climate_zones = buildings_df['climate_zone'].value_counts().sort_index()
52
+ st.markdown("**Climate Zone Coverage:**")
53
+ for zone, count in climate_zones.items():
54
+ st.write(f"β€’ **Zone {zone}**: {count} buildings")
55
+ else:
56
+ st.warning("No building data available")
57
+
58
+ with col2:
59
+ st.subheader("🌍 Weather Dataset Details")
60
+
61
+ if not weather_df.empty:
62
+ # Geographic coverage
63
+ st.markdown("**Geographic Coverage:**")
64
+ st.write(f"β€’ **Total Locations**: {len(weather_df)}")
65
+ st.write(f"β€’ **Countries Covered**: {weather_df['country'].nunique()}")
66
+ st.write(f"β€’ **Climate Zones**: {weather_df['climate_zone_code'].nunique()}")
67
+
68
+ # Climate zone distribution in weather data
69
+ weather_climate_zones = weather_df['climate_zone_code'].value_counts().sort_index()
70
+ st.markdown("**Weather Locations by Climate Zone:**")
71
+ for zone, count in weather_climate_zones.head(10).items():
72
+ st.write(f"β€’ **Zone {zone}**: {count} locations")
73
+
74
+ st.markdown("---")
75
+
76
+ # Top countries by location count
77
+ top_countries = weather_df['country'].value_counts().head(8)
78
+ st.markdown("**Top Countries by Weather Locations:**")
79
+ for country, count in top_countries.items():
80
+ st.write(f"β€’ **{country}**: {count} locations")
81
+
82
+ # Data sources if available
83
+ if 'data_source' in weather_df.columns:
84
+ st.markdown("---")
85
+ data_sources = weather_df['data_source'].value_counts()
86
+ st.markdown("**Data Sources:**")
87
+ for source, count in data_sources.items():
88
+ st.write(f"β€’ **{source}**: {count} files")
89
+ else:
90
+ st.warning("No weather data available")
91
+
92
+ # Dataset quality metrics
93
+ st.subheader("🎯 Dataset Quality Metrics")
94
+
95
+ quality_col1, quality_col2, quality_col3, quality_col4 = st.columns(4)
96
+
97
+ with quality_col1:
98
+ completeness = 0
99
+ if not buildings_df.empty:
100
+ total_fields = len(buildings_df.columns)
101
+ missing_fields = buildings_df.isnull().sum().sum()
102
+ total_possible = len(buildings_df) * total_fields
103
+ completeness = ((total_possible - missing_fields) / total_possible) * 100 if total_possible > 0 else 0
104
+
105
+ st.markdown(f"""
106
+ <div style="background-color: #2d2d2d; border-radius: 10px; padding: 15px; text-align: center;
107
+ border: 1px solid #404040; color: white;">
108
+ <div style="font-size: 1.5em; margin-bottom: 5px;">πŸ“ˆ</div>
109
+ <div style="font-size: 1.8em; font-weight: bold; color: #4caf50;">{completeness:.1f}%</div>
110
+ <div style="font-size: 0.9em; opacity: 0.8;">Data Completeness</div>
111
+ </div>
112
+ """, unsafe_allow_html=True)
113
+
114
+ with quality_col2:
115
+ file_coverage = 0
116
+ if not buildings_df.empty and 'filepath' in buildings_df.columns:
117
+ existing_files = 0
118
+ for _, row in buildings_df.iterrows():
119
+ filepath = Path("data") / row['filepath']
120
+ if filepath.exists():
121
+ existing_files += 1
122
+ file_coverage = (existing_files / len(buildings_df)) * 100
123
+
124
+ st.markdown(f"""
125
+ <div style="background-color: #2d2d2d; border-radius: 10px; padding: 15px; text-align: center;
126
+ border: 1px solid #404040; color: white;">
127
+ <div style="font-size: 1.5em; margin-bottom: 5px;">πŸ“</div>
128
+ <div style="font-size: 1.8em; font-weight: bold; color: #2196f3;">{file_coverage:.1f}%</div>
129
+ <div style="font-size: 0.9em; opacity: 0.8;">File Availability</div>
130
+ </div>
131
+ """, unsafe_allow_html=True)
132
+
133
+ with quality_col3:
134
+ diversity_score = 0
135
+ if not buildings_df.empty:
136
+ type_entropy = len(buildings_df['building_type'].unique()) / len(buildings_df) * 100
137
+ climate_entropy = len(buildings_df['climate_zone'].unique()) / len(buildings_df) * 100
138
+ diversity_score = (type_entropy + climate_entropy) / 2
139
+
140
+ st.markdown(f"""
141
+ <div style="background-color: #2d2d2d; border-radius: 10px; padding: 15px; text-align: center;
142
+ border: 1px solid #404040; color: white;">
143
+ <div style="font-size: 1.5em; margin-bottom: 5px;">🎨</div>
144
+ <div style="font-size: 1.8em; font-weight: bold; color: #ff9800;">{diversity_score:.1f}%</div>
145
+ <div style="font-size: 0.9em; opacity: 0.8;">Dataset Diversity</div>
146
+ </div>
147
+ """, unsafe_allow_html=True)
148
+
149
+ with quality_col4:
150
+ simulation_readiness = 0
151
+ if not combinations_df.empty:
152
+ simulation_readiness = 100
153
+ elif not buildings_df.empty and not weather_df.empty:
154
+ simulation_readiness = 75
155
+ elif not buildings_df.empty or not weather_df.empty:
156
+ simulation_readiness = 50
157
+
158
+ st.markdown(f"""
159
+ <div style="background-color: #2d2d2d; border-radius: 10px; padding: 15px; text-align: center;
160
+ border: 1px solid #404040; color: white;">
161
+ <div style="font-size: 1.5em; margin-bottom: 5px;">⚑</div>
162
+ <div style="font-size: 1.8em; font-weight: bold; color: #9c27b0;">{simulation_readiness}%</div>
163
+ <div style="font-size: 0.9em; opacity: 0.8;">Simulation Ready</div>
164
+ </div>
165
+ """, unsafe_allow_html=True)
166
+
167
+ # Usage recommendations
168
+ st.subheader("πŸ’‘ Usage Recommendations")
169
+
170
+ recommendation_col1, recommendation_col2 = st.columns(2)
171
+
172
+ with recommendation_col1:
173
+ st.markdown("""
174
+ <div style="background-color: #1a237e; border-radius: 10px; padding: 20px; margin: 10px 0;
175
+ border-left: 4px solid #3f51b5; color: white;">
176
+ <h5 style="color: #64b5f6; margin-bottom: 15px;">πŸ”¬ Research Applications</h5>
177
+ <ul style="line-height: 1.8;">
178
+ <li>Building energy performance analysis</li>
179
+ <li>Climate change impact studies</li>
180
+ <li>HVAC system optimization</li>
181
+ <li>Retrofit strategy evaluation</li>
182
+ <li>Code compliance verification</li>
183
+ </ul>
184
+ </div>
185
+ """, unsafe_allow_html=True)
186
+
187
+ with recommendation_col2:
188
+ st.markdown("""
189
+ <div style="background-color: #1b5e20; border-radius: 10px; padding: 20px; margin: 10px 0;
190
+ border-left: 4px solid #4caf50; color: white;">
191
+ <h5 style="color: #81c784; margin-bottom: 15px;">βš™οΈ Getting Started</h5>
192
+ <ul style="line-height: 1.8;">
193
+ <li>Use <strong>Building Explorer</strong> to browse models</li>
194
+ <li>Check <strong>Weather Data</strong> for climate coverage</li>
195
+ <li>Generate combinations for simulations</li>
196
+ <li>Export filtered datasets for analysis</li>
197
+ <li>Run quality checks before processing</li>
198
+ </ul>
199
+ </div>
200
+ """, unsafe_allow_html=True)# dashboard/streamlit_app.py
201
+ """
202
+ Building Generator Dashboard - Main Streamlit Application
203
+ Interactive web interface for exploring building energy models and weather data
204
+ """
205
+
206
+ import streamlit as st
207
+ import pandas as pd
208
+ import plotly.express as px
209
+ import plotly.graph_objects as go
210
+ from plotly.subplots import make_subplots
211
+ import numpy as np
212
+ from pathlib import Path
213
+ import sys
214
+ import json
215
+ from typing import Dict, List, Optional
216
+ import logging
217
+
218
+ # Add the project root to Python path
219
+ PROJECT_ROOT = Path(__file__).parent.parent
220
+ sys.path.insert(0, str(PROJECT_ROOT))
221
+
222
+ from building_gen.core.pipeline import BuildingPipeline
223
+
224
+ # Configure page
225
+ st.set_page_config(
226
+ page_title="Building Generator Dashboard",
227
+ page_icon="πŸ—οΈ",
228
+ layout="wide",
229
+ initial_sidebar_state="expanded"
230
+ )
231
+
232
+ # Custom CSS for dark theme styling
233
+ st.markdown("""
234
+ <style>
235
+ .main > div {
236
+ padding-top: 2rem;
237
+ }
238
+
239
+ /* Dark theme metric cards */
240
+ .stMetric {
241
+ background-color: #1e1e1e;
242
+ border: 1px solid #333;
243
+ border-radius: 10px;
244
+ padding: 15px;
245
+ margin: 5px 0;
246
+ color: white;
247
+ }
248
+
249
+ /* Dark theme filter container */
250
+ .filter-container {
251
+ background-color: #2d2d2d;
252
+ border: 1px solid #404040;
253
+ border-radius: 10px;
254
+ padding: 15px;
255
+ margin: 10px 0;
256
+ color: white;
257
+ }
258
+
259
+ /* Dark theme building cards */
260
+ .building-card {
261
+ border: 2px solid #404040;
262
+ border-radius: 10px;
263
+ padding: 15px;
264
+ margin: 10px 0;
265
+ background-color: #1e1e1e;
266
+ color: white;
267
+ }
268
+
269
+ /* Dark theme comparison highlight */
270
+ .comparison-highlight {
271
+ background-color: #1a237e;
272
+ border-left: 4px solid #3f51b5;
273
+ border-radius: 5px;
274
+ padding: 15px;
275
+ margin: 5px 0;
276
+ color: white;
277
+ }
278
+
279
+ /* Plotly chart dark theme */
280
+ .js-plotly-plot {
281
+ background-color: transparent !important;
282
+ }
283
+
284
+ /* Data editor dark theme */
285
+ .stDataFrame {
286
+ background-color: #1e1e1e;
287
+ }
288
+
289
+ /* Sidebar dark theme adjustments */
290
+ .css-1d391kg {
291
+ background-color: #1e1e1e;
292
+ }
293
+
294
+ /* Success/Info/Warning message styling */
295
+ .stSuccess {
296
+ background-color: #1b5e20;
297
+ border: 1px solid #4caf50;
298
+ }
299
+
300
+ .stInfo {
301
+ background-color: #0d47a1;
302
+ border: 1px solid #2196f3;
303
+ }
304
+
305
+ .stWarning {
306
+ background-color: #e65100;
307
+ border: 1px solid #ff9800;
308
+ }
309
+
310
+ .stError {
311
+ background-color: #b71c1c;
312
+ border: 1px solid #f44336;
313
+ }
314
+ </style>
315
+ """, unsafe_allow_html=True)
316
+
317
+ @st.cache_data
318
+ def load_pipeline_data(data_dir: str = "data"):
319
+ """Load and cache pipeline data"""
320
+ try:
321
+ pipeline = BuildingPipeline(data_dir)
322
+
323
+ # Load building data
324
+ buildings_path = Path(data_dir) / "tables/buildings.csv"
325
+ buildings_df = pd.read_csv(buildings_path) if buildings_path.exists() else pd.DataFrame()
326
+
327
+ # Load weather data
328
+ weather_path = Path(data_dir) / "weather/tables/all_weather.csv"
329
+ weather_df = pd.read_csv(weather_path) if weather_path.exists() else pd.DataFrame()
330
+
331
+ # Load combinations if available
332
+ combinations_path = Path(data_dir) / "tables/building_weather_combinations.csv"
333
+ combinations_df = pd.read_csv(combinations_path) if combinations_path.exists() else pd.DataFrame()
334
+
335
+ return pipeline, buildings_df, weather_df, combinations_df
336
+ except Exception as e:
337
+ st.error(f"Failed to load data: {e}")
338
+ return None, pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
339
+
340
+ def create_building_filters(buildings_df: pd.DataFrame) -> Dict:
341
+ """Create filter widgets for buildings"""
342
+ st.markdown('<div class="filter-container">', unsafe_allow_html=True)
343
+ st.subheader("πŸ” Filter Buildings")
344
+
345
+ col1, col2, col3 = st.columns(3)
346
+
347
+ with col1:
348
+ building_types = st.multiselect(
349
+ "Building Type",
350
+ options=sorted(buildings_df['building_type'].unique()) if not buildings_df.empty else [],
351
+ default=[]
352
+ )
353
+
354
+ climate_zones = st.multiselect(
355
+ "Climate Zone",
356
+ options=sorted(buildings_df['climate_zone'].unique()) if not buildings_df.empty else [],
357
+ default=[]
358
+ )
359
+
360
+ with col2:
361
+ variation_types = st.multiselect(
362
+ "Variation Type",
363
+ options=sorted(buildings_df['variation_type'].unique()) if not buildings_df.empty else [],
364
+ default=[]
365
+ )
366
+
367
+ # Floor area range
368
+ if not buildings_df.empty and 'floor_area' in buildings_df.columns:
369
+ min_area = float(buildings_df['floor_area'].min())
370
+ max_area = float(buildings_df['floor_area'].max())
371
+ area_range = st.slider(
372
+ "Floor Area Range (mΒ²)",
373
+ min_value=min_area,
374
+ max_value=max_area,
375
+ value=(min_area, max_area),
376
+ format="%.0f"
377
+ )
378
+ else:
379
+ area_range = (0, 10000)
380
+
381
+ with col3:
382
+ # Window-to-wall ratio if available
383
+ if not buildings_df.empty and 'window_wall_ratio' in buildings_df.columns:
384
+ min_wwr = float(buildings_df['window_wall_ratio'].min())
385
+ max_wwr = float(buildings_df['window_wall_ratio'].max())
386
+ wwr_range = st.slider(
387
+ "Window-to-Wall Ratio",
388
+ min_value=min_wwr,
389
+ max_value=max_wwr,
390
+ value=(min_wwr, max_wwr),
391
+ format="%.2f"
392
+ )
393
+ else:
394
+ wwr_range = (0.0, 1.0)
395
+
396
+ # Number of zones range
397
+ if not buildings_df.empty and 'num_zones' in buildings_df.columns:
398
+ min_zones = int(buildings_df['num_zones'].min())
399
+ max_zones = int(buildings_df['num_zones'].max())
400
+ zones_range = st.slider(
401
+ "Number of Zones",
402
+ min_value=min_zones,
403
+ max_value=max_zones,
404
+ value=(min_zones, max_zones)
405
+ )
406
+ else:
407
+ zones_range = (1, 100)
408
+
409
+ st.markdown('</div>', unsafe_allow_html=True)
410
+
411
+ return {
412
+ 'building_types': building_types,
413
+ 'climate_zones': climate_zones,
414
+ 'variation_types': variation_types,
415
+ 'area_range': area_range,
416
+ 'wwr_range': wwr_range,
417
+ 'zones_range': zones_range
418
+ }
419
+
420
+ def apply_building_filters(buildings_df: pd.DataFrame, filters: Dict) -> pd.DataFrame:
421
+ """Apply filters to buildings dataframe"""
422
+ filtered_df = buildings_df.copy()
423
+
424
+ if filters['building_types']:
425
+ filtered_df = filtered_df[filtered_df['building_type'].isin(filters['building_types'])]
426
+
427
+ if filters['climate_zones']:
428
+ filtered_df = filtered_df[filtered_df['climate_zone'].isin(filters['climate_zones'])]
429
+
430
+ if filters['variation_types']:
431
+ filtered_df = filtered_df[filtered_df['variation_type'].isin(filters['variation_types'])]
432
+
433
+ if 'floor_area' in filtered_df.columns:
434
+ filtered_df = filtered_df[
435
+ (filtered_df['floor_area'] >= filters['area_range'][0]) &
436
+ (filtered_df['floor_area'] <= filters['area_range'][1])
437
+ ]
438
+
439
+ if 'window_wall_ratio' in filtered_df.columns:
440
+ filtered_df = filtered_df[
441
+ (filtered_df['window_wall_ratio'] >= filters['wwr_range'][0]) &
442
+ (filtered_df['window_wall_ratio'] <= filters['wwr_range'][1])
443
+ ]
444
+
445
+ if 'num_zones' in filtered_df.columns:
446
+ filtered_df = filtered_df[
447
+ (filtered_df['num_zones'] >= filters['zones_range'][0]) &
448
+ (filtered_df['num_zones'] <= filters['zones_range'][1])
449
+ ]
450
+
451
+ return filtered_df
452
+
453
+ def create_overview_metrics(buildings_df: pd.DataFrame, weather_df: pd.DataFrame, combinations_df: pd.DataFrame):
454
+ """Create overview metrics display with consistent sizing and dark theme"""
455
+ col1, col2, col3, col4 = st.columns(4)
456
+
457
+ with col1:
458
+ st.markdown(f"""
459
+ <div style="background-color: #2d2d2d; border-radius: 15px; padding: 25px; margin: 10px 0;
460
+ border: 1px solid #404040; color: white; height: 200px; display: flex;
461
+ flex-direction: column; justify-content: space-between;">
462
+ <div style="display: flex; align-items: center; margin-bottom: 15px;">
463
+ <span style="font-size: 2em; margin-right: 15px;">🏒</span>
464
+ <span style="font-size: 1.3em; font-weight: bold;">Buildings</span>
465
+ </div>
466
+ <div style="font-size: 3.5em; font-weight: bold; text-align: center; margin: 15px 0;">{len(buildings_df)}</div>
467
+ <div style="font-size: 1em; text-align: center; opacity: 0.8;">
468
+ {len(buildings_df[buildings_df['variation_type'] != 'base']) if not buildings_df.empty else 0} variations
469
+ </div>
470
+ </div>
471
+ """, unsafe_allow_html=True)
472
+
473
+ with col2:
474
+ st.markdown(f"""
475
+ <div style="background-color: #2d2d2d; border-radius: 15px; padding: 25px; margin: 10px 0;
476
+ border: 1px solid #404040; color: white; height: 200px; display: flex;
477
+ flex-direction: column; justify-content: space-between;">
478
+ <div style="display: flex; align-items: center; margin-bottom: 15px;">
479
+ <span style="font-size: 2em; margin-right: 15px;">🌍</span>
480
+ <span style="font-size: 1.3em; font-weight: bold;">Weather Locations</span>
481
+ </div>
482
+ <div style="font-size: 3.5em; font-weight: bold; text-align: center; margin: 15px 0;">{len(weather_df)}</div>
483
+ <div style="font-size: 1em; text-align: center; opacity: 0.8;">
484
+ {weather_df['country'].nunique() if not weather_df.empty else 0} countries
485
+ </div>
486
+ </div>
487
+ """, unsafe_allow_html=True)
488
+
489
+ with col3:
490
+ combinations_status = "Not created" if len(combinations_df) == 0 else "Ready"
491
+ st.markdown(f"""
492
+ <div style="background-color: #2d2d2d; border-radius: 15px; padding: 25px; margin: 10px 0;
493
+ border: 1px solid #404040; color: white; height: 200px; display: flex;
494
+ flex-direction: column; justify-content: space-between;">
495
+ <div style="display: flex; align-items: center; margin-bottom: 15px;">
496
+ <span style="font-size: 2em; margin-right: 15px;">πŸ”„</span>
497
+ <span style="font-size: 1.3em; font-weight: bold;">Combinations</span>
498
+ </div>
499
+ <div style="font-size: 3.5em; font-weight: bold; text-align: center; margin: 15px 0;">{len(combinations_df)}</div>
500
+ <div style="font-size: 1em; text-align: center; opacity: 0.8;">
501
+ {combinations_status}
502
+ </div>
503
+ </div>
504
+ """, unsafe_allow_html=True)
505
+
506
+ with col4:
507
+ climate_zones = buildings_df['climate_zone'].nunique() if not buildings_df.empty else 0
508
+ st.markdown(f"""
509
+ <div style="background-color: #2d2d2d; border-radius: 15px; padding: 25px; margin: 10px 0;
510
+ border: 1px solid #404040; color: white; height: 200px; display: flex;
511
+ flex-direction: column; justify-content: space-between;">
512
+ <div style="display: flex; align-items: center; margin-bottom: 15px;">
513
+ <span style="font-size: 2em; margin-right: 15px;">🌑️</span>
514
+ <span style="font-size: 1.3em; font-weight: bold;">Climate Zones</span>
515
+ </div>
516
+ <div style="font-size: 3.5em; font-weight: bold; text-align: center; margin: 15px 0;">{climate_zones}</div>
517
+ <div style="font-size: 1em; text-align: center; opacity: 0.8;">
518
+ ASHRAE zones
519
+ </div>
520
+ </div>
521
+ """, unsafe_allow_html=True)
522
+
523
+ def create_dark_theme_plotly_layout():
524
+ """Create consistent dark theme layout for Plotly charts"""
525
+ return {
526
+ 'plot_bgcolor': 'rgba(0,0,0,0)',
527
+ 'paper_bgcolor': 'rgba(0,0,0,0)',
528
+ 'font': {'color': 'white'},
529
+ 'xaxis': {
530
+ 'gridcolor': '#404040',
531
+ 'linecolor': '#404040',
532
+ 'tickcolor': '#404040',
533
+ 'color': 'white'
534
+ },
535
+ 'yaxis': {
536
+ 'gridcolor': '#404040',
537
+ 'linecolor': '#404040',
538
+ 'tickcolor': '#404040',
539
+ 'color': 'white'
540
+ }
541
+ }
542
+
543
+ def create_building_characteristics_chart(buildings_df: pd.DataFrame):
544
+ """Create building characteristics visualization with dark theme"""
545
+ if buildings_df.empty:
546
+ st.warning("No building data available")
547
+ return
548
+
549
+ tab1, tab2, tab3, tab4 = st.tabs(["πŸ“Š Distribution", "πŸ—ΊοΈ Climate Zones", "πŸ—οΈ Types", "πŸ“ Properties"])
550
+
551
+ with tab1:
552
+ col1, col2 = st.columns(2)
553
+
554
+ with col1:
555
+ # Building type distribution
556
+ type_counts = buildings_df['building_type'].value_counts()
557
+ fig_types = px.pie(
558
+ values=type_counts.values,
559
+ names=type_counts.index,
560
+ title="Building Types Distribution",
561
+ color_discrete_sequence=px.colors.qualitative.Set3
562
+ )
563
+ fig_types.update_layout(**create_dark_theme_plotly_layout(), height=400)
564
+ st.plotly_chart(fig_types, use_container_width=True)
565
+
566
+ with col2:
567
+ # Variation type distribution
568
+ var_counts = buildings_df['variation_type'].value_counts()
569
+ fig_vars = px.bar(
570
+ x=var_counts.index,
571
+ y=var_counts.values,
572
+ title="Variation Types",
573
+ color=var_counts.index,
574
+ color_discrete_sequence=px.colors.qualitative.Pastel
575
+ )
576
+ fig_vars.update_layout(**create_dark_theme_plotly_layout(), height=400, showlegend=False)
577
+ st.plotly_chart(fig_vars, use_container_width=True)
578
+
579
+ with tab2:
580
+ # Climate zone analysis
581
+ climate_counts = buildings_df['climate_zone'].value_counts()
582
+ fig_climate = px.bar(
583
+ x=climate_counts.index,
584
+ y=climate_counts.values,
585
+ title="Buildings by Climate Zone",
586
+ color=climate_counts.values,
587
+ color_continuous_scale='viridis'
588
+ )
589
+ fig_climate.update_layout(**create_dark_theme_plotly_layout(), height=400)
590
+ st.plotly_chart(fig_climate, use_container_width=True)
591
+
592
+ # Climate zone descriptions
593
+ climate_descriptions = {
594
+ '1A': 'Very Hot - Humid', '1B': 'Very Hot - Dry',
595
+ '2A': 'Hot - Humid', '2B': 'Hot - Dry',
596
+ '3A': 'Warm - Humid', '3B': 'Warm - Dry', '3C': 'Warm - Marine',
597
+ '4A': 'Mixed - Humid', '4B': 'Mixed - Dry', '4C': 'Mixed - Marine',
598
+ '5A': 'Cool - Humid', '5B': 'Cool - Dry', '5C': 'Cool - Marine',
599
+ '6A': 'Cold - Humid', '6B': 'Cold - Dry',
600
+ '7': 'Very Cold', '8': 'Subarctic'
601
+ }
602
+
603
+ st.subheader("Climate Zone Descriptions")
604
+ for zone in sorted(buildings_df['climate_zone'].unique()):
605
+ if zone in climate_descriptions:
606
+ st.info(f"**{zone}**: {climate_descriptions[zone]}")
607
+
608
+ with tab3:
609
+ # Building type details
610
+ st.subheader("Building Type Analysis")
611
+
612
+ # Check which columns exist before grouping
613
+ agg_dict = {'floor_area': ['count']}
614
+ if 'floor_area' in buildings_df.columns:
615
+ agg_dict['floor_area'] = ['count', 'mean', 'std']
616
+ if 'num_zones' in buildings_df.columns:
617
+ agg_dict['num_zones'] = ['mean', 'std']
618
+ if 'window_wall_ratio' in buildings_df.columns:
619
+ agg_dict['window_wall_ratio'] = ['mean', 'std']
620
+
621
+ type_summary = buildings_df.groupby('building_type').agg(agg_dict).round(2)
622
+ st.dataframe(type_summary, use_container_width=True)
623
+
624
+ with tab4:
625
+ # Property correlations
626
+ numeric_cols = []
627
+ for col in ['floor_area', 'num_zones', 'window_wall_ratio']:
628
+ if col in buildings_df.columns:
629
+ numeric_cols.append(col)
630
+
631
+ if len(numeric_cols) >= 2:
632
+ corr_matrix = buildings_df[numeric_cols].corr()
633
+
634
+ fig_corr = px.imshow(
635
+ corr_matrix,
636
+ color_continuous_scale='RdBu',
637
+ aspect='auto',
638
+ title='Building Property Correlations'
639
+ )
640
+ fig_corr.update_layout(**create_dark_theme_plotly_layout())
641
+ st.plotly_chart(fig_corr, use_container_width=True)
642
+
643
+ # Scatter plots
644
+ if len(numeric_cols) >= 2:
645
+ col1, col2 = st.columns(2)
646
+ with col1:
647
+ if 'floor_area' in numeric_cols and 'num_zones' in numeric_cols:
648
+ fig_scatter1 = px.scatter(
649
+ buildings_df,
650
+ x='floor_area',
651
+ y='num_zones',
652
+ color='building_type',
653
+ title='Floor Area vs Number of Zones',
654
+ hover_data=['name']
655
+ )
656
+ fig_scatter1.update_layout(**create_dark_theme_plotly_layout())
657
+ st.plotly_chart(fig_scatter1, use_container_width=True)
658
+
659
+ with col2:
660
+ if 'window_wall_ratio' in numeric_cols and 'floor_area' in numeric_cols:
661
+ fig_scatter2 = px.scatter(
662
+ buildings_df,
663
+ x='window_wall_ratio',
664
+ y='floor_area',
665
+ color='building_type',
666
+ title='Window-Wall Ratio vs Floor Area',
667
+ hover_data=['name']
668
+ )
669
+ fig_scatter2.update_layout(**create_dark_theme_plotly_layout())
670
+ st.plotly_chart(fig_scatter2, use_container_width=True)
671
+
672
+ def display_buildings_table(buildings_df: pd.DataFrame):
673
+ """Display interactive buildings table"""
674
+ st.subheader("πŸ“‹ Buildings Database")
675
+
676
+ if buildings_df.empty:
677
+ st.warning("No buildings found matching the current filters.")
678
+ return
679
+
680
+ # Prepare column config based on available columns
681
+ column_config = {
682
+ "id": st.column_config.NumberColumn("ID", width="small"),
683
+ "name": st.column_config.TextColumn("Building Name", width="large"),
684
+ "building_type": st.column_config.TextColumn("Type", width="medium"),
685
+ "climate_zone": st.column_config.TextColumn("Climate", width="small"),
686
+ "variation_type": st.column_config.TextColumn("Variation", width="medium"),
687
+ "filepath": st.column_config.TextColumn("File Path", width="large")
688
+ }
689
+
690
+ # Add optional columns if they exist
691
+ if 'floor_area' in buildings_df.columns:
692
+ column_config["floor_area"] = st.column_config.NumberColumn("Floor Area (mΒ²)", format="%.0f", width="medium")
693
+ if 'num_zones' in buildings_df.columns:
694
+ column_config["num_zones"] = st.column_config.NumberColumn("Zones", width="small")
695
+ if 'window_wall_ratio' in buildings_df.columns:
696
+ column_config["window_wall_ratio"] = st.column_config.NumberColumn("WWR", format="%.2f", width="small")
697
+ if 'created_date' in buildings_df.columns:
698
+ column_config["created_date"] = st.column_config.DatetimeColumn("Created", width="medium")
699
+
700
+ # Display the table
701
+ selected_buildings = st.data_editor(
702
+ buildings_df,
703
+ use_container_width=True,
704
+ hide_index=True,
705
+ column_config=column_config,
706
+ disabled=list(buildings_df.columns) # Make all columns read-only
707
+ )
708
+
709
+ # Export functionality
710
+ col1, col2, col3 = st.columns([1, 1, 2])
711
+ with col1:
712
+ if st.button("πŸ“₯ Export to CSV"):
713
+ csv = buildings_df.to_csv(index=False)
714
+ st.download_button(
715
+ label="Download CSV",
716
+ data=csv,
717
+ file_name=f"buildings_filtered_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.csv",
718
+ mime="text/csv"
719
+ )
720
+
721
+ def load_building_epjson(filepath: str, data_dir: str = "data") -> Optional[Dict]:
722
+ """Load building epJSON file"""
723
+ try:
724
+ full_path = Path(data_dir) / filepath
725
+ if full_path.exists():
726
+ with open(full_path, 'r') as f:
727
+ return json.load(f)
728
+ else:
729
+ st.error(f"Building file not found: {full_path}")
730
+ return None
731
+ except Exception as e:
732
+ st.error(f"Error loading building file: {e}")
733
+ return None
734
+
735
+ def analyze_building_epjson(epjson_data: Dict) -> Dict:
736
+ """Analyze epJSON building data and extract key metrics"""
737
+ analysis = {
738
+ 'zones': 0,
739
+ 'surfaces': 0,
740
+ 'windows': 0,
741
+ 'hvac_systems': 0,
742
+ 'schedules': 0,
743
+ 'materials': 0,
744
+ 'constructions': 0,
745
+ 'has_meters': False,
746
+ 'has_setpoints': False,
747
+ 'timestep': None
748
+ }
749
+
750
+ # Count building components
751
+ if 'Zone' in epjson_data:
752
+ analysis['zones'] = len(epjson_data['Zone'])
753
+
754
+ if 'BuildingSurface:Detailed' in epjson_data:
755
+ analysis['surfaces'] = len(epjson_data['BuildingSurface:Detailed'])
756
+
757
+ if 'FenestrationSurface:Detailed' in epjson_data:
758
+ analysis['windows'] = len(epjson_data['FenestrationSurface:Detailed'])
759
+
760
+ if 'Schedule:Compact' in epjson_data:
761
+ analysis['schedules'] = len(epjson_data['Schedule:Compact'])
762
+
763
+ if 'Material' in epjson_data:
764
+ analysis['materials'] = len(epjson_data['Material'])
765
+
766
+ if 'Construction' in epjson_data:
767
+ analysis['constructions'] = len(epjson_data['Construction'])
768
+
769
+ # Check for HVAC systems
770
+ hvac_objects = ['AirLoopHVAC', 'PlantLoop', 'ZoneHVAC:IdealLoadsAirSystem']
771
+ analysis['hvac_systems'] = sum(len(epjson_data.get(obj, {})) for obj in hvac_objects)
772
+
773
+ # Check for meters and outputs
774
+ analysis['has_meters'] = 'Output:Meter' in epjson_data
775
+ analysis['has_setpoints'] = any('Setpoint' in key for key in epjson_data.keys())
776
+
777
+ # Get timestep
778
+ if 'Timestep' in epjson_data:
779
+ timestep_obj = list(epjson_data['Timestep'].values())[0]
780
+ analysis['timestep'] = timestep_obj.get('number_of_timesteps_per_hour', 'Unknown')
781
+
782
+ return analysis
783
+
784
+ def create_mock_energy_profile(building_name: str):
785
+ """Create mock energy profile for demonstration with dark theme"""
786
+ st.subheader("⚑ Energy Profile (Demo)")
787
+ st.info("πŸ“ Note: This is demonstration data. Connect to actual EnergyPlus simulation results for real data.")
788
+
789
+ # Mock hourly load profile
790
+ hours = list(range(24))
791
+ base_load = 100
792
+ peak_factor = np.sin(np.array(hours) * np.pi / 12)
793
+ mock_load = base_load + 50 * peak_factor + np.random.normal(0, 10, 24)
794
+ mock_load = np.maximum(mock_load, 20) # Minimum load
795
+
796
+ # Mock monthly energy
797
+ months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
798
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
799
+ heating_load = [150, 120, 80, 40, 10, 0, 0, 0, 20, 60, 100, 140]
800
+ cooling_load = [0, 0, 10, 30, 60, 100, 120, 110, 70, 30, 5, 0]
801
+
802
+ col1, col2 = st.columns(2)
803
+
804
+ with col1:
805
+ # Hourly profile
806
+ fig_hourly = px.line(
807
+ x=hours,
808
+ y=mock_load,
809
+ title="Typical Daily Load Profile",
810
+ labels={'x': 'Hour of Day', 'y': 'Power (kW)'}
811
+ )
812
+ fig_hourly.update_traces(line_color='#00d4ff')
813
+ fig_hourly.update_layout(**create_dark_theme_plotly_layout())
814
+ st.plotly_chart(fig_hourly, use_container_width=True)
815
+
816
+ with col2:
817
+ # Monthly profile
818
+ fig_monthly = go.Figure()
819
+ fig_monthly.add_trace(go.Bar(x=months, y=heating_load, name='Heating', marker_color='#ff6b6b'))
820
+ fig_monthly.add_trace(go.Bar(x=months, y=cooling_load, name='Cooling', marker_color='#4ecdc4'))
821
+ fig_monthly.update_layout(
822
+ **create_dark_theme_plotly_layout(),
823
+ title="Monthly Energy Consumption",
824
+ xaxis_title="Month",
825
+ yaxis_title="Energy (kWh/mΒ²)",
826
+ barmode='stack'
827
+ )
828
+ st.plotly_chart(fig_monthly, use_container_width=True)
829
+
830
+
831
+
832
+ def main():
833
+ """Main Streamlit application"""
834
+ st.title("πŸ—οΈ Building Generator Dashboard")
835
+ st.markdown("Interactive exploration of building energy models and weather data")
836
+
837
+ # Load data
838
+ with st.spinner("Loading building and weather data..."):
839
+ pipeline, buildings_df, weather_df, combinations_df = load_pipeline_data()
840
+
841
+ if pipeline is None:
842
+ st.error("Failed to initialize application. Please check your data directory.")
843
+ st.info("Make sure you have run: `python scripts/main.py --create-table` and `python scripts/main.py --create-weather-table`")
844
+ return
845
+
846
+ # Sidebar for navigation
847
+ st.sidebar.title("πŸ—‚οΈ Navigation")
848
+
849
+ # Initialize session state for page navigation
850
+ if 'current_page' not in st.session_state:
851
+ st.session_state.current_page = "🏠 Overview"
852
+
853
+ # Use session state to control the selectbox
854
+ page_options = ["🏠 Overview", "🏒 Building Explorer", "🌍 Weather Data", "βš–οΈ Compare Buildings", "πŸ“Š Analysis & Reports"]
855
+ current_index = page_options.index(st.session_state.current_page) if st.session_state.current_page in page_options else 0
856
+
857
+ page = st.sidebar.selectbox(
858
+ "Choose a page:",
859
+ page_options,
860
+ index=current_index,
861
+ key="page_selector"
862
+ )
863
+
864
+ # Update session state when selectbox changes
865
+ if page != st.session_state.current_page:
866
+ st.session_state.current_page = page
867
+
868
+ # Use the current page from session state
869
+ current_page = st.session_state.current_page
870
+
871
+ if current_page == "🏠 Overview":
872
+ st.header("System Overview")
873
+
874
+ # System overview
875
+ create_overview_metrics(buildings_df, weather_df, combinations_df)
876
+
877
+ elif current_page == "🏒 Building Explorer":
878
+ st.header("Building Explorer")
879
+
880
+ if buildings_df.empty:
881
+ st.warning("No building data available. Run `python scripts/main.py --create-table` first.")
882
+ return
883
+
884
+ # Filters
885
+ filters = create_building_filters(buildings_df)
886
+
887
+ # Apply filters
888
+ filtered_buildings = apply_building_filters(buildings_df, filters)
889
+
890
+ st.subheader(f"πŸ“Š Found {len(filtered_buildings)} buildings")
891
+
892
+ # Visualizations
893
+ if not filtered_buildings.empty:
894
+ create_building_characteristics_chart(filtered_buildings)
895
+
896
+ # Buildings table
897
+ display_buildings_table(filtered_buildings)
898
+
899
+ # Building details expander
900
+ if not filtered_buildings.empty:
901
+ st.subheader("πŸ” Building Details")
902
+ selected_building = st.selectbox(
903
+ "Select a building to analyze:",
904
+ options=filtered_buildings['name'].tolist(),
905
+ index=0
906
+ )
907
+
908
+ if selected_building:
909
+ building_info = filtered_buildings[filtered_buildings['name'] == selected_building].iloc[0]
910
+
911
+ col1, col2 = st.columns([1, 2])
912
+
913
+ with col1:
914
+ st.markdown('<div class="building-card">', unsafe_allow_html=True)
915
+ st.subheader(f"πŸ“‹ {building_info['name']}")
916
+ st.write(f"**Type**: {building_info['building_type']}")
917
+ st.write(f"**Climate Zone**: {building_info['climate_zone']}")
918
+ st.write(f"**Variation**: {building_info['variation_type']}")
919
+
920
+ # Add optional fields if they exist
921
+ if 'floor_area' in building_info and pd.notna(building_info['floor_area']):
922
+ st.write(f"**Floor Area**: {building_info['floor_area']:.0f} mΒ²")
923
+ if 'num_zones' in building_info and pd.notna(building_info['num_zones']):
924
+ st.write(f"**Zones**: {building_info['num_zones']}")
925
+ if 'window_wall_ratio' in building_info and pd.notna(building_info['window_wall_ratio']):
926
+ st.write(f"**WWR**: {building_info['window_wall_ratio']:.2%}")
927
+
928
+ st.markdown('</div>', unsafe_allow_html=True)
929
+
930
+ with col2:
931
+ # Load and analyze building file
932
+ epjson_data = load_building_epjson(building_info['filepath'])
933
+
934
+ if epjson_data:
935
+ analysis = analyze_building_epjson(epjson_data)
936
+
937
+ st.subheader("πŸ”§ Building Analysis")
938
+
939
+ # Create metrics display
940
+ metric_col1, metric_col2, metric_col3 = st.columns(3)
941
+
942
+ with metric_col1:
943
+ st.metric("Zones", analysis['zones'])
944
+ st.metric("Surfaces", analysis['surfaces'])
945
+
946
+ with metric_col2:
947
+ st.metric("Windows", analysis['windows'])
948
+ st.metric("HVAC Systems", analysis['hvac_systems'])
949
+
950
+ with metric_col3:
951
+ st.metric("Schedules", analysis['schedules'])
952
+ st.metric("Materials", analysis['materials'])
953
+
954
+ # Status indicators
955
+ st.subheader("⚑ Processing Status")
956
+ status_col1, status_col2, status_col3 = st.columns(3)
957
+
958
+ with status_col1:
959
+ meter_status = "βœ… Yes" if analysis['has_meters'] else "❌ No"
960
+ st.metric("Has Meters", meter_status)
961
+
962
+ with status_col2:
963
+ setpoint_status = "βœ… Yes" if analysis['has_setpoints'] else "❌ No"
964
+ st.metric("Has Setpoints", setpoint_status)
965
+
966
+ with status_col3:
967
+ timestep_value = analysis['timestep'] or "Not set"
968
+ st.metric("Timesteps/Hour", timestep_value)
969
+
970
+ # Mock energy profile
971
+ create_mock_energy_profile(selected_building)
972
+
973
+ elif current_page == "🌍 Weather Data":
974
+ st.header("Weather Data Explorer")
975
+
976
+ if weather_df.empty:
977
+ st.warning("No weather data available. Run `python scripts/main.py --create-weather-table` first.")
978
+ return
979
+
980
+ # Weather filters
981
+ st.subheader("🌑️ Filter Weather Locations")
982
+
983
+ col1, col2, col3 = st.columns(3)
984
+
985
+ with col1:
986
+ countries = st.multiselect(
987
+ "Countries",
988
+ options=sorted(weather_df['country'].unique()),
989
+ default=[]
990
+ )
991
+
992
+ climate_zones_weather = st.multiselect(
993
+ "Climate Zones",
994
+ options=sorted(weather_df['climate_zone_code'].unique()),
995
+ default=[]
996
+ )
997
+
998
+ with col2:
999
+ if 'data_source' in weather_df.columns:
1000
+ data_sources = st.multiselect(
1001
+ "Data Sources",
1002
+ options=weather_df['data_source'].unique(),
1003
+ default=[]
1004
+ )
1005
+ else:
1006
+ data_sources = []
1007
+
1008
+ lat_range = st.slider(
1009
+ "Latitude Range",
1010
+ min_value=float(weather_df['latitude'].min()),
1011
+ max_value=float(weather_df['latitude'].max()),
1012
+ value=(float(weather_df['latitude'].min()), float(weather_df['latitude'].max()))
1013
+ )
1014
+
1015
+ with col3:
1016
+ lon_range = st.slider(
1017
+ "Longitude Range",
1018
+ min_value=float(weather_df['longitude'].min()),
1019
+ max_value=float(weather_df['longitude'].max()),
1020
+ value=(float(weather_df['longitude'].min()), float(weather_df['longitude'].max()))
1021
+ )
1022
+
1023
+ # Apply weather filters
1024
+ filtered_weather = weather_df.copy()
1025
+
1026
+ if countries:
1027
+ filtered_weather = filtered_weather[filtered_weather['country'].isin(countries)]
1028
+ if climate_zones_weather:
1029
+ filtered_weather = filtered_weather[filtered_weather['climate_zone_code'].isin(climate_zones_weather)]
1030
+ if data_sources:
1031
+ filtered_weather = filtered_weather[filtered_weather['data_source'].isin(data_sources)]
1032
+
1033
+ filtered_weather = filtered_weather[
1034
+ (filtered_weather['latitude'] >= lat_range[0]) &
1035
+ (filtered_weather['latitude'] <= lat_range[1]) &
1036
+ (filtered_weather['longitude'] >= lon_range[0]) &
1037
+ (filtered_weather['longitude'] <= lon_range[1])
1038
+ ]
1039
+
1040
+ st.subheader(f"🌍 Found {len(filtered_weather)} weather locations")
1041
+
1042
+ # Weather visualizations
1043
+ tab1, tab2, tab3 = st.tabs(["πŸ—ΊοΈ Map", "πŸ“Š Distribution", "πŸ“‹ Table"])
1044
+
1045
+ with tab1:
1046
+ # World map of weather locations
1047
+ fig_map = px.scatter_mapbox(
1048
+ filtered_weather,
1049
+ lat='latitude',
1050
+ lon='longitude',
1051
+ color='climate_zone_code',
1052
+ hover_data=['place', 'country'],
1053
+ mapbox_style='carto-darkmatter', # Dark theme map
1054
+ zoom=1,
1055
+ title='Weather Locations Worldwide'
1056
+ )
1057
+ fig_map.update_layout(**create_dark_theme_plotly_layout(), height=600)
1058
+ st.plotly_chart(fig_map, use_container_width=True)
1059
+
1060
+ with tab2:
1061
+ col1, col2 = st.columns(2)
1062
+
1063
+ with col1:
1064
+ # Country distribution
1065
+ country_counts = filtered_weather['country'].value_counts().head(15)
1066
+ fig_countries = px.bar(
1067
+ x=country_counts.values,
1068
+ y=country_counts.index,
1069
+ orientation='h',
1070
+ title='Top 15 Countries by Weather Locations',
1071
+ color=country_counts.values,
1072
+ color_continuous_scale='viridis'
1073
+ )
1074
+ fig_countries.update_layout(**create_dark_theme_plotly_layout(), height=500)
1075
+ st.plotly_chart(fig_countries, use_container_width=True)
1076
+
1077
+ with col2:
1078
+ # Climate zone distribution
1079
+ climate_counts = filtered_weather['climate_zone_code'].value_counts()
1080
+ fig_climate = px.pie(
1081
+ values=climate_counts.values,
1082
+ names=climate_counts.index,
1083
+ title='Climate Zone Distribution',
1084
+ color_discrete_sequence=px.colors.qualitative.Set3
1085
+ )
1086
+ fig_climate.update_layout(**create_dark_theme_plotly_layout(), height=500)
1087
+ st.plotly_chart(fig_climate, use_container_width=True)
1088
+
1089
+ with tab3:
1090
+ # Weather locations table
1091
+ st.dataframe(
1092
+ filtered_weather,
1093
+ use_container_width=True,
1094
+ hide_index=True,
1095
+ column_config={
1096
+ "id": st.column_config.NumberColumn("ID", width="small"),
1097
+ "place": st.column_config.TextColumn("Location", width="large"),
1098
+ "country": st.column_config.TextColumn("Country", width="small"),
1099
+ "climate_zone_code": st.column_config.TextColumn("Climate", width="small"),
1100
+ "latitude": st.column_config.NumberColumn("Latitude", format="%.2f", width="medium"),
1101
+ "longitude": st.column_config.NumberColumn("Longitude", format="%.2f", width="medium"),
1102
+ "elevation": st.column_config.NumberColumn("Elevation (m)", width="medium") if 'elevation' in filtered_weather.columns else None,
1103
+ "data_source": st.column_config.TextColumn("Source", width="small") if 'data_source' in filtered_weather.columns else None
1104
+ }
1105
+ )
1106
+
1107
+ elif current_page == "βš–οΈ Compare Buildings":
1108
+ st.header("Building Comparison Tool")
1109
+
1110
+ if buildings_df.empty:
1111
+ st.warning("No building data available for comparison.")
1112
+ return
1113
+
1114
+ st.subheader("Select Buildings to Compare")
1115
+
1116
+ # Building selection for comparison
1117
+ col1, col2 = st.columns(2)
1118
+
1119
+ with col1:
1120
+ building1 = st.selectbox(
1121
+ "Building 1:",
1122
+ options=buildings_df['name'].tolist(),
1123
+ key="building1"
1124
+ )
1125
+
1126
+ with col2:
1127
+ building2 = st.selectbox(
1128
+ "Building 2:",
1129
+ options=buildings_df['name'].tolist(),
1130
+ key="building2"
1131
+ )
1132
+
1133
+ if building1 and building2 and building1 != building2:
1134
+ # Get building data
1135
+ building1_data = buildings_df[buildings_df['name'] == building1].iloc[0]
1136
+ building2_data = buildings_df[buildings_df['name'] == building2].iloc[0]
1137
+
1138
+ # Comparison display
1139
+ st.subheader("πŸ” Building Comparison")
1140
+
1141
+ col1, col2 = st.columns(2)
1142
+
1143
+ with col1:
1144
+ st.markdown('<div class="comparison-highlight">', unsafe_allow_html=True)
1145
+ st.subheader(f"🏒 {building1}")
1146
+ st.write(f"**Type**: {building1_data['building_type']}")
1147
+ st.write(f"**Climate Zone**: {building1_data['climate_zone']}")
1148
+ st.write(f"**Variation**: {building1_data['variation_type']}")
1149
+
1150
+ # Add optional fields if they exist
1151
+ for field, label in [('floor_area', 'Floor Area'), ('num_zones', 'Zones'), ('window_wall_ratio', 'WWR')]:
1152
+ if field in building1_data and pd.notna(building1_data[field]):
1153
+ if field == 'floor_area':
1154
+ st.write(f"**{label}**: {building1_data[field]:.0f} mΒ²")
1155
+ elif field == 'window_wall_ratio':
1156
+ st.write(f"**{label}**: {building1_data[field]:.2%}")
1157
+ else:
1158
+ st.write(f"**{label}**: {building1_data[field]}")
1159
+
1160
+ st.markdown('</div>', unsafe_allow_html=True)
1161
+
1162
+ with col2:
1163
+ st.markdown('<div class="comparison-highlight">', unsafe_allow_html=True)
1164
+ st.subheader(f"🏒 {building2}")
1165
+ st.write(f"**Type**: {building2_data['building_type']}")
1166
+ st.write(f"**Climate Zone**: {building2_data['climate_zone']}")
1167
+ st.write(f"**Variation**: {building2_data['variation_type']}")
1168
+
1169
+ # Add optional fields if they exist
1170
+ for field, label in [('floor_area', 'Floor Area'), ('num_zones', 'Zones'), ('window_wall_ratio', 'WWR')]:
1171
+ if field in building2_data and pd.notna(building2_data[field]):
1172
+ if field == 'floor_area':
1173
+ st.write(f"**{label}**: {building2_data[field]:.0f} mΒ²")
1174
+ elif field == 'window_wall_ratio':
1175
+ st.write(f"**{label}**: {building2_data[field]:.2%}")
1176
+ else:
1177
+ st.write(f"**{label}**: {building2_data[field]}")
1178
+
1179
+ st.markdown('</div>', unsafe_allow_html=True)
1180
+
1181
+ # Load and compare epJSON files
1182
+ st.subheader("πŸ”§ Technical Comparison")
1183
+
1184
+ epjson1 = load_building_epjson(building1_data['filepath'])
1185
+ epjson2 = load_building_epjson(building2_data['filepath'])
1186
+
1187
+ if epjson1 and epjson2:
1188
+ analysis1 = analyze_building_epjson(epjson1)
1189
+ analysis2 = analyze_building_epjson(epjson2)
1190
+
1191
+ # Technical comparison table
1192
+ tech_comparison = pd.DataFrame({
1193
+ 'Component': ['Zones', 'Surfaces', 'Windows', 'HVAC Systems', 'Schedules', 'Materials'],
1194
+ building1: [
1195
+ analysis1['zones'], analysis1['surfaces'], analysis1['windows'],
1196
+ analysis1['hvac_systems'], analysis1['schedules'], analysis1['materials']
1197
+ ],
1198
+ building2: [
1199
+ analysis2['zones'], analysis2['surfaces'], analysis2['windows'],
1200
+ analysis2['hvac_systems'], analysis2['schedules'], analysis2['materials']
1201
+ ]
1202
+ })
1203
+
1204
+ # Add difference column
1205
+ tech_comparison['Difference'] = tech_comparison[building2] - tech_comparison[building1]
1206
+
1207
+ st.dataframe(tech_comparison, use_container_width=True)
1208
+
1209
+ # Processing status comparison
1210
+ st.subheader("⚑ Processing Status Comparison")
1211
+
1212
+ status_comparison = pd.DataFrame({
1213
+ 'Status': ['Has Meters', 'Has Setpoints', 'Timesteps/Hour'],
1214
+ building1: [
1215
+ "βœ…" if analysis1['has_meters'] else "❌",
1216
+ "βœ…" if analysis1['has_setpoints'] else "❌",
1217
+ str(analysis1['timestep'] or 'Not set')
1218
+ ],
1219
+ building2: [
1220
+ "βœ…" if analysis2['has_meters'] else "❌",
1221
+ "βœ…" if analysis2['has_setpoints'] else "❌",
1222
+ str(analysis2['timestep'] or 'Not set')
1223
+ ]
1224
+ })
1225
+
1226
+ st.dataframe(status_comparison, use_container_width=True)
1227
+ else:
1228
+ st.info("Please select two different buildings to compare.")
1229
+
1230
+ elif current_page == "πŸ“Š Analysis & Reports":
1231
+ st.header("Analysis & Reports")
1232
+
1233
+ if buildings_df.empty:
1234
+ st.warning("No building data available for analysis.")
1235
+ return
1236
+
1237
+ # Analysis options
1238
+ analysis_type = st.selectbox(
1239
+ "Choose analysis type:",
1240
+ ["πŸ“ˆ Statistical Summary", "πŸ” Data Quality Check", "πŸ“‹ Detailed Report", "🎯 Custom Analysis"]
1241
+ )
1242
+
1243
+ if analysis_type == "πŸ“ˆ Statistical Summary":
1244
+ st.subheader("Statistical Summary")
1245
+
1246
+ # Numeric column statistics
1247
+ numeric_cols = []
1248
+ for col in ['floor_area', 'num_zones', 'window_wall_ratio']:
1249
+ if col in buildings_df.columns:
1250
+ numeric_cols.append(col)
1251
+
1252
+ if numeric_cols:
1253
+ st.write("**Numeric Properties Statistics:**")
1254
+ stats_df = buildings_df[numeric_cols].describe()
1255
+ st.dataframe(stats_df, use_container_width=True)
1256
+
1257
+ # Categorical distributions
1258
+ st.write("**Categorical Distributions:**")
1259
+
1260
+ col1, col2 = st.columns(2)
1261
+ with col1:
1262
+ if 'building_type' in buildings_df.columns:
1263
+ type_dist = buildings_df['building_type'].value_counts()
1264
+ st.write("Building Types:")
1265
+ st.bar_chart(type_dist)
1266
+
1267
+ with col2:
1268
+ if 'climate_zone' in buildings_df.columns:
1269
+ climate_dist = buildings_df['climate_zone'].value_counts()
1270
+ st.write("Climate Zones:")
1271
+ st.bar_chart(climate_dist)
1272
+
1273
+ elif analysis_type == "πŸ” Data Quality Check":
1274
+ st.subheader("Data Quality Assessment")
1275
+
1276
+ # Missing data check
1277
+ missing_data = buildings_df.isnull().sum()
1278
+ if missing_data.sum() > 0:
1279
+ st.write("**Missing Data:**")
1280
+ missing_df = missing_data[missing_data > 0].to_frame('Missing Count')
1281
+ missing_df['Percentage'] = (missing_df['Missing Count'] / len(buildings_df) * 100).round(2)
1282
+ st.dataframe(missing_df)
1283
+ else:
1284
+ st.success("βœ… No missing data found!")
1285
+
1286
+ # Duplicate check
1287
+ duplicates = buildings_df.duplicated().sum()
1288
+ if duplicates > 0:
1289
+ st.warning(f"⚠️ Found {duplicates} duplicate rows")
1290
+ else:
1291
+ st.success("βœ… No duplicate rows found!")
1292
+
1293
+ # File existence check
1294
+ if 'filepath' in buildings_df.columns:
1295
+ st.write("**File Existence Check:**")
1296
+ missing_files = []
1297
+ for idx, row in buildings_df.iterrows():
1298
+ filepath = Path("data") / row['filepath']
1299
+ if not filepath.exists():
1300
+ missing_files.append(row['name'])
1301
+
1302
+ if missing_files:
1303
+ st.error(f"❌ {len(missing_files)} building files not found")
1304
+ with st.expander("Show missing files"):
1305
+ for file in missing_files[:10]: # Show first 10
1306
+ st.write(f"- {file}")
1307
+ if len(missing_files) > 10:
1308
+ st.write(f"... and {len(missing_files) - 10} more")
1309
+ else:
1310
+ st.success("βœ… All building files exist!")
1311
+
1312
+ elif analysis_type == "πŸ“‹ Detailed Report":
1313
+ st.subheader("Generate Detailed Report")
1314
+
1315
+ # Report options
1316
+ include_weather = st.checkbox("Include weather data analysis", value=True)
1317
+ include_combinations = st.checkbox("Include combination analysis", value=True)
1318
+
1319
+ if st.button("Generate Report"):
1320
+ with st.spinner("Generating report..."):
1321
+ # Generate comprehensive report
1322
+ report_data = {
1323
+ 'timestamp': pd.Timestamp.now(),
1324
+ 'buildings_total': len(buildings_df),
1325
+ 'weather_total': len(weather_df) if not weather_df.empty else 0,
1326
+ 'combinations_total': len(combinations_df) if not combinations_df.empty else 0
1327
+ }
1328
+
1329
+ st.success("πŸ“Š Report generated successfully!")
1330
+
1331
+ # Display key metrics
1332
+ metric_col1, metric_col2, metric_col3 = st.columns(3)
1333
+
1334
+ with metric_col1:
1335
+ st.metric("Buildings Analyzed", report_data['buildings_total'])
1336
+
1337
+ with metric_col2:
1338
+ if include_weather:
1339
+ st.metric("Weather Locations", report_data['weather_total'])
1340
+
1341
+ with metric_col3:
1342
+ if include_combinations:
1343
+ st.metric("Combinations", report_data['combinations_total'])
1344
+
1345
+ # Download report
1346
+ report_text = f"""
1347
+ Building Generator Analysis Report
1348
+ Generated: {report_data['timestamp']}
1349
+
1350
+ Summary Statistics:
1351
+ - Total Buildings: {report_data['buildings_total']}
1352
+ - Weather Locations: {report_data['weather_total']}
1353
+ - Simulation Combinations: {report_data['combinations_total']}
1354
+
1355
+ Building Type Distribution:
1356
+ {buildings_df['building_type'].value_counts().to_string() if not buildings_df.empty else 'No data'}
1357
+
1358
+ Climate Zone Distribution:
1359
+ {buildings_df['climate_zone'].value_counts().to_string() if not buildings_df.empty else 'No data'}
1360
+ """
1361
+
1362
+ st.download_button(
1363
+ label="πŸ“₯ Download Report",
1364
+ data=report_text,
1365
+ file_name=f"building_analysis_report_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.txt",
1366
+ mime="text/plain"
1367
+ )
1368
+
1369
+ elif analysis_type == "🎯 Custom Analysis":
1370
+ st.subheader("Custom Analysis")
1371
+ st.info("🚧 Custom analysis features coming soon! This will include:")
1372
+
1373
+ col1, col2 = st.columns(2)
1374
+ with col1:
1375
+ st.markdown("""
1376
+ **Planned Features:**
1377
+ - Building performance correlation analysis
1378
+ - Climate impact assessment
1379
+ - Variation effectiveness studies
1380
+ - Energy consumption modeling
1381
+ - Optimization recommendations
1382
+ """)
1383
+
1384
+ with col2:
1385
+ st.markdown("""
1386
+ **Interactive Tools:**
1387
+ - Custom filter combinations
1388
+ - Advanced statistical analysis
1389
+ - Machine learning insights
1390
+ - Predictive modeling
1391
+ - Export to research formats
1392
+ """)
1393
+
1394
+ if __name__ == "__main__":
1395
+ main()