Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- app/data_export.py +558 -611
- app/data_persistence.py +1 -17
- app/main.py +472 -455
app/data_export.py
CHANGED
|
@@ -13,33 +13,11 @@ import base64
|
|
| 13 |
import io
|
| 14 |
from datetime import datetime
|
| 15 |
import xlsxwriter
|
| 16 |
-
import logging
|
| 17 |
|
| 18 |
-
# Configure logging
|
| 19 |
-
logging.basicConfig(level=logging.INFO)
|
| 20 |
-
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
class DataExport:
|
| 23 |
"""Class for data export functionality."""
|
| 24 |
|
| 25 |
-
def __init__(self):
|
| 26 |
-
"""Initialize DataExport."""
|
| 27 |
-
logger.info("Initializing DataExport")
|
| 28 |
-
|
| 29 |
-
def display(self, session_state: Dict[str, Any]) -> None:
|
| 30 |
-
"""
|
| 31 |
-
Display the export interface in Streamlit.
|
| 32 |
-
|
| 33 |
-
Args:
|
| 34 |
-
session_state: Streamlit session state containing calculation results
|
| 35 |
-
"""
|
| 36 |
-
try:
|
| 37 |
-
logger.info("Displaying export interface")
|
| 38 |
-
self.display_export_interface(session_state)
|
| 39 |
-
except Exception as e:
|
| 40 |
-
logger.error(f"Error in display method: {str(e)}")
|
| 41 |
-
st.error(f"Failed to display export interface: {str(e)}")
|
| 42 |
-
|
| 43 |
@staticmethod
|
| 44 |
def export_to_csv(data: Dict[str, Any], file_path: str = None) -> Optional[str]:
|
| 45 |
"""
|
|
@@ -68,8 +46,7 @@ class DataExport:
|
|
| 68 |
return csv_data
|
| 69 |
|
| 70 |
except Exception as e:
|
| 71 |
-
|
| 72 |
-
st.error(f"Error exporting to CSV: {str(e)}")
|
| 73 |
return None
|
| 74 |
|
| 75 |
@staticmethod
|
|
@@ -116,8 +93,7 @@ class DataExport:
|
|
| 116 |
return None
|
| 117 |
|
| 118 |
except Exception as e:
|
| 119 |
-
|
| 120 |
-
st.error(f"Error exporting to Excel: {str(e)}")
|
| 121 |
return None
|
| 122 |
|
| 123 |
@staticmethod
|
|
@@ -146,8 +122,7 @@ class DataExport:
|
|
| 146 |
return json_data
|
| 147 |
|
| 148 |
except Exception as e:
|
| 149 |
-
|
| 150 |
-
st.error(f"Error exporting scenario to JSON: {str(e)}")
|
| 151 |
return None
|
| 152 |
|
| 153 |
@staticmethod
|
|
@@ -164,18 +139,13 @@ class DataExport:
|
|
| 164 |
Returns:
|
| 165 |
HTML string with download link
|
| 166 |
"""
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
return href
|
| 175 |
-
except Exception as e:
|
| 176 |
-
logger.error(f"Error generating download link: {str(e)}")
|
| 177 |
-
st.error(f"Error generating download link: {str(e)}")
|
| 178 |
-
return ""
|
| 179 |
|
| 180 |
@staticmethod
|
| 181 |
def create_cooling_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
|
|
@@ -188,175 +158,170 @@ class DataExport:
|
|
| 188 |
Returns:
|
| 189 |
Dictionary with DataFrames for Excel export
|
| 190 |
"""
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
"
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
"
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
"
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
"W/m²"
|
| 213 |
-
]
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
dataframes["Cooling Summary"] = pd.DataFrame(summary_data)
|
| 217 |
-
|
| 218 |
-
# Create component breakdown DataFrame
|
| 219 |
-
component_data = {
|
| 220 |
-
"Component": [
|
| 221 |
-
"Walls",
|
| 222 |
-
"Roof",
|
| 223 |
-
"Windows",
|
| 224 |
-
"Doors",
|
| 225 |
-
"People",
|
| 226 |
-
"Lighting",
|
| 227 |
-
"Equipment",
|
| 228 |
-
"Infiltration",
|
| 229 |
-
"Ventilation"
|
| 230 |
-
],
|
| 231 |
-
"Load (kW)": [
|
| 232 |
-
results["cooling"]["component_loads"]["walls"],
|
| 233 |
-
results["cooling"]["component_loads"]["roof"],
|
| 234 |
-
results["cooling"]["component_loads"]["windows"],
|
| 235 |
-
results["cooling"]["component_loads"]["doors"],
|
| 236 |
-
results["cooling"]["component_loads"]["people"],
|
| 237 |
-
results["cooling"]["component_loads"]["lighting"],
|
| 238 |
-
results["cooling"]["component_loads"]["equipment"],
|
| 239 |
-
results["cooling"]["component_loads"]["infiltration"],
|
| 240 |
-
results["cooling"]["component_loads"]["ventilation"]
|
| 241 |
-
],
|
| 242 |
-
"Percentage (%)": [
|
| 243 |
-
results["cooling"]["component_loads"]["walls"] / results["cooling"]["total_load"] * 100,
|
| 244 |
-
results["cooling"]["component_loads"]["roof"] / results["cooling"]["total_load"] * 100,
|
| 245 |
-
results["cooling"]["component_loads"]["windows"] / results["cooling"]["total_load"] * 100,
|
| 246 |
-
results["cooling"]["component_loads"]["doors"] / results["cooling"]["total_load"] * 100,
|
| 247 |
-
results["cooling"]["component_loads"]["people"] / results["cooling"]["total_load"] * 100,
|
| 248 |
-
results["cooling"]["component_loads"]["lighting"] / results["cooling"]["total_load"] * 100,
|
| 249 |
-
results["cooling"]["component_loads"]["equipment"] / results["cooling"]["total_load"] * 100,
|
| 250 |
-
results["cooling"]["component_loads"]["infiltration"] / results["cooling"]["total_load"] * 100,
|
| 251 |
-
results["cooling"]["component_loads"]["ventilation"] / results["cooling"]["total_load"] * 100
|
| 252 |
-
]
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
dataframes["Cooling Components"] = pd.DataFrame(component_data)
|
| 256 |
-
|
| 257 |
-
# Create detailed loads DataFrames
|
| 258 |
-
|
| 259 |
-
# Walls
|
| 260 |
-
wall_data = []
|
| 261 |
-
for wall in results["cooling"]["detailed_loads"]["walls"]:
|
| 262 |
-
wall_data.append({
|
| 263 |
-
"Name": wall["name"],
|
| 264 |
-
"Orientation": wall["orientation"],
|
| 265 |
-
"Area (m²)": wall["area"],
|
| 266 |
-
"U-Value (W/m²·K)": wall["u_value"],
|
| 267 |
-
"CLTD (°C)": wall["cltd"],
|
| 268 |
-
"Load (kW)": wall["load"]
|
| 269 |
-
})
|
| 270 |
-
|
| 271 |
-
if wall_data:
|
| 272 |
-
dataframes["Cooling Walls"] = pd.DataFrame(wall_data)
|
| 273 |
-
|
| 274 |
-
# Roofs
|
| 275 |
-
roof_data = []
|
| 276 |
-
for roof in results["cooling"]["detailed_loads"]["roofs"]:
|
| 277 |
-
roof_data.append({
|
| 278 |
-
"Name": roof["name"],
|
| 279 |
-
"Orientation": roof["orientation"],
|
| 280 |
-
"Area (m²)": roof["area"],
|
| 281 |
-
"U-Value (W/m²·K)": wall["u_value"],
|
| 282 |
-
"CLTD (°C)": roof["cltd"],
|
| 283 |
-
"Load (kW)": roof["load"]
|
| 284 |
-
})
|
| 285 |
-
|
| 286 |
-
if roof_data:
|
| 287 |
-
dataframes["Cooling Roofs"] = pd.DataFrame(roof_data)
|
| 288 |
-
|
| 289 |
-
# Windows
|
| 290 |
-
window_data = []
|
| 291 |
-
for window in results["cooling"]["detailed_loads"]["windows"]:
|
| 292 |
-
window_data.append({
|
| 293 |
-
"Name": window["name"],
|
| 294 |
-
"Orientation": wall["orientation"],
|
| 295 |
-
"Area (m²)": wall["area"],
|
| 296 |
-
"U-Value (W/m²·K)": wall["u_value"],
|
| 297 |
-
"SHGC": window["shgc"],
|
| 298 |
-
"SCL (W/m²)": window["scl"],
|
| 299 |
-
"Load (kW)": window["load"]
|
| 300 |
-
})
|
| 301 |
-
|
| 302 |
-
if window_data:
|
| 303 |
-
dataframes["Cooling Windows"] = pd.DataFrame(window_data)
|
| 304 |
-
|
| 305 |
-
# Doors
|
| 306 |
-
door_data = []
|
| 307 |
-
for door in results["cooling"]["detailed_loads"]["doors"]:
|
| 308 |
-
door_data.append({
|
| 309 |
-
"Name": door["name"],
|
| 310 |
-
"Orientation": door["orientation"],
|
| 311 |
-
"Area (m²)": door["area"],
|
| 312 |
-
"U-Value (W/m²·K)": door["u_value"],
|
| 313 |
-
"CLTD (°C)": door["cltd"],
|
| 314 |
-
"Load (kW)": door["load"]
|
| 315 |
-
})
|
| 316 |
-
|
| 317 |
-
if door_data:
|
| 318 |
-
dataframes["Cooling Doors"] = pd.DataFrame(door_data)
|
| 319 |
-
|
| 320 |
-
# Internal loads
|
| 321 |
-
internal_data = []
|
| 322 |
-
for internal_load in results["cooling"]["detailed_loads"]["internal"]:
|
| 323 |
-
internal_data.append({
|
| 324 |
-
"Type": internal_load["type"],
|
| 325 |
-
"Name": internal_load["name"],
|
| 326 |
-
"Quantity": internal_load["quantity"],
|
| 327 |
-
"Heat Gain (W)": internal_load["heat_gain"],
|
| 328 |
-
"CLF": internal_load["clf"],
|
| 329 |
-
"Load (kW)": internal_load["load"]
|
| 330 |
-
})
|
| 331 |
-
|
| 332 |
-
if internal_data:
|
| 333 |
-
dataframes["Cooling Internal Loads"] = pd.DataFrame(internal_data)
|
| 334 |
-
|
| 335 |
-
# Infiltration and ventilation
|
| 336 |
-
air_data = [
|
| 337 |
-
{
|
| 338 |
-
"Type": "Infiltration",
|
| 339 |
-
"Air Flow (m³/s)": results["cooling"]["detailed_loads"]["infiltration"]["air_flow"],
|
| 340 |
-
"Sensible Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["sensible_load"],
|
| 341 |
-
"Latent Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["latent_load"],
|
| 342 |
-
"Total Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["total_load"]
|
| 343 |
-
},
|
| 344 |
-
{
|
| 345 |
-
"Type": "Ventilation",
|
| 346 |
-
"Air Flow (m³/s)": results["cooling"]["detailed_loads"]["ventilation"]["air_flow"],
|
| 347 |
-
"Sensible Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["sensible_load"],
|
| 348 |
-
"Latent Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["latent_load"],
|
| 349 |
-
"Total Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["total_load"]
|
| 350 |
-
}
|
| 351 |
]
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
@staticmethod
|
| 362 |
def create_heating_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
|
|
@@ -369,165 +334,160 @@ class DataExport:
|
|
| 369 |
Returns:
|
| 370 |
Dictionary with DataFrames for Excel export
|
| 371 |
"""
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
"
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
"
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
"
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
"%"
|
| 394 |
-
]
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
dataframes["Heating Summary"] = pd.DataFrame(summary_data)
|
| 398 |
-
|
| 399 |
-
# Create component breakdown DataFrame
|
| 400 |
-
component_data = {
|
| 401 |
-
"Component": [
|
| 402 |
-
"Walls",
|
| 403 |
-
"Roof",
|
| 404 |
-
"Floor",
|
| 405 |
-
"Windows",
|
| 406 |
-
"Doors",
|
| 407 |
-
"Infiltration",
|
| 408 |
-
"Ventilation"
|
| 409 |
-
],
|
| 410 |
-
"Load (kW)": [
|
| 411 |
-
results["heating"]["component_loads"]["walls"],
|
| 412 |
-
results["heating"]["component_loads"]["roof"],
|
| 413 |
-
results["heating"]["component_loads"]["floor"],
|
| 414 |
-
results["heating"]["component_loads"]["windows"],
|
| 415 |
-
results["heating"]["component_loads"]["doors"],
|
| 416 |
-
results["heating"]["component_loads"]["infiltration"],
|
| 417 |
-
results["heating"]["component_loads"]["ventilation"]
|
| 418 |
-
],
|
| 419 |
-
"Percentage (%)": [
|
| 420 |
-
results["heating"]["component_loads"]["walls"] / results["heating"]["total_load"] * 100,
|
| 421 |
-
results["heating"]["component_loads"]["roof"] / results["heating"]["total_load"] * 100,
|
| 422 |
-
results["heating"]["component_loads"]["floor"] / results["heating"]["total_load"] * 100,
|
| 423 |
-
results["heating"]["component_loads"]["windows"] / results["heating"]["total_load"] * 100,
|
| 424 |
-
results["heating"]["component_loads"]["doors"] / results["heating"]["total_load"] * 100,
|
| 425 |
-
results["heating"]["component_loads"]["infiltration"] / results["heating"]["total_load"] * 100,
|
| 426 |
-
results["heating"]["component_loads"]["ventilation"] / results["heating"]["total_load"] * 100
|
| 427 |
-
]
|
| 428 |
-
}
|
| 429 |
-
|
| 430 |
-
dataframes["Heating Components"] = pd.DataFrame(component_data)
|
| 431 |
-
|
| 432 |
-
# Create detailed loads DataFrames
|
| 433 |
-
|
| 434 |
-
# Walls
|
| 435 |
-
wall_data = []
|
| 436 |
-
for wall in results["heating"]["detailed_loads"]["walls"]:
|
| 437 |
-
wall_data.append({
|
| 438 |
-
"Name": wall["name"],
|
| 439 |
-
"Orientation": wall["orientation"],
|
| 440 |
-
"Area (m²)": wall["area"],
|
| 441 |
-
"U-Value (W/m²·K)": wall["u_value"],
|
| 442 |
-
"Temperature Difference (°C)": wall["delta_t"],
|
| 443 |
-
"Load (kW)": wall["load"]
|
| 444 |
-
})
|
| 445 |
-
|
| 446 |
-
if wall_data:
|
| 447 |
-
dataframes["Heating Walls"] = pd.DataFrame(wall_data)
|
| 448 |
-
|
| 449 |
-
# Roofs
|
| 450 |
-
roof_data = []
|
| 451 |
-
for roof in results["heating"]["detailed_loads"]["roofs"]:
|
| 452 |
-
roof_data.append({
|
| 453 |
-
"Name": roof["name"],
|
| 454 |
-
"Orientation": roof["orientation"],
|
| 455 |
-
"Area (m²)": roof["area"],
|
| 456 |
-
"U-Value (W/m²·K)": roof["u_value"],
|
| 457 |
-
"Temperature Difference (°C)": roof["delta_t"],
|
| 458 |
-
"Load (kW)": roof["load"]
|
| 459 |
-
})
|
| 460 |
-
|
| 461 |
-
if roof_data:
|
| 462 |
-
dataframes["Heating Roofs"] = pd.DataFrame(roof_data)
|
| 463 |
-
|
| 464 |
-
# Floors
|
| 465 |
-
floor_data = []
|
| 466 |
-
for floor in results["heating"]["detailed_loads"]["floors"]:
|
| 467 |
-
floor_data.append({
|
| 468 |
-
"Name": floor["name"],
|
| 469 |
-
"Area (m²)": floor["area"],
|
| 470 |
-
"U-Value (W/m²·K)": floor["u_value"],
|
| 471 |
-
"Temperature Difference (°C)": floor["delta_t"],
|
| 472 |
-
"Load (kW)": floor["load"]
|
| 473 |
-
})
|
| 474 |
-
|
| 475 |
-
if floor_data:
|
| 476 |
-
dataframes["Heating Floors"] = pd.DataFrame(floor_data)
|
| 477 |
-
|
| 478 |
-
# Windows
|
| 479 |
-
window_data = []
|
| 480 |
-
for window in results["heating"]["detailed_loads"]["windows"]:
|
| 481 |
-
window_data.append({
|
| 482 |
-
"Name": window["name"],
|
| 483 |
-
"Orientation": window["orientation"],
|
| 484 |
-
"Area (m²)": window["area"],
|
| 485 |
-
"U-Value (W/m²·K)": window["u_value"],
|
| 486 |
-
"Temperature Difference (°C)": window["delta_t"],
|
| 487 |
-
"Load (kW)": window["load"]
|
| 488 |
-
})
|
| 489 |
-
|
| 490 |
-
if window_data:
|
| 491 |
-
dataframes["Heating Windows"] = pd.DataFrame(window_data)
|
| 492 |
-
|
| 493 |
-
# Doors
|
| 494 |
-
door_data = []
|
| 495 |
-
for door in results["heating"]["detailed_loads"]["doors"]:
|
| 496 |
-
door_data.append({
|
| 497 |
-
"Name": door["name"],
|
| 498 |
-
"Orientation": door["orientation"],
|
| 499 |
-
"Area (m²)": door["area"],
|
| 500 |
-
"U-Value (W/m²·K)": door["u_value"],
|
| 501 |
-
"Temperature Difference (°C)": door["delta_t"],
|
| 502 |
-
"Load (kW)": door["load"]
|
| 503 |
-
})
|
| 504 |
-
|
| 505 |
-
if door_data:
|
| 506 |
-
dataframes["Heating Doors"] = pd.DataFrame(door_data)
|
| 507 |
-
|
| 508 |
-
# Infiltration and ventilation
|
| 509 |
-
air_data = [
|
| 510 |
-
{
|
| 511 |
-
"Type": "Infiltration",
|
| 512 |
-
"Air Flow (m³/s)": results["heating"]["detailed_loads"]["infiltration"]["air_flow"],
|
| 513 |
-
"Temperature Difference (°C)": results["heating"]["detailed_loads"]["infiltration"]["delta_t"],
|
| 514 |
-
"Load (kW)": results["heating"]["detailed_loads"]["infiltration"]["load"]
|
| 515 |
-
},
|
| 516 |
-
{
|
| 517 |
-
"Type": "Ventilation",
|
| 518 |
-
"Air Flow (m³/s)": results["heating"]["detailed_loads"]["ventilation"]["air_flow"],
|
| 519 |
-
"Temperature Difference (°C)": results["heating"]["detailed_loads"]["ventilation"]["delta_t"],
|
| 520 |
-
"Load (kW)": results["heating"]["detailed_loads"]["ventilation"]["load"]
|
| 521 |
-
}
|
| 522 |
]
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
|
| 532 |
@staticmethod
|
| 533 |
def display_export_interface(session_state: Dict[str, Any]) -> None:
|
|
@@ -537,28 +497,24 @@ class DataExport:
|
|
| 537 |
Args:
|
| 538 |
session_state: Streamlit session state containing calculation results
|
| 539 |
"""
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
DataExport._display_scenario_export(session_state)
|
| 559 |
-
except Exception as e:
|
| 560 |
-
logger.error(f"Error displaying export interface: {str(e)}")
|
| 561 |
-
st.error(f"Error displaying export interface: {str(e)}")
|
| 562 |
|
| 563 |
@staticmethod
|
| 564 |
def _display_csv_export(session_state: Dict[str, Any]) -> None:
|
|
@@ -568,49 +524,45 @@ class DataExport:
|
|
| 568 |
Args:
|
| 569 |
session_state: Streamlit session state containing calculation results
|
| 570 |
"""
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
|
| 584 |
-
#
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
#
|
| 598 |
-
|
| 599 |
|
| 600 |
-
#
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
csv_data = DataExport.export_to_csv(df)
|
| 607 |
-
if csv_data:
|
| 608 |
-
filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 609 |
-
download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
|
| 610 |
-
st.markdown(download_link, unsafe_allow_html=True)
|
| 611 |
-
except Exception as e:
|
| 612 |
-
logger.error(f"Error in CSV export interface: {str(e)}")
|
| 613 |
-
st.error(f"Error in CSV export interface: {str(e)}")
|
| 614 |
|
| 615 |
@staticmethod
|
| 616 |
def _display_excel_export(session_state: Dict[str, Any]) -> None:
|
|
@@ -620,113 +572,109 @@ class DataExport:
|
|
| 620 |
Args:
|
| 621 |
session_state: Streamlit session state containing calculation results
|
| 622 |
"""
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
]
|
| 697 |
-
|
| 698 |
-
combined_dfs["Project Information"] = pd.DataFrame(project_info)
|
| 699 |
-
|
| 700 |
-
# Add cooling load DataFrames
|
| 701 |
-
cooling_dfs = DataExport.create_cooling_load_dataframes(results)
|
| 702 |
-
for sheet_name, df in cooling_dfs.items():
|
| 703 |
-
combined_dfs[sheet_name] = df
|
| 704 |
-
|
| 705 |
-
# Add heating load DataFrames
|
| 706 |
-
heating_dfs = DataExport.create_heating_load_dataframes(results)
|
| 707 |
-
for sheet_name, df in heating_dfs.items():
|
| 708 |
-
combined_dfs[sheet_name] = df
|
| 709 |
-
|
| 710 |
-
# Add download button
|
| 711 |
-
excel_data = DataExport.export_to_excel(combined_dfs)
|
| 712 |
-
if excel_data:
|
| 713 |
-
filename = f"hvac_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
| 714 |
-
download_link = DataExport.get_download_link(
|
| 715 |
-
excel_data,
|
| 716 |
-
filename,
|
| 717 |
-
"Download Combined Excel Report",
|
| 718 |
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
| 719 |
-
)
|
| 720 |
-
st.markdown(download_link, unsafe_allow_html=True)
|
| 721 |
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
|
| 731 |
@staticmethod
|
| 732 |
def _display_scenario_export(session_state: Dict[str, Any]) -> None:
|
|
@@ -736,90 +684,90 @@ class DataExport:
|
|
| 736 |
Args:
|
| 737 |
session_state: Streamlit session state containing calculation results
|
| 738 |
"""
|
| 739 |
-
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
| 741 |
|
| 742 |
-
#
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
"building_info": session_state["building_info"],
|
| 757 |
-
"components": session_state["components"],
|
| 758 |
-
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 759 |
-
}
|
| 760 |
-
|
| 761 |
-
st.success(f"Scenario '{scenario_name}' saved successfully!")
|
| 762 |
-
st.experimental_rerun()
|
| 763 |
-
else:
|
| 764 |
-
# Display saved scenarios
|
| 765 |
-
st.write("### Saved Scenarios")
|
| 766 |
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 770 |
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
# Display scenario information
|
| 776 |
-
st.write(f"**Scenario:** {selected_scenario}")
|
| 777 |
-
st.write(f"**Timestamp:** {scenario['timestamp']}")
|
| 778 |
-
|
| 779 |
-
# Add download button
|
| 780 |
-
json_data = DataExport.export_scenario_to_json(scenario)
|
| 781 |
-
if json_data:
|
| 782 |
-
filename = f"{selected_scenario.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 783 |
-
download_link = DataExport.get_download_link(
|
| 784 |
-
json_data,
|
| 785 |
-
filename,
|
| 786 |
-
"Download Scenario JSON",
|
| 787 |
-
"application/json"
|
| 788 |
-
)
|
| 789 |
-
st.markdown(download_link, unsafe_allow_html=True)
|
| 790 |
|
| 791 |
-
# Add
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
from io import BytesIO
|
| 796 |
-
|
| 797 |
-
zip_buffer = BytesIO()
|
| 798 |
-
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
| 799 |
-
for scenario_name, scenario in session_state["saved_scenarios"].items():
|
| 800 |
-
# Export scenario to JSON
|
| 801 |
-
json_data = DataExport.export_scenario_to_json(scenario)
|
| 802 |
-
if json_data:
|
| 803 |
-
filename = f"{scenario_name.replace(' ', '_').lower()}.json"
|
| 804 |
-
zip_file.writestr(filename, json_data)
|
| 805 |
-
|
| 806 |
-
# Add download button for zip file
|
| 807 |
-
zip_buffer.seek(0)
|
| 808 |
-
zip_data = zip_buffer.getvalue()
|
| 809 |
-
|
| 810 |
-
filename = f"all_scenarios_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
| 811 |
download_link = DataExport.get_download_link(
|
| 812 |
-
|
| 813 |
filename,
|
| 814 |
-
"Download
|
| 815 |
-
"application/
|
| 816 |
)
|
| 817 |
st.markdown(download_link, unsafe_allow_html=True)
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
|
|
|
|
|
|
|
| 822 |
|
|
|
|
| 823 |
if __name__ == "__main__":
|
| 824 |
import streamlit as st
|
| 825 |
|
|
@@ -918,6 +866,5 @@ if __name__ == "__main__":
|
|
| 918 |
}
|
| 919 |
}
|
| 920 |
|
| 921 |
-
#
|
| 922 |
-
data_export
|
| 923 |
-
data_export.display(st.session_state)
|
|
|
|
| 13 |
import io
|
| 14 |
from datetime import datetime
|
| 15 |
import xlsxwriter
|
|
|
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
class DataExport:
|
| 19 |
"""Class for data export functionality."""
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
@staticmethod
|
| 22 |
def export_to_csv(data: Dict[str, Any], file_path: str = None) -> Optional[str]:
|
| 23 |
"""
|
|
|
|
| 46 |
return csv_data
|
| 47 |
|
| 48 |
except Exception as e:
|
| 49 |
+
st.error(f"Error exporting to CSV: {e}")
|
|
|
|
| 50 |
return None
|
| 51 |
|
| 52 |
@staticmethod
|
|
|
|
| 93 |
return None
|
| 94 |
|
| 95 |
except Exception as e:
|
| 96 |
+
st.error(f"Error exporting to Excel: {e}")
|
|
|
|
| 97 |
return None
|
| 98 |
|
| 99 |
@staticmethod
|
|
|
|
| 122 |
return json_data
|
| 123 |
|
| 124 |
except Exception as e:
|
| 125 |
+
st.error(f"Error exporting scenario to JSON: {e}")
|
|
|
|
| 126 |
return None
|
| 127 |
|
| 128 |
@staticmethod
|
|
|
|
| 139 |
Returns:
|
| 140 |
HTML string with download link
|
| 141 |
"""
|
| 142 |
+
if isinstance(data, str):
|
| 143 |
+
b64 = base64.b64encode(data.encode()).decode()
|
| 144 |
+
else:
|
| 145 |
+
b64 = base64.b64encode(data).decode()
|
| 146 |
+
|
| 147 |
+
href = f'<a href="data:{mime_type};base64,{b64}" download="{filename}">{text}</a>'
|
| 148 |
+
return href
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
@staticmethod
|
| 151 |
def create_cooling_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
|
|
|
|
| 158 |
Returns:
|
| 159 |
Dictionary with DataFrames for Excel export
|
| 160 |
"""
|
| 161 |
+
dataframes = {}
|
| 162 |
+
|
| 163 |
+
# Create summary DataFrame
|
| 164 |
+
summary_data = {
|
| 165 |
+
"Metric": [
|
| 166 |
+
"Total Cooling Load",
|
| 167 |
+
"Sensible Cooling Load",
|
| 168 |
+
"Latent Cooling Load",
|
| 169 |
+
"Cooling Load per Area"
|
| 170 |
+
],
|
| 171 |
+
"Value": [
|
| 172 |
+
results["cooling"]["total_load"],
|
| 173 |
+
results["cooling"]["sensible_load"],
|
| 174 |
+
results["cooling"]["latent_load"],
|
| 175 |
+
results["cooling"]["load_per_area"]
|
| 176 |
+
],
|
| 177 |
+
"Unit": [
|
| 178 |
+
"kW",
|
| 179 |
+
"kW",
|
| 180 |
+
"kW",
|
| 181 |
+
"W/m²"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
]
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
dataframes["Cooling Summary"] = pd.DataFrame(summary_data)
|
| 186 |
+
|
| 187 |
+
# Create component breakdown DataFrame
|
| 188 |
+
component_data = {
|
| 189 |
+
"Component": [
|
| 190 |
+
"Walls",
|
| 191 |
+
"Roof",
|
| 192 |
+
"Windows",
|
| 193 |
+
"Doors",
|
| 194 |
+
"People",
|
| 195 |
+
"Lighting",
|
| 196 |
+
"Equipment",
|
| 197 |
+
"Infiltration",
|
| 198 |
+
"Ventilation"
|
| 199 |
+
],
|
| 200 |
+
"Load (kW)": [
|
| 201 |
+
results["cooling"]["component_loads"]["walls"],
|
| 202 |
+
results["cooling"]["component_loads"]["roof"],
|
| 203 |
+
results["cooling"]["component_loads"]["windows"],
|
| 204 |
+
results["cooling"]["component_loads"]["doors"],
|
| 205 |
+
results["cooling"]["component_loads"]["people"],
|
| 206 |
+
results["cooling"]["component_loads"]["lighting"],
|
| 207 |
+
results["cooling"]["component_loads"]["equipment"],
|
| 208 |
+
results["cooling"]["component_loads"]["infiltration"],
|
| 209 |
+
results["cooling"]["component_loads"]["ventilation"]
|
| 210 |
+
],
|
| 211 |
+
"Percentage (%)": [
|
| 212 |
+
results["cooling"]["component_loads"]["walls"] / results["cooling"]["total_load"] * 100,
|
| 213 |
+
results["cooling"]["component_loads"]["roof"] / results["cooling"]["total_load"] * 100,
|
| 214 |
+
results["cooling"]["component_loads"]["windows"] / results["cooling"]["total_load"] * 100,
|
| 215 |
+
results["cooling"]["component_loads"]["doors"] / results["cooling"]["total_load"] * 100,
|
| 216 |
+
results["cooling"]["component_loads"]["people"] / results["cooling"]["total_load"] * 100,
|
| 217 |
+
results["cooling"]["component_loads"]["lighting"] / results["cooling"]["total_load"] * 100,
|
| 218 |
+
results["cooling"]["component_loads"]["equipment"] / results["cooling"]["total_load"] * 100,
|
| 219 |
+
results["cooling"]["component_loads"]["infiltration"] / results["cooling"]["total_load"] * 100,
|
| 220 |
+
results["cooling"]["component_loads"]["ventilation"] / results["cooling"]["total_load"] * 100
|
| 221 |
+
]
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
dataframes["Cooling Components"] = pd.DataFrame(component_data)
|
| 225 |
+
|
| 226 |
+
# Create detailed loads DataFrames
|
| 227 |
+
|
| 228 |
+
# Walls
|
| 229 |
+
wall_data = []
|
| 230 |
+
for wall in results["cooling"]["detailed_loads"]["walls"]:
|
| 231 |
+
wall_data.append({
|
| 232 |
+
"Name": wall["name"],
|
| 233 |
+
"Orientation": wall["orientation"],
|
| 234 |
+
"Area (m²)": wall["area"],
|
| 235 |
+
"U-Value (W/m²·K)": wall["u_value"],
|
| 236 |
+
"CLTD (°C)": wall["cltd"],
|
| 237 |
+
"Load (kW)": wall["load"]
|
| 238 |
+
})
|
| 239 |
+
|
| 240 |
+
if wall_data:
|
| 241 |
+
dataframes["Cooling Walls"] = pd.DataFrame(wall_data)
|
| 242 |
+
|
| 243 |
+
# Roofs
|
| 244 |
+
roof_data = []
|
| 245 |
+
for roof in results["cooling"]["detailed_loads"]["roofs"]:
|
| 246 |
+
roof_data.append({
|
| 247 |
+
"Name": roof["name"],
|
| 248 |
+
"Orientation": roof["orientation"],
|
| 249 |
+
"Area (m²)": roof["area"],
|
| 250 |
+
"U-Value (W/m²·K)": roof["u_value"],
|
| 251 |
+
"CLTD (°C)": roof["cltd"],
|
| 252 |
+
"Load (kW)": roof["load"]
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
if roof_data:
|
| 256 |
+
dataframes["Cooling Roofs"] = pd.DataFrame(roof_data)
|
| 257 |
+
|
| 258 |
+
# Windows
|
| 259 |
+
window_data = []
|
| 260 |
+
for window in results["cooling"]["detailed_loads"]["windows"]:
|
| 261 |
+
window_data.append({
|
| 262 |
+
"Name": window["name"],
|
| 263 |
+
"Orientation": window["orientation"],
|
| 264 |
+
"Area (m²)": window["area"],
|
| 265 |
+
"U-Value (W/m²·K)": window["u_value"],
|
| 266 |
+
"SHGC": window["shgc"],
|
| 267 |
+
"SCL (W/m²)": window["scl"],
|
| 268 |
+
"Load (kW)": window["load"]
|
| 269 |
+
})
|
| 270 |
+
|
| 271 |
+
if window_data:
|
| 272 |
+
dataframes["Cooling Windows"] = pd.DataFrame(window_data)
|
| 273 |
+
|
| 274 |
+
# Doors
|
| 275 |
+
door_data = []
|
| 276 |
+
for door in results["cooling"]["detailed_loads"]["doors"]:
|
| 277 |
+
door_data.append({
|
| 278 |
+
"Name": door["name"],
|
| 279 |
+
"Orientation": door["orientation"],
|
| 280 |
+
"Area (m²)": door["area"],
|
| 281 |
+
"U-Value (W/m²·K)": door["u_value"],
|
| 282 |
+
"CLTD (°C)": door["cltd"],
|
| 283 |
+
"Load (kW)": door["load"]
|
| 284 |
+
})
|
| 285 |
+
|
| 286 |
+
if door_data:
|
| 287 |
+
dataframes["Cooling Doors"] = pd.DataFrame(door_data)
|
| 288 |
+
|
| 289 |
+
# Internal loads
|
| 290 |
+
internal_data = []
|
| 291 |
+
for internal_load in results["cooling"]["detailed_loads"]["internal"]:
|
| 292 |
+
internal_data.append({
|
| 293 |
+
"Type": internal_load["type"],
|
| 294 |
+
"Name": internal_load["name"],
|
| 295 |
+
"Quantity": internal_load["quantity"],
|
| 296 |
+
"Heat Gain (W)": internal_load["heat_gain"],
|
| 297 |
+
"CLF": internal_load["clf"],
|
| 298 |
+
"Load (kW)": internal_load["load"]
|
| 299 |
+
})
|
| 300 |
+
|
| 301 |
+
if internal_data:
|
| 302 |
+
dataframes["Cooling Internal Loads"] = pd.DataFrame(internal_data)
|
| 303 |
+
|
| 304 |
+
# Infiltration and ventilation
|
| 305 |
+
air_data = [
|
| 306 |
+
{
|
| 307 |
+
"Type": "Infiltration",
|
| 308 |
+
"Air Flow (m³/s)": results["cooling"]["detailed_loads"]["infiltration"]["air_flow"],
|
| 309 |
+
"Sensible Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["sensible_load"],
|
| 310 |
+
"Latent Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["latent_load"],
|
| 311 |
+
"Total Load (kW)": results["cooling"]["detailed_loads"]["infiltration"]["total_load"]
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
"Type": "Ventilation",
|
| 315 |
+
"Air Flow (m³/s)": results["cooling"]["detailed_loads"]["ventilation"]["air_flow"],
|
| 316 |
+
"Sensible Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["sensible_load"],
|
| 317 |
+
"Latent Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["latent_load"],
|
| 318 |
+
"Total Load (kW)": results["cooling"]["detailed_loads"]["ventilation"]["total_load"]
|
| 319 |
+
}
|
| 320 |
+
]
|
| 321 |
+
|
| 322 |
+
dataframes["Cooling Air Exchange"] = pd.DataFrame(air_data)
|
| 323 |
+
|
| 324 |
+
return dataframes
|
| 325 |
|
| 326 |
@staticmethod
|
| 327 |
def create_heating_load_dataframes(results: Dict[str, Any]) -> Dict[str, pd.DataFrame]:
|
|
|
|
| 334 |
Returns:
|
| 335 |
Dictionary with DataFrames for Excel export
|
| 336 |
"""
|
| 337 |
+
dataframes = {}
|
| 338 |
+
|
| 339 |
+
# Create summary DataFrame
|
| 340 |
+
summary_data = {
|
| 341 |
+
"Metric": [
|
| 342 |
+
"Total Heating Load",
|
| 343 |
+
"Heating Load per Area",
|
| 344 |
+
"Design Heat Loss",
|
| 345 |
+
"Safety Factor"
|
| 346 |
+
],
|
| 347 |
+
"Value": [
|
| 348 |
+
results["heating"]["total_load"],
|
| 349 |
+
results["heating"]["load_per_area"],
|
| 350 |
+
results["heating"]["design_heat_loss"],
|
| 351 |
+
results["heating"]["safety_factor"]
|
| 352 |
+
],
|
| 353 |
+
"Unit": [
|
| 354 |
+
"kW",
|
| 355 |
+
"W/m²",
|
| 356 |
+
"kW",
|
| 357 |
+
"%"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
]
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
dataframes["Heating Summary"] = pd.DataFrame(summary_data)
|
| 362 |
+
|
| 363 |
+
# Create component breakdown DataFrame
|
| 364 |
+
component_data = {
|
| 365 |
+
"Component": [
|
| 366 |
+
"Walls",
|
| 367 |
+
"Roof",
|
| 368 |
+
"Floor",
|
| 369 |
+
"Windows",
|
| 370 |
+
"Doors",
|
| 371 |
+
"Infiltration",
|
| 372 |
+
"Ventilation"
|
| 373 |
+
],
|
| 374 |
+
"Load (kW)": [
|
| 375 |
+
results["heating"]["component_loads"]["walls"],
|
| 376 |
+
results["heating"]["component_loads"]["roof"],
|
| 377 |
+
results["heating"]["component_loads"]["floor"],
|
| 378 |
+
results["heating"]["component_loads"]["windows"],
|
| 379 |
+
results["heating"]["component_loads"]["doors"],
|
| 380 |
+
results["heating"]["component_loads"]["infiltration"],
|
| 381 |
+
results["heating"]["component_loads"]["ventilation"]
|
| 382 |
+
],
|
| 383 |
+
"Percentage (%)": [
|
| 384 |
+
results["heating"]["component_loads"]["walls"] / results["heating"]["total_load"] * 100,
|
| 385 |
+
results["heating"]["component_loads"]["roof"] / results["heating"]["total_load"] * 100,
|
| 386 |
+
results["heating"]["component_loads"]["floor"] / results["heating"]["total_load"] * 100,
|
| 387 |
+
results["heating"]["component_loads"]["windows"] / results["heating"]["total_load"] * 100,
|
| 388 |
+
results["heating"]["component_loads"]["doors"] / results["heating"]["total_load"] * 100,
|
| 389 |
+
results["heating"]["component_loads"]["infiltration"] / results["heating"]["total_load"] * 100,
|
| 390 |
+
results["heating"]["component_loads"]["ventilation"] / results["heating"]["total_load"] * 100
|
| 391 |
+
]
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
dataframes["Heating Components"] = pd.DataFrame(component_data)
|
| 395 |
+
|
| 396 |
+
# Create detailed loads DataFrames
|
| 397 |
+
|
| 398 |
+
# Walls
|
| 399 |
+
wall_data = []
|
| 400 |
+
for wall in results["heating"]["detailed_loads"]["walls"]:
|
| 401 |
+
wall_data.append({
|
| 402 |
+
"Name": wall["name"],
|
| 403 |
+
"Orientation": wall["orientation"],
|
| 404 |
+
"Area (m²)": wall["area"],
|
| 405 |
+
"U-Value (W/m²·K)": wall["u_value"],
|
| 406 |
+
"Temperature Difference (°C)": wall["delta_t"],
|
| 407 |
+
"Load (kW)": wall["load"]
|
| 408 |
+
})
|
| 409 |
+
|
| 410 |
+
if wall_data:
|
| 411 |
+
dataframes["Heating Walls"] = pd.DataFrame(wall_data)
|
| 412 |
+
|
| 413 |
+
# Roofs
|
| 414 |
+
roof_data = []
|
| 415 |
+
for roof in results["heating"]["detailed_loads"]["roofs"]:
|
| 416 |
+
roof_data.append({
|
| 417 |
+
"Name": roof["name"],
|
| 418 |
+
"Orientation": roof["orientation"],
|
| 419 |
+
"Area (m²)": roof["area"],
|
| 420 |
+
"U-Value (W/m²·K)": roof["u_value"],
|
| 421 |
+
"Temperature Difference (°C)": roof["delta_t"],
|
| 422 |
+
"Load (kW)": roof["load"]
|
| 423 |
+
})
|
| 424 |
+
|
| 425 |
+
if roof_data:
|
| 426 |
+
dataframes["Heating Roofs"] = pd.DataFrame(roof_data)
|
| 427 |
+
|
| 428 |
+
# Floors
|
| 429 |
+
floor_data = []
|
| 430 |
+
for floor in results["heating"]["detailed_loads"]["floors"]:
|
| 431 |
+
floor_data.append({
|
| 432 |
+
"Name": floor["name"],
|
| 433 |
+
"Area (m²)": floor["area"],
|
| 434 |
+
"U-Value (W/m²·K)": floor["u_value"],
|
| 435 |
+
"Temperature Difference (°C)": floor["delta_t"],
|
| 436 |
+
"Load (kW)": floor["load"]
|
| 437 |
+
})
|
| 438 |
+
|
| 439 |
+
if floor_data:
|
| 440 |
+
dataframes["Heating Floors"] = pd.DataFrame(floor_data)
|
| 441 |
+
|
| 442 |
+
# Windows
|
| 443 |
+
window_data = []
|
| 444 |
+
for window in results["heating"]["detailed_loads"]["windows"]:
|
| 445 |
+
window_data.append({
|
| 446 |
+
"Name": window["name"],
|
| 447 |
+
"Orientation": window["orientation"],
|
| 448 |
+
"Area (m²)": window["area"],
|
| 449 |
+
"U-Value (W/m²·K)": window["u_value"],
|
| 450 |
+
"Temperature Difference (°C)": window["delta_t"],
|
| 451 |
+
"Load (kW)": window["load"]
|
| 452 |
+
})
|
| 453 |
+
|
| 454 |
+
if window_data:
|
| 455 |
+
dataframes["Heating Windows"] = pd.DataFrame(window_data)
|
| 456 |
+
|
| 457 |
+
# Doors
|
| 458 |
+
door_data = []
|
| 459 |
+
for door in results["heating"]["detailed_loads"]["doors"]:
|
| 460 |
+
door_data.append({
|
| 461 |
+
"Name": door["name"],
|
| 462 |
+
"Orientation": door["orientation"],
|
| 463 |
+
"Area (m²)": door["area"],
|
| 464 |
+
"U-Value (W/m²·K)": door["u_value"],
|
| 465 |
+
"Temperature Difference (°C)": door["delta_t"],
|
| 466 |
+
"Load (kW)": door["load"]
|
| 467 |
+
})
|
| 468 |
+
|
| 469 |
+
if door_data:
|
| 470 |
+
dataframes["Heating Doors"] = pd.DataFrame(door_data)
|
| 471 |
+
|
| 472 |
+
# Infiltration and ventilation
|
| 473 |
+
air_data = [
|
| 474 |
+
{
|
| 475 |
+
"Type": "Infiltration",
|
| 476 |
+
"Air Flow (m³/s)": results["heating"]["detailed_loads"]["infiltration"]["air_flow"],
|
| 477 |
+
"Temperature Difference (°C)": results["heating"]["detailed_loads"]["infiltration"]["delta_t"],
|
| 478 |
+
"Load (kW)": results["heating"]["detailed_loads"]["infiltration"]["load"]
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"Type": "Ventilation",
|
| 482 |
+
"Air Flow (m³/s)": results["heating"]["detailed_loads"]["ventilation"]["air_flow"],
|
| 483 |
+
"Temperature Difference (°C)": results["heating"]["detailed_loads"]["ventilation"]["delta_t"],
|
| 484 |
+
"Load (kW)": results["heating"]["detailed_loads"]["ventilation"]["load"]
|
| 485 |
+
}
|
| 486 |
+
]
|
| 487 |
+
|
| 488 |
+
dataframes["Heating Air Exchange"] = pd.DataFrame(air_data)
|
| 489 |
+
|
| 490 |
+
return dataframes
|
| 491 |
|
| 492 |
@staticmethod
|
| 493 |
def display_export_interface(session_state: Dict[str, Any]) -> None:
|
|
|
|
| 497 |
Args:
|
| 498 |
session_state: Streamlit session state containing calculation results
|
| 499 |
"""
|
| 500 |
+
st.header("Export Results")
|
| 501 |
+
|
| 502 |
+
# Check if calculations have been performed
|
| 503 |
+
if "calculation_results" not in session_state or not session_state["calculation_results"]:
|
| 504 |
+
st.warning("No calculation results available. Please run calculations first.")
|
| 505 |
+
return
|
| 506 |
+
|
| 507 |
+
# Create tabs for different export options
|
| 508 |
+
tab1, tab2, tab3 = st.tabs(["CSV Export", "Excel Export", "Scenario Export"])
|
| 509 |
+
|
| 510 |
+
with tab1:
|
| 511 |
+
DataExport._display_csv_export(session_state)
|
| 512 |
+
|
| 513 |
+
with tab2:
|
| 514 |
+
DataExport._display_excel_export(session_state)
|
| 515 |
+
|
| 516 |
+
with tab3:
|
| 517 |
+
DataExport._display_scenario_export(session_state)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
|
| 519 |
@staticmethod
|
| 520 |
def _display_csv_export(session_state: Dict[str, Any]) -> None:
|
|
|
|
| 524 |
Args:
|
| 525 |
session_state: Streamlit session state containing calculation results
|
| 526 |
"""
|
| 527 |
+
st.subheader("CSV Export")
|
| 528 |
+
|
| 529 |
+
# Get results
|
| 530 |
+
results = session_state["calculation_results"]
|
| 531 |
+
|
| 532 |
+
# Create tabs for cooling and heating loads
|
| 533 |
+
tab1, tab2 = st.tabs(["Cooling Load CSV", "Heating Load CSV"])
|
| 534 |
+
|
| 535 |
+
with tab1:
|
| 536 |
+
# Create cooling load DataFrames
|
| 537 |
+
cooling_dfs = DataExport.create_cooling_load_dataframes(results)
|
| 538 |
+
|
| 539 |
+
# Display and export each DataFrame
|
| 540 |
+
for sheet_name, df in cooling_dfs.items():
|
| 541 |
+
st.write(f"### {sheet_name}")
|
| 542 |
+
st.dataframe(df)
|
| 543 |
|
| 544 |
+
# Add download button
|
| 545 |
+
csv_data = DataExport.export_to_csv(df)
|
| 546 |
+
if csv_data:
|
| 547 |
+
filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 548 |
+
download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
|
| 549 |
+
st.markdown(download_link, unsafe_allow_html=True)
|
| 550 |
+
|
| 551 |
+
with tab2:
|
| 552 |
+
# Create heating load DataFrames
|
| 553 |
+
heating_dfs = DataExport.create_heating_load_dataframes(results)
|
| 554 |
+
|
| 555 |
+
# Display and export each DataFrame
|
| 556 |
+
for sheet_name, df in heating_dfs.items():
|
| 557 |
+
st.write(f"### {sheet_name}")
|
| 558 |
+
st.dataframe(df)
|
| 559 |
|
| 560 |
+
# Add download button
|
| 561 |
+
csv_data = DataExport.export_to_csv(df)
|
| 562 |
+
if csv_data:
|
| 563 |
+
filename = f"{sheet_name.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 564 |
+
download_link = DataExport.get_download_link(csv_data, filename, f"Download {sheet_name} CSV")
|
| 565 |
+
st.markdown(download_link, unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
@staticmethod
|
| 568 |
def _display_excel_export(session_state: Dict[str, Any]) -> None:
|
|
|
|
| 572 |
Args:
|
| 573 |
session_state: Streamlit session state containing calculation results
|
| 574 |
"""
|
| 575 |
+
st.subheader("Excel Export")
|
| 576 |
+
|
| 577 |
+
# Get results
|
| 578 |
+
results = session_state["calculation_results"]
|
| 579 |
+
|
| 580 |
+
# Create tabs for cooling, heating, and combined loads
|
| 581 |
+
tab1, tab2, tab3 = st.tabs(["Cooling Load Excel", "Heating Load Excel", "Combined Excel"])
|
| 582 |
+
|
| 583 |
+
with tab1:
|
| 584 |
+
# Create cooling load DataFrames
|
| 585 |
+
cooling_dfs = DataExport.create_cooling_load_dataframes(results)
|
| 586 |
+
|
| 587 |
+
# Add download button
|
| 588 |
+
excel_data = DataExport.export_to_excel(cooling_dfs)
|
| 589 |
+
if excel_data:
|
| 590 |
+
filename = f"cooling_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
| 591 |
+
download_link = DataExport.get_download_link(
|
| 592 |
+
excel_data,
|
| 593 |
+
filename,
|
| 594 |
+
"Download Cooling Load Excel",
|
| 595 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
| 596 |
+
)
|
| 597 |
+
st.markdown(download_link, unsafe_allow_html=True)
|
| 598 |
+
|
| 599 |
+
# Display preview
|
| 600 |
+
st.write("### Excel Preview")
|
| 601 |
+
st.write("The Excel file will contain the following sheets:")
|
| 602 |
+
for sheet_name in cooling_dfs.keys():
|
| 603 |
+
st.write(f"- {sheet_name}")
|
| 604 |
+
|
| 605 |
+
with tab2:
|
| 606 |
+
# Create heating load DataFrames
|
| 607 |
+
heating_dfs = DataExport.create_heating_load_dataframes(results)
|
| 608 |
+
|
| 609 |
+
# Add download button
|
| 610 |
+
excel_data = DataExport.export_to_excel(heating_dfs)
|
| 611 |
+
if excel_data:
|
| 612 |
+
filename = f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
| 613 |
+
download_link = DataExport.get_download_link(
|
| 614 |
+
excel_data,
|
| 615 |
+
filename,
|
| 616 |
+
"Download Heating Load Excel",
|
| 617 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
| 618 |
+
)
|
| 619 |
+
st.markdown(download_link, unsafe_allow_html=True)
|
| 620 |
+
|
| 621 |
+
# Display preview
|
| 622 |
+
st.write("### Excel Preview")
|
| 623 |
+
st.write("The Excel file will contain the following sheets:")
|
| 624 |
+
for sheet_name in heating_dfs.keys():
|
| 625 |
+
st.write(f"- {sheet_name}")
|
| 626 |
+
|
| 627 |
+
with tab3:
|
| 628 |
+
# Create combined DataFrames
|
| 629 |
+
combined_dfs = {}
|
| 630 |
+
|
| 631 |
+
# Add project information
|
| 632 |
+
if "building_info" in session_state:
|
| 633 |
+
project_info = [
|
| 634 |
+
{"Parameter": "Project Name", "Value": session_state["building_info"].get("project_name", "")},
|
| 635 |
+
{"Parameter": "Building Name", "Value": session_state["building_info"].get("building_name", "")},
|
| 636 |
+
{"Parameter": "Location", "Value": session_state["building_info"].get("location", "")},
|
| 637 |
+
{"Parameter": "Climate Zone", "Value": session_state["building_info"].get("climate_zone", "")},
|
| 638 |
+
{"Parameter": "Building Type", "Value": session_state["building_info"].get("building_type", "")},
|
| 639 |
+
{"Parameter": "Floor Area", "Value": session_state["building_info"].get("floor_area", "")},
|
| 640 |
+
{"Parameter": "Number of Floors", "Value": session_state["building_info"].get("num_floors", "")},
|
| 641 |
+
{"Parameter": "Floor Height", "Value": session_state["building_info"].get("floor_height", "")},
|
| 642 |
+
{"Parameter": "Orientation", "Value": session_state["building_info"].get("orientation", "")},
|
| 643 |
+
{"Parameter": "Occupancy", "Value": session_state["building_info"].get("occupancy", "")},
|
| 644 |
+
{"Parameter": "Operating Hours", "Value": session_state["building_info"].get("operating_hours", "")},
|
| 645 |
+
{"Parameter": "Date", "Value": datetime.now().strftime("%Y-%m-%d")},
|
| 646 |
+
{"Parameter": "Time", "Value": datetime.now().strftime("%H:%M:%S")}
|
| 647 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
+
combined_dfs["Project Information"] = pd.DataFrame(project_info)
|
| 650 |
+
|
| 651 |
+
# Add cooling load DataFrames
|
| 652 |
+
cooling_dfs = DataExport.create_cooling_load_dataframes(results)
|
| 653 |
+
for sheet_name, df in cooling_dfs.items():
|
| 654 |
+
combined_dfs[sheet_name] = df
|
| 655 |
+
|
| 656 |
+
# Add heating load DataFrames
|
| 657 |
+
heating_dfs = DataExport.create_heating_load_dataframes(results)
|
| 658 |
+
for sheet_name, df in heating_dfs.items():
|
| 659 |
+
combined_dfs[sheet_name] = df
|
| 660 |
+
|
| 661 |
+
# Add download button
|
| 662 |
+
excel_data = DataExport.export_to_excel(combined_dfs)
|
| 663 |
+
if excel_data:
|
| 664 |
+
filename = f"hvac_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
| 665 |
+
download_link = DataExport.get_download_link(
|
| 666 |
+
excel_data,
|
| 667 |
+
filename,
|
| 668 |
+
"Download Combined Excel Report",
|
| 669 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
| 670 |
+
)
|
| 671 |
+
st.markdown(download_link, unsafe_allow_html=True)
|
| 672 |
+
|
| 673 |
+
# Display preview
|
| 674 |
+
st.write("### Excel Preview")
|
| 675 |
+
st.write("The Excel file will contain the following sheets:")
|
| 676 |
+
for sheet_name in combined_dfs.keys():
|
| 677 |
+
st.write(f"- {sheet_name}")
|
| 678 |
|
| 679 |
@staticmethod
|
| 680 |
def _display_scenario_export(session_state: Dict[str, Any]) -> None:
|
|
|
|
| 684 |
Args:
|
| 685 |
session_state: Streamlit session state containing calculation results
|
| 686 |
"""
|
| 687 |
+
st.subheader("Scenario Export")
|
| 688 |
+
|
| 689 |
+
# Check if there are saved scenarios
|
| 690 |
+
if "saved_scenarios" not in session_state or not session_state["saved_scenarios"]:
|
| 691 |
+
st.info("No saved scenarios available for export. Save the current results as a scenario to enable export.")
|
| 692 |
|
| 693 |
+
# Add button to save current results as a scenario
|
| 694 |
+
scenario_name = st.text_input("Scenario Name", value="Baseline")
|
| 695 |
+
|
| 696 |
+
if st.button("Save Current Results as Scenario"):
|
| 697 |
+
if "saved_scenarios" not in session_state:
|
| 698 |
+
session_state["saved_scenarios"] = {}
|
| 699 |
|
| 700 |
+
# Save current results as a scenario
|
| 701 |
+
session_state["saved_scenarios"][scenario_name] = {
|
| 702 |
+
"results": session_state["calculation_results"],
|
| 703 |
+
"building_info": session_state["building_info"],
|
| 704 |
+
"components": session_state["components"],
|
| 705 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 706 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
|
| 708 |
+
st.success(f"Scenario '{scenario_name}' saved successfully!")
|
| 709 |
+
st.experimental_rerun()
|
| 710 |
+
else:
|
| 711 |
+
# Display saved scenarios
|
| 712 |
+
st.write("### Saved Scenarios")
|
| 713 |
+
|
| 714 |
+
# Create selectbox for scenarios
|
| 715 |
+
scenario_names = list(session_state["saved_scenarios"].keys())
|
| 716 |
+
selected_scenario = st.selectbox("Select Scenario to Export", scenario_names)
|
| 717 |
+
|
| 718 |
+
if selected_scenario:
|
| 719 |
+
# Get selected scenario
|
| 720 |
+
scenario = session_state["saved_scenarios"][selected_scenario]
|
| 721 |
|
| 722 |
+
# Display scenario information
|
| 723 |
+
st.write(f"**Scenario:** {selected_scenario}")
|
| 724 |
+
st.write(f"**Timestamp:** {scenario['timestamp']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
|
| 726 |
+
# Add download button
|
| 727 |
+
json_data = DataExport.export_scenario_to_json(scenario)
|
| 728 |
+
if json_data:
|
| 729 |
+
filename = f"{selected_scenario.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
download_link = DataExport.get_download_link(
|
| 731 |
+
json_data,
|
| 732 |
filename,
|
| 733 |
+
"Download Scenario JSON",
|
| 734 |
+
"application/json"
|
| 735 |
)
|
| 736 |
st.markdown(download_link, unsafe_allow_html=True)
|
| 737 |
+
|
| 738 |
+
# Add button to export all scenarios
|
| 739 |
+
if st.button("Export All Scenarios"):
|
| 740 |
+
# Create a zip file in memory
|
| 741 |
+
import zipfile
|
| 742 |
+
from io import BytesIO
|
| 743 |
+
|
| 744 |
+
zip_buffer = BytesIO()
|
| 745 |
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
| 746 |
+
for scenario_name, scenario in session_state["saved_scenarios"].items():
|
| 747 |
+
# Export scenario to JSON
|
| 748 |
+
json_data = DataExport.export_scenario_to_json(scenario)
|
| 749 |
+
if json_data:
|
| 750 |
+
filename = f"{scenario_name.replace(' ', '_').lower()}.json"
|
| 751 |
+
zip_file.writestr(filename, json_data)
|
| 752 |
+
|
| 753 |
+
# Add download button for zip file
|
| 754 |
+
zip_buffer.seek(0)
|
| 755 |
+
zip_data = zip_buffer.getvalue()
|
| 756 |
+
|
| 757 |
+
filename = f"all_scenarios_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
| 758 |
+
download_link = DataExport.get_download_link(
|
| 759 |
+
zip_data,
|
| 760 |
+
filename,
|
| 761 |
+
"Download All Scenarios (ZIP)",
|
| 762 |
+
"application/zip"
|
| 763 |
+
)
|
| 764 |
+
st.markdown(download_link, unsafe_allow_html=True)
|
| 765 |
+
|
| 766 |
|
| 767 |
+
# Create a singleton instance
|
| 768 |
+
data_export = DataExport()
|
| 769 |
|
| 770 |
+
# Example usage
|
| 771 |
if __name__ == "__main__":
|
| 772 |
import streamlit as st
|
| 773 |
|
|
|
|
| 866 |
}
|
| 867 |
}
|
| 868 |
|
| 869 |
+
# Display export interface
|
| 870 |
+
data_export.display_export_interface(st.session_state)
|
|
|
app/data_persistence.py
CHANGED
|
@@ -41,7 +41,6 @@ class DataPersistence:
|
|
| 41 |
"internal_loads": session_state.get("internal_loads", {}),
|
| 42 |
"calculation_settings": session_state.get("calculation_settings", {}),
|
| 43 |
"saved_scenarios": DataPersistence._serialize_scenarios(session_state.get("saved_scenarios", {})),
|
| 44 |
-
"climate_data": session_state.get("climate_data", {}),
|
| 45 |
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 46 |
}
|
| 47 |
|
|
@@ -83,21 +82,6 @@ class DataPersistence:
|
|
| 83 |
if json_data:
|
| 84 |
project_data = json.loads(json_data)
|
| 85 |
|
| 86 |
-
# Validate climate data latitude
|
| 87 |
-
if "climate_data" in project_data:
|
| 88 |
-
valid_latitudes = [24, 32, 40, 48, 56]
|
| 89 |
-
for location_id, location in project_data["climate_data"].items():
|
| 90 |
-
if "latitude" in location:
|
| 91 |
-
try:
|
| 92 |
-
input_latitude = float(location["latitude"])
|
| 93 |
-
nearest_latitude = min(valid_latitudes, key=lambda x: abs(x - input_latitude))
|
| 94 |
-
if abs(input_latitude - nearest_latitude) > 0.1: # Allow small float differences
|
| 95 |
-
st.warning(f"Latitude {input_latitude} for {location_id} is invalid for ASHRAE tables. Using nearest valid latitude: {nearest_latitude}.")
|
| 96 |
-
location["latitude"] = nearest_latitude
|
| 97 |
-
except ValueError:
|
| 98 |
-
st.error(f"Invalid latitude format for {location_id}: {location['latitude']}.")
|
| 99 |
-
return None
|
| 100 |
-
|
| 101 |
# Deserialize components
|
| 102 |
if "components" in project_data:
|
| 103 |
project_data["components"] = DataPersistence._deserialize_components(project_data["components"])
|
|
@@ -553,4 +537,4 @@ if __name__ == "__main__":
|
|
| 553 |
}
|
| 554 |
|
| 555 |
# Display project management interface
|
| 556 |
-
data_persistence.display_project_management(st.session_state)
|
|
|
|
| 41 |
"internal_loads": session_state.get("internal_loads", {}),
|
| 42 |
"calculation_settings": session_state.get("calculation_settings", {}),
|
| 43 |
"saved_scenarios": DataPersistence._serialize_scenarios(session_state.get("saved_scenarios", {})),
|
|
|
|
| 44 |
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 45 |
}
|
| 46 |
|
|
|
|
| 82 |
if json_data:
|
| 83 |
project_data = json.loads(json_data)
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
# Deserialize components
|
| 86 |
if "components" in project_data:
|
| 87 |
project_data["components"] = DataPersistence._deserialize_components(project_data["components"])
|
|
|
|
| 537 |
}
|
| 538 |
|
| 539 |
# Display project management interface
|
| 540 |
+
data_persistence.display_project_management(st.session_state)
|
app/main.py
CHANGED
|
@@ -11,129 +11,111 @@ import json
|
|
| 11 |
import pycountry
|
| 12 |
import os
|
| 13 |
import sys
|
| 14 |
-
import logging
|
| 15 |
from typing import Dict, List, Any, Optional, Tuple
|
| 16 |
-
from uuid import uuid4
|
| 17 |
-
|
| 18 |
-
# Configure logging
|
| 19 |
-
logging.basicConfig(level=logging.INFO)
|
| 20 |
-
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
# Import application modules
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
from app.data_export import DataExport
|
| 30 |
-
except ImportError as e:
|
| 31 |
-
logger.error(f"Error importing application modules: {str(e)}")
|
| 32 |
-
raise
|
| 33 |
|
| 34 |
# Import data modules
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
from data.building_components import Wall as WallModel, Roof as RoofModel
|
| 40 |
-
except ImportError as e:
|
| 41 |
-
logger.error(f"Error importing data modules: {str(e)}")
|
| 42 |
-
raise
|
| 43 |
|
| 44 |
# Import utility modules
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
from utils.time_based_visualization import TimeBasedVisualization
|
| 57 |
-
except ImportError as e:
|
| 58 |
-
logger.error(f"Error importing utility modules: {str(e)}")
|
| 59 |
-
raise
|
| 60 |
-
|
| 61 |
|
| 62 |
class HVACCalculator:
|
| 63 |
def __init__(self):
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
| 118 |
if 'climate_data_obj' not in st.session_state:
|
| 119 |
st.session_state.climate_data_obj = ClimateData()
|
| 120 |
self.climate_data = st.session_state.climate_data_obj
|
| 121 |
-
|
|
|
|
| 122 |
try:
|
| 123 |
if not self.climate_data.locations:
|
| 124 |
self.climate_data = ClimateData.from_json("/home/user/app/climate_data.json")
|
| 125 |
st.session_state.climate_data_obj = self.climate_data
|
| 126 |
except FileNotFoundError:
|
| 127 |
st.warning("Default climate data file not found. Please enter climate data manually.")
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
def setup_layout(self):
|
| 133 |
-
"""Setup the application layout."""
|
| 134 |
st.sidebar.title("HVAC Load Calculator")
|
| 135 |
st.sidebar.markdown("---")
|
| 136 |
-
|
| 137 |
st.sidebar.subheader("Navigation")
|
| 138 |
pages = [
|
| 139 |
"Building Information",
|
|
@@ -143,294 +125,328 @@ class HVACCalculator:
|
|
| 143 |
"Calculation Results",
|
| 144 |
"Export Data"
|
| 145 |
]
|
| 146 |
-
|
| 147 |
selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page))
|
|
|
|
| 148 |
if selected_page != st.session_state.page:
|
| 149 |
st.session_state.page = selected_page
|
| 150 |
-
|
| 151 |
self.display_page(st.session_state.page)
|
| 152 |
-
|
| 153 |
st.sidebar.markdown("---")
|
| 154 |
st.sidebar.info(
|
| 155 |
-
"HVAC Load Calculator v1.0.
|
| 156 |
"Based on ASHRAE steady-state calculation methods\n\n"
|
| 157 |
"Developed by: Dr Majed Abuseif\n\n"
|
| 158 |
"School of Architecture and Built Environment\n\n"
|
| 159 |
"Deakin University\n\n"
|
| 160 |
"© 2025"
|
| 161 |
)
|
| 162 |
-
|
| 163 |
def display_page(self, page: str):
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
st.error(f"Error displaying {page} page: {str(e)}")
|
| 178 |
-
except Exception as e:
|
| 179 |
-
logger.error(f"Unexpected error displaying page {page}: {str(e)}")
|
| 180 |
-
st.error(f"Unexpected error displaying {page} page: {str(e)}")
|
| 181 |
-
|
| 182 |
-
def _display_export_data(self, state):
|
| 183 |
-
"""Wrapper for DataExport.display with error handling."""
|
| 184 |
-
try:
|
| 185 |
-
self.data_export.display(state)
|
| 186 |
-
except AttributeError as e:
|
| 187 |
-
logger.error(f"DataExport.display failed: {str(e)}")
|
| 188 |
-
st.error("Export Data functionality is unavailable. Please check DataExport implementation.")
|
| 189 |
-
st.write("Placeholder for Export Data page. Select export options below:")
|
| 190 |
-
st.button("Export to CSV (Not Implemented)")
|
| 191 |
-
st.button("Export to JSON (Not Implemented)")
|
| 192 |
-
|
| 193 |
def generate_climate_id(self, country: str, city: str) -> str:
|
| 194 |
"""Generate a climate ID from country and city names."""
|
| 195 |
try:
|
| 196 |
country = country.strip().title()
|
| 197 |
city = city.strip().title()
|
| 198 |
if len(country) < 2 or len(city) < 3:
|
| 199 |
-
raise ValueError("Country and city names must be at least 2 and 3 characters long.")
|
| 200 |
-
|
| 201 |
-
return f"{country_code}-{city[:3].upper()}"
|
| 202 |
except Exception as e:
|
| 203 |
-
logger.error(f"Error generating climate ID: {str(e)}")
|
| 204 |
raise ValueError(f"Invalid country or city name: {str(e)}")
|
| 205 |
-
|
| 206 |
def validate_calculation_inputs(self) -> Tuple[bool, str]:
|
| 207 |
"""Validate inputs for cooling and heating calculations."""
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
if comp.u_value <= 0:
|
| 225 |
-
return False, f"Invalid U-value for {component_type}: {comp.name}"
|
| 226 |
-
|
| 227 |
-
return True, "Inputs valid."
|
| 228 |
-
except Exception as e:
|
| 229 |
-
logger.error(f"Error validating inputs: {str(e)}")
|
| 230 |
-
return False, f"Validation error: {str(e)}"
|
| 231 |
-
|
| 232 |
def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
|
| 233 |
"""Validate if a new internal load is unique and within limits."""
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
def display_internal_loads(self):
|
| 258 |
-
"""Display internal loads interface."""
|
| 259 |
st.title("Internal Loads")
|
| 260 |
-
|
|
|
|
| 261 |
if st.button("Reset All Internal Loads"):
|
| 262 |
st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []}
|
| 263 |
st.success("All internal loads reset!")
|
| 264 |
st.rerun()
|
| 265 |
-
|
| 266 |
tabs = st.tabs(["People", "Lighting", "Equipment"])
|
| 267 |
-
|
| 268 |
with tabs[0]:
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
with tabs[1]:
|
| 272 |
-
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
with tabs[2]:
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
col1, col2 = st.columns(2)
|
| 278 |
with col1:
|
| 279 |
-
st.button(
|
|
|
|
|
|
|
|
|
|
| 280 |
with col2:
|
| 281 |
-
st.button(
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
activity_levels = list(self.reference_data.internal_loads.get('occupancy', {}).keys())
|
| 287 |
-
|
| 288 |
-
with st.form("people_form"):
|
| 289 |
-
num_people = st.number_input("Number of People", min_value=0, value=0, step=1)
|
| 290 |
-
activity_level = st.selectbox("Activity Level", activity_levels if activity_levels else ["office_typing", "retail_sales"])
|
| 291 |
-
zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
|
| 292 |
-
hours_in_operation = st.number_input("Hours in Operation", min_value=0.0, max_value=24.0, value=8.0, step=0.5)
|
| 293 |
-
people_name = st.text_input("Name", value="Occupants")
|
| 294 |
-
|
| 295 |
-
if st.form_submit_button("Add People Load"):
|
| 296 |
-
people_load = {
|
| 297 |
-
"id": f"people_{str(uuid4())}",
|
| 298 |
-
"name": people_name,
|
| 299 |
-
"num_people": num_people,
|
| 300 |
-
"activity_level": activity_level,
|
| 301 |
-
"zone_type": zone_type,
|
| 302 |
-
"hours_in_operation": hours_in_operation
|
| 303 |
-
}
|
| 304 |
-
is_valid, message = self.validate_internal_load('people', people_load)
|
| 305 |
-
if is_valid:
|
| 306 |
-
st.session_state.internal_loads['people'].append(people_load)
|
| 307 |
-
st.success("People load added!")
|
| 308 |
-
st.rerun()
|
| 309 |
-
else:
|
| 310 |
-
st.error(message)
|
| 311 |
-
|
| 312 |
-
if st.session_state.internal_loads['people']:
|
| 313 |
-
people_df = pd.DataFrame(st.session_state.internal_loads['people'])
|
| 314 |
-
st.dataframe(people_df, use_container_width=True)
|
| 315 |
-
|
| 316 |
-
selected_people = st.multiselect("Select People Loads to Delete", [load['id'] for load in st.session_state.internal_loads['people']])
|
| 317 |
-
if st.button("Delete Selected People Loads"):
|
| 318 |
-
st.session_state.internal_loads['people'] = [load for load in st.session_state.internal_loads['people'] if load['id'] not in selected_people]
|
| 319 |
-
st.success("Selected people loads deleted!")
|
| 320 |
-
st.rerun()
|
| 321 |
-
|
| 322 |
-
def _display_lighting_loads(self):
|
| 323 |
-
"""Display lighting loads form and table."""
|
| 324 |
-
st.subheader("Lighting")
|
| 325 |
-
lighting_types = list(self.reference_data.internal_loads.get('lighting', {}).keys())
|
| 326 |
-
|
| 327 |
-
with st.form("lighting_form"):
|
| 328 |
-
lighting_type = st.selectbox("Lighting Type", lighting_types if lighting_types else ["led_high_efficiency"])
|
| 329 |
-
power = st.number_input("Power (W)", min_value=0.0, value=1000.0, step=100.0)
|
| 330 |
-
usage_factor = st.number_input("Usage Factor", min_value=0.0, max_value=1.0, value=0.8, step=0.1)
|
| 331 |
-
zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
|
| 332 |
-
hours_in_operation = st.number_input("Hours in Operation", min_value=0.0, max_value=24.0, value=8.0, step=0.5)
|
| 333 |
-
lighting_name = st.text_input("Name", value="General Lighting")
|
| 334 |
-
|
| 335 |
-
if st.form_submit_button("Add Lighting Load"):
|
| 336 |
-
lighting_load = {
|
| 337 |
-
"id": f"lighting_{str(uuid4())}",
|
| 338 |
-
"name": lighting_name,
|
| 339 |
-
"type": lighting_type,
|
| 340 |
-
"power": power,
|
| 341 |
-
"usage_factor": usage_factor,
|
| 342 |
-
"zone_type": zone_type,
|
| 343 |
-
"hours_in_operation": hours_in_operation
|
| 344 |
-
}
|
| 345 |
-
is_valid, message = self.validate_internal_load('lighting', lighting_load)
|
| 346 |
-
if is_valid:
|
| 347 |
-
st.session_state.internal_loads['lighting'].append(lighting_load)
|
| 348 |
-
st.success("Lighting load added!")
|
| 349 |
-
st.rerun()
|
| 350 |
-
else:
|
| 351 |
-
st.error(message)
|
| 352 |
-
|
| 353 |
-
if st.session_state.internal_loads['lighting']:
|
| 354 |
-
lighting_df = pd.DataFrame(st.session_state.internal_loads['lighting'])
|
| 355 |
-
st.dataframe(lighting_df, use_container_width=True)
|
| 356 |
-
|
| 357 |
-
selected_lighting = st.multiselect("Select Lighting Loads to Delete", [load['id'] for load in st.session_state.internal_loads['lighting']])
|
| 358 |
-
if st.button("Delete Selected Lighting Loads"):
|
| 359 |
-
st.session_state.internal_loads['lighting'] = [load for load in st.session_state.internal_loads['lighting'] if load['id'] not in selected_lighting]
|
| 360 |
-
st.success("Selected lighting loads deleted!")
|
| 361 |
-
st.rerun()
|
| 362 |
-
|
| 363 |
-
def _display_equipment_loads(self):
|
| 364 |
-
"""Display equipment loads form and table."""
|
| 365 |
-
st.subheader("Equipment")
|
| 366 |
-
equipment_types = list(self.reference_data.internal_loads.get('equipment', {}).keys())
|
| 367 |
-
|
| 368 |
-
with st.form("equipment_form"):
|
| 369 |
-
equipment_type = st.selectbox("Equipment Type", equipment_types if equipment_types else ["computer_workstation"])
|
| 370 |
-
power = st.number_input("Power (W)", min_value=0.0, value=500.0, step=100.0)
|
| 371 |
-
usage_factor = st.number_input("Usage Factor", min_value=0.0, max_value=1.0, value=0.7, step=0.1)
|
| 372 |
-
radiation_fraction = st.number_input("Radiation Fraction", min_value=0.0, max_value=1.0, value=0.3, step=0.1)
|
| 373 |
-
zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
|
| 374 |
-
hours_in_operation = st.number_input("Hours in Operation", min_value=0.0, max_value=24.0, value=8.0, step=0.5)
|
| 375 |
-
equipment_name = st.text_input("Name", value="Office Equipment")
|
| 376 |
-
|
| 377 |
-
if st.form_submit_button("Add Equipment Load"):
|
| 378 |
-
equipment_load = {
|
| 379 |
-
"id": f"equipment_{str(uuid4())}",
|
| 380 |
-
"name": equipment_name,
|
| 381 |
-
"type": equipment_type,
|
| 382 |
-
"power": power,
|
| 383 |
-
"usage_factor": usage_factor,
|
| 384 |
-
"radiation_fraction": radiation_fraction,
|
| 385 |
-
"zone_type": zone_type,
|
| 386 |
-
"hours_in_operation": hours_in_operation
|
| 387 |
-
}
|
| 388 |
-
is_valid, message = self.validate_internal_load('equipment', equipment_load)
|
| 389 |
-
if is_valid:
|
| 390 |
-
st.session_state.internal_loads['equipment'].append(equipment_load)
|
| 391 |
-
st.success("Equipment load added!")
|
| 392 |
-
st.rerun()
|
| 393 |
-
else:
|
| 394 |
-
st.error(message)
|
| 395 |
-
|
| 396 |
-
if st.session_state.internal_loads['equipment']:
|
| 397 |
-
equipment_df = pd.DataFrame(st.session_state.internal_loads['equipment'])
|
| 398 |
-
st.dataframe(equipment_df, use_container_width=True)
|
| 399 |
-
|
| 400 |
-
selected_equipment = st.multiselect("Select Equipment Loads to Delete", [load['id'] for load in st.session_state.internal_loads['equipment']])
|
| 401 |
-
if st.button("Delete Selected Equipment Loads"):
|
| 402 |
-
st.session_state.internal_loads['equipment'] = [load for load in st.session_state.internal_loads['equipment'] if load['id'] not in selected_equipment]
|
| 403 |
-
st.success("Selected equipment loads deleted!")
|
| 404 |
-
st.rerun()
|
| 405 |
-
|
| 406 |
def calculate_cooling(self) -> Tuple[bool, str, Dict]:
|
| 407 |
"""
|
| 408 |
Calculate cooling loads using CoolingLoadCalculator.
|
| 409 |
Returns: (success, message, results)
|
| 410 |
"""
|
| 411 |
try:
|
|
|
|
| 412 |
valid, message = self.validate_calculation_inputs()
|
| 413 |
if not valid:
|
| 414 |
return False, message, {}
|
| 415 |
-
|
|
|
|
| 416 |
building_components = st.session_state.get('components', {})
|
| 417 |
internal_loads = st.session_state.get('internal_loads', {})
|
| 418 |
building_info = st.session_state.get('building_info', {})
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
country = building_info.get('country', '').strip().title()
|
| 421 |
city = building_info.get('city', '').strip().title()
|
| 422 |
if not country or not city:
|
| 423 |
return False, "Country and city must be set in Building Information.", {}
|
| 424 |
-
|
| 425 |
climate_id = self.generate_climate_id(country, city)
|
| 426 |
location = self.climate_data.get_location_by_id(climate_id, st.session_state)
|
| 427 |
if not location:
|
| 428 |
available_locations = list(self.climate_data.locations.keys())[:5]
|
| 429 |
return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
|
| 430 |
-
|
|
|
|
| 431 |
if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
|
| 432 |
return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
|
| 433 |
-
|
|
|
|
| 434 |
outdoor_conditions = {
|
| 435 |
'temperature': location['summer_design_temp_db'],
|
| 436 |
'relative_humidity': location['monthly_humidity'].get('Jul', 50.0),
|
|
@@ -438,26 +454,46 @@ class HVACCalculator:
|
|
| 438 |
'month': 'Jul',
|
| 439 |
'latitude': f"{location['latitude']}N" if location['latitude'] >= 0 else f"{abs(location['latitude'])}S",
|
| 440 |
'wind_speed': building_info.get('wind_speed', 4.0),
|
| 441 |
-
'day_of_year': 204
|
| 442 |
}
|
| 443 |
indoor_conditions = {
|
| 444 |
'temperature': building_info.get('indoor_temp', 24.0),
|
| 445 |
'relative_humidity': building_info.get('indoor_rh', 50.0)
|
| 446 |
}
|
| 447 |
-
|
| 448 |
if st.session_state.get('debug_mode', False):
|
| 449 |
st.write("Debug: Cooling Input State", {
|
| 450 |
'climate_id': climate_id,
|
| 451 |
'outdoor_conditions': outdoor_conditions,
|
| 452 |
'indoor_conditions': indoor_conditions,
|
| 453 |
'components': {k: len(v) for k, v in building_components.items()},
|
| 454 |
-
'internal_loads': {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
})
|
| 456 |
-
|
|
|
|
| 457 |
formatted_internal_loads = {
|
| 458 |
-
'people':
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
'infiltration': {
|
| 462 |
'flow_rate': building_info.get('infiltration_rate', 0.05),
|
| 463 |
'height': building_info.get('building_height', 3.0),
|
|
@@ -468,36 +504,8 @@ class HVACCalculator:
|
|
| 468 |
},
|
| 469 |
'operating_hours': building_info.get('operating_hours', '8:00-18:00')
|
| 470 |
}
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
ref_load = self.reference_data.get_internal_load('occupancy', load['activity_level'])
|
| 474 |
-
formatted_internal_loads['people'].append({
|
| 475 |
-
'number': load['num_people'],
|
| 476 |
-
'sensible_heat': ref_load.get('sensible_heat', 70) if ref_load else 70,
|
| 477 |
-
'latent_heat': ref_load.get('latent_heat', 45) if ref_load else 45,
|
| 478 |
-
'hours_operation': f"{load['hours_in_operation']}:00-{load['hours_in_operation']+10}:00"
|
| 479 |
-
})
|
| 480 |
-
|
| 481 |
-
for load in internal_loads.get('lighting', []):
|
| 482 |
-
ref_load = self.reference_data.get_internal_load('lighting', load['type'])
|
| 483 |
-
formatted_internal_loads['lights'].append({
|
| 484 |
-
'power': load['power'],
|
| 485 |
-
'use_factor': load['usage_factor'],
|
| 486 |
-
'heat_to_space': ref_load.get('heat_to_space', 0.9) if ref_load else 0.9,
|
| 487 |
-
'hours_operation': f"{load['hours_in_operation']}h"
|
| 488 |
-
})
|
| 489 |
-
|
| 490 |
-
for load in internal_loads.get('equipment', []):
|
| 491 |
-
ref_load = self.reference_data.get_internal_load('equipment', load['type'])
|
| 492 |
-
formatted_internal_loads['equipment'].append({
|
| 493 |
-
'power': load['power'],
|
| 494 |
-
'use_factor': load['usage_factor'],
|
| 495 |
-
'radiation_factor': load['radiation_fraction'],
|
| 496 |
-
'sensible_fraction': ref_load.get('sensible_fraction', 0.9) if ref_load else 0.9,
|
| 497 |
-
'latent_fraction': ref_load.get('latent_fraction', 0.1) if ref_load else 0.1,
|
| 498 |
-
'hours_operation': f"{load['hours_in_operation']}h"
|
| 499 |
-
})
|
| 500 |
-
|
| 501 |
hourly_loads = self.cooling_calculator.calculate_hourly_cooling_loads(
|
| 502 |
building_components=building_components,
|
| 503 |
outdoor_conditions=outdoor_conditions,
|
|
@@ -506,16 +514,19 @@ class HVACCalculator:
|
|
| 506 |
building_volume=building_info.get('floor_area', 100.0) * building_info.get('building_height', 3.0)
|
| 507 |
)
|
| 508 |
if not hourly_loads:
|
| 509 |
-
return False, "Cooling hourly loads calculation failed.", {}
|
| 510 |
-
|
|
|
|
| 511 |
design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads)
|
| 512 |
if not design_loads:
|
| 513 |
-
return False, "Cooling design loads calculation failed.", {}
|
| 514 |
-
|
|
|
|
| 515 |
summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads)
|
| 516 |
if not summary:
|
| 517 |
-
return False, "Cooling load summary calculation failed.", {}
|
| 518 |
-
|
|
|
|
| 519 |
floor_area = building_info.get('floor_area', 100.0) or 100.0
|
| 520 |
results = {
|
| 521 |
'total_load': summary['total'] / 1000, # kW
|
|
@@ -554,7 +565,7 @@ class HVACCalculator:
|
|
| 554 |
},
|
| 555 |
'building_info': building_info
|
| 556 |
}
|
| 557 |
-
|
| 558 |
# Populate detailed loads
|
| 559 |
for wall in building_components.get('walls', []):
|
| 560 |
load = self.cooling_calculator.calculate_wall_cooling_load(
|
|
@@ -582,7 +593,7 @@ class HVACCalculator:
|
|
| 582 |
),
|
| 583 |
'load': load / 1000
|
| 584 |
})
|
| 585 |
-
|
| 586 |
for roof in building_components.get('roofs', []):
|
| 587 |
load = self.cooling_calculator.calculate_roof_cooling_load(
|
| 588 |
roof=roof,
|
|
@@ -608,7 +619,7 @@ class HVACCalculator:
|
|
| 608 |
),
|
| 609 |
'load': load / 1000
|
| 610 |
})
|
| 611 |
-
|
| 612 |
for window in building_components.get('windows', []):
|
| 613 |
load_dict = self.cooling_calculator.calculate_window_cooling_load(
|
| 614 |
window=window,
|
|
@@ -636,7 +647,7 @@ class HVACCalculator:
|
|
| 636 |
),
|
| 637 |
'load': load_dict['total'] / 1000
|
| 638 |
})
|
| 639 |
-
|
| 640 |
for door in building_components.get('doors', []):
|
| 641 |
load = self.cooling_calculator.calculate_door_cooling_load(
|
| 642 |
door=door,
|
|
@@ -651,18 +662,16 @@ class HVACCalculator:
|
|
| 651 |
'cltd': outdoor_conditions['temperature'] - indoor_conditions['temperature'],
|
| 652 |
'load': load / 1000
|
| 653 |
})
|
| 654 |
-
|
| 655 |
for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
|
| 656 |
for load in internal_loads.get(key, []):
|
| 657 |
if load_type == 'people':
|
| 658 |
-
ref_load = self.reference_data.get_internal_load('occupancy', load['activity_level'])
|
| 659 |
load_dict = self.cooling_calculator.calculate_people_cooling_load(
|
| 660 |
num_people=load['num_people'],
|
| 661 |
activity_level=load['activity_level'],
|
| 662 |
hour=design_loads['design_hour']
|
| 663 |
)
|
| 664 |
elif load_type == 'lighting':
|
| 665 |
-
ref_load = self.reference_data.get_internal_load('lighting', load['type'])
|
| 666 |
load_dict = {'total': self.cooling_calculator.calculate_lights_cooling_load(
|
| 667 |
power=load['power'],
|
| 668 |
use_factor=load['usage_factor'],
|
|
@@ -670,7 +679,6 @@ class HVACCalculator:
|
|
| 670 |
hour=design_loads['design_hour']
|
| 671 |
)}
|
| 672 |
else:
|
| 673 |
-
ref_load = self.reference_data.get_internal_load('equipment', load['type'])
|
| 674 |
load_dict = self.cooling_calculator.calculate_equipment_cooling_load(
|
| 675 |
power=load['power'],
|
| 676 |
use_factor=load['usage_factor'],
|
|
@@ -689,54 +697,62 @@ class HVACCalculator:
|
|
| 689 |
) if load_type == 'people' else 1.0,
|
| 690 |
'load': load_dict['total'] / 1000
|
| 691 |
})
|
| 692 |
-
|
| 693 |
if st.session_state.get('debug_mode', False):
|
| 694 |
st.write("Debug: Cooling Results", {
|
| 695 |
'total_load': results.get('total_load', 'N/A'),
|
| 696 |
'component_loads': results.get('component_loads', 'N/A'),
|
| 697 |
'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
|
| 698 |
})
|
| 699 |
-
|
| 700 |
return True, "Cooling calculation completed.", results
|
| 701 |
-
|
| 702 |
except ValueError as ve:
|
| 703 |
-
|
| 704 |
return False, f"Input error: {str(ve)}", {}
|
| 705 |
except KeyError as ke:
|
| 706 |
-
|
| 707 |
return False, f"Missing data: {str(ke)}", {}
|
| 708 |
except Exception as e:
|
| 709 |
-
|
| 710 |
return False, f"Unexpected error: {str(e)}", {}
|
| 711 |
-
|
| 712 |
def calculate_heating(self) -> Tuple[bool, str, Dict]:
|
| 713 |
"""
|
| 714 |
Calculate heating loads using HeatingLoadCalculator.
|
| 715 |
Returns: (success, message, results)
|
| 716 |
"""
|
| 717 |
try:
|
|
|
|
| 718 |
valid, message = self.validate_calculation_inputs()
|
| 719 |
if not valid:
|
| 720 |
return False, message, {}
|
| 721 |
-
|
|
|
|
| 722 |
building_components = st.session_state.get('components', {})
|
| 723 |
internal_loads = st.session_state.get('internal_loads', {})
|
| 724 |
building_info = st.session_state.get('building_info', {})
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
country = building_info.get('country', '').strip().title()
|
| 727 |
city = building_info.get('city', '').strip().title()
|
| 728 |
if not country or not city:
|
| 729 |
return False, "Country and city must be set in Building Information.", {}
|
| 730 |
-
|
| 731 |
climate_id = self.generate_climate_id(country, city)
|
| 732 |
location = self.climate_data.get_location_by_id(climate_id, st.session_state)
|
| 733 |
if not location:
|
| 734 |
available_locations = list(self.climate_data.locations.keys())[:5]
|
| 735 |
return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
|
| 736 |
-
|
|
|
|
| 737 |
if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
|
| 738 |
return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
|
| 739 |
-
|
|
|
|
| 740 |
outdoor_conditions = {
|
| 741 |
'design_temperature': location['winter_design_temp'],
|
| 742 |
'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
|
|
@@ -747,20 +763,38 @@ class HVACCalculator:
|
|
| 747 |
'temperature': building_info.get('indoor_temp', 21.0),
|
| 748 |
'relative_humidity': building_info.get('indoor_rh', 40.0)
|
| 749 |
}
|
| 750 |
-
|
| 751 |
if st.session_state.get('debug_mode', False):
|
| 752 |
st.write("Debug: Heating Input State", {
|
| 753 |
'climate_id': climate_id,
|
| 754 |
'outdoor_conditions': outdoor_conditions,
|
| 755 |
'indoor_conditions': indoor_conditions,
|
| 756 |
'components': {k: len(v) for k, v in building_components.items()},
|
| 757 |
-
'internal_loads': {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
})
|
| 759 |
-
|
|
|
|
| 760 |
formatted_internal_loads = {
|
| 761 |
-
'people':
|
| 762 |
-
|
| 763 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
'infiltration': {
|
| 765 |
'flow_rate': building_info.get('infiltration_rate', 0.05),
|
| 766 |
'height': building_info.get('building_height', 3.0),
|
|
@@ -772,33 +806,8 @@ class HVACCalculator:
|
|
| 772 |
'usage_factor': 0.7,
|
| 773 |
'operating_hours': building_info.get('operating_hours', '8:00-18:00')
|
| 774 |
}
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
ref_load = self.reference_data.get_internal_load('occupancy', load['activity_level'])
|
| 778 |
-
formatted_internal_loads['people'].append({
|
| 779 |
-
'number': load['num_people'],
|
| 780 |
-
'sensible_heat': ref_load.get('sensible_heat', 70) if ref_load else 70,
|
| 781 |
-
'hours_operation': f"{load['hours_in_operation']}:00-{load['hours_in_operation']+10}:00"
|
| 782 |
-
})
|
| 783 |
-
|
| 784 |
-
for load in internal_loads.get('lighting', []):
|
| 785 |
-
ref_load = self.reference_data.get_internal_load('lighting', load['type'])
|
| 786 |
-
formatted_internal_loads['lights'].append({
|
| 787 |
-
'power': load['power'],
|
| 788 |
-
'use_factor': load['usage_factor'],
|
| 789 |
-
'heat_to_space': ref_load.get('heat_to_space', 0.9) if ref_load else 0.9,
|
| 790 |
-
'hours_operation': f"{load['hours_in_operation']}h"
|
| 791 |
-
})
|
| 792 |
-
|
| 793 |
-
for load in internal_loads.get('equipment', []):
|
| 794 |
-
ref_load = self.reference_data.get_internal_load('equipment', load['type'])
|
| 795 |
-
formatted_internal_loads['equipment'].append({
|
| 796 |
-
'power': load['power'],
|
| 797 |
-
'use_factor': load['usage_factor'],
|
| 798 |
-
'sensible_fraction': ref_load.get('sensible_fraction', 0.9) if ref_load else 0.9,
|
| 799 |
-
'hours_operation': f"{load['hours_in_operation']}h"
|
| 800 |
-
})
|
| 801 |
-
|
| 802 |
design_loads = self.heating_calculator.calculate_design_heating_load(
|
| 803 |
building_components=building_components,
|
| 804 |
outdoor_conditions=outdoor_conditions,
|
|
@@ -806,12 +815,14 @@ class HVACCalculator:
|
|
| 806 |
internal_loads=formatted_internal_loads
|
| 807 |
)
|
| 808 |
if not design_loads:
|
| 809 |
-
return False, "Heating design loads calculation failed.", {}
|
| 810 |
-
|
|
|
|
| 811 |
summary = self.heating_calculator.calculate_heating_load_summary(design_loads)
|
| 812 |
if not summary:
|
| 813 |
-
return False, "Heating load summary calculation failed.", {}
|
| 814 |
-
|
|
|
|
| 815 |
floor_area = building_info.get('floor_area', 100.0) or 100.0
|
| 816 |
results = {
|
| 817 |
'total_load': summary['total'] / 1000, # kW
|
|
@@ -846,7 +857,8 @@ class HVACCalculator:
|
|
| 846 |
},
|
| 847 |
'building_info': building_info
|
| 848 |
}
|
| 849 |
-
|
|
|
|
| 850 |
delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
|
| 851 |
for wall in building_components.get('walls', []):
|
| 852 |
load = self.heating_calculator.calculate_wall_heating_load(
|
|
@@ -862,7 +874,7 @@ class HVACCalculator:
|
|
| 862 |
'delta_t': delta_t,
|
| 863 |
'load': load / 1000
|
| 864 |
})
|
| 865 |
-
|
| 866 |
for roof in building_components.get('roofs', []):
|
| 867 |
load = self.heating_calculator.calculate_roof_heating_load(
|
| 868 |
roof=roof,
|
|
@@ -877,7 +889,7 @@ class HVACCalculator:
|
|
| 877 |
'delta_t': delta_t,
|
| 878 |
'load': load / 1000
|
| 879 |
})
|
| 880 |
-
|
| 881 |
for floor in building_components.get('floors', []):
|
| 882 |
load = self.heating_calculator.calculate_floor_heating_load(
|
| 883 |
floor=floor,
|
|
@@ -891,7 +903,7 @@ class HVACCalculator:
|
|
| 891 |
'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
|
| 892 |
'load': load / 1000
|
| 893 |
})
|
| 894 |
-
|
| 895 |
for window in building_components.get('windows', []):
|
| 896 |
load = self.heating_calculator.calculate_window_heating_load(
|
| 897 |
window=window,
|
|
@@ -906,7 +918,7 @@ class HVACCalculator:
|
|
| 906 |
'delta_t': delta_t,
|
| 907 |
'load': load / 1000
|
| 908 |
})
|
| 909 |
-
|
| 910 |
for door in building_components.get('doors', []):
|
| 911 |
load = self.heating_calculator.calculate_door_heating_load(
|
| 912 |
door=door,
|
|
@@ -921,66 +933,71 @@ class HVACCalculator:
|
|
| 921 |
'delta_t': delta_t,
|
| 922 |
'load': load / 1000
|
| 923 |
})
|
| 924 |
-
|
| 925 |
if st.session_state.get('debug_mode', False):
|
| 926 |
st.write("Debug: Heating Results", {
|
| 927 |
'total_load': results.get('total_load', 'N/A'),
|
| 928 |
'component_loads': results.get('component_loads', 'N/A'),
|
| 929 |
'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
|
| 930 |
})
|
| 931 |
-
|
| 932 |
return True, "Heating calculation completed.", results
|
| 933 |
-
|
| 934 |
except ValueError as ve:
|
| 935 |
-
|
| 936 |
return False, f"Input error: {str(ve)}", {}
|
| 937 |
except KeyError as ke:
|
| 938 |
-
|
| 939 |
return False, f"Missing data: {str(ke)}", {}
|
| 940 |
except Exception as e:
|
| 941 |
-
|
| 942 |
return False, f"Unexpected error: {str(e)}", {}
|
| 943 |
-
|
| 944 |
def display_calculation_results(self):
|
| 945 |
-
"""Display calculation results interface."""
|
| 946 |
st.title("Calculation Results")
|
| 947 |
-
|
| 948 |
col1, col2 = st.columns(2)
|
| 949 |
with col1:
|
| 950 |
calculate_button = st.button("Calculate Loads")
|
| 951 |
with col2:
|
| 952 |
st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False))
|
| 953 |
-
|
| 954 |
if calculate_button:
|
|
|
|
| 955 |
st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
|
| 956 |
-
|
| 957 |
with st.spinner("Calculating loads..."):
|
|
|
|
| 958 |
cooling_success, cooling_message, cooling_results = self.calculate_cooling()
|
| 959 |
if cooling_success:
|
| 960 |
st.session_state.calculation_results['cooling'] = cooling_results
|
| 961 |
st.success(cooling_message)
|
| 962 |
else:
|
| 963 |
st.error(cooling_message)
|
| 964 |
-
|
|
|
|
| 965 |
heating_success, heating_message, heating_results = self.calculate_heating()
|
| 966 |
if heating_success:
|
| 967 |
st.session_state.calculation_results['heating'] = heating_results
|
| 968 |
st.success(heating_message)
|
| 969 |
else:
|
| 970 |
st.error(heating_message)
|
| 971 |
-
|
|
|
|
| 972 |
self.results_display.display_results(st.session_state)
|
| 973 |
-
|
|
|
|
| 974 |
col1, col2 = st.columns(2)
|
| 975 |
with col1:
|
| 976 |
-
st.button(
|
|
|
|
|
|
|
|
|
|
| 977 |
with col2:
|
| 978 |
-
st.button(
|
| 979 |
-
|
|
|
|
|
|
|
| 980 |
|
| 981 |
if __name__ == "__main__":
|
| 982 |
-
|
| 983 |
-
app = HVACCalculator()
|
| 984 |
-
except Exception as e:
|
| 985 |
-
logger.error(f"Error initializing HVACCalculator: {str(e)}")
|
| 986 |
-
st.error(f"Failed to initialize application: {str(e)}")
|
|
|
|
| 11 |
import pycountry
|
| 12 |
import os
|
| 13 |
import sys
|
|
|
|
| 14 |
from typing import Dict, List, Any, Optional, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Import application modules
|
| 17 |
+
from app.building_info_form import BuildingInfoForm
|
| 18 |
+
from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door
|
| 19 |
+
from app.results_display import ResultsDisplay
|
| 20 |
+
from app.data_validation import DataValidation
|
| 21 |
+
from app.data_persistence import DataPersistence
|
| 22 |
+
from app.data_export import DataExport
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# Import data modules
|
| 25 |
+
from data.reference_data import ReferenceData
|
| 26 |
+
from data.climate_data import ClimateData, ClimateLocation
|
| 27 |
+
from data.ashrae_tables import ASHRAETables
|
| 28 |
+
from data.building_components import Wall as WallModel, Roof as RoofModel
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# Import utility modules
|
| 31 |
+
from utils.u_value_calculator import UValueCalculator
|
| 32 |
+
from utils.shading_system import ShadingSystem
|
| 33 |
+
from utils.area_calculation_system import AreaCalculationSystem
|
| 34 |
+
from utils.psychrometrics import Psychrometrics
|
| 35 |
+
from utils.heat_transfer import HeatTransferCalculations
|
| 36 |
+
from utils.cooling_load import CoolingLoadCalculator
|
| 37 |
+
from utils.heating_load import HeatingLoadCalculator
|
| 38 |
+
from utils.component_visualization import ComponentVisualization
|
| 39 |
+
from utils.scenario_comparison import ScenarioComparisonVisualization
|
| 40 |
+
from utils.psychrometric_visualization import PsychrometricVisualization
|
| 41 |
+
from utils.time_based_visualization import TimeBasedVisualization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
class HVACCalculator:
|
| 44 |
def __init__(self):
|
| 45 |
+
st.set_page_config(
|
| 46 |
+
page_title="HVAC Load Calculator",
|
| 47 |
+
page_icon="🌡️",
|
| 48 |
+
layout="wide",
|
| 49 |
+
initial_sidebar_state="expanded"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Initialize session state
|
| 53 |
+
if 'page' not in st.session_state:
|
| 54 |
+
st.session_state.page = 'Building Information'
|
| 55 |
+
|
| 56 |
+
if 'building_info' not in st.session_state:
|
| 57 |
+
st.session_state.building_info = {"project_name": ""}
|
| 58 |
+
|
| 59 |
+
if 'components' not in st.session_state:
|
| 60 |
+
st.session_state.components = {
|
| 61 |
+
'walls': [],
|
| 62 |
+
'roofs': [],
|
| 63 |
+
'floors': [],
|
| 64 |
+
'windows': [],
|
| 65 |
+
'doors': []
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if 'internal_loads' not in st.session_state:
|
| 69 |
+
st.session_state.internal_loads = {
|
| 70 |
+
'people': [],
|
| 71 |
+
'lighting': [],
|
| 72 |
+
'equipment': []
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if 'calculation_results' not in st.session_state:
|
| 76 |
+
st.session_state.calculation_results = {
|
| 77 |
+
'cooling': {},
|
| 78 |
+
'heating': {}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if 'saved_scenarios' not in st.session_state:
|
| 82 |
+
st.session_state.saved_scenarios = {}
|
| 83 |
+
|
| 84 |
+
if 'climate_data' not in st.session_state:
|
| 85 |
+
st.session_state.climate_data = {}
|
| 86 |
+
|
| 87 |
+
if 'debug_mode' not in st.session_state:
|
| 88 |
+
st.session_state.debug_mode = False
|
| 89 |
+
|
| 90 |
+
# Initialize modules
|
| 91 |
+
self.building_info_form = BuildingInfoForm()
|
| 92 |
+
self.component_selection = ComponentSelectionInterface()
|
| 93 |
+
self.results_display = ResultsDisplay()
|
| 94 |
+
self.data_validation = DataValidation()
|
| 95 |
+
self.data_persistence = DataPersistence()
|
| 96 |
+
self.data_export = DataExport()
|
| 97 |
+
self.cooling_calculator = CoolingLoadCalculator()
|
| 98 |
+
self.heating_calculator = HeatingLoadCalculator()
|
| 99 |
+
|
| 100 |
+
# Persist ClimateData in session_state
|
| 101 |
if 'climate_data_obj' not in st.session_state:
|
| 102 |
st.session_state.climate_data_obj = ClimateData()
|
| 103 |
self.climate_data = st.session_state.climate_data_obj
|
| 104 |
+
|
| 105 |
+
# Load default climate data if locations are empty
|
| 106 |
try:
|
| 107 |
if not self.climate_data.locations:
|
| 108 |
self.climate_data = ClimateData.from_json("/home/user/app/climate_data.json")
|
| 109 |
st.session_state.climate_data_obj = self.climate_data
|
| 110 |
except FileNotFoundError:
|
| 111 |
st.warning("Default climate data file not found. Please enter climate data manually.")
|
| 112 |
+
|
| 113 |
+
self.setup_layout()
|
| 114 |
+
|
|
|
|
| 115 |
def setup_layout(self):
|
|
|
|
| 116 |
st.sidebar.title("HVAC Load Calculator")
|
| 117 |
st.sidebar.markdown("---")
|
| 118 |
+
|
| 119 |
st.sidebar.subheader("Navigation")
|
| 120 |
pages = [
|
| 121 |
"Building Information",
|
|
|
|
| 125 |
"Calculation Results",
|
| 126 |
"Export Data"
|
| 127 |
]
|
| 128 |
+
|
| 129 |
selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page))
|
| 130 |
+
|
| 131 |
if selected_page != st.session_state.page:
|
| 132 |
st.session_state.page = selected_page
|
| 133 |
+
|
| 134 |
self.display_page(st.session_state.page)
|
| 135 |
+
|
| 136 |
st.sidebar.markdown("---")
|
| 137 |
st.sidebar.info(
|
| 138 |
+
"HVAC Load Calculator v1.0.1\n\n"
|
| 139 |
"Based on ASHRAE steady-state calculation methods\n\n"
|
| 140 |
"Developed by: Dr Majed Abuseif\n\n"
|
| 141 |
"School of Architecture and Built Environment\n\n"
|
| 142 |
"Deakin University\n\n"
|
| 143 |
"© 2025"
|
| 144 |
)
|
| 145 |
+
|
| 146 |
def display_page(self, page: str):
|
| 147 |
+
if page == "Building Information":
|
| 148 |
+
self.building_info_form.display_building_info_form(st.session_state)
|
| 149 |
+
elif page == "Climate Data":
|
| 150 |
+
self.climate_data.display_climate_input(st.session_state)
|
| 151 |
+
elif page == "Building Components":
|
| 152 |
+
self.component_selection.display_component_selection(st.session_state)
|
| 153 |
+
elif page == "Internal Loads":
|
| 154 |
+
self.display_internal_loads()
|
| 155 |
+
elif page == "Calculation Results":
|
| 156 |
+
self.display_calculation_results()
|
| 157 |
+
elif page == "Export Data":
|
| 158 |
+
self.data_export.display()
|
| 159 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
def generate_climate_id(self, country: str, city: str) -> str:
|
| 161 |
"""Generate a climate ID from country and city names."""
|
| 162 |
try:
|
| 163 |
country = country.strip().title()
|
| 164 |
city = city.strip().title()
|
| 165 |
if len(country) < 2 or len(city) < 3:
|
| 166 |
+
raise ValueError("Country and city names must be at least 2 and 3 characters long, respectively.")
|
| 167 |
+
return f"{country[:2].upper()}-{city[:3].upper()}"
|
|
|
|
| 168 |
except Exception as e:
|
|
|
|
| 169 |
raise ValueError(f"Invalid country or city name: {str(e)}")
|
| 170 |
+
|
| 171 |
def validate_calculation_inputs(self) -> Tuple[bool, str]:
|
| 172 |
"""Validate inputs for cooling and heating calculations."""
|
| 173 |
+
building_info = st.session_state.get('building_info', {})
|
| 174 |
+
components = st.session_state.get('components', {})
|
| 175 |
+
if not building_info.get('floor_area', 0) > 0:
|
| 176 |
+
return False, "Floor area must be positive."
|
| 177 |
+
if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows']):
|
| 178 |
+
return False, "At least one wall, roof, or window must be defined."
|
| 179 |
+
if not st.session_state.get('climate_data'):
|
| 180 |
+
return False, "Climate data is missing."
|
| 181 |
+
for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors']:
|
| 182 |
+
for comp in components.get(component_type, []):
|
| 183 |
+
if comp.area <= 0:
|
| 184 |
+
return False, f"Invalid area for {component_type}: {comp.name}"
|
| 185 |
+
if comp.u_value <= 0:
|
| 186 |
+
return False, f"Invalid U-value for {component_type}: {comp.name}"
|
| 187 |
+
return True, "Inputs valid."
|
| 188 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
|
| 190 |
"""Validate if a new internal load is unique and within limits."""
|
| 191 |
+
loads = st.session_state.internal_loads.get(load_type, [])
|
| 192 |
+
max_loads = 50
|
| 193 |
+
|
| 194 |
+
if len(loads) >= max_loads:
|
| 195 |
+
return False, f"Maximum of {max_loads} {load_type} loads reached."
|
| 196 |
+
|
| 197 |
+
# Check for duplicates based on key attributes
|
| 198 |
+
for existing_load in loads:
|
| 199 |
+
if load_type == 'people':
|
| 200 |
+
if (existing_load['name'] == new_load['name'] and
|
| 201 |
+
existing_load['num_people'] == new_load['num_people'] and
|
| 202 |
+
existing_load['activity_level'] == new_load['activity_level'] and
|
| 203 |
+
existing_load['zone_type'] == new_load['zone_type'] and
|
| 204 |
+
existing_load['hours_in_operation'] == new_load['hours_in_operation']):
|
| 205 |
+
return False, f"Duplicate people load '{new_load['name']}' already exists."
|
| 206 |
+
elif load_type == 'lighting':
|
| 207 |
+
if (existing_load['name'] == new_load['name'] and
|
| 208 |
+
existing_load['power'] == new_load['power'] and
|
| 209 |
+
existing_load['usage_factor'] == new_load['usage_factor'] and
|
| 210 |
+
existing_load['zone_type'] == new_load['zone_type'] and
|
| 211 |
+
existing_load['hours_in_operation'] == new_load['hours_in_operation']):
|
| 212 |
+
return False, f"Duplicate lighting load '{new_load['name']}' already exists."
|
| 213 |
+
elif load_type == 'equipment':
|
| 214 |
+
if (existing_load['name'] == new_load['name'] and
|
| 215 |
+
existing_load['power'] == new_load['power'] and
|
| 216 |
+
existing_load['usage_factor'] == new_load['usage_factor'] and
|
| 217 |
+
existing_load['radiation_fraction'] == new_load['radiation_fraction'] and
|
| 218 |
+
existing_load['zone_type'] == new_load['zone_type'] and
|
| 219 |
+
existing_load['hours_in_operation'] == new_load['hours_in_operation']):
|
| 220 |
+
return False, f"Duplicate equipment load '{new_load['name']}' already exists."
|
| 221 |
+
|
| 222 |
+
return True, "Valid load."
|
| 223 |
+
|
| 224 |
def display_internal_loads(self):
|
|
|
|
| 225 |
st.title("Internal Loads")
|
| 226 |
+
|
| 227 |
+
# Reset button for all internal loads
|
| 228 |
if st.button("Reset All Internal Loads"):
|
| 229 |
st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []}
|
| 230 |
st.success("All internal loads reset!")
|
| 231 |
st.rerun()
|
| 232 |
+
|
| 233 |
tabs = st.tabs(["People", "Lighting", "Equipment"])
|
| 234 |
+
|
| 235 |
with tabs[0]:
|
| 236 |
+
st.subheader("People")
|
| 237 |
+
with st.form("people_form"):
|
| 238 |
+
num_people = st.number_input("Number of People", min_value=0, value=0, step=1)
|
| 239 |
+
activity_level = st.selectbox(
|
| 240 |
+
"Activity Level",
|
| 241 |
+
["Seated/Resting", "Light Work", "Moderate Work", "Heavy Work"]
|
| 242 |
+
)
|
| 243 |
+
zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
|
| 244 |
+
hours_in_operation = st.number_input(
|
| 245 |
+
"Hours in Operation",
|
| 246 |
+
min_value=0.0,
|
| 247 |
+
max_value=24.0,
|
| 248 |
+
value=8.0,
|
| 249 |
+
step=0.5
|
| 250 |
+
)
|
| 251 |
+
people_name = st.text_input("Name", value="Occupants")
|
| 252 |
+
|
| 253 |
+
if st.form_submit_button("Add People Load"):
|
| 254 |
+
people_load = {
|
| 255 |
+
"id": f"people_{len(st.session_state.internal_loads['people'])}",
|
| 256 |
+
"name": people_name,
|
| 257 |
+
"num_people": num_people,
|
| 258 |
+
"activity_level": activity_level,
|
| 259 |
+
"zone_type": zone_type,
|
| 260 |
+
"hours_in_operation": hours_in_operation
|
| 261 |
+
}
|
| 262 |
+
is_valid, message = self.validate_internal_load('people', people_load)
|
| 263 |
+
if is_valid:
|
| 264 |
+
st.session_state.internal_loads['people'].append(people_load)
|
| 265 |
+
st.success("People load added!")
|
| 266 |
+
st.rerun()
|
| 267 |
+
else:
|
| 268 |
+
st.error(message)
|
| 269 |
+
|
| 270 |
+
if st.session_state.internal_loads['people']:
|
| 271 |
+
people_df = pd.DataFrame(st.session_state.internal_loads['people'])
|
| 272 |
+
st.dataframe(people_df, use_container_width=True)
|
| 273 |
+
|
| 274 |
+
selected_people = st.multiselect(
|
| 275 |
+
"Select People Loads to Delete",
|
| 276 |
+
[load['id'] for load in st.session_state.internal_loads['people']]
|
| 277 |
+
)
|
| 278 |
+
if st.button("Delete Selected People Loads"):
|
| 279 |
+
st.session_state.internal_loads['people'] = [
|
| 280 |
+
load for load in st.session_state.internal_loads['people']
|
| 281 |
+
if load['id'] not in selected_people
|
| 282 |
+
]
|
| 283 |
+
st.success("Selected people loads deleted!")
|
| 284 |
+
st.rerun()
|
| 285 |
+
|
| 286 |
with tabs[1]:
|
| 287 |
+
st.subheader("Lighting")
|
| 288 |
+
with st.form("lighting_form"):
|
| 289 |
+
power = st.number_input("Power (W)", min_value=0.0, value=1000.0, step=100.0)
|
| 290 |
+
usage_factor = st.number_input(
|
| 291 |
+
"Usage Factor",
|
| 292 |
+
min_value=0.0,
|
| 293 |
+
max_value=1.0,
|
| 294 |
+
value=0.8,
|
| 295 |
+
step=0.1
|
| 296 |
+
)
|
| 297 |
+
zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
|
| 298 |
+
hours_in_operation = st.number_input(
|
| 299 |
+
"Hours in Operation",
|
| 300 |
+
min_value=0.0,
|
| 301 |
+
max_value=24.0,
|
| 302 |
+
value=8.0,
|
| 303 |
+
step=0.5
|
| 304 |
+
)
|
| 305 |
+
lighting_name = st.text_input("Name", value="General Lighting")
|
| 306 |
+
|
| 307 |
+
if st.form_submit_button("Add Lighting Load"):
|
| 308 |
+
lighting_load = {
|
| 309 |
+
"id": f"lighting_{len(st.session_state.internal_loads['lighting'])}",
|
| 310 |
+
"name": lighting_name,
|
| 311 |
+
"power": power,
|
| 312 |
+
"usage_factor": usage_factor,
|
| 313 |
+
"zone_type": zone_type,
|
| 314 |
+
"hours_in_operation": hours_in_operation
|
| 315 |
+
}
|
| 316 |
+
is_valid, message = self.validate_internal_load('lighting', lighting_load)
|
| 317 |
+
if is_valid:
|
| 318 |
+
st.session_state.internal_loads['lighting'].append(lighting_load)
|
| 319 |
+
st.success("Lighting load added!")
|
| 320 |
+
st.rerun()
|
| 321 |
+
else:
|
| 322 |
+
st.error(message)
|
| 323 |
+
|
| 324 |
+
if st.session_state.internal_loads['lighting']:
|
| 325 |
+
lighting_df = pd.DataFrame(st.session_state.internal_loads['lighting'])
|
| 326 |
+
st.dataframe(lighting_df, use_container_width=True)
|
| 327 |
+
|
| 328 |
+
selected_lighting = st.multiselect(
|
| 329 |
+
"Select Lighting Loads to Delete",
|
| 330 |
+
[load['id'] for load in st.session_state.internal_loads['lighting']]
|
| 331 |
+
)
|
| 332 |
+
if st.button("Delete Selected Lighting Loads"):
|
| 333 |
+
st.session_state.internal_loads['lighting'] = [
|
| 334 |
+
load for load in st.session_state.internal_loads['lighting']
|
| 335 |
+
if load['id'] not in selected_lighting
|
| 336 |
+
]
|
| 337 |
+
st.success("Selected lighting loads deleted!")
|
| 338 |
+
st.rerun()
|
| 339 |
+
|
| 340 |
with tabs[2]:
|
| 341 |
+
st.subheader("Equipment")
|
| 342 |
+
with st.form("equipment_form"):
|
| 343 |
+
power = st.number_input("Power (W)", min_value=0.0, value=500.0, step=100.0)
|
| 344 |
+
usage_factor = st.number_input(
|
| 345 |
+
"Usage Factor",
|
| 346 |
+
min_value=0.0,
|
| 347 |
+
max_value=1.0,
|
| 348 |
+
value=0.7,
|
| 349 |
+
step=0.1
|
| 350 |
+
)
|
| 351 |
+
radiation_fraction = st.number_input(
|
| 352 |
+
"Radiation Fraction",
|
| 353 |
+
min_value=0.0,
|
| 354 |
+
max_value=1.0,
|
| 355 |
+
value=0.3,
|
| 356 |
+
step=0.1
|
| 357 |
+
)
|
| 358 |
+
zone_type = st.selectbox("Zone Type", ["Office", "Classroom", "Retail", "Residential"])
|
| 359 |
+
hours_in_operation = st.number_input(
|
| 360 |
+
"Hours in Operation",
|
| 361 |
+
min_value=0.0,
|
| 362 |
+
max_value=24.0,
|
| 363 |
+
value=8.0,
|
| 364 |
+
step=0.5
|
| 365 |
+
)
|
| 366 |
+
equipment_name = st.text_input("Name", value="Office Equipment")
|
| 367 |
+
|
| 368 |
+
if st.form_submit_button("Add Equipment Load"):
|
| 369 |
+
equipment_load = {
|
| 370 |
+
"id": f"equipment_{len(st.session_state.internal_loads['equipment'])}",
|
| 371 |
+
"name": equipment_name,
|
| 372 |
+
"power": power,
|
| 373 |
+
"usage_factor": usage_factor,
|
| 374 |
+
"radiation_fraction": radiation_fraction,
|
| 375 |
+
"zone_type": zone_type,
|
| 376 |
+
"hours_in_operation": hours_in_operation
|
| 377 |
+
}
|
| 378 |
+
is_valid, message = self.validate_internal_load('equipment', equipment_load)
|
| 379 |
+
if is_valid:
|
| 380 |
+
st.session_state.internal_loads['equipment'].append(equipment_load)
|
| 381 |
+
st.success("Equipment load added!")
|
| 382 |
+
st.rerun()
|
| 383 |
+
else:
|
| 384 |
+
st.error(message)
|
| 385 |
+
|
| 386 |
+
if st.session_state.internal_loads['equipment']:
|
| 387 |
+
equipment_df = pd.DataFrame(st.session_state.internal_loads['equipment'])
|
| 388 |
+
st.dataframe(equipment_df, use_container_width=True)
|
| 389 |
+
|
| 390 |
+
selected_equipment = st.multiselect(
|
| 391 |
+
"Select Equipment Loads to Delete",
|
| 392 |
+
[load['id'] for load in st.session_state.internal_loads['equipment']]
|
| 393 |
+
)
|
| 394 |
+
if st.button("Delete Selected Equipment Loads"):
|
| 395 |
+
st.session_state.internal_loads['equipment'] = [
|
| 396 |
+
load for load in st.session_state.internal_loads['equipment']
|
| 397 |
+
if load['id'] not in selected_equipment
|
| 398 |
+
]
|
| 399 |
+
st.success("Selected equipment loads deleted!")
|
| 400 |
+
st.rerun()
|
| 401 |
+
|
| 402 |
col1, col2 = st.columns(2)
|
| 403 |
with col1:
|
| 404 |
+
st.button(
|
| 405 |
+
"Back to Building Components",
|
| 406 |
+
on_click=lambda: setattr(st.session_state, "page", "Building Components")
|
| 407 |
+
)
|
| 408 |
with col2:
|
| 409 |
+
st.button(
|
| 410 |
+
"Continue to Calculation Results",
|
| 411 |
+
on_click=lambda: setattr(st.session_state, "page", "Calculation Results")
|
| 412 |
+
)
|
| 413 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
def calculate_cooling(self) -> Tuple[bool, str, Dict]:
|
| 415 |
"""
|
| 416 |
Calculate cooling loads using CoolingLoadCalculator.
|
| 417 |
Returns: (success, message, results)
|
| 418 |
"""
|
| 419 |
try:
|
| 420 |
+
# Validate inputs
|
| 421 |
valid, message = self.validate_calculation_inputs()
|
| 422 |
if not valid:
|
| 423 |
return False, message, {}
|
| 424 |
+
|
| 425 |
+
# Gather inputs
|
| 426 |
building_components = st.session_state.get('components', {})
|
| 427 |
internal_loads = st.session_state.get('internal_loads', {})
|
| 428 |
building_info = st.session_state.get('building_info', {})
|
| 429 |
+
|
| 430 |
+
# Check climate data
|
| 431 |
+
if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
|
| 432 |
+
return False, "Please enter climate data in the 'Climate Data' page.", {}
|
| 433 |
+
|
| 434 |
+
# Extract climate data
|
| 435 |
country = building_info.get('country', '').strip().title()
|
| 436 |
city = building_info.get('city', '').strip().title()
|
| 437 |
if not country or not city:
|
| 438 |
return False, "Country and city must be set in Building Information.", {}
|
|
|
|
| 439 |
climate_id = self.generate_climate_id(country, city)
|
| 440 |
location = self.climate_data.get_location_by_id(climate_id, st.session_state)
|
| 441 |
if not location:
|
| 442 |
available_locations = list(self.climate_data.locations.keys())[:5]
|
| 443 |
return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
|
| 444 |
+
|
| 445 |
+
# Validate climate data
|
| 446 |
if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
|
| 447 |
return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
|
| 448 |
+
|
| 449 |
+
# Format conditions
|
| 450 |
outdoor_conditions = {
|
| 451 |
'temperature': location['summer_design_temp_db'],
|
| 452 |
'relative_humidity': location['monthly_humidity'].get('Jul', 50.0),
|
|
|
|
| 454 |
'month': 'Jul',
|
| 455 |
'latitude': f"{location['latitude']}N" if location['latitude'] >= 0 else f"{abs(location['latitude'])}S",
|
| 456 |
'wind_speed': building_info.get('wind_speed', 4.0),
|
| 457 |
+
'day_of_year': 204 # Approx. July 23
|
| 458 |
}
|
| 459 |
indoor_conditions = {
|
| 460 |
'temperature': building_info.get('indoor_temp', 24.0),
|
| 461 |
'relative_humidity': building_info.get('indoor_rh', 50.0)
|
| 462 |
}
|
| 463 |
+
|
| 464 |
if st.session_state.get('debug_mode', False):
|
| 465 |
st.write("Debug: Cooling Input State", {
|
| 466 |
'climate_id': climate_id,
|
| 467 |
'outdoor_conditions': outdoor_conditions,
|
| 468 |
'indoor_conditions': indoor_conditions,
|
| 469 |
'components': {k: len(v) for k, v in building_components.items()},
|
| 470 |
+
'internal_loads': {
|
| 471 |
+
'people': len(internal_loads.get('people', [])),
|
| 472 |
+
'lighting': len(internal_loads.get('lighting', [])),
|
| 473 |
+
'equipment': len(internal_loads.get('equipment', []))
|
| 474 |
+
},
|
| 475 |
+
'building_info': building_info
|
| 476 |
})
|
| 477 |
+
|
| 478 |
+
# Format internal loads
|
| 479 |
formatted_internal_loads = {
|
| 480 |
+
'people': {
|
| 481 |
+
'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
|
| 482 |
+
'activity_level': internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
|
| 483 |
+
'operating_hours': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}:00-{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)+10}:00"
|
| 484 |
+
},
|
| 485 |
+
'lights': {
|
| 486 |
+
'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
|
| 487 |
+
'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
|
| 488 |
+
'special_allowance': 0.1,
|
| 489 |
+
'hours_operation': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h"
|
| 490 |
+
},
|
| 491 |
+
'equipment': {
|
| 492 |
+
'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
|
| 493 |
+
'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
|
| 494 |
+
'radiation_factor': internal_loads.get('equipment', [{}])[0].get('radiation_fraction', 0.3),
|
| 495 |
+
'hours_operation': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h"
|
| 496 |
+
},
|
| 497 |
'infiltration': {
|
| 498 |
'flow_rate': building_info.get('infiltration_rate', 0.05),
|
| 499 |
'height': building_info.get('building_height', 3.0),
|
|
|
|
| 504 |
},
|
| 505 |
'operating_hours': building_info.get('operating_hours', '8:00-18:00')
|
| 506 |
}
|
| 507 |
+
|
| 508 |
+
# Calculate hourly loads
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
hourly_loads = self.cooling_calculator.calculate_hourly_cooling_loads(
|
| 510 |
building_components=building_components,
|
| 511 |
outdoor_conditions=outdoor_conditions,
|
|
|
|
| 514 |
building_volume=building_info.get('floor_area', 100.0) * building_info.get('building_height', 3.0)
|
| 515 |
)
|
| 516 |
if not hourly_loads:
|
| 517 |
+
return False, "Cooling hourly loads calculation failed. Check input data.", {}
|
| 518 |
+
|
| 519 |
+
# Get design loads
|
| 520 |
design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads)
|
| 521 |
if not design_loads:
|
| 522 |
+
return False, "Cooling design loads calculation failed. Check input data.", {}
|
| 523 |
+
|
| 524 |
+
# Get summary
|
| 525 |
summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads)
|
| 526 |
if not summary:
|
| 527 |
+
return False, "Cooling load summary calculation failed. Check input data.", {}
|
| 528 |
+
|
| 529 |
+
# Format results for results_display.py
|
| 530 |
floor_area = building_info.get('floor_area', 100.0) or 100.0
|
| 531 |
results = {
|
| 532 |
'total_load': summary['total'] / 1000, # kW
|
|
|
|
| 565 |
},
|
| 566 |
'building_info': building_info
|
| 567 |
}
|
| 568 |
+
|
| 569 |
# Populate detailed loads
|
| 570 |
for wall in building_components.get('walls', []):
|
| 571 |
load = self.cooling_calculator.calculate_wall_cooling_load(
|
|
|
|
| 593 |
),
|
| 594 |
'load': load / 1000
|
| 595 |
})
|
| 596 |
+
|
| 597 |
for roof in building_components.get('roofs', []):
|
| 598 |
load = self.cooling_calculator.calculate_roof_cooling_load(
|
| 599 |
roof=roof,
|
|
|
|
| 619 |
),
|
| 620 |
'load': load / 1000
|
| 621 |
})
|
| 622 |
+
|
| 623 |
for window in building_components.get('windows', []):
|
| 624 |
load_dict = self.cooling_calculator.calculate_window_cooling_load(
|
| 625 |
window=window,
|
|
|
|
| 647 |
),
|
| 648 |
'load': load_dict['total'] / 1000
|
| 649 |
})
|
| 650 |
+
|
| 651 |
for door in building_components.get('doors', []):
|
| 652 |
load = self.cooling_calculator.calculate_door_cooling_load(
|
| 653 |
door=door,
|
|
|
|
| 662 |
'cltd': outdoor_conditions['temperature'] - indoor_conditions['temperature'],
|
| 663 |
'load': load / 1000
|
| 664 |
})
|
| 665 |
+
|
| 666 |
for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
|
| 667 |
for load in internal_loads.get(key, []):
|
| 668 |
if load_type == 'people':
|
|
|
|
| 669 |
load_dict = self.cooling_calculator.calculate_people_cooling_load(
|
| 670 |
num_people=load['num_people'],
|
| 671 |
activity_level=load['activity_level'],
|
| 672 |
hour=design_loads['design_hour']
|
| 673 |
)
|
| 674 |
elif load_type == 'lighting':
|
|
|
|
| 675 |
load_dict = {'total': self.cooling_calculator.calculate_lights_cooling_load(
|
| 676 |
power=load['power'],
|
| 677 |
use_factor=load['usage_factor'],
|
|
|
|
| 679 |
hour=design_loads['design_hour']
|
| 680 |
)}
|
| 681 |
else:
|
|
|
|
| 682 |
load_dict = self.cooling_calculator.calculate_equipment_cooling_load(
|
| 683 |
power=load['power'],
|
| 684 |
use_factor=load['usage_factor'],
|
|
|
|
| 697 |
) if load_type == 'people' else 1.0,
|
| 698 |
'load': load_dict['total'] / 1000
|
| 699 |
})
|
| 700 |
+
|
| 701 |
if st.session_state.get('debug_mode', False):
|
| 702 |
st.write("Debug: Cooling Results", {
|
| 703 |
'total_load': results.get('total_load', 'N/A'),
|
| 704 |
'component_loads': results.get('component_loads', 'N/A'),
|
| 705 |
'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
|
| 706 |
})
|
| 707 |
+
|
| 708 |
return True, "Cooling calculation completed.", results
|
| 709 |
+
|
| 710 |
except ValueError as ve:
|
| 711 |
+
st.error(f"Input error in cooling calculation: {str(ve)}")
|
| 712 |
return False, f"Input error: {str(ve)}", {}
|
| 713 |
except KeyError as ke:
|
| 714 |
+
st.error(f"Missing data in cooling calculation: {str(ke)}")
|
| 715 |
return False, f"Missing data: {str(ke)}", {}
|
| 716 |
except Exception as e:
|
| 717 |
+
st.error(f"Unexpected error in cooling calculation: {str(e)}")
|
| 718 |
return False, f"Unexpected error: {str(e)}", {}
|
| 719 |
+
|
| 720 |
def calculate_heating(self) -> Tuple[bool, str, Dict]:
|
| 721 |
"""
|
| 722 |
Calculate heating loads using HeatingLoadCalculator.
|
| 723 |
Returns: (success, message, results)
|
| 724 |
"""
|
| 725 |
try:
|
| 726 |
+
# Validate inputs
|
| 727 |
valid, message = self.validate_calculation_inputs()
|
| 728 |
if not valid:
|
| 729 |
return False, message, {}
|
| 730 |
+
|
| 731 |
+
# Gather inputs
|
| 732 |
building_components = st.session_state.get('components', {})
|
| 733 |
internal_loads = st.session_state.get('internal_loads', {})
|
| 734 |
building_info = st.session_state.get('building_info', {})
|
| 735 |
+
|
| 736 |
+
# Check climate data
|
| 737 |
+
if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
|
| 738 |
+
return False, "Please enter climate data in the 'Climate Data' page.", {}
|
| 739 |
+
|
| 740 |
+
# Extract climate data
|
| 741 |
country = building_info.get('country', '').strip().title()
|
| 742 |
city = building_info.get('city', '').strip().title()
|
| 743 |
if not country or not city:
|
| 744 |
return False, "Country and city must be set in Building Information.", {}
|
|
|
|
| 745 |
climate_id = self.generate_climate_id(country, city)
|
| 746 |
location = self.climate_data.get_location_by_id(climate_id, st.session_state)
|
| 747 |
if not location:
|
| 748 |
available_locations = list(self.climate_data.locations.keys())[:5]
|
| 749 |
return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
|
| 750 |
+
|
| 751 |
+
# Validate climate data
|
| 752 |
if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
|
| 753 |
return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
|
| 754 |
+
|
| 755 |
+
# Format conditions
|
| 756 |
outdoor_conditions = {
|
| 757 |
'design_temperature': location['winter_design_temp'],
|
| 758 |
'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
|
|
|
|
| 763 |
'temperature': building_info.get('indoor_temp', 21.0),
|
| 764 |
'relative_humidity': building_info.get('indoor_rh', 40.0)
|
| 765 |
}
|
| 766 |
+
|
| 767 |
if st.session_state.get('debug_mode', False):
|
| 768 |
st.write("Debug: Heating Input State", {
|
| 769 |
'climate_id': climate_id,
|
| 770 |
'outdoor_conditions': outdoor_conditions,
|
| 771 |
'indoor_conditions': indoor_conditions,
|
| 772 |
'components': {k: len(v) for k, v in building_components.items()},
|
| 773 |
+
'internal_loads': {
|
| 774 |
+
'people': len(internal_loads.get('people', [])),
|
| 775 |
+
'lighting': len(internal_loads.get('lighting', [])),
|
| 776 |
+
'equipment': len(internal_loads.get('equipment', []))
|
| 777 |
+
},
|
| 778 |
+
'building_info': building_info
|
| 779 |
})
|
| 780 |
+
|
| 781 |
+
# Format internal loads
|
| 782 |
formatted_internal_loads = {
|
| 783 |
+
'people': {
|
| 784 |
+
'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
|
| 785 |
+
'sensible_gain': 70,
|
| 786 |
+
'operating_hours': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}:00-{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)+10}:00"
|
| 787 |
+
},
|
| 788 |
+
'lights': {
|
| 789 |
+
'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
|
| 790 |
+
'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
|
| 791 |
+
'hours_operation': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h"
|
| 792 |
+
},
|
| 793 |
+
'equipment': {
|
| 794 |
+
'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
|
| 795 |
+
'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
|
| 796 |
+
'hours_operation': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h"
|
| 797 |
+
},
|
| 798 |
'infiltration': {
|
| 799 |
'flow_rate': building_info.get('infiltration_rate', 0.05),
|
| 800 |
'height': building_info.get('building_height', 3.0),
|
|
|
|
| 806 |
'usage_factor': 0.7,
|
| 807 |
'operating_hours': building_info.get('operating_hours', '8:00-18:00')
|
| 808 |
}
|
| 809 |
+
|
| 810 |
+
# Calculate design loads
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
design_loads = self.heating_calculator.calculate_design_heating_load(
|
| 812 |
building_components=building_components,
|
| 813 |
outdoor_conditions=outdoor_conditions,
|
|
|
|
| 815 |
internal_loads=formatted_internal_loads
|
| 816 |
)
|
| 817 |
if not design_loads:
|
| 818 |
+
return False, "Heating design loads calculation failed. Check input data.", {}
|
| 819 |
+
|
| 820 |
+
# Get summary
|
| 821 |
summary = self.heating_calculator.calculate_heating_load_summary(design_loads)
|
| 822 |
if not summary:
|
| 823 |
+
return False, "Heating load summary calculation failed. Check input data.", {}
|
| 824 |
+
|
| 825 |
+
# Format results
|
| 826 |
floor_area = building_info.get('floor_area', 100.0) or 100.0
|
| 827 |
results = {
|
| 828 |
'total_load': summary['total'] / 1000, # kW
|
|
|
|
| 857 |
},
|
| 858 |
'building_info': building_info
|
| 859 |
}
|
| 860 |
+
|
| 861 |
+
# Populate detailed loads
|
| 862 |
delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
|
| 863 |
for wall in building_components.get('walls', []):
|
| 864 |
load = self.heating_calculator.calculate_wall_heating_load(
|
|
|
|
| 874 |
'delta_t': delta_t,
|
| 875 |
'load': load / 1000
|
| 876 |
})
|
| 877 |
+
|
| 878 |
for roof in building_components.get('roofs', []):
|
| 879 |
load = self.heating_calculator.calculate_roof_heating_load(
|
| 880 |
roof=roof,
|
|
|
|
| 889 |
'delta_t': delta_t,
|
| 890 |
'load': load / 1000
|
| 891 |
})
|
| 892 |
+
|
| 893 |
for floor in building_components.get('floors', []):
|
| 894 |
load = self.heating_calculator.calculate_floor_heating_load(
|
| 895 |
floor=floor,
|
|
|
|
| 903 |
'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
|
| 904 |
'load': load / 1000
|
| 905 |
})
|
| 906 |
+
|
| 907 |
for window in building_components.get('windows', []):
|
| 908 |
load = self.heating_calculator.calculate_window_heating_load(
|
| 909 |
window=window,
|
|
|
|
| 918 |
'delta_t': delta_t,
|
| 919 |
'load': load / 1000
|
| 920 |
})
|
| 921 |
+
|
| 922 |
for door in building_components.get('doors', []):
|
| 923 |
load = self.heating_calculator.calculate_door_heating_load(
|
| 924 |
door=door,
|
|
|
|
| 933 |
'delta_t': delta_t,
|
| 934 |
'load': load / 1000
|
| 935 |
})
|
| 936 |
+
|
| 937 |
if st.session_state.get('debug_mode', False):
|
| 938 |
st.write("Debug: Heating Results", {
|
| 939 |
'total_load': results.get('total_load', 'N/A'),
|
| 940 |
'component_loads': results.get('component_loads', 'N/A'),
|
| 941 |
'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
|
| 942 |
})
|
| 943 |
+
|
| 944 |
return True, "Heating calculation completed.", results
|
| 945 |
+
|
| 946 |
except ValueError as ve:
|
| 947 |
+
st.error(f"Input error in heating calculation: {str(ve)}")
|
| 948 |
return False, f"Input error: {str(ve)}", {}
|
| 949 |
except KeyError as ke:
|
| 950 |
+
st.error(f"Missing data in heating calculation: {str(ke)}")
|
| 951 |
return False, f"Missing data: {str(ke)}", {}
|
| 952 |
except Exception as e:
|
| 953 |
+
st.error(f"Unexpected error in heating calculation: {str(e)}")
|
| 954 |
return False, f"Unexpected error: {str(e)}", {}
|
| 955 |
+
|
| 956 |
def display_calculation_results(self):
|
|
|
|
| 957 |
st.title("Calculation Results")
|
| 958 |
+
|
| 959 |
col1, col2 = st.columns(2)
|
| 960 |
with col1:
|
| 961 |
calculate_button = st.button("Calculate Loads")
|
| 962 |
with col2:
|
| 963 |
st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False))
|
| 964 |
+
|
| 965 |
if calculate_button:
|
| 966 |
+
# Reset results
|
| 967 |
st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
|
| 968 |
+
|
| 969 |
with st.spinner("Calculating loads..."):
|
| 970 |
+
# Calculate cooling load
|
| 971 |
cooling_success, cooling_message, cooling_results = self.calculate_cooling()
|
| 972 |
if cooling_success:
|
| 973 |
st.session_state.calculation_results['cooling'] = cooling_results
|
| 974 |
st.success(cooling_message)
|
| 975 |
else:
|
| 976 |
st.error(cooling_message)
|
| 977 |
+
|
| 978 |
+
# Calculate heating load
|
| 979 |
heating_success, heating_message, heating_results = self.calculate_heating()
|
| 980 |
if heating_success:
|
| 981 |
st.session_state.calculation_results['heating'] = heating_results
|
| 982 |
st.success(heating_message)
|
| 983 |
else:
|
| 984 |
st.error(heating_message)
|
| 985 |
+
|
| 986 |
+
# Display results
|
| 987 |
self.results_display.display_results(st.session_state)
|
| 988 |
+
|
| 989 |
+
# Navigation
|
| 990 |
col1, col2 = st.columns(2)
|
| 991 |
with col1:
|
| 992 |
+
st.button(
|
| 993 |
+
"Back to Internal Loads",
|
| 994 |
+
on_click=lambda: setattr(st.session_state, "page", "Internal Loads")
|
| 995 |
+
)
|
| 996 |
with col2:
|
| 997 |
+
st.button(
|
| 998 |
+
"Continue to Export Data",
|
| 999 |
+
on_click=lambda: setattr(st.session_state, "page", "Export Data")
|
| 1000 |
+
)
|
| 1001 |
|
| 1002 |
if __name__ == "__main__":
|
| 1003 |
+
app = HVACCalculator()
|
|
|
|
|
|
|
|
|
|
|
|