mabuseif commited on
Commit
5d7ca69
·
verified ·
1 Parent(s): 7c1093a

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +24 -173
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.4
8
  """
9
 
10
  from typing import Dict, List, Any, Optional
@@ -19,6 +19,11 @@ from io import StringIO
19
  import pvlib
20
  from datetime import datetime, timedelta
21
  import re
 
 
 
 
 
22
 
23
  # Define paths
24
  DATA_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -52,12 +57,6 @@ STYLE = """
52
  </style>
53
  """
54
 
55
- # Country code to full name mapping
56
- COUNTRY_MAP = {
57
- "AUS": "Australia",
58
- # Add other mappings as needed
59
- }
60
-
61
  @dataclass
62
  class ClimateLocation:
63
  """Class representing a climate location with ASHRAE 169 data derived from EPW files."""
@@ -81,9 +80,8 @@ class ClimateLocation:
81
  hourly_data: List[Dict] # Hourly data for integration with main.py
82
  typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
83
  ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
84
- design_conditions: Dict[str, Any] # Design conditions from EPW header
85
 
86
- def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, design_conditions: Dict, **kwargs):
87
  """Initialize ClimateLocation with EPW file data and header information."""
88
  self.id = kwargs.get("id")
89
  self.country = kwargs.get("country")
@@ -94,7 +92,6 @@ class ClimateLocation:
94
  self.elevation = kwargs.get("elevation")
95
  self.typical_extreme_periods = typical_extreme_periods
96
  self.ground_temperatures = ground_temperatures
97
- self.design_conditions = design_conditions
98
 
99
  # Extract columns from EPW data
100
  months = pd.to_numeric(epw_file[1], errors='coerce').values
@@ -105,7 +102,12 @@ class ClimateLocation:
105
  pressure = pd.to_numeric(epw_file[9], errors='coerce').values
106
  global_radiation = pd.to_numeric(epw_file[13], errors='coerce').values
107
  wind_direction = pd.to_numeric(epw_file[20], errors='coerce').values
108
- wind_speed = pd.to_numeric(epw_file[21], errors='coerce').values
 
 
 
 
 
109
 
110
  # Calculate wet-bulb temperature
111
  wet_bulb = ClimateData.calculate_wet_bulb(dry_bulb, humidity)
@@ -129,6 +131,9 @@ class ClimateLocation:
129
  self.wind_speed = round(np.nanmean(wind_speed), 1)
130
  self.pressure = round(np.nanmean(pressure), 1)
131
 
 
 
 
132
  # Assign climate zone
133
  self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
134
 
@@ -174,8 +179,7 @@ class ClimateLocation:
174
  "pressure": self.pressure,
175
  "hourly_data": self.hourly_data,
176
  "typical_extreme_periods": self.typical_extreme_periods,
177
- "ground_temperatures": self.ground_temperatures,
178
- "design_conditions": self.design_conditions
179
  }
180
 
181
  class ClimateData:
@@ -251,7 +255,7 @@ class ClimateData:
251
  if data["summer_daily_range"] < 0:
252
  st.error("Validation failed: Negative summer daily range")
253
  return False
254
- if not (0 <= data["wind_speed"] <= 20):
255
  st.error(f"Validation failed: Wind speed {data['wind_speed']} outside range")
256
  return False
257
  if not (80000 <= data["pressure"] <= 110000):
@@ -283,7 +287,7 @@ class ClimateData:
283
  if not (0 <= record["global_horizontal_radiation"] <= 1200):
284
  st.error(f"Validation failed: Global radiation {record['global_horizontal_radiation']} outside range")
285
  return False
286
- if not (0 <= record["wind_speed"] <= 20):
287
  st.error(f"Validation failed: Wind speed {record['wind_speed']} outside range")
288
  return False
289
  if not (0 <= record["wind_direction"] <= 360):
@@ -309,17 +313,6 @@ class ClimateData:
309
  st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
310
  return False
311
 
312
- # Validate design conditions (optional)
313
- if "design_conditions" in data and data["design_conditions"]:
314
- if not all(key in data["design_conditions"] for key in ["heating", "cooling", "extremes"]):
315
- st.warning("Validation warning: Incomplete design conditions")
316
- for section in ["heating", "cooling", "extremes"]:
317
- if section in data["design_conditions"]:
318
- for key, value in data["design_conditions"][section].items():
319
- if isinstance(value, (int, float)) and not (-50 <= value <= 50):
320
- st.error(f"Validation failed: Invalid {section} design condition {key}: {value}")
321
- return False
322
-
323
  return True
324
 
325
  @staticmethod
@@ -377,113 +370,16 @@ class ClimateData:
377
  # Parse header
378
  header = next(line for line in epw_lines if line.startswith("LOCATION"))
379
  header_parts = header.split(",")
380
- city = header_parts[1].strip()
381
- state_province = header_parts[2].strip() or "N/A"
382
- country = header_parts[3].strip()
383
-
384
- # Override with session_state["building_info"] if available
385
- building_info = session_state.get("building_info", {})
386
- country = building_info.get("country", COUNTRY_MAP.get(country, country)) or "Unknown"
387
- city = building_info.get("city", re.sub(r'\..*', '', city)) or "Unknown"
388
 
389
  latitude = float(header_parts[6])
390
  longitude = float(header_parts[7])
391
  elevation = float(header_parts[8])
392
 
393
- # Parse DESIGN CONDITIONS
394
- design_conditions = {"heating": {}, "cooling": {}, "extremes": {}}
395
- for line in epw_lines:
396
- if line.startswith("DESIGN CONDITIONS"):
397
- parts = line.strip().split(',')
398
- if len(parts) < 41:
399
- st.warning(f"DESIGN CONDITIONS has {len(parts)} fields, expected 41. Using partial data.")
400
- break
401
- try:
402
- # Heating section
403
- if parts[3] and self.is_numeric(parts[3]):
404
- design_conditions["heating"]["coldest_month"] = int(parts[3])
405
- if parts[4] and self.is_numeric(parts[4]):
406
- design_conditions["heating"]["dry_bulb"] = float(parts[4])
407
- else:
408
- st.warning(f"Invalid heating dry_bulb: '{parts[4]}'")
409
- if parts[5] and self.is_numeric(parts[5]):
410
- design_conditions["heating"]["mean_coincident_db"] = float(parts[5])
411
- if parts[6] and self.is_numeric(parts[6]):
412
- design_conditions["heating"]["humidification_db"] = float(parts[6])
413
- if parts[7] and self.is_numeric(parts[7]):
414
- design_conditions["heating"]["mean_coincident_wb"] = float(parts[7])
415
- if parts[8] and self.is_numeric(parts[8]):
416
- design_conditions["heating"]["wind_speed_1"] = float(parts[8])
417
- if parts[9] and self.is_numeric(parts[9]):
418
- design_conditions["heating"]["wind_direction_1"] = float(parts[9])
419
- if parts[10] and self.is_numeric(parts[10]):
420
- design_conditions["heating"]["wind_speed_2"] = float(parts[10])
421
- if parts[11] and self.is_numeric(parts[11]):
422
- design_conditions["heating"]["wind_direction_2"] = float(parts[11])
423
- # Cooling section
424
- if parts[12] and self.is_numeric(parts[12]):
425
- design_conditions["cooling"]["hottest_month"] = int(parts[12])
426
- if parts[13] and self.is_numeric(parts[13]):
427
- design_conditions["cooling"]["dry_bulb_0_4"] = float(parts[13])
428
- if parts[14] and self.is_numeric(parts[14]):
429
- design_conditions["cooling"]["mean_coincident_wb_0_4"] = float(parts[14])
430
- if parts[15] and self.is_numeric(parts[15]):
431
- design_conditions["cooling"]["wet_bulb_0_4"] = float(parts[15])
432
- if parts[16] and self.is_numeric(parts[16]):
433
- design_conditions["cooling"]["mean_coincident_db_0_4"] = float(parts[16])
434
- if parts[17] and self.is_numeric(parts[17]):
435
- design_conditions["cooling"]["dry_bulb_1_0"] = float(parts[17])
436
- if parts[18] and self.is_numeric(parts[18]):
437
- design_conditions["cooling"]["mean_coincident_wb_1_0"] = float(parts[18])
438
- if parts[19] and self.is_numeric(parts[19]):
439
- design_conditions["cooling"]["wet_bulb_1_0"] = float(parts[19])
440
- if parts[20] and self.is_numeric(parts[20]):
441
- design_conditions["cooling"]["mean_coincident_db_1_0"] = float(parts[20])
442
- if parts[21] and self.is_numeric(parts[21]):
443
- design_conditions["cooling"]["dry_bulb_2_0"] = float(parts[21])
444
- if parts[22] and self.is_numeric(parts[22]):
445
- design_conditions["cooling"]["mean_coincident_wb_2_0"] = float(parts[22])
446
- if parts[23] and self.is_numeric(parts[23]):
447
- design_conditions["cooling"]["wet_bulb_2_0"] = float(parts[23])
448
- if parts[24] and self.is_numeric(parts[24]):
449
- design_conditions["cooling"]["mean_coincident_db_2_0"] = float(parts[24])
450
- if parts[25] and self.is_numeric(parts[25]):
451
- design_conditions["cooling"]["evaporation_wb_0_4"] = float(parts[25])
452
- if parts[26] and self.is_numeric(parts[26]):
453
- design_conditions["cooling"]["mean_coincident_db_wb_0_4"] = float(parts[26])
454
- # Extremes section
455
- if parts[27] and self.is_numeric(parts[27]):
456
- design_conditions["extremes"]["annual_max_db"] = float(parts[27])
457
- if parts[28] and self.is_numeric(parts[28]):
458
- design_conditions["extremes"]["mean_db"] = float(parts[28])
459
- if parts[29] and self.is_numeric(parts[29]):
460
- design_conditions["extremes"]["std_dev_db"] = float(parts[29])
461
- if parts[30] and self.is_numeric(parts[30]):
462
- design_conditions["extremes"]["min_db"] = float(parts[30])
463
- if parts[31] and self.is_numeric(parts[31]):
464
- design_conditions["extremes"]["max_db_1"] = float(parts[31])
465
- if parts[32] and self.is_numeric(parts[32]):
466
- design_conditions["extremes"]["min_db_1"] = float(parts[32])
467
- if parts[33] and self.is_numeric(parts[33]):
468
- design_conditions["extremes"]["max_db_2"] = float(parts[33])
469
- if parts[34] and self.is_numeric(parts[34]):
470
- design_conditions["extremes"]["min_db_2"] = float(parts[34])
471
- if parts[35] and self.is_numeric(parts[35]):
472
- design_conditions["extremes"]["max_db_3"] = float(parts[35])
473
- if parts[36] and self.is_numeric(parts[36]):
474
- design_conditions["extremes"]["min_db_3"] = float(parts[36])
475
- if parts[37] and self.is_numeric(parts[37]):
476
- design_conditions["extremes"]["max_db_4"] = float(parts[37])
477
- if parts[38] and self.is_numeric(parts[38]):
478
- design_conditions["extremes"]["min_db_4"] = float(parts[38])
479
- if parts[39] and self.is_numeric(parts[39]):
480
- design_conditions["extremes"]["max_db_5"] = float(parts[39])
481
- if parts[40] and self.is_numeric(parts[40]):
482
- design_conditions["extremes"]["min_db_5"] = float(parts[40])
483
- except Exception as e:
484
- st.warning(f"Error parsing DESIGN CONDITIONS field: {str(e)}. Continuing with partial data.")
485
- break
486
-
487
  # Parse TYPICAL/EXTREME PERIODS
488
  typical_extreme_periods = {}
489
  date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
@@ -575,7 +471,6 @@ class ClimateData:
575
  epw_file=epw_data,
576
  typical_extreme_periods=typical_extreme_periods,
577
  ground_temperatures=ground_temperatures,
578
- design_conditions=design_conditions,
579
  id=f"{country[:1].upper()}{city[:3].upper()}",
580
  country=country,
581
  state_province=state_province,
@@ -617,7 +512,6 @@ class ClimateData:
617
  epw_file=epw_data,
618
  typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
619
  ground_temperatures=climate_data_dict["ground_temperatures"],
620
- design_conditions=climate_data_dict["design_conditions"],
621
  id=climate_data_dict["id"],
622
  country=climate_data_dict["country"],
623
  state_province=climate_data_dict["state_province"],
@@ -696,48 +590,6 @@ class ClimateData:
696
  </div>
697
  """, unsafe_allow_html=True)
