mabuseif commited on
Commit
b8450da
·
verified ·
1 Parent(s): d18e12d

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +378 -275
data/climate_data.py CHANGED
@@ -1,10 +1,6 @@
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
@@ -13,9 +9,6 @@ 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__))
@@ -35,10 +28,14 @@ class ClimateLocation:
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
 
@@ -69,303 +66,409 @@ class ClimateData:
69
 
70
  def __init__(self):
71
  """Initialize climate data."""
72
- self.locations = {}
73
- self.countries = []
74
- self.country_states = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  def _group_locations_by_country_state(self) -> Dict[str, Dict[str, List[str]]]:
77
- """Group locations by country and state/province."""
 
 
 
 
 
78
  result = {}
 
79
  for loc in self.locations.values():
80
  if loc.country not in result:
81
  result[loc.country] = {}
 
82
  if loc.state_province not in result[loc.country]:
83
  result[loc.country][loc.state_province] = []
 
84
  result[loc.country][loc.state_province].append(loc.city)
 
 
85
  for country in result:
86
  for state in result[country]:
87
  result[country][state] = sorted(result[country][state])
 
88
  return result
89
 
90
- def add_location(self, location: ClimateLocation):
91
- """Add a new location to the dictionary."""
92
- self.locations[location.id] = location
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 display_climate_input(self, session_state: Dict[str, Any]):
97
- """Display form for manual input or EPW upload in Streamlit."""
98
- st.title("Climate Data")
99
 
100
- # Check if building info has country and city
101
- if not session_state.building_info.get("country") or not session_state.building_info.get("city"):
102
- st.warning("Please enter country and city in Building Information first.")
103
- st.button("Go to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))
104
- return
105
-
106
- st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")
107
-
108
- # Tabs for manual input or EPW upload
109
- tab1, tab2 = st.tabs(["Manual Input", "Upload EPW File"])
110
-
111
- # Manual Input Tab
112
- with tab1:
113
- with st.form("manual_climate_form"):
114
- col1, col2 = st.columns(2)
115
- with col1:
116
- id_input = st.text_input("Location ID", value=f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}")
117
- state_province = st.text_input("State/Province", value="N/A")
118
- latitude = st.number_input("Latitude", min_value=-90.0, max_value=90.0, value=0.0, step=0.1)
119
- longitude = st.number_input("Longitude", min_value=-180.0, max_value=180.0, value=0.0, step=0.1)
120
- elevation = st.number_input("Elevation (m)", min_value=0.0, value=0.0, step=10.0)
121
- climate_zone = st.selectbox("Climate Zone", ["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"])
122
-
123
- with col2:
124
- hdd = st.number_input("Heating Degree Days (base 18°C)", min_value=0.0, value=0.0, step=100.0)
125
- cdd = st.number_input("Cooling Degree Days (base 18°C)", min_value=0.0, value=0.0, step=100.0)
126
- winter_design_temp = st.number_input("Winter Design Temp (99.6%) (°C)", min_value=-50.0, max_value=20.0, value=0.0, step=0.5)
127
- summer_design_temp_db = st.number_input("Summer Design Temp DB (0.4%) (°C)", min_value=0.0, max_value=50.0, value=35.0, step=0.5)
128
- summer_design_temp_wb = st.number_input("Summer Design Temp WB (0.4%) (°C)", min_value=0.0, max_value=40.0, value=25.0, step=0.5)
129
- summer_daily_range = st.number_input("Summer Daily Range (°C)", min_value=0.0, value=5.0, step=0.5)
130
-
131
- st.subheader("Monthly Data")
132
- monthly_temps = {}
133
- monthly_humidity = {}
134
- month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
135
- col1, col2 = st.columns(2)
136
- with col1:
137
- for month in month_names[:6]:
138
- monthly_temps[month] = st.number_input(f"{month} Temp (°C)", min_value=-50.0, max_value=50.0, value=20.0, step=0.5, key=f"temp_{month}")
139
- with col2:
140
- for month in month_names[6:]:
141
- monthly_temps[month] = st.number_input(f"{month} Temp (°C)", min_value=-50.0, max_value=50.0, value=20.0, step=0.5, key=f"temp_{month}")
142
- col1, col2 = st.columns(2)
143
- with col1:
144
- for month in month_names[:6]:
145
- monthly_humidity[month] = st.number_input(f"{month} Humidity (%)", min_value=0.0, max_value=100.0, value=50.0, step=5.0, key=f"hum_{month}")
146
- with col2:
147
- for month in month_names[6:]:
148
- monthly_humidity[month] = st.number_input(f"{month} Humidity (%)", min_value=0.0, max_value=100.0, value=50.0, step=5.0, key=f"hum_{month}")
149
-
150
- if st.form_submit_button("Save Climate Data"):
151
- location = ClimateLocation(
152
- id=id_input,
153
- country=session_state.building_info["country"],
154
- state_province=state_province,
155
- city=session_state.building_info["city"],
156
- latitude=latitude,
157
- longitude=longitude,
158
- elevation=elevation,
159
- climate_zone=climate_zone,
160
- heating_degree_days=hdd,
161
- cooling_degree_days=cdd,
162
- winter_design_temp=winter_design_temp,
163
- summer_design_temp_db=summer_design_temp_db,
164
- summer_design_temp_wb=summer_design_temp_wb,
165
- summer_daily_range=summer_daily_range,
166
- monthly_temps=monthly_temps,
167
- monthly_humidity=monthly_humidity
168
- )
169
- self.add_location(location)
170
- st.success("Climate data saved manually!")
171
- self.display_design_conditions(location)
172
- self.visualize_data(location)
173
-
174
- # EPW Upload Tab
175
- with tab2:
176
- uploaded_file = st.file_uploader("Upload EPW File", type=["epw"])
177
- if uploaded_file:
178
- try:
179
- epw_content = uploaded_file.read().decode("utf-8")
180
- epw_lines = epw_content.splitlines()
181
- header = next(line for line in epw_lines if line.startswith("LOCATION"))
182
- header_parts = header.split(",")
183
- latitude = float(header_parts[6])
184
- longitude = float(header_parts[7])
185
- elevation = float(header_parts[8])
186
-
187
- epw_data = pd.read_csv(StringIO("\n".join(epw_lines[8:])), header=None)
188
- if len(epw_data) != 8760:
189
- raise ValueError("EPW file must contain 8760 hourly records.")
190
- months = epw_data[1].values # Month column
191
- dry_bulb = epw_data[6].values # Dry-bulb temperature
192
- wet_bulb = epw_data[8].values # Wet-bulb temperature
193
- humidity = epw_data[9].values # Relative humidity
194
-
195
- # Calculate daily averages for HDD/CDD
196
- daily_temps = dry_bulb.reshape(-1, 24).mean(axis=1)
197
- hdd = round(sum(max(18 - temp, 0) for temp in daily_temps))
198
- cdd = round(sum(max(temp - 18, 0) for temp in daily_temps))
199
-
200
- # Design conditions (ASHRAE standards)
201
- winter_design_temp = round(np.percentile(dry_bulb, 0.4), 1) # 99.6% heating design
202
- summer_design_temp_db = round(np.percentile(dry_bulb, 99.6), 1) # 0.4% cooling design DB
203
- summer_idx = np.argmax(dry_bulb >= summer_design_temp_db)
204
- summer_design_temp_wb = round(wet_bulb[summer_idx], 1) # Corresponding WB
205
- summer_mask = (months >= 6) & (months <= 8)
206
- summer_temps = dry_bulb[summer_mask].reshape(-1, 24)
207
- summer_daily_range = round(np.mean(summer_temps.max(axis=1) - summer_temps.min(axis=1)), 1)
208
-
209
- # Monthly averages
210
- monthly_temps = {}
211
- monthly_humidity = {}
212
- month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
213
- for i in range(1, 13):
214
- month_mask = (months == i)
215
- monthly_temps[month_names[i-1]] = round(np.mean(dry_bulb[month_mask]), 1)
216
- monthly_humidity[month_names[i-1]] = round(np.mean(humidity[month_mask]), 1)
217
-
218
- # Assign climate zone
219
- avg_humidity = np.mean(humidity)
220
- climate_zone = self.assign_climate_zone(hdd, cdd, avg_humidity)
221
-
222
- location = ClimateLocation(
223
- id=f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}",
224
- country=session_state.building_info["country"],
225
- state_province="N/A",
226
- city=session_state.building_info["city"],
227
- latitude=latitude,
228
- longitude=longitude,
229
- elevation=elevation,
230
- climate_zone=climate_zone,
231
- heating_degree_days=hdd,
232
- cooling_degree_days=cdd,
233
- winter_design_temp=winter_design_temp,
234
- summer_design_temp_db=summer_design_temp_db,
235
- summer_design_temp_wb=summer_design_temp_wb,
236
- summer_daily_range=summer_daily_range,
237
- monthly_temps=monthly_temps,
238
- monthly_humidity=monthly_humidity
239
- )
240
- self.add_location(location)
241
- st.success("Climate data extracted from EPW file!")
242
- self.display_design_conditions(location)
243
- self.visualize_data(location)
244
- except Exception as e:
245
- st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
246
-
247
- # Navigation buttons
248
- col1, col2 = st.columns(2)
249
- with col1:
250
- st.button("Back to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))
251
- with col2:
252
- if self.locations:
253
- st.button("Continue to Building Components", on_click=lambda: setattr(session_state, "page", "Building Components"))
254
- else:
255
- st.button("Continue to Building Components", disabled=True)
256
-
257
- def display_design_conditions(self, location: ClimateLocation):
258
- """Display a table of design conditions for calculations."""
259
- st.subheader("Design Conditions for HVAC Calculations")
260
- design_data = pd.DataFrame({
261
- "Parameter": [
262
- "Climate Zone",
263
- "Heating Degree Days (base 18°C)",
264
- "Cooling Degree Days (base 18°C)",
265
- "Winter Design Temperature (99.6%)",
266
- "Summer Design Dry-Bulb Temp (0.4%)",
267
- "Summer Design Wet-Bulb Temp (0.4%)",
268
- "Summer Daily Temperature Range"
269
- ],
270
- "Value": [
271
- location.climate_zone,
272
- f"{location.heating_degree_days} HDD",
273
- f"{location.cooling_degree_days} CDD",
274
- f"{location.winter_design_temp} °C",
275
- f"{location.summer_design_temp_db} °C",
276
- f"{location.summer_design_temp_wb} °C",
277
- f"{location.summer_daily_range} °C"
278
- ]
279
- })
280
- st.table(design_data)
281
-
282
- @staticmethod
283
- def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
284
- """Assign ASHRAE 169 climate zone based on HDD, CDD, and humidity."""
285
- if cdd > 10000:
286
- return "0A" if avg_humidity > 60 else "0B"
287
- elif cdd > 5000:
288
- return "1A" if avg_humidity > 60 else "1B"
289
- elif cdd > 2500:
290
- return "2A" if avg_humidity > 60 else "2B"
291
- elif hdd < 2000 and cdd > 1000:
292
- return "3A" if avg_humidity > 60 else "3B" if avg_humidity < 40 else "3C"
293
- elif hdd < 3000:
294
- return "4A" if avg_humidity > 60 else "4B" if avg_humidity < 40 else "4C"
295
- elif hdd < 4000:
296
- return "5A" if avg_humidity > 60 else "5B" if avg_humidity < 40 else "5C"
297
- elif hdd < 5000:
298
- return "6A" if avg_humidity > 60 else "6B"
299
- elif hdd < 7000:
300
- return "7"
301
- else:
302
- return "8"
303
-
304
- @staticmethod
305
- def visualize_data(location: ClimateLocation):
306
- """Visualize monthly temperature and humidity data."""
307
- st.subheader("Monthly Climate Data Visualization")
308
-
309
- months = list(range(1, 13))
310
- month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
311
- temps = [location.monthly_temps[m] for m in month_names]
312
- humidity = [location.monthly_humidity[m] for m in month_names]
313
-
314
- # Temperature Plot
315
- fig_temp = go.Figure()
316
- fig_temp.add_trace(go.Scatter(
317
- x=months,
318
- y=temps,
319
- mode='lines+markers',
320
- name='Temperature (°C)',
321
- line=dict(color='red')
322
- ))
323
- fig_temp.update_layout(
324
- title='Monthly Temperatures',
325
- xaxis_title='Month',
326
- yaxis_title='Temperature (°C)',
327
- xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names)
328
- )
329
- st.plotly_chart(fig_temp, use_container_width=True)
330
-
331
- # Humidity Plot
332
- fig_hum = go.Figure()
333
- fig_hum.add_trace(go.Scatter(
334
- x=months,
335
- y=humidity,
336
- mode='lines+markers',
337
- name='Humidity (%)',
338
- line=dict(color='blue')
339
- ))
340
- fig_hum.update_layout(
341
- title='Monthly Relative Humidity',
342
- xaxis_title='Month',
343
- yaxis_title='Relative Humidity (%)',
344
- xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names)
345
- yaxis=dict(range=[0, 100]), # Set y-axis range from 0 to 100
346
- )
347
- st.plotly_chart(fig_hum, use_container_width=True)
348
-
349
  def export_to_json(self, file_path: str) -> None:
350
- """Export all climate data to a JSON file."""
 
 
 
 
 
351
  data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
 
352
  with open(file_path, 'w') as f:
353
  json.dump(data, f, indent=4)
354
-
355
  @classmethod
356
  def from_json(cls, file_path: str) -> 'ClimateData':
357
- """Create a ClimateData instance from a JSON file."""
 
 
 
 
 
 
 
 
358
  with open(file_path, 'r') as f:
359
  data = json.load(f)
 
360
  climate_data = cls()
361
  climate_data.locations = {}
 
362
  for loc_id, loc_dict in data.items():
363
- climate_data.locations[loc_id] = ClimateLocation(**loc_dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values())))
365
  climate_data.country_states = climate_data._group_locations_by_country_state()
 
366
  return climate_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
 
 
368
 
 
369
  if __name__ == "__main__":
370
- climate_data = ClimateData()
371
- climate_data.display_climate_input(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
 
6
  from typing import Dict, List, Any, Optional, Tuple
 
9
  import os
10
  import json
11
  from dataclasses import dataclass
 
 
 
12
 
13
  # Define paths
14
  DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
28
  climate_zone: str
29
  heating_degree_days: float # base 18°C
30
  cooling_degree_days: float # base 18°C
31
+
32
+ # Design conditions
33
  winter_design_temp: float # 99.6% heating design temperature (°C)
34
  summer_design_temp_db: float # 0.4% cooling design dry-bulb temperature (°C)
35
  summer_design_temp_wb: float # 0.4% cooling design wet-bulb temperature (°C)
36
  summer_daily_range: float # Mean daily temperature range in summer (°C)
37
+
38
+ # Monthly data
39
  monthly_temps: Dict[str, float] # Average monthly temperatures (°C)
40
  monthly_humidity: Dict[str, float] # Average monthly relative humidity (%)
41
 
 
66
 
67
  def __init__(self):
68
  """Initialize climate data."""
69
+ self.locations = self._load_climate_locations()
70
+ self.countries = sorted(list(set(loc.country for loc in self.locations.values())))
71
+ self.country_states = self._group_locations_by_country_state()
72
+
73
+ def _load_climate_locations(self) -> Dict[str, ClimateLocation]:
74
+ """
75
+ Load climate location data.
76
+
77
+ Returns:
78
+ Dictionary of climate locations indexed by ID
79
+ """
80
+ # This would typically load from a JSON or CSV file with ASHRAE 169 data
81
+ # For now, we'll define some sample locations inline
82
+
83
+ # Sample monthly data (for all locations in this example)
84
+ months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
85
+
86
+ # New York monthly temperatures (°C)
87
+ ny_temps = {
88
+ "Jan": 0.5, "Feb": 2.1, "Mar": 6.3, "Apr": 12.5, "May": 18.2,
89
+ "Jun": 23.1, "Jul": 25.8, "Aug": 24.9, "Sep": 20.7, "Oct": 14.3,
90
+ "Nov": 8.2, "Dec": 2.4
91
+ }
92
+
93
+ # New York monthly humidity (%)
94
+ ny_humidity = {
95
+ "Jan": 65, "Feb": 62, "Mar": 58, "Apr": 55, "May": 60,
96
+ "Jun": 65, "Jul": 68, "Aug": 70, "Sep": 68, "Oct": 63,
97
+ "Nov": 67, "Dec": 68
98
+ }
99
+
100
+ # Los Angeles monthly temperatures (°C)
101
+ la_temps = {
102
+ "Jan": 14.6, "Feb": 15.1, "Mar": 15.8, "Apr": 17.1, "May": 18.3,
103
+ "Jun": 20.1, "Jul": 22.3, "Aug": 22.9, "Sep": 22.1, "Oct": 20.3,
104
+ "Nov": 17.2, "Dec": 14.9
105
+ }
106
+
107
+ # Los Angeles monthly humidity (%)
108
+ la_humidity = {
109
+ "Jan": 63, "Feb": 67, "Mar": 70, "Apr": 71, "May": 74,
110
+ "Jun": 75, "Jul": 76, "Aug": 76, "Sep": 74, "Oct": 70,
111
+ "Nov": 65, "Dec": 63
112
+ }
113
+
114
+ # Chicago monthly temperatures (°C)
115
+ chi_temps = {
116
+ "Jan": -3.5, "Feb": -1.2, "Mar": 4.1, "Apr": 10.3, "May": 16.5,
117
+ "Jun": 22.1, "Jul": 24.8, "Aug": 23.9, "Sep": 19.7, "Oct": 12.8,
118
+ "Nov": 5.2, "Dec": -1.4
119
+ }
120
+
121
+ # Chicago monthly humidity (%)
122
+ chi_humidity = {
123
+ "Jan": 72, "Feb": 70, "Mar": 65, "Apr": 60, "May": 64,
124
+ "Jun": 67, "Jul": 70, "Aug": 73, "Sep": 71, "Oct": 68,
125
+ "Nov": 72, "Dec": 75
126
+ }
127
+
128
+ # London monthly temperatures (°C)
129
+ lon_temps = {
130
+ "Jan": 5.2, "Feb": 5.5, "Mar": 7.4, "Apr": 9.9, "May": 13.3,
131
+ "Jun": 16.7, "Jul": 18.7, "Aug": 18.3, "Sep": 15.9, "Oct": 12.2,
132
+ "Nov": 8.3, "Dec": 5.9
133
+ }
134
+
135
+ # London monthly humidity (%)
136
+ lon_humidity = {
137
+ "Jan": 84, "Feb": 80, "Mar": 76, "Apr": 72, "May": 70,
138
+ "Jun": 70, "Jul": 71, "Aug": 72, "Sep": 75, "Oct": 80,
139
+ "Nov": 84, "Dec": 86
140
+ }
141
+
142
+ # Sydney monthly temperatures (°C)
143
+ syd_temps = {
144
+ "Jan": 23.5, "Feb": 23.4, "Mar": 22.1, "Apr": 19.5, "May": 16.5,
145
+ "Jun": 14.1, "Jul": 13.4, "Aug": 14.5, "Sep": 16.6, "Oct": 18.8,
146
+ "Nov": 20.6, "Dec": 22.6
147
+ }
148
+
149
+ # Sydney monthly humidity (%)
150
+ syd_humidity = {
151
+ "Jan": 65, "Feb": 68, "Mar": 68, "Apr": 67, "May": 70,
152
+ "Jun": 70, "Jul": 68, "Aug": 63, "Sep": 60, "Oct": 60,
153
+ "Nov": 62, "Dec": 63
154
+ }
155
+
156
+ # Create sample locations
157
+ locations = {
158
+ "US-NY-NYC": ClimateLocation(
159
+ id="US-NY-NYC",
160
+ country="United States",
161
+ state_province="New York",
162
+ city="New York",
163
+ latitude=40.7128,
164
+ longitude=-74.0060,
165
+ elevation=10.0,
166
+ climate_zone="4A",
167
+ heating_degree_days=2600,
168
+ cooling_degree_days=1200,
169
+ winter_design_temp=-8.3,
170
+ summer_design_temp_db=32.8,
171
+ summer_design_temp_wb=25.6,
172
+ summer_daily_range=8.3,
173
+ monthly_temps=ny_temps,
174
+ monthly_humidity=ny_humidity
175
+ ),
176
+ "US-CA-LAX": ClimateLocation(
177
+ id="US-CA-LAX",
178
+ country="United States",
179
+ state_province="California",
180
+ city="Los Angeles",
181
+ latitude=34.0522,
182
+ longitude=-118.2437,
183
+ elevation=93.0,
184
+ climate_zone="3B",
185
+ heating_degree_days=800,
186
+ cooling_degree_days=1200,
187
+ winter_design_temp=8.3,
188
+ summer_design_temp_db=32.2,
189
+ summer_design_temp_wb=23.3,
190
+ summer_daily_range=6.7,
191
+ monthly_temps=la_temps,
192
+ monthly_humidity=la_humidity
193
+ ),
194
+ "US-IL-CHI": ClimateLocation(
195
+ id="US-IL-CHI",
196
+ country="United States",
197
+ state_province="Illinois",
198
+ city="Chicago",
199
+ latitude=41.8781,
200
+ longitude=-87.6298,
201
+ elevation=179.0,
202
+ climate_zone="5A",
203
+ heating_degree_days=3500,
204
+ cooling_degree_days=1000,
205
+ winter_design_temp=-16.7,
206
+ summer_design_temp_db=33.3,
207
+ summer_design_temp_wb=25.6,
208
+ summer_daily_range=8.9,
209
+ monthly_temps=chi_temps,
210
+ monthly_humidity=chi_humidity
211
+ ),
212
+ "UK-LDN": ClimateLocation(
213
+ id="UK-LDN",
214
+ country="United Kingdom",
215
+ state_province="England",
216
+ city="London",
217
+ latitude=51.5074,
218
+ longitude=-0.1278,
219
+ elevation=35.0,
220
+ climate_zone="4A",
221
+ heating_degree_days=2500,
222
+ cooling_degree_days=200,
223
+ winter_design_temp=-3.9,
224
+ summer_design_temp_db=28.3,
225
+ summer_design_temp_wb=20.0,
226
+ summer_daily_range=10.0,
227
+ monthly_temps=lon_temps,
228
+ monthly_humidity=lon_humidity
229
+ ),
230
+ "AU-NSW-SYD": ClimateLocation(
231
+ id="AU-NSW-SYD",
232
+ country="Australia",
233
+ state_province="New South Wales",
234
+ city="Sydney",
235
+ latitude=-33.8688,
236
+ longitude=151.2093,
237
+ elevation=3.0,
238
+ climate_zone="3C",
239
+ heating_degree_days=600,
240
+ cooling_degree_days=900,
241
+ winter_design_temp=7.2,
242
+ summer_design_temp_db=31.1,
243
+ summer_design_temp_wb=24.4,
244
+ summer_daily_range=7.8,
245
+ monthly_temps=syd_temps,
246
+ monthly_humidity=syd_humidity
247
+ )
248
+ }
249
+
250
+ return locations
251
 
252
  def _group_locations_by_country_state(self) -> Dict[str, Dict[str, List[str]]]:
253
+ """
254
+ Group locations by country and state/province.
255
+
256
+ Returns:
257
+ Nested dictionary of countries, states, and cities
258
+ """
259
  result = {}
260
+
261
  for loc in self.locations.values():
262
  if loc.country not in result:
263
  result[loc.country] = {}
264
+
265
  if loc.state_province not in result[loc.country]:
266
  result[loc.country][loc.state_province] = []
267
+
268
  result[loc.country][loc.state_province].append(loc.city)
269
+
270
+ # Sort states and cities
271
  for country in result:
272
  for state in result[country]:
273
  result[country][state] = sorted(result[country][state])
274
+
275
  return result
276
 
277
+ def get_location(self, location_id: str) -> Optional[ClimateLocation]:
278
+ """
279
+ Get climate location by ID.
 
 
 
 
 
 
280
 
281
+ Args:
282
+ location_id: Location identifier
283
+
284
+ Returns:
285
+ ClimateLocation object or None if not found
286
+ """
287
+ return self.locations.get(location_id)
288
+
289
+ def find_location(self, country: str, state_province: str = None, city: str = None) -> Optional[ClimateLocation]:
290
+ """
291
+ Find a climate location by country, state/province, and city.
292
+
293
+ Args:
294
+ country: Country name
295
+ state_province: State or province name (optional)
296
+ city: City name (optional)
297
+
298
+ Returns:
299
+ ClimateLocation object or None if not found
300
+ """
301
+ for loc in self.locations.values():
302
+ if loc.country == country:
303
+ if state_province is None or loc.state_province == state_province:
304
+ if city is None or loc.city == city:
305
+ return loc
306
+ return None
307
+
308
+ def find_locations_by_climate_zone(self, climate_zone: str) -> List[ClimateLocation]:
309
+ """
310
+ Find climate locations by climate zone.
311
+
312
+ Args:
313
+ climate_zone: ASHRAE climate zone
314
+
315
+ Returns:
316
+ List of ClimateLocation objects
317
+ """
318
+ return [loc for loc in self.locations.values() if loc.climate_zone == climate_zone]
319
+
320
+ def get_states_for_country(self, country: str) -> List[str]:
321
+ """
322
+ Get states/provinces for a country.
323
+
324
+ Args:
325
+ country: Country name
326
+
327
+ Returns:
328
+ List of state/province names
329
+ """
330
+ if country in self.country_states:
331
+ return sorted(self.country_states[country].keys())
332
+ return []
333
+
334
+ def get_cities_for_state(self, country: str, state_province: str) -> List[str]:
335
+ """
336
+ Get cities for a state/province.
337
+
338
+ Args:
339
+ country: Country name
340
+ state_province: State or province name
341
+
342
+ Returns:
343
+ List of city names
344
+ """
345
+ if country in self.country_states and state_province in self.country_states[country]:
346
+ return self.country_states[country][state_province]
347
+ return []
348
+
349
+ def get_location_id(self, country: str, state_province: str, city: str) -> Optional[str]:
350
+ """
351
+ Get location ID for a city.
352
+
353
+ Args:
354
+ country: Country name
355
+ state_province: State or province name
356
+ city: City name
357
+
358
+ Returns:
359
+ Location ID or None if not found
360
+ """
361
+ for loc_id, loc in self.locations.items():
362
+ if (loc.country == country and
363
+ loc.state_province == state_province and
364
+ loc.city == city):
365
+ return loc_id
366
+ return None
367
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  def export_to_json(self, file_path: str) -> None:
369
+ """
370
+ Export all climate data to a JSON file.
371
+
372
+ Args:
373
+ file_path: Path to the output JSON file
374
+ """
375
  data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
376
+
377
  with open(file_path, 'w') as f:
378
  json.dump(data, f, indent=4)
379
+
380
  @classmethod
381
  def from_json(cls, file_path: str) -> 'ClimateData':
382
+ """
383
+ Create a ClimateData instance from a JSON file.
384
+
385
+ Args:
386
+ file_path: Path to the input JSON file
387
+
388
+ Returns:
389
+ A new ClimateData instance
390
+ """
391
  with open(file_path, 'r') as f:
392
  data = json.load(f)
393
+
394
  climate_data = cls()
395
  climate_data.locations = {}
396
+
397
  for loc_id, loc_dict in data.items():
398
+ climate_data.locations[loc_id] = ClimateLocation(
399
+ id=loc_dict["id"],
400
+ country=loc_dict["country"],
401
+ state_province=loc_dict["state_province"],
402
+ city=loc_dict["city"],
403
+ latitude=loc_dict["latitude"],
404
+ longitude=loc_dict["longitude"],
405
+ elevation=loc_dict["elevation"],
406
+ climate_zone=loc_dict["climate_zone"],
407
+ heating_degree_days=loc_dict["heating_degree_days"],
408
+ cooling_degree_days=loc_dict["cooling_degree_days"],
409
+ winter_design_temp=loc_dict["winter_design_temp"],
410
+ summer_design_temp_db=loc_dict["summer_design_temp_db"],
411
+ summer_design_temp_wb=loc_dict["summer_design_temp_wb"],
412
+ summer_daily_range=loc_dict["summer_daily_range"],
413
+ monthly_temps=loc_dict["monthly_temps"],
414
+ monthly_humidity=loc_dict["monthly_humidity"]
415
+ )
416
+
417
  climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values())))
