mabuseif commited on
Commit
69e71f6
·
verified ·
1 Parent(s): 10d8a4d

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +183 -82
data/climate_data.py CHANGED
@@ -4,7 +4,7 @@ Extracts climate data from EPW files and provides visualizations inspired by Cli
4
 
5
  Author: Dr Majed Abuseif
6
  Date: May 2025
7
- Version: 2.1.1
8
  """
9
 
10
  from typing import Dict, List, Any, Optional
@@ -23,6 +23,24 @@ import re
23
  # Define paths
24
  DATA_DIR = os.path.dirname(os.path.abspath(__file__))
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  @dataclass
27
  class ClimateLocation:
28
  """Class representing a climate location with ASHRAE 169 data derived from EPW files."""
@@ -258,8 +276,9 @@ class ClimateData:
258
  # Validate typical/extreme periods (optional)
259
  if "typical_extreme_periods" in data and data["typical_extreme_periods"]:
260
  expected_periods = ["summer_extreme", "summer_typical", "winter_extreme", "winter_typical"]
261
- if not all(key in data["typical_extreme_periods"] for key in expected_periods):
262
- st.warning("Validation warning: Missing some typical/extreme periods")
 
263
  for period in data["typical_extreme_periods"].values():
264
  for date in ["start", "end"]:
265
  if not (1 <= period[date]["month"] <= 12 and 1 <= period[date]["day"] <= 31):
@@ -309,6 +328,9 @@ class ClimateData:
309
  """Display Streamlit interface for EPW upload and visualizations."""
310
  st.title("Climate Data Analysis")
311
 
 
 
 
312
  # Clear invalid session_state["climate_data"] to prevent validation errors
313
  if "climate_data" in session_state and not all(key in session_state["climate_data"] for key in ["id", "country", "city"]):
314
  st.warning("Clearing invalid climate data from session state.")
@@ -342,55 +364,96 @@ class ClimateData:
342
  if line.startswith("DESIGN CONDITIONS"):
343
  parts = line.strip().split(',')
344
  if len(parts) < 41:
345
- st.warning("Incomplete DESIGN CONDITIONS data, skipping parsing.")
346
  break
347
  try:
348
  design_conditions = {
349
- "heating": {
350
- "coldest_month": int(parts[3]),
351
- "dry_bulb": float(parts[4]),
352
- "mean_coincident_db": float(parts[5]),
353
- "humidification_db": float(parts[6]),
354
- "mean_coincident_wb": float(parts[7]),
355
- "wind_speed_1": float(parts[8]),
356
- "wind_direction_1": float(parts[9]),
357
- "wind_speed_2": float(parts[10]),
358
- "wind_direction_2": float(parts[11])
359
- },
360
- "cooling": {
361
- "hottest_month": int(parts[12]),
362
- "dry_bulb_0_4": float(parts[13]),
363
- "mean_coincident_wb_0_4": float(parts[14]),
364
- "wet_bulb_0_4": float(parts[15]),
365
- "mean_coincident_db_0_4": float(parts[16]),
366
- "dry_bulb_1_0": float(parts[17]),
367
- "mean_coincident_wb_1_0": float(parts[18]),
368
- "wet_bulb_1_0": float(parts[19]),
369
- "mean_coincident_db_1_0": float(parts[20]),
370
- "dry_bulb_2_0": float(parts[21]),
371
- "mean_coincident_wb_2_0": float(parts[22]),
372
- "wet_bulb_2_0": float(parts[23]),
373
- "mean_coincident_db_2_0": float(parts[24]),
374
- "evaporation_wb_0_4": float(parts[25]),
375
- "mean_coincident_db_wb_0_4": float(parts[26])
376
- },
377
- "extremes": {
378
- "annual_max_db": float(parts[27]),
379
- "mean_db": float(parts[28]),
380
- "std_dev_db": float(parts[29]),
381
- "min_db": float(parts[30]),
382
- "max_db_1": float(parts[31]),
383
- "min_db_1": float(parts[32]),
384
- "max_db_2": float(parts[33]),
385
- "min_db_2": float(parts[34]),
386
- "max_db_3": float(parts[35]),
387
- "min_db_3": float(parts[36]),
388
- "max_db_4": float(parts[37]),
389
- "min_db_4": float(parts[38]),
390
- "max_db_5": float(parts[39]),
391
- "min_db_5": float(parts[40])
392
- }
393
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  except (ValueError, IndexError) as e:
395
  st.warning(f"Error parsing DESIGN CONDITIONS: {str(e)}. Using empty design conditions.")
396
  design_conditions = {}
@@ -398,7 +461,7 @@ class ClimateData:
398
 
399
  # Parse TYPICAL/EXTREME PERIODS
400
  typical_extreme_periods = {}
401
- date_pattern = r'^\d{1,2}/\d{1,2}$'
402
  for line in epw_lines:
403
  if line.startswith("TYPICAL/EXTREME PERIODS"):
404
  parts = line.strip().split(',')
@@ -409,10 +472,13 @@ class ClimateData:
409
  break
410
  for i in range(num_periods):
411
  try:
 
 
 
412
  period_name = parts[2 + i*4]
413
  period_type = parts[3 + i*4]
414
- start_date = parts[4 + i*4]
415
- end_date = parts[5 + i*4]
416
  if period_name in [
417
  "Summer - Week Nearest Max Temperature For Period",
418
  "Summer - Week Nearest Average Temperature For Period",
@@ -420,11 +486,15 @@ class ClimateData:
420
  "Winter - Week Nearest Average Temperature For Period"
421
  ]:
422
  key = f"{'summer' if 'Summer' in period_name else 'winter'}_{'extreme' if 'Max' in period_name else 'typical' if 'Average' in period_name else ''}"
423
- if not re.match(date_pattern, start_date) or not re.match(date_pattern, end_date.replace(' ', '')):
 
 
 
 
 
424
  st.warning(f"Invalid date format for period {period_name}: {start_date} to {end_date}, skipping.")
425
  continue
426
- start_month, start_day = map(int, start_date.split('/'))
427
- end_date_clean = re.sub(r'\s+', '', end_date)
428
  end_month, end_day = map(int, end_date_clean.split('/'))
429
  typical_extreme_periods[key] = {
430
  "start": {"month": start_month, "day": start_day},
@@ -447,8 +517,14 @@ class ClimateData:
447
  break
448
  for i in range(num_depths):
449
  try:
 
 
 
450
  depth = parts[2 + i*16]
451
- temps = [float(t) for t in parts[6 + i*16:18 + i*16]]
 
 
 
452
  ground_temperatures[depth] = temps
453
  except (ValueError, IndexError) as e:
454
  st.warning(f"Error parsing ground temperatures for depth {i+1}: {str(e)}, skipping.")
@@ -563,7 +639,8 @@ class ClimateData:
563
  st.subheader("Design Conditions")
564
 
565
  # Location Details
566
- st.markdown("""
 