698
 
699
- # EPW Header Design Conditions
700
- if location.design_conditions and any(location.design_conditions[section] for section in ["heating", "cooling", "extremes"]):
701
- heating_items = [
702
- f"<li><strong>Coldest Month:</strong> {location.design_conditions['heating'].get('coldest_month', 'N/A')}</li>",
703
- f"<li><strong>Dry-Bulb Temp:</strong> {location.design_conditions['heating'].get('dry_bulb', 'N/A')} °C</li>",
704
- f"<li><strong>Mean Coincident Dry-Bulb:</strong> {location.design_conditions['heating'].get('mean_coincident_db', 'N/A')} °C</li>",
705
- f"<li><strong>Humidification Dry-Bulb:</strong> {location.design_conditions['heating'].get('humidification_db', 'N/A')} °C</li>",
706
- f"<li><strong>Mean Coincident Wet-Bulb:</strong> {location.design_conditions['heating'].get('mean_coincident_wb', 'N/A')} °C</li>"
707
- ]
708
- cooling_items = [
709
- f"<li><strong>Hottest Month:</strong> {location.design_conditions['cooling'].get('hottest_month', 'N/A')}</li>",
710
- f"<li><strong>Dry-Bulb Temp (0.4%):</strong> {location.design_conditions['cooling'].get('dry_bulb_0_4', 'N/A')} °C</li>",
711
- f"<li><strong>Wet-Bulb Temp (0.4%):</strong> {location.design_conditions['cooling'].get('wet_bulb_0_4', 'N/A')} °C</li>",
712
- f"<li><strong>Mean Coincident Wet-Bulb (0.4%):</strong> {location.design_conditions['cooling'].get('mean_coincident_wb_0_4', 'N/A')} °C</li>"
713
- ]
714
- extremes_items = [
715
- f"<li><strong>Annual Max Dry-Bulb:</strong> {location.design_conditions['extremes'].get('annual_max_db', 'N/A')} °C</li>",
716
- f"<li><strong>Annual Min Dry-Bulb:</strong> {location.design_conditions['extremes'].get('min_db', 'N/A')} °C</li>"
717
- ]
718
- st.markdown(f"""
719
- <div class="markdown-text">
720
- <h3>Design Conditions (EPW Header)</h3>
721
- <ul>
722
- <li><strong>Heating:</strong>
723
- <ul>
724
- {''.join(heating_items)}
725
- </ul>
726
- </li>
727
- <li><strong>Cooling:</strong>
728
- <ul>
729
- {''.join(cooling_items)}
730
- </ul>
731
- </li>
732
- <li><strong>Extremes:</strong>
733
- <ul>
734
- {''.join(extremes_items)}
735
- </ul>
736
- </li>
737
- </ul>
738
- </div>
739
- """, unsafe_allow_html=True)
740
-
741
  # Typical/Extreme Periods