418
  climate_data.country_states = climate_data._group_locations_by_country_state()
419
+
420
  return climate_data
421
+
422
+ def display_climate_input(self, session_state):
423
+ """Display climate data input form in Streamlit and store selection in session state."""
424
+ import streamlit as st
425
+
426
+ st.title("Climate Data")
427
+ st.write("Select a location to load its ASHRAE 169 climate data.")
428
+
429
+ # Dropdown for country selection
430
+ country = st.selectbox("Country", self.countries, key="climate_country")
431
+
432
+ # Dropdown for state/province selection
433
+ states = self.get_states_for_country(country)
434
+ state = st.selectbox("State/Province", states, key="climate_state") if states else None
435
+
436
+ # Dropdown for city selection
437
+ cities = self.get_cities_for_state(country, state) if state else []
438
+ city = st.selectbox("City", cities, key="climate_city") if cities else None
439
+
440
+ # Button to confirm selection
441
+ if st.button("Load Climate Data") and country and state and city:
442
+ location_id = self.get_location_id(country, state, city)
443
+ if location_id:
444
+ location = self.get_location(location_id)
445
+ if location:
446
+ # Store the selected location in session state
447
+ session_state["climate_data"] = location.to_dict()
448
+ st.success(f"Loaded climate data for {city}, {state}, {country}")
449
+
450
+ # Display key climate data
451
+ st.subheader("Selected Location Climate Data")
452
+ st.write(f"Climate Zone: {location.climate_zone}")
453
+ st.write(f"Winter Design Temperature: {location.winter_design_temp}°C")
454
+ st.write(f"Summer Design Dry-Bulb Temperature: {location.summer_design_temp_db}°C")
455
+ st.write(f"Summer Design Wet-Bulb Temperature: {location.summer_design_temp_wb}°C")
456
+ st.write(f"Heating Degree Days: {location.heating_degree_days}")
457
+ st.write(f"Cooling Degree Days: {location.cooling_degree_days}")
458
+ else:
459
+ st.error("Location data not found.")
460
+ else:
461
+ st.error("Invalid location selection.")
462
+
463
+ # Display existing selection if available
464
+ if "climate_data" in session_state and session_state["climate_data"]:
465
+ st.subheader("Current Selection")
466
+ st.json(session_state["climate_data"])
467
+
468
 
469
+ # Create a singleton instance
470
+ climate_data = ClimateData()
471
 
472
+ # Export climate data to JSON if needed
473
  if __name__ == "__main__":
474
+ climate_data.export_to_json(os.path.join(DATA_DIR, "climate_data.json"))