Update data/climate_data.py
Browse files- data/climate_data.py +107 -214
data/climate_data.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
ASHRAE 169 climate data module for HVAC Load Calculator.
|
| 3 |
Extracts climate data from EPW files and provides visualizations inspired by Climate Consultant.
|
| 4 |
-
Includes solar radiation and position calculations for heat gain analysis.
|
| 5 |
|
| 6 |
Author: Dr Majed Abuseif
|
| 7 |
Date: May 2025
|
| 8 |
-
Version: 2.1.
|
| 9 |
"""
|
| 10 |
|
| 11 |
from typing import Dict, List, Any, Optional
|
|
@@ -85,9 +84,10 @@ class ClimateLocation:
|
|
| 85 |
hourly_data: List[Dict] # Hourly data for integration with main.py
|
| 86 |
typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
|
| 87 |
ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
| 91 |
"""Initialize ClimateLocation with EPW file data and header information."""
|
| 92 |
self.id = kwargs.get("id")
|
| 93 |
self.country = kwargs.get("country")
|
|
@@ -99,9 +99,8 @@ class ClimateLocation:
|
|
| 99 |
self.time_zone = kwargs.get("time_zone")
|
| 100 |
self.typical_extreme_periods = typical_extreme_periods
|
| 101 |
self.ground_temperatures = ground_temperatures
|
| 102 |
-
|
| 103 |
-
#
|
| 104 |
-
self._solar_angle_cache = {}
|
| 105 |
|
| 106 |
# Extract columns from EPW data
|
| 107 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
@@ -114,16 +113,14 @@ class ClimateLocation:
|
|
| 114 |
direct_normal_irradiance = pd.to_numeric(epw_file[14], errors='coerce').values
|
| 115 |
diffuse_horizontal_irradiance = pd.to_numeric(epw_file[15], errors='coerce').values
|
| 116 |
wind_direction = pd.to_numeric(epw_file[20], errors='coerce').values
|
| 117 |
-
wind_speed = pd.to_numeric(epw_file[21], errors='coerce')
|
| 118 |
|
| 119 |
# Filter wind speed outliers and log high values
|
| 120 |
wind_speed = wind_speed[wind_speed <= 50] # Remove extreme outliers
|
| 121 |
if (wind_speed > 15).any():
|
| 122 |
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 123 |
|
| 124 |
-
# Filter
|
| 125 |
-
direct_normal_irradiance = np.where(direct_normal_irradiance < 0, 0, direct_normal_irradiance)
|
| 126 |
-
diffuse_horizontal_irradiance = np.where(diffuse_horizontal_irradiance < 0, 0, diffuse_horizontal_irradiance)
|
| 127 |
direct_normal_irradiance = direct_normal_irradiance[direct_normal_irradiance <= 1200]
|
| 128 |
diffuse_horizontal_irradiance = diffuse_horizontal_irradiance[diffuse_horizontal_irradiance <= 1200]
|
| 129 |
if (direct_normal_irradiance > 1000).any():
|
|
@@ -166,41 +163,25 @@ class ClimateLocation:
|
|
| 166 |
# Assign climate zone
|
| 167 |
self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
|
| 168 |
|
| 169 |
-
# Calculate solstice
|
| 170 |
-
self.
|
| 171 |
|
| 172 |
-
# Store hourly data with enhanced fields
|
| 173 |
self.hourly_data = []
|
| 174 |
-
negative_dni_count = 0
|
| 175 |
-
negative_dhi_count = 0
|
| 176 |
for i in range(len(months)):
|
| 177 |
if np.isnan(months[i]) or np.isnan(days[i]) or np.isnan(hours[i]) or np.isnan(dry_bulb[i]):
|
| 178 |
continue # Skip records with missing critical fields
|
| 179 |
-
|
| 180 |
# Calculate solar time and angles
|
| 181 |
-
timestamp = datetime(2025, int(months[i]), int(days[i]), int(hours[i]) - 1) #
|
| 182 |
solar_time = self._calculate_solar_time(timestamp)
|
| 183 |
solar_angles = self._calculate_solar_angles(timestamp)
|
| 184 |
|
| 185 |
-
#
|
| 186 |
-
dni = 0.0
|
| 187 |
-
dhi = 0.0
|
| 188 |
-
ground_radiation = 0.0
|
| 189 |
|
| 190 |
-
#
|
| 191 |
-
|
| 192 |
-
# Validate or fallback DNI/DHI
|
| 193 |
-
dni = float(direct_normal_irradiance[i]) if not np.isnan(direct_normal_irradiance[i]) else self._calculate_clear_sky_irradiance(timestamp, 'dni')
|
| 194 |
-
dhi = float(diffuse_horizontal_irradiance[i]) if not np.isnan(diffuse_horizontal_irradiance[i]) else self._calculate_clear_sky_irradiance(timestamp, 'dhi')
|
| 195 |
-
if dni < 0:
|
| 196 |
-
negative_dni_count += 1
|
| 197 |
-
dni = self._calculate_clear_sky_irradiance(timestamp, 'dni')
|
| 198 |
-
if dhi < 0:
|
| 199 |
-
negative_dhi_count += 1
|
| 200 |
-
dhi = self._calculate_clear_sky_irradiance(timestamp, 'dhi')
|
| 201 |
-
|
| 202 |
-
# Calculate ground-reflected radiation
|
| 203 |
-
ground_radiation = self._calculate_ground_reflected_radiation(dni, dhi, solar_angles['zenith'])
|
| 204 |
|
| 205 |
record = {
|
| 206 |
"month": int(months[i]),
|
|
@@ -212,99 +193,81 @@ class ClimateLocation:
|
|
| 212 |
"global_horizontal_radiation": float(global_radiation[i]) if not np.isnan(global_radiation[i]) else 0.0,
|
| 213 |
"direct_normal_irradiance": dni,
|
| 214 |
"diffuse_horizontal_irradiance": dhi,
|
| 215 |
-
"ground_reflected_radiation": ground_radiation,
|
| 216 |
"wind_speed": float(wind_speed[i]) if not np.isnan(wind_speed[i]) else 0.0,
|
| 217 |
"wind_direction": float(wind_direction[i]) if not np.isnan(wind_direction[i]) else 0.0,
|
| 218 |
-
"
|
| 219 |
-
"
|
|
|
|
|
|
|
| 220 |
}
|
| 221 |
self.hourly_data.append(record)
|
| 222 |
|
| 223 |
-
# Log summary of negative DNI/DHI corrections
|
| 224 |
-
if negative_dni_count > 0:
|
| 225 |
-
logger.warning(f"Corrected {negative_dni_count} negative DNI values using clear-sky model")
|
| 226 |
-
if negative_dhi_count > 0:
|
| 227 |
-
logger.warning(f"Corrected {negative_dhi_count} negative DHI values using clear-sky model")
|
| 228 |
-
|
| 229 |
if len(self.hourly_data) != 8760:
|
| 230 |
st.warning(f"Hourly data has {len(self.hourly_data)} records instead of 8760. Some records may have been excluded due to missing data.")
|
| 231 |
|
| 232 |
-
def _calculate_solar_time(self, timestamp: datetime) ->
|
| 233 |
-
"""Calculate solar time
|
| 234 |
# Standard meridian for the time zone
|
| 235 |
-
standard_meridian = self.time_zone * 15 #
|
| 236 |
-
# Longitude correction
|
| 237 |
-
longitude_correction =
|
| 238 |
-
# Equation of time (simplified,
|
| 239 |
day_of_year = timestamp.timetuple().tm_yday
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
solar_time = timestamp + timedelta(minutes=time_diff)
|
| 250 |
return solar_time
|
| 251 |
|
| 252 |
def _calculate_solar_angles(self, timestamp: datetime) -> Dict[str, float]:
|
| 253 |
-
"""Calculate solar zenith and azimuth angles
|
| 254 |
-
# Create
|
| 255 |
-
cache_key = hashlib.md5(
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
if cache_key in self.
|
| 258 |
-
return self.
|
| 259 |
|
|
|
|
| 260 |
solpos = pvlib.solarposition.get_solarposition(
|
| 261 |
time=timestamp,
|
| 262 |
latitude=self.latitude,
|
| 263 |
longitude=self.longitude,
|
| 264 |
altitude=self.elevation
|
| 265 |
)
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
'azimuth': round(float(solpos['azimuth'].iloc[0]), 2)
|
| 269 |
-
}
|
| 270 |
|
| 271 |
-
# Cache
|
| 272 |
-
self.
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
clearsky = pvlib.clearsky.ineichen(
|
| 278 |
-
time=timestamp,
|
| 279 |
-
latitude=self.latitude,
|
| 280 |
-
longitude=self.longitude,
|
| 281 |
-
altitude=self.elevation,
|
| 282 |
-
pressure=self.pressure
|
| 283 |
-
)
|
| 284 |
-
return float(clearsky[component].iloc[0]) if component in ['dni', 'dhi'] else 0.0
|
| 285 |
-
|
| 286 |
-
def _calculate_ground_reflected_radiation(self, dni: float, dhi: float, zenith: float) -> float:
|
| 287 |
-
"""Calculate ground-reflected radiation using albedo (ASHRAE Fundamentals, Chapter 14, Table 4)."""
|
| 288 |
-
albedo = 0.2 # Default albedo per ASHRAE
|
| 289 |
-
cos_zenith = np.cos(np.radians(zenith))
|
| 290 |
-
ground_radiation = albedo * (dni * cos_zenith + dhi)
|
| 291 |
-
return round(max(0, ground_radiation), 1)
|
| 292 |
|
| 293 |
-
def
|
| 294 |
-
"""Calculate solar angles
|
| 295 |
dates = [
|
| 296 |
-
datetime(2025,
|
| 297 |
-
datetime(2025,
|
| 298 |
]
|
| 299 |
-
|
| 300 |
for date in dates:
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
| 308 |
|
| 309 |
def to_dict(self) -> Dict[str, Any]:
|
| 310 |
"""Convert the climate location to a dictionary."""
|
|
@@ -328,10 +291,11 @@ class ClimateLocation:
|
|
| 328 |
"pressure": self.pressure,
|
| 329 |
"direct_normal_irradiance": self.direct_normal_irradiance,
|
| 330 |
"diffuse_horizontal_irradiance": self.diffuse_horizontal_irradiance,
|
|
|
|
| 331 |
"hourly_data": self.hourly_data,
|
| 332 |
"typical_extreme_periods": self.typical_extreme_periods,
|
| 333 |
"ground_temperatures": self.ground_temperatures,
|
| 334 |
-
"
|
| 335 |
}
|
| 336 |
|
| 337 |
class ClimateData:
|
|
@@ -380,7 +344,7 @@ class ClimateData:
|
|
| 380 |
"winter_design_temp", "summer_design_temp_db", "summer_design_temp_wb",
|
| 381 |
"summer_daily_range", "wind_speed", "pressure",
|
| 382 |
"direct_normal_irradiance", "diffuse_horizontal_irradiance", "hourly_data",
|
| 383 |
-
"
|
| 384 |
]
|
| 385 |
|
| 386 |
for field in required_fields:
|
|
@@ -424,7 +388,9 @@ class ClimateData:
|
|
| 424 |
if not (0 <= data["diffuse_horizontal_irradiance"] <= 1200):
|
| 425 |
st.error(f"Validation failed: Diffuse horizontal irradiance {data['diffuse_horizontal_irradiance']} outside range")
|
| 426 |
return False
|
| 427 |
-
|
|
|
|
|
|
|
| 428 |
if not data["hourly_data"] or len(data["hourly_data"]) < 8700:
|
| 429 |
st.error(f"Validation failed: Hourly data has {len(data['hourly_data'])} records, expected ~8760")
|
| 430 |
return False
|
|
@@ -456,8 +422,14 @@ class ClimateData:
|
|
| 456 |
if not (0 <= record["diffuse_horizontal_irradiance"] <= 1200):
|
| 457 |
st.error(f"Validation failed: Diffuse horizontal irradiance {record['diffuse_horizontal_irradiance']} outside range")
|
| 458 |
return False
|
| 459 |
-
if not (0 <= record["
|
| 460 |
-
st.error(f"Validation failed:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
return False
|
| 462 |
if not (0 <= record["solar_zenith"] <= 180):
|
| 463 |
st.error(f"Validation failed: Solar zenith {record['solar_zenith']} outside range")
|
|
@@ -465,11 +437,8 @@ class ClimateData:
|
|
| 465 |
if not (0 <= record["solar_azimuth"] <= 360):
|
| 466 |
st.error(f"Validation failed: Solar azimuth {record['solar_azimuth']} outside range")
|
| 467 |
return False
|
| 468 |
-
if not (0 <= record["
|
| 469 |
-
st.error(f"Validation failed:
|
| 470 |
-
return False
|
| 471 |
-
if not (0 <= record["wind_direction"] <= 360):
|
| 472 |
-
st.error(f"Validation failed: Wind direction {record['wind_direction']} outside range")
|
| 473 |
return False
|
| 474 |
|
| 475 |
# Validate typical/extreme periods (optional)
|
|
@@ -491,22 +460,12 @@ class ClimateData:
|
|
| 491 |
st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
|
| 492 |
return False
|
| 493 |
|
| 494 |
-
# Validate solstice
|
| 495 |
-
if "
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
angles = data["solstice_solar_angles"][season]
|
| 501 |
-
if not (0 <= angles["zenith"] <= 180 and 0 <= angles["azimuth"] <= 360):
|
| 502 |
-
st.error(f"Validation failed: Invalid {season} angles: {angles}")
|
| 503 |
-
return False
|
| 504 |
-
|
| 505 |
-
# Log presence of ground_reflected_radiation during import
|
| 506 |
-
if all("ground_reflected_radiation" in record for record in data["hourly_data"]):
|
| 507 |
-
logger.info("Validated ground_reflected_radiation present in all hourly data records")
|
| 508 |
-
else:
|
| 509 |
-
st.error("Validation failed: Missing ground_reflected_radiation in some hourly data records")
|
| 510 |
return False
|
| 511 |
|
| 512 |
return True
|
|
@@ -539,8 +498,8 @@ class ClimateData:
|
|
| 539 |
except ValueError:
|
| 540 |
return False
|
| 541 |
|
| 542 |
-
def display_climate_input(self, session_state: Dict[str, Any]):
|
| 543 |
-
"""Display Streamlit interface for EPW
|
| 544 |
st.title("Climate Data Analysis")
|
| 545 |
|
| 546 |
# Apply consistent styling
|
|
@@ -551,64 +510,16 @@ class ClimateData:
|
|
| 551 |
st.warning("Clearing invalid climate data from session state.")
|
| 552 |
del session_state["climate_data"]
|
| 553 |
|
| 554 |
-
|
| 555 |
-
uploaded_epw_file = st.file_uploader("Upload EPW File", type=["epw"])
|
| 556 |
-
uploaded_json_file = st.file_uploader("Upload JSON File", type=["json"])
|
| 557 |
|
| 558 |
# Initialize location and epw_data for display
|
| 559 |
location = None
|
| 560 |
epw_data = None
|
| 561 |
|
| 562 |
-
|
| 563 |
-
if uploaded_json_file:
|
| 564 |
-
try:
|
| 565 |
-
json_content = uploaded_json_file.read().decode("utf-8")
|
| 566 |
-
json_data = json.loads(json_content)
|
| 567 |
-
for loc_id, loc_dict in json_data.items():
|
| 568 |
-
if not self.validate_climate_data(loc_dict):
|
| 569 |
-
raise ValueError("Invalid climate data in JSON file.")
|
| 570 |
-
# Rebuild epw_data from hourly_data
|
| 571 |
-
hourly_data = loc_dict["hourly_data"]
|
| 572 |
-
epw_data = pd.DataFrame({
|
| 573 |
-
1: [d["month"] for d in hourly_data],
|
| 574 |
-
2: [d["day"] for d in hourly_data],
|
| 575 |
-
3: [d["hour"] for d in hourly_data],
|
| 576 |
-
6: [d["dry_bulb"] for d in hourly_data],
|
| 577 |
-
8: [d["relative_humidity"] for d in hourly_data],
|
| 578 |
-
9: [d["atmospheric_pressure"] for d in hourly_data],
|
| 579 |
-
13: [d["global_horizontal_radiation"] for d in hourly_data],
|
| 580 |
-
14: [d["direct_normal_irradiance"] for d in hourly_data],
|
| 581 |
-
15: [d["diffuse_horizontal_irradiance"] for d in hourly_data],
|
| 582 |
-
20: [d["wind_direction"] for d in hourly_data],
|
| 583 |
-
21: [d["wind_speed"] for d in hourly_data],
|
| 584 |
-
})
|
| 585 |
-
location = ClimateLocation(
|
| 586 |
-
epw_file=epw_data,
|
| 587 |
-
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 588 |
-
ground_temperatures=loc_dict["ground_temperatures"],
|
| 589 |
-
id=loc_dict["id"],
|
| 590 |
-
country=loc_dict["country"],
|
| 591 |
-
state_province=loc_dict["state_province"],
|
| 592 |
-
city=loc_dict["city"],
|
| 593 |
-
latitude=loc_dict["latitude"],
|
| 594 |
-
longitude=loc_dict["longitude"],
|
| 595 |
-
elevation=loc_dict["elevation"],
|
| 596 |
-
time_zone=loc_dict["time_zone"]
|
| 597 |
-
)
|
| 598 |
-
location.hourly_data = loc_dict["hourly_data"]
|
| 599 |
-
location.solstice_solar_angles = loc_dict["solstice_solar_angles"]
|
| 600 |
-
self.add_location(location)
|
| 601 |
-
session_state["climate_data"] = loc_dict
|
| 602 |
-
st.success(f"Climate data loaded from JSON for {loc_dict['city']}")
|
| 603 |
-
logger.info(f"Successfully imported JSON with ground_reflected_radiation for {loc_dict['city']}")
|
| 604 |
-
except Exception as e:
|
| 605 |
-
st.error(f"Error processing JSON file: {str(e)}. Ensure it has valid climate data.")
|
| 606 |
-
|
| 607 |
-
# Process EPW file if uploaded
|
| 608 |
-
elif uploaded_epw_file:
|
| 609 |
try:
|
| 610 |
# Process new EPW file
|
| 611 |
-
epw_content =
|
| 612 |
epw_lines = epw_content.splitlines()
|
| 613 |
|
| 614 |
# Parse header
|
|
@@ -649,7 +560,7 @@ class ClimateData:
|
|
| 649 |
period_name = parts[2 + i*4]
|
| 650 |
period_type = parts[3 + i*4]
|
| 651 |
start_date = parts[4 + i*4].strip()
|
| 652 |
-
end_date =
|
| 653 |
if period_name in [
|
| 654 |
"Summer - Week Nearest Max Temperature For Period",
|
| 655 |
"Summer - Week Nearest Average Temperature For Period",
|
|
@@ -721,6 +632,7 @@ class ClimateData:
|
|
| 721 |
epw_file=epw_data,
|
| 722 |
typical_extreme_periods=typical_extreme_periods,
|
| 723 |
ground_temperatures=ground_temperatures,
|
|
|
|
| 724 |
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 725 |
country=country,
|
| 726 |
state_province=state_province,
|
|
@@ -733,12 +645,9 @@ class ClimateData:
|
|
| 733 |
climate_data_dict = location.to_dict()
|
| 734 |
if not self.validate_climate_data(climate_data_dict):
|
| 735 |
raise ValueError("Invalid climate data extracted from EPW file.")
|
| 736 |
-
|
| 737 |
-
# Add location and save to session state
|
| 738 |
-
self.add_location(location)
|
| 739 |
session_state["climate_data"] = climate_data_dict
|
| 740 |
st.success("Climate data extracted from EPW file!")
|
| 741 |
-
|
| 742 |
except Exception as e:
|
| 743 |
st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
|
| 744 |
|
|
@@ -767,6 +676,7 @@ class ClimateData:
|
|
| 767 |
epw_file=epw_data,
|
| 768 |
typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
|
| 769 |
ground_temperatures=climate_data_dict["ground_temperatures"],
|
|
|
|
| 770 |
id=climate_data_dict["id"],
|
| 771 |
country=climate_data_dict["country"],
|
| 772 |
state_province=climate_data_dict["state_province"],
|
|
@@ -778,7 +688,7 @@ class ClimateData:
|
|
| 778 |
)
|
| 779 |
# Override hourly_data to ensure consistency
|
| 780 |
location.hourly_data = climate_data_dict["hourly_data"]
|
| 781 |
-
location.
|
| 782 |
self.add_location(location)
|
| 783 |
st.info("Displaying previously extracted climate data.")
|
| 784 |
|
|
@@ -808,18 +718,13 @@ class ClimateData:
|
|
| 808 |
self.plot_wind_rose(epw_data)
|
| 809 |
|
| 810 |
else:
|
| 811 |
-
st.info("No climate data available. Please upload an EPW
|
| 812 |
|
| 813 |
def display_design_conditions(self, location: ClimateLocation):
|
| 814 |
"""Display design conditions for HVAC calculations using styled HTML."""
|
| 815 |
st.subheader("Design Conditions")
|
| 816 |
|
| 817 |
# Location Details
|
| 818 |
-
solstice_items = [
|
| 819 |
-
f"<li><strong>{key.replace('_', ' ').title()} Zenith:</strong> {angles['zenith']}°</li>"
|
| 820 |
-
f"<li><strong>{key.replace('_', ' ').title()} Azimuth:</strong> {angles['azimuth']}°</li>"
|
| 821 |
-
for key, angles in location.solstice_solar_angles.items()
|
| 822 |
-
]
|
| 823 |
st.markdown(f"""
|
| 824 |
<div class="markdown-text">
|
| 825 |
<h3>Location Details</h3>
|
|
@@ -831,7 +736,8 @@ class ClimateData:
|
|
| 831 |
<li><strong>Longitude:</strong> {location.longitude}°</li>
|
| 832 |
<li><strong>Elevation:</strong> {location.elevation} m</li>
|
| 833 |
<li><strong>Time Zone:</strong> UTC{location.time_zone:+.1f}</li>
|
| 834 |
-
{''
|
|
|
|
| 835 |
</ul>
|
| 836 |
</div>
|
| 837 |
""", unsafe_allow_html=True)
|
|
@@ -852,6 +758,7 @@ class ClimateData:
|
|
| 852 |
<li><strong>Mean Atmospheric Pressure:</strong> {location.pressure} Pa</li>
|
| 853 |
<li><strong>Mean Direct Normal Irradiance:</strong> {location.direct_normal_irradiance} W/m²</li>
|
| 854 |
<li><strong>Mean Diffuse Horizontal Irradiance:</strong> {location.diffuse_horizontal_irradiance} W/m²</li>
|
|
|
|
| 855 |
</ul>
|
| 856 |
</div>
|
| 857 |
""", unsafe_allow_html=True)
|
|
@@ -871,21 +778,6 @@ class ClimateData:
|
|
| 871 |
</div>
|
| 872 |
""", unsafe_allow_html=True)
|
| 873 |
|
| 874 |
-
# Ground Temperatures (Table)
|
| 875 |
-
if location.typical_extreme_periods:
|
| 876 |
-
period_items = [
|
| 877 |
-
f"<li><strong>{key.replace('_', ' ').title()}:</strong> {period['start']['month']}/{period['start']['day']} to {period['end']['month']}/{period['end']['day']}</li>"
|
| 878 |
-
for key, period in location.typical_extreme_periods.items()
|
| 879 |
-
]
|
| 880 |
-
st.markdown(f"""
|
| 881 |
-
<div class="markdown-text">
|
| 882 |
-
<h3>Typical/Extreme Periods</h3>
|
| 883 |
-
<ul>
|
| 884 |
-
{''.join(period_items)}
|
| 885 |
-
</ul>
|
| 886 |
-
</div>
|
| 887 |
-
""", unsafe_allow_html=True)
|
| 888 |
-
|
| 889 |
# Ground Temperatures (Table)
|
| 890 |
if location.ground_temperatures:
|
| 891 |
st.markdown('<div class="markdown-text"><h3>Ground Temperatures</h3></div>', unsafe_allow_html=True)
|
|
@@ -1215,6 +1107,7 @@ class ClimateData:
|
|
| 1215 |
epw_file=epw_data,
|
| 1216 |
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 1217 |
ground_temperatures=loc_dict["ground_temperatures"],
|
|
|
|
| 1218 |
id=loc_dict["id"],
|
| 1219 |
country=loc_dict["country"],
|
| 1220 |
state_province=loc_dict["state_province"],
|
|
@@ -1224,12 +1117,12 @@ class ClimateData:
|
|
| 1224 |
elevation=loc_dict["elevation"],
|
| 1225 |
time_zone=loc_dict["time_zone"]
|
| 1226 |
)
|
| 1227 |
-
location.hourly_data = loc_dict["hourly_data"]
|
| 1228 |
-
location.
|
| 1229 |
climate_data.add_location(location)
|
| 1230 |
return climate_data
|
| 1231 |
|
| 1232 |
if __name__ == "__main__":
|
| 1233 |
climate_data = ClimateData()
|
| 1234 |
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
| 1235 |
-
climate_data.display_climate_input(session_state)
|
|
|
|
| 1 |
"""
|
| 2 |
ASHRAE 169 climate data module for HVAC Load Calculator.
|
| 3 |
Extracts climate data from EPW files and provides visualizations inspired by Climate Consultant.
|
|
|
|
| 4 |
|
| 5 |
Author: Dr Majed Abuseif
|
| 6 |
Date: May 2025
|
| 7 |
+
Version: 2.1.6
|
| 8 |
"""
|
| 9 |
|
| 10 |
from typing import Dict, List, Any, Optional
|
|
|
|
| 84 |
hourly_data: List[Dict] # Hourly data for integration with main.py
|
| 85 |
typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
|
| 86 |
ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
|
| 87 |
+
solstice_zenith_angles: Dict[str, float] # Summer and winter solstice zenith angles
|
| 88 |
+
_solar_cache: Dict[str, Dict[str, float]] # Cache for solar angles
|
| 89 |
+
|
| 90 |
+
def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, albedo: float = 0.2, **kwargs):
|
| 91 |
"""Initialize ClimateLocation with EPW file data and header information."""
|
| 92 |
self.id = kwargs.get("id")
|
| 93 |
self.country = kwargs.get("country")
|
|
|
|
| 99 |
self.time_zone = kwargs.get("time_zone")
|
| 100 |
self.typical_extreme_periods = typical_extreme_periods
|
| 101 |
self.ground_temperatures = ground_temperatures
|
| 102 |
+
self._solar_cache = {}
|
| 103 |
+
self.albedo = albedo # Default albedo from ASHRAE Fundamentals, Chapter 14, Table 4
|
|
|
|
| 104 |
|
| 105 |
# Extract columns from EPW data
|
| 106 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
|
|
| 113 |
direct_normal_irradiance = pd.to_numeric(epw_file[14], errors='coerce').values
|
| 114 |
diffuse_horizontal_irradiance = pd.to_numeric(epw_file[15], errors='coerce').values
|
| 115 |
wind_direction = pd.to_numeric(epw_file[20], errors='coerce').values
|
| 116 |
+
wind_speed = pd.to_numeric(epw_file[21], errors='coerce')
|
| 117 |
|
| 118 |
# Filter wind speed outliers and log high values
|
| 119 |
wind_speed = wind_speed[wind_speed <= 50] # Remove extreme outliers
|
| 120 |
if (wind_speed > 15).any():
|
| 121 |
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 122 |
|
| 123 |
+
# Filter irradiance outliers
|
|
|
|
|
|
|
| 124 |
direct_normal_irradiance = direct_normal_irradiance[direct_normal_irradiance <= 1200]
|
| 125 |
diffuse_horizontal_irradiance = diffuse_horizontal_irradiance[diffuse_horizontal_irradiance <= 1200]
|
| 126 |
if (direct_normal_irradiance > 1000).any():
|
|
|
|
| 163 |
# Assign climate zone
|
| 164 |
self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
|
| 165 |
|
| 166 |
+
# Calculate solstice zenith angles
|
| 167 |
+
self.solstice_zenith_angles = self._calculate_solstice_zenith_angles()
|
| 168 |
|
| 169 |
+
# Store hourly data with enhanced fields
|
| 170 |
self.hourly_data = []
|
|
|
|
|
|
|
| 171 |
for i in range(len(months)):
|
| 172 |
if np.isnan(months[i]) or np.isnan(days[i]) or np.isnan(hours[i]) or np.isnan(dry_bulb[i]):
|
| 173 |
continue # Skip records with missing critical fields
|
|
|
|
| 174 |
# Calculate solar time and angles
|
| 175 |
+
timestamp = datetime(2025, int(months[i]), int(days[i]), int(hours[i]) - 1) # Hour is 1-24, adjust to 0-23
|
| 176 |
solar_time = self._calculate_solar_time(timestamp)
|
| 177 |
solar_angles = self._calculate_solar_angles(timestamp)
|
| 178 |
|
| 179 |
+
# Handle DNI and DHI: set to 0 if missing or negative
|
| 180 |
+
dni = float(direct_normal_irradiance[i]) if not np.isnan(direct_normal_irradiance[i]) and direct_normal_irradiance[i] >= 0 else 0.0
|
| 181 |
+
dhi = float(diffuse_horizontal_irradiance[i]) if not np.isnan(diffuse_horizontal_irradiance[i]) and diffuse_horizontal_irradiance[i] >= 0 else 0.0
|
|
|
|
| 182 |
|
| 183 |
+
# Calculate ground-reflected radiation
|
| 184 |
+
ground_reflected = self.albedo * (dni * np.cos(np.radians(solar_angles['zenith'])) + dhi)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
record = {
|
| 187 |
"month": int(months[i]),
|
|
|
|
| 193 |
"global_horizontal_radiation": float(global_radiation[i]) if not np.isnan(global_radiation[i]) else 0.0,
|
| 194 |
"direct_normal_irradiance": dni,
|
| 195 |
"diffuse_horizontal_irradiance": dhi,
|
|
|
|
| 196 |
"wind_speed": float(wind_speed[i]) if not np.isnan(wind_speed[i]) else 0.0,
|
| 197 |
"wind_direction": float(wind_direction[i]) if not np.isnan(wind_direction[i]) else 0.0,
|
| 198 |
+
"solar_time": float(solar_time),
|
| 199 |
+
"solar_zenith": float(solar_angles['zenith']),
|
| 200 |
+
"solar_azimuth": float(solar_angles['azimuth']),
|
| 201 |
+
"ground_reflected_radiation": float(ground_reflected)
|
| 202 |
}
|
| 203 |
self.hourly_data.append(record)
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
if len(self.hourly_data) != 8760:
|
| 206 |
st.warning(f"Hourly data has {len(self.hourly_data)} records instead of 8760. Some records may have been excluded due to missing data.")
|
| 207 |
|
| 208 |
+
def _calculate_solar_time(self, timestamp: datetime) -> float:
|
| 209 |
+
"""Calculate solar time from local time, adjusting for longitude and time zone (ASHRAE Fundamentals, Chapter 14, Section 14.7)."""
|
| 210 |
# Standard meridian for the time zone
|
| 211 |
+
standard_meridian = self.time_zone * 15 # Time zone offset in degrees (15° per hour)
|
| 212 |
+
# Longitude correction (4 minutes per degree)
|
| 213 |
+
longitude_correction = (self.longitude - standard_meridian) * 4 / 60 # in hours
|
| 214 |
+
# Equation of time (simplified approximation, could use pvlib for precision)
|
| 215 |
day_of_year = timestamp.timetuple().tm_yday
|
| 216 |
+
B = (day_of_year - 1) * 360 / 365.242
|
| 217 |
+
E = 229.18 * (0.000075 + 0.001868 * np.cos(np.radians(B)) - 0.032077 * np.sin(np.radians(B)) -
|
| 218 |
+
0.014615 * np.cos(np.radians(2 * B)) - 0.04089 * np.sin(np.radians(2 * B))) / 60 # in hours
|
| 219 |
+
# Local standard time in hours
|
| 220 |
+
local_std_time = timestamp.hour + timestamp.minute / 60
|
| 221 |
+
# Solar time
|
| 222 |
+
solar_time = local_std_time + longitude_correction + E
|
| 223 |
+
# Normalize to 0-24 hours
|
| 224 |
+
solar_time = solar_time % 24
|
|
|
|
| 225 |
return solar_time
|
| 226 |
|
| 227 |
def _calculate_solar_angles(self, timestamp: datetime) -> Dict[str, float]:
|
| 228 |
+
"""Calculate solar zenith and azimuth angles, caching results (ASHRAE Fundamentals, Chapter 14, Section 14.7)."""
|
| 229 |
+
# Create cache key
|
| 230 |
+
cache_key = hashlib.md5(
|
| 231 |
+
f"{timestamp.strftime('%Y-%m-%d_%H')}_{self.latitude}_{self.longitude}_{self.time_zone}".encode()
|
| 232 |
+
).hexdigest()
|
| 233 |
|
| 234 |
+
if cache_key in self._solar_cache:
|
| 235 |
+
return self._solar_cache[cache_key]
|
| 236 |
|
| 237 |
+
# Use pvlib for solar position
|
| 238 |
solpos = pvlib.solarposition.get_solarposition(
|
| 239 |
time=timestamp,
|
| 240 |
latitude=self.latitude,
|
| 241 |
longitude=self.longitude,
|
| 242 |
altitude=self.elevation
|
| 243 |
)
|
| 244 |
+
zenith = solpos['zenith'].iloc[0]
|
| 245 |
+
azimuth = solpos['azimuth'].iloc[0]
|
|
|
|
|
|
|
| 246 |
|
| 247 |
+
# Cache results
|
| 248 |
+
self._solar_cache[cache_key] = {
|
| 249 |
+
'zenith': zenith,
|
| 250 |
+
'azimuth': azimuth
|
| 251 |
+
}
|
| 252 |
+
return self._solar_cache[cache_key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
+
def _calculate_solstice_zenith_angles(self) -> Dict[str, float]:
|
| 255 |
+
"""Calculate solar zenith angles for summer and winter solstices at noon."""
|
| 256 |
dates = [
|
| 257 |
+
datetime(2025, 12, 21, 12), # Summer solstice (Southern Hemisphere)
|
| 258 |
+
datetime(2025, 6, 21, 12) # Winter solstice (Southern Hemisphere)
|
| 259 |
]
|
| 260 |
+
solstice_zeniths = {}
|
| 261 |
for date in dates:
|
| 262 |
+
solpos = pvlib.solarposition.get_solarposition(
|
| 263 |
+
time=date,
|
| 264 |
+
latitude=self.latitude,
|
| 265 |
+
longitude=self.longitude,
|
| 266 |
+
altitude=self.elevation
|
| 267 |
+
)
|
| 268 |
+
key = 'summer' if date.month == 12 else 'winter'
|
| 269 |
+
solstice_zeniths[key] = round(float(solpos['zenith'].iloc[0]), 1)
|
| 270 |
+
return solstice_zeniths
|
| 271 |
|
| 272 |
def to_dict(self) -> Dict[str, Any]:
|
| 273 |
"""Convert the climate location to a dictionary."""
|
|
|
|
| 291 |
"pressure": self.pressure,
|
| 292 |
"direct_normal_irradiance": self.direct_normal_irradiance,
|
| 293 |
"diffuse_horizontal_irradiance": self.diffuse_horizontal_irradiance,
|
| 294 |
+
"albedo": self.albedo,
|
| 295 |
"hourly_data": self.hourly_data,
|
| 296 |
"typical_extreme_periods": self.typical_extreme_periods,
|
| 297 |
"ground_temperatures": self.ground_temperatures,
|
| 298 |
+
"solstice_zenith_angles": self.solstice_zenith_angles
|
| 299 |
}
|
| 300 |
|
| 301 |
class ClimateData:
|
|
|
|
| 344 |
"winter_design_temp", "summer_design_temp_db", "summer_design_temp_wb",
|
| 345 |
"summer_daily_range", "wind_speed", "pressure",
|
| 346 |
"direct_normal_irradiance", "diffuse_horizontal_irradiance", "hourly_data",
|
| 347 |
+
"albedo", "solstice_zenith_angles"
|
| 348 |
]
|
| 349 |
|
| 350 |
for field in required_fields:
|
|
|
|
| 388 |
if not (0 <= data["diffuse_horizontal_irradiance"] <= 1200):
|
| 389 |
st.error(f"Validation failed: Diffuse horizontal irradiance {data['diffuse_horizontal_irradiance']} outside range")
|
| 390 |
return False
|
| 391 |
+
if not (0 <= data["albedo"] <= 1):
|
| 392 |
+
st.error(f"Validation failed: Albedo {data['albedo']} outside range")
|
| 393 |
+
return False
|
| 394 |
if not data["hourly_data"] or len(data["hourly_data"]) < 8700:
|
| 395 |
st.error(f"Validation failed: Hourly data has {len(data['hourly_data'])} records, expected ~8760")
|
| 396 |
return False
|
|
|
|
| 422 |
if not (0 <= record["diffuse_horizontal_irradiance"] <= 1200):
|
| 423 |
st.error(f"Validation failed: Diffuse horizontal irradiance {record['diffuse_horizontal_irradiance']} outside range")
|
| 424 |
return False
|
| 425 |
+
if not (0 <= record["wind_speed"] <= 30):
|
| 426 |
+
st.error(f"Validation failed: Wind speed {record['wind_speed']} outside range")
|
| 427 |
+
return False
|
| 428 |
+
if not (0 <= record["wind_direction"] <= 360):
|
| 429 |
+
st.error(f"Validation failed: Wind direction {record['wind_direction']} outside range")
|
| 430 |
+
return False
|
| 431 |
+
if not (0 <= record["solar_time"] < 24):
|
| 432 |
+
st.error(f"Validation failed: Solar time {record['solar_time']} outside range")
|
| 433 |
return False
|
| 434 |
if not (0 <= record["solar_zenith"] <= 180):
|
| 435 |
st.error(f"Validation failed: Solar zenith {record['solar_zenith']} outside range")
|
|
|
|
| 437 |
if not (0 <= record["solar_azimuth"] <= 360):
|
| 438 |
st.error(f"Validation failed: Solar azimuth {record['solar_azimuth']} outside range")
|
| 439 |
return False
|
| 440 |
+
if not (0 <= record["ground_reflected_radiation"] <= 1200):
|
| 441 |
+
st.error(f"Validation failed: Ground reflected radiation {record['ground_reflected_radiation']} outside range")
|
|
|
|
|
|
|
|
|
|
| 442 |
return False
|
| 443 |
|
| 444 |
# Validate typical/extreme periods (optional)
|
|
|
|
| 460 |
st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
|
| 461 |
return False
|
| 462 |
|
| 463 |
+
# Validate solstice zenith angles
|
| 464 |
+
if "solstice_zenith_angles" not in data or not all(key in data["solstice_zenith_angles"] for key in ["summer", "winter"]):
|
| 465 |
+
st.error("Validation failed: Missing or incomplete solstice zenith angles")
|
| 466 |
+
return False
|
| 467 |
+
if not (0 <= data["solstice_zenith_angles"]["summer"] <= 180 and 0 <= data["solstice_zenith_angles"]["winter"] <= 180):
|
| 468 |
+
st.error("Validation failed: Invalid solstice zenith angles")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
return False
|
| 470 |
|
| 471 |
return True
|
|
|
|
| 498 |
except ValueError:
|
| 499 |
return False
|
| 500 |
|
| 501 |
+
def display_climate_input(self, session_state: Dict[str, Any], albedo: float = 0.2):
|
| 502 |
+
"""Display Streamlit interface for EPW upload and visualizations."""
|
| 503 |
st.title("Climate Data Analysis")
|
| 504 |
|
| 505 |
# Apply consistent styling
|
|
|
|
| 510 |
st.warning("Clearing invalid climate data from session state.")
|
| 511 |
del session_state["climate_data"]
|
| 512 |
|
| 513 |
+
uploaded_file = st.file_uploader("Upload EPW File", type=["epw"])
|
|
|
|
|
|
|
| 514 |
|
| 515 |
# Initialize location and epw_data for display
|
| 516 |
location = None
|
| 517 |
epw_data = None
|
| 518 |
|
| 519 |
+
if uploaded_file:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
try:
|
| 521 |
# Process new EPW file
|
| 522 |
+
epw_content = uploaded_file.read().decode("utf-8")
|
| 523 |
epw_lines = epw_content.splitlines()
|
| 524 |
|
| 525 |
# Parse header
|
|
|
|
| 560 |
period_name = parts[2 + i*4]
|
| 561 |
period_type = parts[3 + i*4]
|
| 562 |
start_date = parts[4 + i*4].strip()
|
| 563 |
+
end_date = bits[5 + i*4].strip()
|
| 564 |
if period_name in [
|
| 565 |
"Summer - Week Nearest Max Temperature For Period",
|
| 566 |
"Summer - Week Nearest Average Temperature For Period",
|
|
|
|
| 632 |
epw_file=epw_data,
|
| 633 |
typical_extreme_periods=typical_extreme_periods,
|
| 634 |
ground_temperatures=ground_temperatures,
|
| 635 |
+
albedo=albedo, # Pass albedo from main
|
| 636 |
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 637 |
country=country,
|
| 638 |
state_province=state_province,
|
|
|
|
| 645 |
climate_data_dict = location.to_dict()
|
| 646 |
if not self.validate_climate_data(climate_data_dict):
|
| 647 |
raise ValueError("Invalid climate data extracted from EPW file.")
|
|
|
|
|
|
|
|
|
|
| 648 |
session_state["climate_data"] = climate_data_dict
|
| 649 |
st.success("Climate data extracted from EPW file!")
|
| 650 |
+
|
| 651 |
except Exception as e:
|
| 652 |
st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
|
| 653 |
|
|
|
|
| 676 |
epw_file=epw_data,
|
| 677 |
typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
|
| 678 |
ground_temperatures=climate_data_dict["ground_temperatures"],
|
| 679 |
+
albedo=climate_data_dict["albedo"],
|
| 680 |
id=climate_data_dict["id"],
|
| 681 |
country=climate_data_dict["country"],
|
| 682 |
state_province=climate_data_dict["state_province"],
|
|
|
|
| 688 |
)
|
| 689 |
# Override hourly_data to ensure consistency
|
| 690 |
location.hourly_data = climate_data_dict["hourly_data"]
|
| 691 |
+
location.solstice_zenith_angles = climate_data_dict["solstice_zenith_angles"]
|
| 692 |
self.add_location(location)
|
| 693 |
st.info("Displaying previously extracted climate data.")
|
| 694 |
|
|
|
|
| 718 |
self.plot_wind_rose(epw_data)
|
| 719 |
|
| 720 |
else:
|
| 721 |
+
st.info("No climate data available. Please upload an EPW file to proceed.")
|
| 722 |
|
| 723 |
def display_design_conditions(self, location: ClimateLocation):
|
| 724 |
"""Display design conditions for HVAC calculations using styled HTML."""
|
| 725 |
st.subheader("Design Conditions")
|
| 726 |
|
| 727 |
# Location Details
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
st.markdown(f"""
|
| 729 |
<div class="markdown-text">
|
| 730 |
<h3>Location Details</h3>
|
|
|
|
| 736 |
<li><strong>Longitude:</strong> {location.longitude}°</li>
|
| 737 |
<li><strong>Elevation:</strong> {location.elevation} m</li>
|
| 738 |
<li><strong>Time Zone:</strong> UTC{location.time_zone:+.1f}</li>
|
| 739 |
+
<li><strong>Summer Solstice Zenith Angle (Dec 21):</strong> {location.solstice_zenith_angles['summer']}°</li>
|
| 740 |
+
<li><strong>Winter Solstice Zenith Angle (Jun 21):</strong> {location.solstice_zenith_angles['winter']}°</li>
|
| 741 |
</ul>
|
| 742 |
</div>
|
| 743 |
""", unsafe_allow_html=True)
|
|
|
|
| 758 |
<li><strong>Mean Atmospheric Pressure:</strong> {location.pressure} Pa</li>
|
| 759 |
<li><strong>Mean Direct Normal Irradiance:</strong> {location.direct_normal_irradiance} W/m²</li>
|
| 760 |
<li><strong>Mean Diffuse Horizontal Irradiance:</strong> {location.diffuse_horizontal_irradiance} W/m²</li>
|
| 761 |
+
<li><strong>Albedo:</strong> {location.albedo}</li>
|
| 762 |
</ul>
|
| 763 |
</div>
|
| 764 |
""", unsafe_allow_html=True)
|
|
|
|
| 778 |
</div>
|
| 779 |
""", unsafe_allow_html=True)
|
| 780 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
# Ground Temperatures (Table)
|
| 782 |
if location.ground_temperatures:
|
| 783 |
st.markdown('<div class="markdown-text"><h3>Ground Temperatures</h3></div>', unsafe_allow_html=True)
|
|
|
|
| 1107 |
epw_file=epw_data,
|
| 1108 |
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 1109 |
ground_temperatures=loc_dict["ground_temperatures"],
|
| 1110 |
+
albedo=loc_dict["albedo"],
|
| 1111 |
id=loc_dict["id"],
|
| 1112 |
country=loc_dict["country"],
|
| 1113 |
state_province=loc_dict["state_province"],
|
|
|
|
| 1117 |
elevation=loc_dict["elevation"],
|
| 1118 |
time_zone=loc_dict["time_zone"]
|
| 1119 |
)
|
| 1120 |
+
location.hourly_data = loc_dict["hourly_data"] # Restore all hourly data including solar and radiation fields
|
| 1121 |
+
location.solstice_zenith_angles = loc_dict["solstice_zenith_angles"]
|
| 1122 |
climate_data.add_location(location)
|
| 1123 |
return climate_data
|
| 1124 |
|
| 1125 |
if __name__ == "__main__":
|
| 1126 |
climate_data = ClimateData()
|
| 1127 |
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
| 1128 |
+
climate_data.display_climate_input(session_state, albedo=0.2) # Default albedo
|