742
  if location.typical_extreme_periods:
743
  period_items = [
@@ -1080,7 +932,6 @@ class ClimateData:
1080
  epw_file=epw_data,
1081
  typical_extreme_periods=loc_dict["typical_extreme_periods"],
1082
  ground_temperatures=loc_dict["ground_temperatures"],
1083
- design_conditions=loc_dict["design_conditions"],
1084
  id=loc_dict["id"],
1085
  country=loc_dict["country"],
1086
  state_province=loc_dict["state_province"],
 
4
 
5
  Author: Dr Majed Abuseif
6
  Date: May 2025
7
+ Version: 2.1.5
8
  """
9
 
10
  from typing import Dict, List, Any, Optional
 
19
  import pvlib
20
  from datetime import datetime, timedelta
21
  import re
22
+ import logging
23
+
24
+ # Set up logging
25
+ logging.basicConfig(level=logging.INFO)
26
+ logger = logging.getLogger(__name__)
27
 
28
  # Define paths
29
  DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
57
  </style>
58
  """
59
 
 
 
 
 
 
 
60
  @dataclass
61
  class ClimateLocation:
62
  """Class representing a climate location with ASHRAE 169 data derived from EPW files."""
 
80
  hourly_data: List[Dict] # Hourly data for integration with main.py
81
  typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
82
  ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
 
83
 
84
+ def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, **kwargs):
85
  """Initialize ClimateLocation with EPW file data and header information."""
86
  self.id = kwargs.get("id")
87
  self.country = kwargs.get("country")
 
92
  self.elevation = kwargs.get("elevation")
93
  self.typical_extreme_periods = typical_extreme_periods
94
  self.ground_temperatures = ground_temperatures
 
95
 
96
  # Extract columns from EPW data
97
  months = pd.to_numeric(epw_file[1], errors='coerce').values
 
102
  pressure = pd.to_numeric(epw_file[9], errors='coerce').values
103
  global_radiation = pd.to_numeric(epw_file[13], errors='coerce').values
104
  wind_direction = pd.to_numeric(epw_file[20], errors='coerce').values
105
+ wind_speed = pd.to_numeric(epw_file[21], errors='coerce')
106
+
107
+ # Filter wind speed outliers and log high values
108
+ wind_speed = wind_speed[wind_speed <= 50] # Remove extreme outliers
109
+ if (wind_speed > 15).any():
110
+ logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
111
 
112
  # Calculate wet-bulb temperature
113
  wet_bulb = ClimateData.calculate_wet_bulb(dry_bulb, humidity)
 
131
  self.wind_speed = round(np.nanmean(wind_speed), 1)
132
  self.pressure = round(np.nanmean(pressure), 1)
133
 
134
+ # Log wind speed diagnostics
135
+ logger.info(f"Wind speed stats: min={wind_speed.min():.1f}, max={wind_speed.max():.1f}, mean={self.wind_speed:.1f}")
136
+
137
  # Assign climate zone
138
  self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
139
 
 
179
  "pressure": self.pressure,
180
  "hourly_data": self.hourly_data,
181
  "typical_extreme_periods": self.typical_extreme_periods,
182
+ "ground_temperatures": self.ground_temperatures
 
183
  }
184
 
185
  class ClimateData:
 
255
  if data["summer_daily_range"] < 0:
256
  st.error("Validation failed: Negative summer daily range")
257
  return False
258
+ if not (0 <= data["wind_speed"] <= 30): # Updated range to 0–30 m/s
259
  st.error(f"Validation failed: Wind speed {data['wind_speed']} outside range")
260
  return False
261
  if not (80000 <= data["pressure"] <= 110000):
 
287
  if not (0 <= record["global_horizontal_radiation"] <= 1200):
288
  st.error(f"Validation failed: Global radiation {record['global_horizontal_radiation']} outside range")
289
  return False
290
+ if not (0 <= record["wind_speed"] <= 30): # Updated range to 0–30 m/s
291
  st.error(f"Validation failed: Wind speed {record['wind_speed']} outside range")
292
  return False
293
  if not (0 <= record["wind_direction"] <= 360):
 
313
  st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
314
  return False
315
 
 
 
 
 
 
 
 
 
 
 
 
316
  return True
317
 
318
  @staticmethod
 
370
  # Parse header
371
  header = next(line for line in epw_lines if line.startswith("LOCATION"))
372
  header_parts = header.split(",")
373
+ city = header_parts[1].strip() or "Unknown"
374
+ # Clean city name by removing suffixes like '.Racecourse'
375
+ city = re.sub(r'\..*', '', city)
376
+ state_province = header_parts[2].strip() or "Unknown"
377
+ country = header_parts[3].strip() or "Unknown"
 
 
 
378
 
379
  latitude = float(header_parts[6])
380
  longitude = float(header_parts[7])
381
  elevation = float(header_parts[8])
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  # Parse TYPICAL/EXTREME PERIODS
384
  typical_extreme_periods = {}
385
  date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
 
471
  epw_file=epw_data,
472
  typical_extreme_periods=typical_extreme_periods,
473
  ground_temperatures=ground_temperatures,
 
474
  id=f"{country[:1].upper()}{city[:3].upper()}",
475
  country=country,
476
  state_province=state_province,
 
512
  epw_file=epw_data,
513
  typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
514
  ground_temperatures=climate_data_dict["ground_temperatures"],
 
515
  id=climate_data_dict["id"],
516
  country=climate_data_dict["country"],
517
  state_province=climate_data_dict["state_province"],
 
590
  </div>
591
  """, unsafe_allow_html=True)
592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  # Typical/Extreme Periods
594
  if location.typical_extreme_periods:
595
  period_items = [
 
932
  epw_file=epw_data,
933
  typical_extreme_periods=loc_dict["typical_extreme_periods"],
934
  ground_temperatures=loc_dict["ground_temperatures"],
 
935
  id=loc_dict["id"],
936
  country=loc_dict["country"],
937
  state_province=loc_dict["state_province"],