mabuseif commited on
Commit
1f47a7b
·
verified ·
1 Parent(s): 1fc4d66

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +32 -203
data/climate_data.py CHANGED
@@ -1,68 +1,4 @@
1
- """
2
- ASHRAE 169 climate data module for HVAC Load Calculator.
3
- This module provides access to climate data for various locations based on ASHRAE 169 standard.
4
-
5
- Author: Dr Majed Abuseif
6
- Date: March 2025
7
- Version: 1.0.0
8
- """
9
-
10
- from typing import Dict, List, Any, Optional, Tuple
11
- import pandas as pd
12
- import numpy as np
13
- import os
14
- import json
15
- from dataclasses import dataclass
16
- import streamlit as st
17
- import plotly.graph_objects as go
18
- from io import StringIO
19
-
20
- # Define paths
21
- DATA_DIR = os.path.dirname(os.path.abspath(__file__))
22
-
23
-
24
- @dataclass
25
- class ClimateLocation:
26
- """Class representing a climate location with ASHRAE 169 data."""
27
-
28
- id: str
29
- country: str
30
- state_province: str
31
- city: str
32
- latitude: float
33
- longitude: float
34
- elevation: float # meters
35
- climate_zone: str
36
- heating_degree_days: float # base 18°C
37
- cooling_degree_days: float # base 18°C
38
- winter_design_temp: float # 99.6% heating design temperature (°C)
39
- summer_design_temp_db: float # 0.4% cooling design dry-bulb temperature (°C)
40
- summer_design_temp_wb: float # 0.4% cooling design wet-bulb temperature (°C)
41
- summer_daily_range: float # Mean daily temperature range in summer (°C)
42
- monthly_temps: Dict[str, float] # Average monthly temperatures (°C)
43
- monthly_humidity: Dict[str, float] # Average monthly relative humidity (%)
44
-
45
- def to_dict(self) -> Dict[str, Any]:
46
- """Convert the climate location to a dictionary."""
47
- return {
48
- "id": self.id,
49
- "country": self.country,
50
- "state_province": self.state_province,
51
- "city": self.city,
52
- "latitude": self.latitude,
53
- "longitude": self.longitude,
54
- "elevation": self.elevation,
55
- "climate_zone": self.climate_zone,
56
- "heating_degree_days": self.heating_degree_days,
57
- "cooling_degree_days": self.cooling_degree_days,
58
- "winter_design_temp": self.winter_design_temp,
59
- "summer_design_temp_db": self.summer_design_temp_db,
60
- "summer_design_temp_wb": self.summer_design_temp_wb,
61
- "summer_daily_range": self.summer_daily_range,
62
- "monthly_temps": self.monthly_temps,
63
- "monthly_humidity": self.monthly_humidity
64
- }
65
-
66
 
67
  class ClimateData:
68
  """Class for managing ASHRAE 169 climate data."""
@@ -93,47 +29,6 @@ class ClimateData:
93
  self.countries = sorted(list(set(loc.country for loc in self.locations.values())))
94
  self.country_states = self._group_locations_by_country_state()
95
 
96
- def _infer_epw_columns(self, epw_data: pd.DataFrame) -> Dict[str, int]:
97
- """Infer column indices for key weather parameters based on data ranges."""
98
- column_map = {}
99
- for col in epw_data.columns:
100
- values = pd.to_numeric(epw_data[col], errors='coerce')
101
- if values.isna().all():
102
- continue # Skip if all values are NaN
103
- mean_val = np.nanmean(values)
104
- min_val = np.nanmin(values)
105
- max_val = np.nanmax(values)
106
-
107
- # Dry Bulb Temperature (°C): -50 to 50°C
108
- if -50 <= min_val <= max_val <= 50 and col not in column_map:
109
- column_map["dry_bulb"] = col
110
- # Wet Bulb Temperature (°C): -50 to 40°C
111
- elif -50 <= min_val <= max_val <= 40 and col not in column_map:
112
- column_map["wet_bulb"] = col
113
- # Relative Humidity (%): 0 to 100%
114
- elif 0 <= min_val <= max_val <= 100 and col not in column_map:
115
- column_map["humidity"] = col
116
- # Atmospheric Pressure (Pa): 80000 to 105000 Pa
117
- elif 80000 <= min_val <= max_val <= 105000 and col not in column_map:
118
- column_map["pressure"] = col
119
-
120
- # Standard EPW column indices as fallback
121
- standard_map = {
122
- "dry_bulb": 6,
123
- "wet_bulb": 8,
124
- "humidity": 21,
125
- "pressure": 9
126
- }
127
-
128
- for key in standard_map:
129
- if key not in column_map:
130
- st.warning(f"Could not infer {key} column. Using standard EPW index {standard_map[key]}.")
131
- column_map[key] = standard_map[key]
132
- elif column_map[key] != standard_map[key]:
133
- st.warning(f"Inferred {key} column ({column_map[key]}) differs from standard EPW ({standard_map[key]}). Proceeding with inferred column.")
134
-
135
- return column_map
136
-
137
  def display_climate_input(self, session_state: Dict[str, Any]):
138
  """Display form for manual input or EPW upload in Streamlit."""
139
  st.title("Climate Data")
@@ -146,7 +41,7 @@ class ClimateData:
146
  st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")
147
  tab1, tab2 = st.tabs(["Manual Input", "Upload EPW File"])
148
 
149
- # Manual Input Tab
150
  with tab1:
151
  with st.form("manual_climate_form"):
152
  col1, col2 = st.columns(2)
@@ -218,49 +113,48 @@ class ClimateData:
218
  epw_lines = epw_content.splitlines()
219
  header = next(line for line in epw_lines if line.startswith("LOCATION"))
220
  header_parts = header.split(",")
221
- latitude = float(header_parts[6])
222
- longitude = float(header_parts[7])
223
- elevation = float(header_parts[8])
224
 
225
- # Load EPW data as strings first, then convert
226
- epw_data = pd.read_csv(StringIO("\n".join(epw_lines[8:])), header=None, dtype=str)
 
227
  if len(epw_data) != 8760:
228
  raise ValueError(f"EPW file has {len(epw_data)} records, expected 8760.")
229
-
230
  # Convert all columns to numeric, coercing errors to NaN
231
  for col in epw_data.columns:
232
  epw_data[col] = pd.to_numeric(epw_data[col], errors='coerce')
233
 
234
- # Debugging: Show NaN counts
235
- nan_counts = epw_data.isna().sum()
 
236
  if nan_counts.max() > 0:
237
- st.warning(f"NaN values detected in columns: {nan_counts[nan_counts > 0].to_dict()}")
238
 
239
- # Infer column indices
240
- column_map = self._infer_epw_columns(epw_data)
241
-
242
- # Extract and validate data
243
- months = epw_data[1].values.astype(float)
244
- dry_bulb = epw_data[column_map["dry_bulb"]].values.astype(float)
245
- wet_bulb = epw_data[column_map["wet_bulb"]].values.astype(float)
246
- humidity = epw_data[column_map["humidity"]].values.astype(float)
247
 
248
  if np.all(np.isnan(dry_bulb)) or np.all(np.isnan(humidity)):
249
- raise ValueError("Critical columns (dry bulb or humidity) are entirely NaN.")
250
 
251
- # Calculate daily averages for HDD/CDD
252
  daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
253
- hdd = round(sum(max(18 - temp, 0) for temp in daily_temps if not np.isnan(temp)))
254
- cdd = round(sum(max(temp - 18, 0) for temp in daily_temps if not np.isnan(temp)))
255
 
256
- # Design conditions with NaN handling
257
  winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
258
  summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
259
  summer_idx = np.argmax(dry_bulb >= summer_design_temp_db)
260
  summer_design_temp_wb = round(wet_bulb[summer_idx], 1) if not np.isnan(wet_bulb[summer_idx]) else 25.0
261
  summer_mask = (months >= 6) & (months <= 8)
262
  summer_temps = dry_bulb[summer_mask].reshape(-1, 24)
263
- summer_daily_range = round(np.nanmean(summer_temps.max(axis=1) - summer_temps.min(axis=1)), 1)
264
 
265
  # Monthly averages
266
  monthly_temps = {}
@@ -295,7 +189,7 @@ class ClimateData:
295
  self.add_location(location)
296
  st.success("Climate data extracted from EPW file!")
297
  self.display_design_conditions(location)
298
- self.visualize_data(location, epw_data=epw_data, column_map=column_map)
299
  except Exception as e:
300
  st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
301
 
@@ -309,54 +203,7 @@ class ClimateData:
309
  else:
310
  st.button("Continue to Building Components", disabled=True)
311
 
