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

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +211 -83
data/climate_data.py CHANGED
@@ -1,4 +1,68 @@
1
- # ... (Previous imports and ClimateLocation class unchanged) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  class ClimateData:
4
  """Class for managing ASHRAE 169 climate data."""
@@ -41,7 +105,7 @@ class ClimateData:
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,43 +177,38 @@ class ClimateData:
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)
@@ -165,6 +224,7 @@ class ClimateData:
165
  monthly_temps[month_names[i-1]] = round(np.nanmean(dry_bulb[month_mask]), 1)
166
  monthly_humidity[month_names[i-1]] = round(np.nanmean(humidity[month_mask]), 1)
167
 
 
168
  avg_humidity = np.nanmean(humidity)
169
  climate_zone = self.assign_climate_zone(hdd, cdd, avg_humidity)
170
 
@@ -203,8 +263,56 @@ class ClimateData:
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
 
210
  months = list(range(1, 13))
@@ -212,27 +320,6 @@ class ClimateData:
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 = []
222
- humidity_min = []
223
- humidity_max = []
224
- for i in range(1, 13):
225
- month_mask = (month_col == i)
226
- temps_min.append(round(np.nanmin(dry_bulb[month_mask]), 1))
227
- temps_max.append(round(np.nanmax(dry_bulb[month_mask]), 1))
228
- humidity_min.append(round(np.nanmin(humidity[month_mask]), 1))
229
- humidity_max.append(round(np.nanmax(humidity[month_mask]), 1))
230
- else:
231
- temps_min = temps_avg
232
- temps_max = temps_avg
233
- humidity_min = humidity_avg
234
- humidity_max = humidity_avg
235
-
236
  # Temperature Plot
237
  fig_temp = go.Figure()
238
  fig_temp.add_trace(go.Scatter(
@@ -243,24 +330,36 @@ class ClimateData:
243
  line=dict(color='red'),
244
  marker=dict(size=8)
245
  ))
246
- fig_temp.add_trace(go.Scatter(
247
- x=months,
248
- y=temps_max,
249
- mode='lines',
250
- name='Max Temperature (°C)',
251
- line=dict(color='red', dash='dash'),
252
- opacity=0.5
253
- ))
254
- fig_temp.add_trace(go.Scatter(
255
- x=months,
256
- y=temps_min,
257
- mode='lines',
258
- name='Min Temperature (°C)',
259
- line=dict(color='red', dash='dash'),
260
- opacity=0.5,
261
- fill='tonexty',
262
- fillcolor='rgba(255, 0, 0, 0.1)'
263
- ))
 
 
 
 
 
 
 
 
 
 
 
 
264
  fig_temp.update_layout(
265
  title='Monthly Temperatures',
266
  xaxis_title='Month',
@@ -280,24 +379,35 @@ class ClimateData:
280
  line=dict(color='blue'),
281
  marker=dict(size=8)
282
  ))
283
- fig_hum.add_trace(go.Scatter(
284
- x=months,
285
- y=humidity_max,
286
- mode='lines',
287
- name='Max Humidity (%)',
288
- line=dict(color='blue', dash='dash'),
289
- opacity=0.5
290
- ))
291
- fig_hum.add_trace(go.Scatter(
292
- x=months,
293
- y=humidity_min,
294
- mode='lines',
295
- name='Min Humidity (%)',
296
- line=dict(color='blue', dash='dash'),
297
- opacity=0.5,
298
- fill='tonexty',
299
- fillcolor='rgba(0, 0, 255, 0.1)'
300
- ))
 
 
 
 
 
 
 
 
 
 
 
