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

Update data/climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +274 -378
data/climate_data.py CHANGED
@@ -1,6 +1,10 @@
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,6 +13,9 @@ import numpy as np
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,14 +35,10 @@ class ClimateLocation:
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,409 +69,302 @@ class ClimateData:
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"))
 
 
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
  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
  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
 
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
+ )
346
+ st.plotly_chart(fig_hum, use_container_width=True)
347
+
348
  def export_to_json(self, file_path: str) -> None:
349
+ """Export all climate data to a JSON file."""
 
 
 
 
 
350
  data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
 
351
  with open(file_path, 'w') as f:
352
  json.dump(data, f, indent=4)
353
+
354
  @classmethod
355
  def from_json(cls, file_path: str) -> 'ClimateData':
356
+ """Create a ClimateData instance from a JSON file."""
 
 
 
 
 
 
 
 
357
  with open(file_path, 'r') as f:
358
  data = json.load(f)
 
359
  climate_data = cls()
360
  climate_data.locations = {}
 
361
  for loc_id, loc_dict in data.items():
362
+ climate_data.locations[loc_id] = ClimateLocation(**loc_dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values())))
364
  climate_data.country_states = climate_data._group_locations_by_country_state()
 
365
  return climate_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
 
 
367
 
 
368
  if __name__ == "__main__":
369
+ climate_data = ClimateData()
370
+ climate_data.display_climate_input(st.session_state)