312
- def display_design_conditions(self, location: ClimateLocation):
313
- """Display a table of design conditions for calculations."""
314
- st.subheader("Design Conditions for HVAC Calculations")
315
- design_data = pd.DataFrame({
316
- "Parameter": [
317
- "Climate Zone",
318
- "Heating Degree Days (base 18°C)",
319
- "Cooling Degree Days (base 18°C)",
320
- "Winter Design Temperature (99.6%)",
321
- "Summer Design Dry-Bulb Temp (0.4%)",
322
- "Summer Design Wet-Bulb Temp (0.4%)",
323
- "Summer Daily Temperature Range"
324
- ],
325
- "Value": [
326
- location.climate_zone,
327
- f"{location.heating_degree_days} HDD",
328
- f"{location.cooling_degree_days} CDD",
329
- f"{location.winter_design_temp} °C",
330
- f"{location.summer_design_temp_db} °C",
331
- f"{location.summer_design_temp_wb} °C",
332
- f"{location.summer_daily_range} °C"
333
- ]
334
- })
335
- st.table(design_data)
336
-
337
- @staticmethod
338
- def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
339
- """Assign ASHRAE 169 climate zone based on HDD, CDD, and humidity."""
340
- if cdd > 10000:
341
- return "0A" if avg_humidity > 60 else "0B"
342
- elif cdd > 5000:
343
- return "1A" if avg_humidity > 60 else "1B"
344
- elif cdd > 2500:
345
- return "2A" if avg_humidity > 60 else "2B"
346
- elif hdd < 2000 and cdd > 1000:
347
- return "3A" if avg_humidity > 60 else "3B" if avg_humidity < 40 else "3C"
348
- elif hdd < 3000:
349
- return "4A" if avg_humidity > 60 else "4B" if avg_humidity < 40 else "4C"
350
- elif hdd < 4000:
351
- return "5A" if avg_humidity > 60 else "5B" if avg_humidity < 40 else "5C"
352
- elif hdd < 5000:
353
- return "6A" if avg_humidity > 60 else "6B"
354
- elif hdd < 7000:
355
- return "7"
356
- else:
357
- return "8"
358
-
359
- def visualize_data(self, location: ClimateLocation, epw_data: Optional[pd.DataFrame] = None, column_map: Optional[Dict[str, int]] = None):
360
  """Visualize monthly temperature and humidity data with min, max, and average."""
361
  st.subheader("Monthly Climate Data Visualization")
362
 
@@ -365,10 +212,10 @@ class ClimateData:
365
  temps_avg = [location.monthly_temps[m] for m in month_names]
366
  humidity_avg = [location.monthly_humidity[m] for m in month_names]
367
 
368
- if epw_data is not None and column_map is not None:
369
- dry_bulb = epw_data[column_map["dry_bulb"]].values.astype(float)
370
- humidity = epw_data[column_map["humidity"]].values.astype(float)
371
- month_col = epw_data[1].values.astype(float)
372
 
373
  temps_min = []
374
  temps_max = []
@@ -460,29 +307,11 @@ class ClimateData:
460
  )
461
  st.plotly_chart(fig_hum, use_container_width=True)
462
 
463
- def export_to_json(self, file_path: str) -> None:
464
- """Export all climate data to a JSON file."""
465
- data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
466
- with open(file_path, 'w') as f:
467
- json.dump(data, f, indent=4)
468
-
469
- @classmethod
470
- def from_json(cls, file_path: str) -> 'ClimateData':
471
- """Create a ClimateData instance from a JSON file."""
472
- with open(file_path, 'r') as f:
473
- data = json.load(f)
474
- climate_data = cls()
475
- climate_data.locations = {}
476
- for loc_id, loc_dict in data.items():
477
- climate_data.locations[loc_id] = ClimateLocation(**loc_dict)
478
- climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values())))
479
- climate_data.country_states = climate_data._group_locations_by_country_state()
480
- return climate_data
481
-
482
 
483
  if __name__ == "__main__":
484
  if "building_info" not in st.session_state:
485
- st.session_state.building_info = {"country": "Australia", "city": "Melbourne"}
486
  if "page" not in st.session_state:
487
  st.session_state.page = "Climate Data"
488
 
 
1
+ # ... (Previous imports and ClimateLocation class unchanged) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  class ClimateData:
4
  """Class for managing ASHRAE 169 climate data."""
 
29
  self.countries = sorted(list(set(loc.country for loc in self.locations.values())))
30
  self.country_states = self._group_locations_by_country_state()
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def display_climate_input(self, session_state: Dict[str, Any]):
33
  """Display form for manual input or EPW upload in Streamlit."""
34
  st.title("Climate Data")
 
41
  st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")
42
  tab1, tab2 = st.tabs(["Manual Input", "Upload EPW File"])
43
 