567
  **Location Details:**
568
  - **Country**: {location.country}
569
  - **City**: {location.city}
@@ -571,10 +648,12 @@ class ClimateData:
571
  - **Latitude**: {location.latitude}°
572
  - **Longitude**: {location.longitude}°
573
  - **Elevation**: {location.elevation} m
574
- """.format(location=location))
 
575
 
576
  # Calculated Climate Parameters
577
- st.markdown("""
 
578
  **Calculated Climate Parameters:**
579
  - **Climate Zone**: {location.climate_zone}
580
  - **Heating Degree Days (base 18°C)**: {location.heating_degree_days} HDD
@@ -585,42 +664,64 @@ class ClimateData:
585
  - **Summer Daily Temperature Range**: {location.summer_daily_range} °C
586
  - **Mean Wind Speed**: {location.wind_speed} m/s
587
  - **Mean Atmospheric Pressure**: {location.pressure} Pa
588
- """.format(location=location))
 
589
 
590
  # EPW Header Design Conditions
591
  if location.design_conditions:
592
- st.markdown("**Design Conditions (EPW Header):**")
593
- st.markdown("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  - **Heating:**
595
- - Coldest Month: {location.design_conditions['heating']['coldest_month']}
596
- - Dry-Bulb Temp: {location.design_conditions['heating']['dry_bulb']} °C
597
- - Mean Coincident Dry-Bulb: {location.design_conditions['heating']['mean_coincident_db']} °C
598
- - Humidification Dry-Bulb: {location.design_conditions['heating']['humidification_db']} °C
599
- - Mean Coincident Wet-Bulb: {location.design_conditions['heating']['mean_coincident_wb']} °C
600
  - **Cooling:**
601
- - Hottest Month: {location.design_conditions['cooling']['hottest_month']}
602
- - Dry-Bulb Temp (0.4%): {location.design_conditions['cooling']['dry_bulb_0_4']} °C
603
- - Wet-Bulb Temp (0.4%): {location.design_conditions['cooling']['wet_bulb_0_4']} °C
604
- - Mean Coincident Wet-Bulb (0.4%): {location.design_conditions['cooling']['mean_coincident_wb_0_4']} °C
605
  - **Extremes:**
606
- - Annual Max Dry-Bulb: {location.design_conditions['extremes']['annual_max_db']} °C
607
- - Annual Min Dry-Bulb: {location.design_conditions['extremes']['min_db']} °C
608
- """.format(location=location))
609
 
610
  # Typical/Extreme Periods
611
  if location.typical_extreme_periods:
612
- st.markdown("**Typical/Extreme Periods:**")
613
- for key, period in location.typical_extreme_periods.items():
614
- period_name = key.replace('_', ' ').title()
615
- st.markdown(f"- **{period_name}**: {period['start']['month']}/{period['start']['day']} to {period['end']['month']}/{period['end']['day']}")
 
 
 
 
 
 
616
 
617
- # Ground Temperatures
618
  if location.ground_temperatures:
619
- st.markdown("**Ground Temperatures:**")
620
  month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
 
621
  for depth, temps in location.ground_temperatures.items():
622
- temp_str = ", ".join(f"{month}: {temp:.2f}°C" for month, temp in zip(month_names, temps))
623
- st.markdown(f"- **Depth {depth}m**: {temp_str}")
 
 
 
624
 
625
  @staticmethod
626
  def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
@@ -660,7 +761,7 @@ class ClimateData:
660
  vapor_pressure = humidity / 100 * saturation_pressure
661
  humidity_ratio = 0.62198 * vapor_pressure / (pressure - vapor_pressure) * 1000 # Convert to g/kg
662
 
663
- fig = go.Figure()
664
 
665
  # Hourly data points
666
  fig.add_trace(go.Scatter(
 
4
 
5
  Author: Dr Majed Abuseif
6
  Date: May 2025
7
+ Version: 2.1.2
8
  """
9
 
10
  from typing import Dict, List, Any, Optional
 
23
  # Define paths
24
  DATA_DIR = os.path.dirname(os.path.abspath(__file__))
25
 
26
+ # CSS for consistent formatting
27
+ STYLE = """
28
+ <style>
29
+ .markdown-text {
30
+ font-family: Roboto, sans-serif;
31
+ font-size: 14px;
32
+ line-height: 1.5;
33
+ }
34
+ .markdown-text ul {
35
+ margin-top: 0;
36
+ margin-bottom: 10px;
37
+ }
38
+ .markdown-text li {
39
+ margin-bottom: 5px;
40
+ }
41
+ </style>
42
+ """
43
+
44
  @dataclass
45
  class ClimateLocation:
46
  """Class representing a climate location with ASHRAE 169 data derived from EPW files."""
 
276
  # Validate typical/extreme periods (optional)
277
  if "typical_extreme_periods" in data and data["typical_extreme_periods"]:
278
  expected_periods = ["summer_extreme", "summer_typical", "winter_extreme", "winter_typical"]
279
+ missing_periods = [p for p in expected_periods if p not in data["typical_extreme_periods"]]
280
+ if missing_periods:
281
+ st.warning(f"Validation warning: Missing typical/extreme periods: {', '.join(missing_periods)}")
282
  for period in data["typical_extreme_periods"].values():
283
  for date in ["start", "end"]:
284
  if not (1 <= period[date]["month"] <= 12 and 1 <= period[date]["day"] <= 31):
 
328
  """Display Streamlit interface for EPW upload and visualizations."""
329
  st.title("Climate Data Analysis")
330
 
331
+ # Apply consistent styling
332
+ st.markdown(STYLE, unsafe_allow_html=True)
333
+
334
  # Clear invalid session_state["climate_data"] to prevent validation errors
335
  if "climate_data" in session_state and not all(key in session_state["climate_data"] for key in ["id", "country", "city"]):
336
  st.warning("Clearing invalid climate data from session state.")
 
364
  if line.startswith("DESIGN CONDITIONS"):
365
  parts = line.strip().split(',')
366
  if len(parts) < 41:
367
+ st.warning(f"DESIGN CONDITIONS has {len(parts)} fields, expected 41. Skipping parsing.")
368
  break
369
  try:
370
  design_conditions = {
371
+ "heating": {},
372
+ "cooling": {},
373
+ "extremes": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
375
+ # Heating section
376
+ if parts[3] and parts[3].strip():
377
+ design_conditions["heating"]["coldest_month"] = int(parts[3])
378
+ if parts[4] and parts[4].strip():
379
+ design_conditions["heating"]["dry_bulb"] = float(parts[4])
380
+ if parts[5] and parts[5].strip():
381
+ design_conditions["heating"]["mean_coincident_db"] = float(parts[5])
382
+ if parts[6] and parts[6].strip():
383
+ design_conditions["heating"]["humidification_db"] = float(parts[6])
384
+ if parts[7] and parts[7].strip():
385
+ design_conditions["heating"]["mean_coincident_wb"] = float(parts[7])
386
+ if parts[8] and parts[8].strip():
387
+ design_conditions["heating"]["wind_speed_1"] = float(parts[8])
388
+ if parts[9] and parts[9].strip():
389
+ design_conditions["heating"]["wind_direction_1"] = float(parts[9])
390
+ if parts[10] and parts[10].strip():
391
+ design_conditions["heating"]["wind_speed_2"] = float(parts[10])
392
+ if parts[11] and parts[11].strip():
393
+ design_conditions["heating"]["wind_direction_2"] = float(parts[11])
394
+ # Cooling section
395
+ if parts[12] and parts[12].strip():
396
+ design_conditions["cooling"]["hottest_month"] = int(parts[12])
397
+ if parts[13] and parts[13].strip():
398
+ design_conditions["cooling"]["dry_bulb_0_4"] = float(parts[13])
399
+ if parts[14] and parts[14].strip():
400
+ design_conditions["cooling"]["mean_coincident_wb_0_4"] = float(parts[14])
401
+ if parts[15] and parts[15].strip():
402
+ design_conditions["cooling"]["wet_bulb_0_4"] = float(parts[15])
403
+ if parts[16] and parts[16].strip():
404
+ design_conditions["cooling"]["mean_coincident_db_0_4"] = float(parts[16])
405
+ if parts[17] and parts[17].strip():
406
+ design_conditions["cooling"]["dry_bulb_1_0"] = float(parts[17])
407
+ if parts[18] and parts[18].strip():
408
+ design_conditions["cooling"]["mean_coincident_wb_1_0"] = float(parts[18])
409
+ if parts[19] and parts[19].strip():
410
+ design_conditions["cooling"]["wet_bulb_1_0"] = float(parts[19])
411
+ if parts[20] and parts[20].strip():
412
+ design_conditions["cooling"]["mean_coincident_db_1_0"] = float(parts[20])
413
+ if parts[21] and parts[21].strip():
414
+ design_conditions["cooling"]["dry_bulb_2_0"] = float(parts[21])
415
+ if parts[22] and parts[22].strip():
416
+ design_conditions["cooling"]["mean_coincident_wb_2_0"] = float(parts[22])
417
+ if parts[23] and parts[23].strip():
418
+ design_conditions["cooling"]["wet_bulb_2_0"] = float(parts[23])
419
+ if parts[24] and parts[24].strip():
420
+ design_conditions["cooling"]["mean_coincident_db_2_0"] = float(parts[24])
421
+ if parts[25] and parts[25].strip():
422
+ design_conditions["cooling"]["evaporation_wb_0_4"] = float(parts[25])
423
+ if parts[26] and parts[26].strip():
424
+ design_conditions["cooling"]["mean_coincident_db_wb_0_4"] = float(parts[26])
425
+ # Extremes section
426
+ if parts[27] and parts[27].strip():
427
+ design_conditions["extremes"]["annual_max_db"] = float(parts[27])
428
+ if parts[28] and parts[28].strip():
429
+ design_conditions["extremes"]["mean_db"] = float(parts[28])
430
+ if parts[29] and parts[29].strip():
431
+ design_conditions["extremes"]["std_dev_db"] = float(parts[29])
432
+ if parts[30] and parts[30].strip():
433
+ design_conditions["extremes"]["min_db"] = float(parts[30])
434
+ if parts[31] and parts[31].strip():
435
+ design_conditions["extremes"]["max_db_1"] = float(parts[31])
436
+ if parts[32] and parts[32].strip():
437
+ design_conditions["extremes"]["min_db_1"] = float(parts[32])
438
+ if parts[33] and parts[33].strip():
439
+ design_conditions["extremes"]["max_db_2"] = float(parts[33])
440
+ if parts[34] and parts[34].strip():
441
+ design_conditions["extremes"]["min_db_2"] = float(parts[34])
442
+ if parts[35] and parts[35].strip():
443
+ design_conditions["extremes"]["max_db_3"] = float(parts[35])
444
+ if parts[36] and parts[36].strip():
445
+ design_conditions["extremes"]["min_db_3"] = float(parts[36])
446
+ if parts[37] and parts[37].strip():
447
+ design_conditions["extremes"]["max_db_4"] = float(parts[37])
448
+ if parts[38] and parts[38].strip():
449
+ design_conditions["extremes"]["min_db_4"] = float(parts[38])
450
+ if parts[39] and parts[39].strip():
451
+ design_conditions["extremes"]["max_db_5"] = float(parts[39])
452
+ if parts[40] and parts[40].strip():
453
+ design_conditions["extremes"]["min_db_5"] = float(parts[40])
454
+ if not design_conditions["heating"] and not design_conditions["cooling"] and not design_conditions["extremes"]:
455
+ design_conditions = {}
456
+ st.warning("No valid DESIGN CONDITIONS fields parsed.")
457
  except (ValueError, IndexError) as e:
458
  st.warning(f"Error parsing DESIGN CONDITIONS: {str(e)}. Using empty design conditions.")
459
  design_conditions = {}
 
461
 
462
  # Parse TYPICAL/EXTREME PERIODS
463
  typical_extreme_periods = {}
464
+ date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
465
  for line in epw_lines:
466
  if line.startswith("TYPICAL/EXTREME PERIODS"):
467
  parts = line.strip().split(',')
 
472
  break
473
  for i in range(num_periods):
474
  try:
475
+ if len(parts) < 2 + i*4 + 4:
476
+ st.warning(f"Insufficient fields for period {i+1}, skipping.")
477
+ continue
478
  period_name = parts[2 + i*4]
479
  period_type = parts[3 + i*4]
480
+ start_date = parts[4 + i*4].strip()
481
+ end_date = parts[5 + i*4].strip()
482
  if period_name in [
483
  "Summer - Week Nearest Max Temperature For Period",
484
  "Summer - Week Nearest Average Temperature For Period",
 
486
  "Winter - Week Nearest Average Temperature For Period"
487
  ]:
488
  key = f"{'summer' if 'Summer' in period_name else 'winter'}_{'extreme' if 'Max' in period_name else 'typical' if 'Average' in period_name else ''}"
489
+ # Clean dates to remove non-standard whitespace
490
+ start_date_clean = re.sub(r'\s+', '', start_date)
491
+ end_date_clean = re.sub(r'\s+', '', end_date)
492
+ # Debug logging
493
+ st.write(f"Parsing period {period_name}: start_date='{start_date_clean}', end_date='{end_date_clean}'")
494
+ if not re.match(date_pattern, start_date) or not re.match(date_pattern, end_date):
495
  st.warning(f"Invalid date format for period {period_name}: {start_date} to {end_date}, skipping.")
496
  continue
497
+ start_month, start_day = map(int, start_date_clean.split('/'))
 
498
  end_month, end_day = map(int, end_date_clean.split('/'))
499
  typical_extreme_periods[key] = {
500
  "start": {"month": start_month, "day": start_day},
 
517
  break
518
  for i in range(num_depths):
519
  try:
520
+ if len(parts) < 2 + i*16 + 16:
521
+ st.warning(f"Insufficient fields for ground temperature depth {i+1}, skipping.")
522
+ continue
523
  depth = parts[2 + i*16]
524
+ temps = [float(t) for t in parts[6 + i*16:18 + i*16] if t.strip()]
525
+ if len(temps) != 12:
526
+ st.warning(f"Invalid number of temperatures for depth {depth}m, expected 12, got {len(temps)}, skipping.")
527
+ continue
528
  ground_temperatures[depth] = temps
529
  except (ValueError, IndexError) as e:
530
  st.warning(f"Error parsing ground temperatures for depth {i+1}: {str(e)}, skipping.")
 
639
  st.subheader("Design Conditions")
640
 
641
  # Location Details
642
+ st.markdown(f"""
643
+ <div class="markdown-text">
644
  **Location Details:**
645
  - **Country**: {location.country}
646
  - **City**: {location.city}
 
648
  - **Latitude**: {location.latitude}°
649
  - **Longitude**: {location.longitude}°
650
  - **Elevation**: {location.elevation} m
651
+ </div>
652
+ """, unsafe_allow_html=True)
653
 
654
  # Calculated Climate Parameters
655
+ st.markdown(f"""
656
+ <div class="markdown-text">
657
  **Calculated Climate Parameters:**
658
  - **Climate Zone**: {location.climate_zone}
659
  - **Heating Degree Days (base 18°C)**: {location.heating_degree_days} HDD
 
664
  - **Summer Daily Temperature Range**: {location.summer_daily_range} °C
665
  - **Mean Wind Speed**: {location.wind_speed} m/s
666
  - **Mean Atmospheric Pressure**: {location.pressure} Pa
667
+ </div>
668
+ """, unsafe_allow_html=True)
669
 
670
  # EPW Header Design Conditions
671
  if location.design_conditions:
672
+ heating_items = [
673
+ f"- Coldest Month: {location.design_conditions['heating'].get('coldest_month', 'N/A')}",
674
+ f"- Dry-Bulb Temp: {location.design_conditions['heating'].get('dry_bulb', 'N/A')} °C",
675
+ f"- Mean Coincident Dry-Bulb: {location.design_conditions['heating'].get('mean_coincident_db', 'N/A')} °C",
676
+ f"- Humidification Dry-Bulb: {location.design_conditions['heating'].get('humidification_db', 'N/A')} °C",
677
+ f"- Mean Coincident Wet-Bulb: {location.design_conditions['heating'].get('mean_coincident_wb', 'N/A')} °C"
678
+ ]
679
+ cooling_items = [
680
+ f"- Hottest Month: {location.design_conditions['cooling'].get('hottest_month', 'N/A')}",
681
+ f"- Dry-Bulb Temp (0.4%): {location.design_conditions['cooling'].get('dry_bulb_0_4', 'N/A')} °C",
682
+ f"- Wet-Bulb Temp (0.4%): {location.design_conditions['cooling'].get('wet_bulb_0_4', 'N/A')} °C",
683
+ f"- Mean Coincident Wet-Bulb (0.4%): {location.design_conditions['cooling'].get('mean_coincident_wb_0_4', 'N/A')} °C"
684
+ ]
685
+ extremes_items = [
686
+ f"- Annual Max Dry-Bulb: {location.design_conditions['extremes'].get('annual_max_db', 'N/A')} °C",
687
+ f"- Annual Min Dry-Bulb: {location.design_conditions['extremes'].get('min_db', 'N/A')} °C"
688
+ ]
689
+ st.markdown(f"""
690
+ <div class="markdown-text">
691
+ **Design Conditions (EPW Header):**
692
  - **Heating:**
693
+ {'<br>'.join(heating_items)}
 
 
 
 
694
  - **Cooling:**
695
+ {'<br>'.join(cooling_items)}
 
 
 
696
  - **Extremes:**
697
+ {'<br>'.join(extremes_items)}
698
+ </div>
699
+ """, unsafe_allow_html=True)
700
 
701
  # Typical/Extreme Periods
702
  if location.typical_extreme_periods:
703
+ period_items = [
704
+ f"- **{key.replace('_', ' ').title()}**: {period['start']['month']}/{period['start']['day']} to {period['end']['month']}/{period['end']['day']}"
705
+ for key, period in location.typical_extreme_periods.items()
706
+ ]
707
+ st.markdown(f"""
708
+ <div class="markdown-text">
709
+ **Typical/Extreme Periods:**
710
+ {'<br>'.join(period_items)}
711
+ </div>
712
+ """, unsafe_allow_html=True)
713
 
714
+ # Ground Temperatures (Table)
715
  if location.ground_temperatures:
716
+ st.markdown('<div class="markdown-text">**Ground Temperatures:**</div>', unsafe_allow_html=True)
717
  month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
718
+ table_data = []
719
  for depth, temps in location.ground_temperatures.items():
720
+ row = {"Depth (m)": float(depth)}
721
+ row.update({month: f"{temp:.2f}" for month, temp in zip(month_names, temps)})
722
+ table_data.append(row)
723
+ df = pd.DataFrame(table_data)
724
+ st.dataframe(df, use_container_width=True)
725
 
726
  @staticmethod
727
  def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
 
761
  vapor_pressure = humidity / 100 * saturation_pressure
762
  humidity_ratio = 0.62198 * vapor_pressure / (pressure - vapor_pressure) * 1000 # Convert to g/kg
763
 
764
+ figastanza = go.Figure()
765
 
766
  # Hourly data points
767
  fig.add_trace(go.Scatter(