301
  fig_hum.update_layout(
302
  title='Monthly Relative Humidity',
303
  xaxis_title='Month',
@@ -307,7 +417,25 @@ class ClimateData:
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:
 
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
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."""
 
105
  st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")
106
  tab1, tab2 = st.tabs(["Manual Input", "Upload EPW File"])
107
 
108
+ # Manual Input Tab
109
  with tab1:
110
  with st.form("manual_climate_form"):
111
  col1, col2 = st.columns(2)
 
177
  epw_lines = epw_content.splitlines()
178
  header = next(line for line in epw_lines if line.startswith("LOCATION"))
179
  header_parts = header.split(",")
180
+ latitude = float(header_parts[6])
181
+ longitude = float(header_parts[7])
182
+ elevation = float(header_parts[8])
183
 
184
+ # Find data start after "DATA PERIODS"
185
+ data_start_idx = next(i for i, line in enumerate(epw_lines) if line.startswith("DATA PERIODS")) + 1
186
  epw_data = pd.read_csv(StringIO("\n".join(epw_lines[data_start_idx:])), header=None, dtype=str)
187
  if len(epw_data) != 8760:
188
  raise ValueError(f"EPW file has {len(epw_data)} records, expected 8760.")
189
+
190
+ # Convert to numeric, handling non-numeric values
191
  for col in epw_data.columns:
192
  epw_data[col] = pd.to_numeric(epw_data[col], errors='coerce')
193
 
194
+ # Extract key columns (corrected humidity to column 21)
 
 
 
 
 
 
195
  months = epw_data[1].values # Month
196
+ dry_bulb = epw_data[6].values # Dry-bulb temperature (°C)
197
+ wet_bulb = epw_data[8].values # Wet-bulb temperature (°C)
198
+ humidity = epw_data[21].values # Relative humidity (%) - corrected from 9
199
 
200
+ # Check for critical NaN issues
201
  if np.all(np.isnan(dry_bulb)) or np.all(np.isnan(humidity)):
202
+ raise ValueError("Dry bulb temperature or humidity data is entirely NaN.")
203
+
204
  # Calculate HDD and CDD (base 18°C)
205
  daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
206
  hdd = round(np.nansum(np.maximum(18 - daily_temps, 0)))
207
  cdd = round(np.nansum(np.maximum(daily_temps - 18, 0)))
208
 
209
  # Design conditions
210
+ winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1) # 99.6% heating design
211
+ summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1) # 0.4% cooling design DB
212
  summer_idx = np.argmax(dry_bulb >= summer_design_temp_db)
213
  summer_design_temp_wb = round(wet_bulb[summer_idx], 1) if not np.isnan(wet_bulb[summer_idx]) else 25.0
214
  summer_mask = (months >= 6) & (months <= 8)
 
224
  monthly_temps[month_names[i-1]] = round(np.nanmean(dry_bulb[month_mask]), 1)
225
  monthly_humidity[month_names[i-1]] = round(np.nanmean(humidity[month_mask]), 1)
226
 
227
+ # Assign climate zone
228
  avg_humidity = np.nanmean(humidity)
229
  climate_zone = self.assign_climate_zone(hdd, cdd, avg_humidity)
230
 
 
263
  else:
264
  st.button("Continue to Building Components", disabled=True)
265
 
266
+ def display_design_conditions(self, location: ClimateLocation):
267
+ """Display a table of design conditions for calculations."""
268
+ st.subheader("Design Conditions for HVAC Calculations")
269
+ design_data = pd.DataFrame({
270
+ "Parameter": [
271
+ "Climate Zone",
272
+ "Heating Degree Days (base 18°C)",
273
+ "Cooling Degree Days (base 18°C)",
274
+ "Winter Design Temperature (99.6%)",
275
+ "Summer Design Dry-Bulb Temp (0.4%)",
276
+ "Summer Design Wet-Bulb Temp (0.4%)",
277
+ "Summer Daily Temperature Range"
278
+ ],
279
+ "Value": [
280
+ location.climate_zone,
281
+ f"{location.heating_degree_days} HDD",
282
+ f"{location.cooling_degree_days} CDD",
283
+ f"{location.winter_design_temp} °C",
284
+ f"{location.summer_design_temp_db} °C",
285
+ f"{location.summer_design_temp_wb} °C",
286
+ f"{location.summer_daily_range} °C"
287
+ ]
288
+ })
289
+ st.table(design_data)
290
+
291
+ @staticmethod
292
+ def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
293
+ """Assign ASHRAE 169 climate zone based on HDD, CDD, and humidity."""
294
+ if cdd > 10000:
295
+ return "0A" if avg_humidity > 60 else "0B"
296
+ elif cdd > 5000:
297
+ return "1A" if avg_humidity > 60 else "1B"
298
+ elif cdd > 2500:
299
+ return "2A" if avg_humidity > 60 else "2B"
300
+ elif hdd < 2000 and cdd > 1000:
301
+ return "3A" if avg_humidity > 60 else "3B" if avg_humidity < 40 else "3C"
302
+ elif hdd < 3000:
303
+ return "4A" if avg_humidity > 60 else "4B" if avg_humidity < 40 else "4C"
304
+ elif hdd < 4000:
305
+ return "5A" if avg_humidity > 60 else "5B" if avg_humidity < 40 else "5C"
306
+ elif hdd < 5000:
307
+ return "6A" if avg_humidity > 60 else "6B"
308
+ elif hdd < 7000:
309
+ return "7"
310
+ else:
311
+ return "8"
312
+
313
+ @staticmethod
314
+ def visualize_data(location: ClimateLocation, epw_data: Optional[pd.DataFrame] = None):
315
+ """Visualize monthly temperature and humidity data."""
316
  st.subheader("Monthly Climate Data Visualization")
317
 
318
  months = list(range(1, 13))
 
320
  temps_avg = [location.monthly_temps[m] for m in month_names]
321
  humidity_avg = [location.monthly_humidity[m] for m in month_names]
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  # Temperature Plot
324
  fig_temp = go.Figure()
325
  fig_temp.add_trace(go.Scatter(
 
330
  line=dict(color='red'),
331
  marker=dict(size=8)
332
  ))
333
+
334
+ # Add min/max for EPW data only
335
+ if epw_data is not None:
336
+ dry_bulb = epw_data[6].values
337
+ month_col = epw_data[1].values
338
+ temps_min = []
339
+ temps_max = []
340
+ for i in range(1, 13):
341
+ month_mask = (month_col == i)
342
+ temps_min.append(round(np.nanmin(dry_bulb[month_mask]), 1))
343
+ temps_max.append(round(np.nanmax(dry_bulb[month_mask]), 1))
344
+ fig_temp.add_trace(go.Scatter(
345
+ x=months,
346
+ y=temps_max,
347
+ mode='lines',
348
+ name='Max Temperature (°C)',
349
+ line=dict(color='red', dash='dash'),
350
+ opacity=0.5
351
+ ))
352
+ fig_temp.add_trace(go.Scatter(
353
+ x=months,
354
+ y=temps_min,
355
+ mode='lines',
356
+ name='Min Temperature (°C)',
357
+ line=dict(color='red', dash='dash'),
358
+ opacity=0.5,
359
+ fill='tonexty',
360
+ fillcolor='rgba(255, 0, 0, 0.1)'
361
+ ))
362
+
363
  fig_temp.update_layout(
364
  title='Monthly Temperatures',
365
  xaxis_title='Month',
 
379
  line=dict(color='blue'),
380
  marker=dict(size=8)
381
  ))
382
+
383
+ # Add min/max for EPW data only
384
+ if epw_data is not None:
385
+ humidity = epw_data[21].values
386
+ humidity_min = []
387
+ humidity_max = []
388
+ for i in range(1, 13):
389
+ month_mask = (month_col == i)
390
+ humidity_min.append(round(np.nanmin(humidity[month_mask]), 1))
391
+ humidity_max.append(round(np.nanmax(humidity[month_mask]), 1))
392
+ fig_hum.add_trace(go.Scatter(
393
+ x=months,
394
+ y=humidity_max,
395
+ mode='lines',
396
+ name='Max Humidity (%)',
397
+ line=dict(color='blue', dash='dash'),
398
+ opacity=0.5
399
+ ))
400
+ fig_hum.add_trace(go.Scatter(
401
+ x=months,
402
+ y=humidity_min,
403
+ mode='lines',
404
+ name='Min Humidity (%)',
405
+ line=dict(color='blue', dash='dash'),
406
+ opacity=0.5,
407
+ fill='tonexty',
408
+ fillcolor='rgba(0, 0, 255, 0.1)'
409
+ ))
410
+
411
  fig_hum.update_layout(
412
  title='Monthly Relative Humidity',
413
  xaxis_title='Month',
 
417
  )
418
  st.plotly_chart(fig_hum, use_container_width=True)
419
 
420
+ def export_to_json(self, file_path: str) -> None:
421
+ """Export all climate data to a JSON file."""
422
+ data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
423
+ with open(file_path, 'w') as f:
424
+ json.dump(data, f, indent=4)
425
+
426
+ @classmethod
427
+ def from_json(cls, file_path: str) -> 'ClimateData':
428
+ """Create a ClimateData instance from a JSON file."""
429
+ with open(file_path, 'r') as f:
430
+ data = json.load(f)
431
+ climate_data = cls()
432
+ climate_data.locations = {}
433
+ for loc_id, loc_dict in data.items():
434
+ climate_data.locations[loc_id] = ClimateLocation(**loc_dict)
435
+ climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values())))
436
+ climate_data.country_states = climate_data._group_locations_by_country_state()
437
+ return climate_data
438
+
439
 
440
  if __name__ == "__main__":
441
  if "building_info" not in st.session_state: