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

Upload climate_data.py

Browse files
Files changed (1) hide show
  1. data/climate_data.py +478 -369
data/climate_data.py CHANGED
@@ -1,370 +1,479 @@
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."""
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)
 
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."""
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 _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 = epw_data[col].values
101
+ mean_val = np.mean(values)
102
+ min_val = np.min(values)
103
+ max_val = np.max(values)
104
+
105
+ # Dry Bulb Temperature (°C): -50 to 50°C
106
+ if -50 <= min_val <= max_val <= 50 and col not in column_map:
107
+ column_map["dry_bulb"] = col
108
+ # Wet Bulb Temperature (°C): -50 to 40°C, usually less than dry bulb
109
+ elif -50 <= min_val <= max_val <= 40 and col not in column_map:
110
+ column_map["wet_bulb"] = col
111
+ # Relative Humidity (%): 0 to 100%
112
+ elif 0 <= min_val <= max_val <= 100 and col not in column_map:
113
+ column_map["humidity"] = col
114
+ # Atmospheric Pressure (Pa): 80000 to 105000 Pa
115
+ elif 80000 <= min_val <= max_val <= 105000 and col not in column_map:
116
+ column_map["pressure"] = col
117
+
118
+ # Standard EPW column indices as fallback
119
+ standard_map = {
120
+ "dry_bulb": 6,
121
+ "wet_bulb": 8,
122
+ "humidity": 21,
123
+ "pressure": 9
124
+ }
125
+
126
+ # Validate inferred columns against standard, warn if mismatched
127
+ for key in standard_map:
128
+ if key not in column_map:
129
+ st.warning(f"Could not infer {key} column. Using standard EPW index {standard_map[key]}.")
130
+ column_map[key] = standard_map[key]
131
+ elif column_map[key] != standard_map[key]:
132
+ st.warning(f"Inferred {key} column ({column_map[key]}) differs from standard EPW ({standard_map[key]}). Proceeding with inferred column.")
133
+
134
+ return column_map
135
+
136
+ def display_climate_input(self, session_state: Dict[str, Any]):
137
+ """Display form for manual input or EPW upload in Streamlit."""
138
+ st.title("Climate Data")
139
+
140
+ if not session_state.building_info.get("country") or not session_state.building_info.get("city"):
141
+ st.warning("Please enter country and city in Building Information first.")
142
+ st.button("Go to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))
143
+ return
144
+
145
+ st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")
146
+ tab1, tab2 = st.tabs(["Manual Input", "Upload EPW File"])
147
+
148
+ # Manual Input Tab
149
+ with tab1:
150
+ with st.form("manual_climate_form"):
151
+ col1, col2 = st.columns(2)
152
+ with col1:
153
+ id_input = st.text_input("Location ID", value=f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}")
154
+ state_province = st.text_input("State/Province", value="N/A")
155
+ latitude = st.number_input("Latitude", min_value=-90.0, max_value=90.0, value=0.0, step=0.1)
156
+ longitude = st.number_input("Longitude", min_value=-180.0, max_value=180.0, value=0.0, step=0.1)
157
+ elevation = st.number_input("Elevation (m)", min_value=0.0, value=0.0, step=10.0)
158
+ 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"])
159
+
160
+ with col2:
161
+ hdd = st.number_input("Heating Degree Days (base 18°C)", min_value=0.0, value=0.0, step=100.0)
162
+ cdd = st.number_input("Cooling Degree Days (base 18°C)", min_value=0.0, value=0.0, step=100.0)
163
+ 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)
164
+ 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)
165
+ 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)
166
+ summer_daily_range = st.number_input("Summer Daily Range (°C)", min_value=0.0, value=5.0, step=0.5)
167
+
168
+ st.subheader("Monthly Data")
169
+ monthly_temps = {}
170
+ monthly_humidity = {}
171
+ month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
172
+ col1, col2 = st.columns(2)
173
+ with col1:
174
+ for month in month_names[:6]:
175
+ 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}")
176
+ with col2:
177
+ for month in month_names[6:]:
178
+ 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}")
179
+ col1, col2 = st.columns(2)
180
+ with col1:
181
+ for month in month_names[:6]:
182
+ 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}")
183
+ with col2:
184
+ for month in month_names[6:]:
185
+ 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}")
186
+
187
+ if st.form_submit_button("Save Climate Data"):
188
+ location = ClimateLocation(
189
+ id=id_input,
190
+ country=session_state.building_info["country"],
191
+ state_province=state_province,
192
+ city=session_state.building_info["city"],
193
+ latitude=latitude,
194
+ longitude=longitude,
195
+ elevation=elevation,
196
+ climate_zone=climate_zone,
197
+ heating_degree_days=hdd,
198
+ cooling_degree_days=cdd,
199
+ winter_design_temp=winter_design_temp,
200
+ summer_design_temp_db=summer_design_temp_db,
201
+ summer_design_temp_wb=summer_design_temp_wb,
202
+ summer_daily_range=summer_daily_range,
203
+ monthly_temps=monthly_temps,
204
+ monthly_humidity=monthly_humidity
205
+ )
206
+ self.add_location(location)
207
+ st.success("Climate data saved manually!")
208
+ self.display_design_conditions(location)
209
+ self.visualize_data(location, epw_data=None) # No EPW data for manual input
210
+
211
+ # EPW Upload Tab
212
+ with tab2:
213
+ uploaded_file = st.file_uploader("Upload EPW File", type=["epw"])
214
+ if uploaded_file:
215
+ try:
216
+ epw_content = uploaded_file.read().decode("utf-8")
217
+ epw_lines = epw_content.splitlines()
218
+ header = next(line for line in epw_lines if line.startswith("LOCATION"))
219
+ header_parts = header.split(",")
220
+ latitude = float(header_parts[6])
221
+ longitude = float(header_parts[7])
222
+ elevation = float(header_parts[8])
223
+
224
+ epw_data = pd.read_csv(StringIO("\n".join(epw_lines[8:])), header=None)
225
+ if len(epw_data) != 8760:
226
+ raise ValueError("EPW file must contain 8760 hourly records.")
227
+
228
+ # Infer column indices
229
+ column_map = self._infer_epw_columns(epw_data)
230
+
231
+ # Extract data with inferred columns
232
+ months = epw_data[1].values # Month column
233
+ dry_bulb = epw_data[column_map["dry_bulb"]].values
234
+ wet_bulb = epw_data[column_map["wet_bulb"]].values
235
+ humidity = epw_data[column_map["humidity"]].values
236
+
237
+ # Calculate daily averages for HDD/CDD
238
+ daily_temps = dry_bulb.reshape(-1, 24).mean(axis=1)
239
+ hdd = round(sum(max(18 - temp, 0) for temp in daily_temps))
240
+ cdd = round(sum(max(temp - 18, 0) for temp in daily_temps))
241
+
242
+ # Design conditions
243
+ winter_design_temp = round(np.percentile(dry_bulb, 0.4), 1)
244
+ summer_design_temp_db = round(np.percentile(dry_bulb, 99.6), 1)
245
+ summer_idx = np.argmax(dry_bulb >= summer_design_temp_db)
246
+ summer_design_temp_wb = round(wet_bulb[summer_idx], 1)
247
+ summer_mask = (months >= 6) & (months <= 8)
248
+ summer_temps = dry_bulb[summer_mask].reshape(-1, 24)
249
+ summer_daily_range = round(np.mean(summer_temps.max(axis=1) - summer_temps.min(axis=1)), 1)
250
+
251
+ # Monthly averages, mins, and maxes (stored only averages in ClimateLocation)
252
+ monthly_temps = {}
253
+ monthly_humidity = {}
254
+ month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
255
+ for i in range(1, 13):
256
+ month_mask = (months == i)
257
+ monthly_temps[month_names[i-1]] = round(np.mean(dry_bulb[month_mask]), 1)
258
+ monthly_humidity[month_names[i-1]] = round(np.mean(humidity[month_mask]), 1)
259
+
260
+ avg_humidity = np.mean(humidity)
261
+ climate_zone = self.assign_climate_zone(hdd, cdd, avg_humidity)
262
+
263
+ location = ClimateLocation(
264
+ id=f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}",
265
+ country=session_state.building_info["country"],
266
+ state_province="N/A",
267
+ city=session_state.building_info["city"],
268
+ latitude=latitude,
269
+ longitude=longitude,
270
+ elevation=elevation,
271
+ climate_zone=climate_zone,
272
+ heating_degree_days=hdd,
273
+ cooling_degree_days=cdd,
274
+ winter_design_temp=winter_design_temp,
275
+ summer_design_temp_db=summer_design_temp_db,
276
+ summer_design_temp_wb=summer_design_temp_wb,
277
+ summer_daily_range=summer_daily_range,
278
+ monthly_temps=monthly_temps,
279
+ monthly_humidity=monthly_humidity
280
+ )
281
+ self.add_location(location)
282
+ st.success("Climate data extracted from EPW file!")
283
+ self.display_design_conditions(location)
284
+ self.visualize_data(location, epw_data=epw_data, column_map=column_map)
285
+ except Exception as e:
286
+ st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
287
+
288
+ # Navigation buttons
289
+ col1, col2 = st.columns(2)
290
+ with col1:
291
+ st.button("Back to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))
292
+ with col2:
293
+ if self.locations:
294
+ st.button("Continue to Building Components", on_click=lambda: setattr(session_state, "page", "Building Components"))
295
+ else:
296
+ st.button("Continue to Building Components", disabled=True)
297
+
298
+ def display_design_conditions(self, location: ClimateLocation):
299
+ """Display a table of design conditions for calculations."""
300
+ st.subheader("Design Conditions for HVAC Calculations")
301
+ design_data = pd.DataFrame({
302
+ "Parameter": [
303
+ "Climate Zone",
304
+ "Heating Degree Days (base 18°C)",
305
+ "Cooling Degree Days (base 18°C)",
306
+ "Winter Design Temperature (99.6%)",
307
+ "Summer Design Dry-Bulb Temp (0.4%)",
308
+ "Summer Design Wet-Bulb Temp (0.4%)",
309
+ "Summer Daily Temperature Range"
310
+ ],
311
+ "Value": [
312
+ location.climate_zone,
313
+ f"{location.heating_degree_days} HDD",
314
+ f"{location.cooling_degree_days} CDD",
315
+ f"{location.winter_design_temp} °C",
316
+ f"{location.summer_design_temp_db} °C",
317
+ f"{location.summer_design_temp_wb} °C",
318
+ f"{location.summer_daily_range} °C"
319
+ ]
320
+ })
321
+ st.table(design_data)
322
+
323
+ @staticmethod
324
+ def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
325
+ """Assign ASHRAE 169 climate zone based on HDD, CDD, and humidity."""
326
+ if cdd > 10000:
327
+ return "0A" if avg_humidity > 60 else "0B"
328
+ elif cdd > 5000:
329
+ return "1A" if avg_humidity > 60 else "1B"
330
+ elif cdd > 2500:
331
+ return "2A" if avg_humidity > 60 else "2B"
332
+ elif hdd < 2000 and cdd > 1000:
333
+ return "3A" if avg_humidity > 60 else "3B" if avg_humidity < 40 else "3C"
334
+ elif hdd < 3000:
335
+ return "4A" if avg_humidity > 60 else "4B" if avg_humidity < 40 else "4C"
336
+ elif hdd < 4000:
337
+ return "5A" if avg_humidity > 60 else "5B" if avg_humidity < 40 else "5C"
338
+ elif hdd < 5000:
339
+ return "6A" if avg_humidity > 60 else "6B"
340
+ elif hdd < 7000:
341
+ return "7"
342
+ else:
343
+ return "8"
344
+
345
+ def visualize_data(self, location: ClimateLocation, epw_data: Optional[pd.DataFrame] = None, column_map: Optional[Dict[str, int]] = None):
346
+ """Visualize monthly temperature and humidity data with min, max, and average."""
347
+ st.subheader("Monthly Climate Data Visualization")
348
+
349
+ months = list(range(1, 13))
350
+ month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
351
+ temps_avg = [location.monthly_temps[m] for m in month_names]
352
+ humidity_avg = [location.monthly_humidity[m] for m in month_names]
353
+
354
+ # If EPW data is provided, calculate min/max from raw data
355
+ if epw_data is not None and column_map is not None:
356
+ dry_bulb = epw_data[column_map["dry_bulb"]].values
357
+ humidity = epw_data[column_map["humidity"]].values
358
+ month_col = epw_data[1].values
359
+
360
+ temps_min = []
361
+ temps_max = []
362
+ humidity_min = []
363
+ humidity_max = []
364
+ for i in range(1, 13):
365
+ month_mask = (month_col == i)
366
+ temps_min.append(round(np.min(dry_bulb[month_mask]), 1))
367
+ temps_max.append(round(np.max(dry_bulb[month_mask]), 1))
368
+ humidity_min.append(round(np.min(humidity[month_mask]), 1))
369
+ humidity_max.append(round(np.max(humidity[month_mask]), 1))
370
+ else:
371
+ # For manual input, only show averages (no min/max available)
372
+ temps_min = temps_avg
373
+ temps_max = temps_avg
374
+ humidity_min = humidity_avg
375
+ humidity_max = humidity_avg
376
+
377
+ # Temperature Plot
378
+ fig_temp = go.Figure()
379
+ fig_temp.add_trace(go.Scatter(
380
+ x=months,
381
+ y=temps_avg,
382
+ mode='lines+markers',
383
+ name='Avg Temperature (°C)',
384
+ line=dict(color='red'),
385
+ marker=dict(size=8)
386
+ ))
387
+ fig_temp.add_trace(go.Scatter(
388
+ x=months,
389
+ y=temps_max,
390
+ mode='lines',
391
+ name='Max Temperature (°C)',
392
+ line=dict(color='red', dash='dash'),
393
+ opacity=0.5
394
+ ))
395
+ fig_temp.add_trace(go.Scatter(
396
+ x=months,
397
+ y=temps_min,
398
+ mode='lines',
399
+ name='Min Temperature (°C)',
400
+ line=dict(color='red', dash='dash'),
401
+ opacity=0.5,
402
+ fill='tonexty', # Fill area between min and max
403
+ fillcolor='rgba(255, 0, 0, 0.1)'
404
+ ))
405
+ fig_temp.update_layout(
406
+ title='Monthly Temperatures',
407
+ xaxis_title='Month',
408
+ yaxis_title='Temperature (°C)',
409
+ xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names),
410
+ legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
411
+ )
412
+ st.plotly_chart(fig_temp, use_container_width=True)
413
+
414
+ # Humidity Plot
415
+ fig_hum = go.Figure()
416
+ fig_hum.add_trace(go.Scatter(
417
+ x=months,
418
+ y=humidity_avg,
419
+ mode='lines+markers',
420
+ name='Avg Humidity (%)',
421
+ line=dict(color='blue'),
422
+ marker=dict(size=8)
423
+ ))
424
+ fig_hum.add_trace(go.Scatter(
425
+ x=months,
426
+ y=humidity_max,
427
+ mode='lines',
428
+ name='Max Humidity (%)',
429
+ line=dict(color='blue', dash='dash'),
430
+ opacity=0.5
431
+ ))
432
+ fig_hum.add_trace(go.Scatter(
433
+ x=months,
434
+ y=humidity_min,
435
+ mode='lines',
436
+ name='Min Humidity (%)',
437
+ line=dict(color='blue', dash='dash'),
438
+ opacity=0.5,
439
+ fill='tonexty', # Fill area between min and max
440
+ fillcolor='rgba(0, 0, 255, 0.1)'
441
+ ))
442
+ fig_hum.update_layout(
443
+ title='Monthly Relative Humidity',
444
+ xaxis_title='Month',
445
+ yaxis_title='Relative Humidity (%)',
446
+ xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names),
447
+ legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
448
+ )
449
+ st.plotly_chart(fig_hum, use_container_width=True)
450
+
451
+ def export_to_json(self, file_path: str) -> None:
452
+ """Export all climate data to a JSON file."""
453
+ data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
454
+ with open(file_path, 'w') as f:
455
+ json.dump(data, f, indent=4)
456
+
457
+ @classmethod
458
+ def from_json(cls, file_path: str) -> 'ClimateData':
459
+ """Create a ClimateData instance from a JSON file."""
460
+ with open(file_path, 'r') as f:
461
+ data = json.load(f)
462
+ climate_data = cls()
463
+ climate_data.locations = {}
464
+ for loc_id, loc_dict in data.items():
465
+ climate_data.locations[loc_id] = ClimateLocation(**loc_dict)
466
+ climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values())))
467
+ climate_data.country_states = climate_data._group_locations_by_country_state()
468
+ return climate_data
469
+
470
+
471
+ if __name__ == "__main__":
472
+ # Ensure session_state is initialized for standalone testing
473
+ if "building_info" not in st.session_state:
474
+ st.session_state.building_info = {"country": "Australia", "city": "Melbourne"}
475
+ if "page" not in st.session_state:
476
+ st.session_state.page = "Climate Data"
477
+
478
+ climate_data = ClimateData()
479
  climate_data.display_climate_input(st.session_state)