44
+ # Manual Input Tab (unchanged)
45
  with tab1:
46
  with st.form("manual_climate_form"):
47
  col1, col2 = st.columns(2)
 
113
  epw_lines = epw_content.splitlines()
114
  header = next(line for line in epw_lines if line.startswith("LOCATION"))
115
  header_parts = header.split(",")
116
+ latitude = float(header_parts[6]) # 64.13
117
+ longitude = float(header_parts[7]) # -21.90
118
+ elevation = float(header_parts[8]) # 61.0
119
 
120
+ # Load data starting after "DATA PERIODS"
121
+ data_start_idx = epw_lines.index("DATA PERIODS,1,1,Data,Sunday, 1/ 1,12/31") + 1
122
+ epw_data = pd.read_csv(StringIO("\n".join(epw_lines[data_start_idx:])), header=None, dtype=str)
123
  if len(epw_data) != 8760:
124
  raise ValueError(f"EPW file has {len(epw_data)} records, expected 8760.")
125
+
126
  # Convert all columns to numeric, coercing errors to NaN
127
  for col in epw_data.columns:
128
  epw_data[col] = pd.to_numeric(epw_data[col], errors='coerce')
129
 
130
+ # Check for NaN in critical columns
131
+ critical_cols = {6: "Dry Bulb", 8: "Wet Bulb", 21: "Humidity", 1: "Month"}
132
+ nan_counts = epw_data[[col for col in critical_cols]].isna().sum()
133
  if nan_counts.max() > 0:
134
+ st.warning(f"NaN values detected: {nan_counts[nan_counts > 0].to_dict()}")
135
 
136
+ # Extract data using fixed EPW column indices
137
+ months = epw_data[1].values # Month
138
+ dry_bulb = epw_data[6].values # Dry Bulb Temperature (°C)
139
+ wet_bulb = epw_data[8].values # Wet Bulb Temperature (°C)
140
+ humidity = epw_data[21].values # Relative Humidity (%)
 
 
 
141
 
142
  if np.all(np.isnan(dry_bulb)) or np.all(np.isnan(humidity)):
143
+ raise ValueError("Dry bulb or humidity data is entirely NaN.")
144
 
145
+ # Calculate HDD and CDD (base 18°C)
146
  daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
147
+ hdd = round(np.nansum(np.maximum(18 - daily_temps, 0)))
148
+ cdd = round(np.nansum(np.maximum(daily_temps - 18, 0)))
149
 
150
+ # Design conditions
151
  winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
152
  summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
153
  summer_idx = np.argmax(dry_bulb >= summer_design_temp_db)
154
  summer_design_temp_wb = round(wet_bulb[summer_idx], 1) if not np.isnan(wet_bulb[summer_idx]) else 25.0
155
  summer_mask = (months >= 6) & (months <= 8)
156
  summer_temps = dry_bulb[summer_mask].reshape(-1, 24)
157
+ summer_daily_range = round(np.nanmean(np.nanmax(summer_temps, axis=1) - np.nanmin(summer_temps, axis=1)), 1)
158
 
159
  # Monthly averages
160
  monthly_temps = {}
 
189
  self.add_location(location)
190
  st.success("Climate data extracted from EPW file!")
191
  self.display_design_conditions(location)
192
+ self.visualize_data(location, epw_data=epw_data)
193
  except Exception as e:
194
  st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
195
 
 
203
  else:
204
  st.button("Continue to Building Components", disabled=True)
205
 
206
+ def visualize_data(self, location: ClimateLocation, epw_data: Optional[pd.DataFrame] = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  """Visualize monthly temperature and humidity data with min, max, and average."""
208
  st.subheader("Monthly Climate Data Visualization")
209
 
 
212
  temps_avg = [location.monthly_temps[m] for m in month_names]
213
  humidity_avg = [location.monthly_humidity[m] for m in month_names]
214
 
215
+ if epw_data is not None:
216
+ dry_bulb = epw_data[6].values
217
+ humidity = epw_data[21].values
218
+ month_col = epw_data[1].values
219
 
220
  temps_min = []
221
  temps_max = []
 
307
  )
308
  st.plotly_chart(fig_hum, use_container_width=True)
309
 
310
+ # ... (Other methods like display_design_conditions, assign_climate_zone unchanged) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
  if __name__ == "__main__":
313
  if "building_info" not in st.session_state:
314
+ st.session_state.building_info = {"country": "Iceland", "city": "Reykjavik"}
315
  if "page" not in st.session_state:
316
  st.session_state.page = "Climate Data"
317