Update data/climate_data.py
Browse files- data/climate_data.py +111 -17
data/climate_data.py
CHANGED
|
@@ -171,6 +171,8 @@ class ClimateLocation:
|
|
| 171 |
|
| 172 |
# Store hourly data with enhanced fields, including solar angles and ground-reflected radiation
|
| 173 |
self.hourly_data = []
|
|
|
|
|
|
|
| 174 |
for i in range(len(months)):
|
| 175 |
if np.isnan(months[i]) or np.isnan(days[i]) or np.isnan(hours[i]) or np.isnan(dry_bulb[i]):
|
| 176 |
continue # Skip records with missing critical fields
|
|
@@ -180,18 +182,25 @@ class ClimateLocation:
|
|
| 180 |
solar_time = self._calculate_solar_time(timestamp)
|
| 181 |
solar_angles = self._calculate_solar_angles(timestamp)
|
| 182 |
|
| 183 |
-
#
|
| 184 |
-
dni =
|
| 185 |
-
dhi =
|
| 186 |
-
|
| 187 |
-
logger.warning(f"Negative DNI at {timestamp}, using clear-sky model: {dni}")
|
| 188 |
-
dni = self._calculate_clear_sky_irradiance(timestamp, 'dni')
|
| 189 |
-
if dhi < 0:
|
| 190 |
-
logger.warning(f"Negative DHI at {timestamp}, using clear-sky model: {dhi}")
|
| 191 |
-
dhi = self._calculate_clear_sky_irradiance(timestamp, 'dhi')
|
| 192 |
|
| 193 |
-
#
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
record = {
|
| 197 |
"month": int(months[i]),
|
|
@@ -211,6 +220,12 @@ class ClimateLocation:
|
|
| 211 |
}
|
| 212 |
self.hourly_data.append(record)
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
if len(self.hourly_data) != 8760:
|
| 215 |
st.warning(f"Hourly data has {len(self.hourly_data)} records instead of 8760. Some records may have been excluded due to missing data.")
|
| 216 |
|
|
@@ -487,6 +502,13 @@ class ClimateData:
|
|
| 487 |
st.error(f"Validation failed: Invalid {season} angles: {angles}")
|
| 488 |
return False
|
| 489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
return True
|
| 491 |
|
| 492 |
@staticmethod
|
|
@@ -518,7 +540,7 @@ class ClimateData:
|
|
| 518 |
return False
|
| 519 |
|
| 520 |
def display_climate_input(self, session_state: Dict[str, Any]):
|
| 521 |
-
"""Display Streamlit interface for EPW upload and visualizations."""
|
| 522 |
st.title("Climate Data Analysis")
|
| 523 |
|
| 524 |
# Apply consistent styling
|
|
@@ -529,16 +551,64 @@ class ClimateData:
|
|
| 529 |
st.warning("Clearing invalid climate data from session state.")
|
| 530 |
del session_state["climate_data"]
|
| 531 |
|
| 532 |
-
|
|
|
|
|
|
|
| 533 |
|
| 534 |
# Initialize location and epw_data for display
|
| 535 |
location = None
|
| 536 |
epw_data = None
|
| 537 |
|
| 538 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
try:
|
| 540 |
# Process new EPW file
|
| 541 |
-
epw_content =
|
| 542 |
epw_lines = epw_content.splitlines()
|
| 543 |
|
| 544 |
# Parse header
|
|
@@ -608,7 +678,7 @@ class ClimateData:
|
|
| 608 |
|
| 609 |
# Parse GROUND TEMPERATURES
|
| 610 |
ground_temperatures = {}
|
| 611 |
-
for
|
| 612 |
if line.startswith("GROUND TEMPERATURES"):
|
| 613 |
parts = line.strip().split(',')
|
| 614 |
try:
|
|
@@ -663,8 +733,17 @@ class ClimateData:
|
|
| 663 |
climate_data_dict = location.to_dict()
|
| 664 |
if not self.validate_climate_data(climate_data_dict):
|
| 665 |
raise ValueError("Invalid climate data extracted from EPW file.")
|
|
|
|
|
|
|
|
|
|
| 666 |
session_state["climate_data"] = climate_data_dict
|
| 667 |
st.success("Climate data extracted from EPW file!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
|
| 669 |
except Exception as e:
|
| 670 |
st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
|
|
@@ -735,7 +814,7 @@ class ClimateData:
|
|
| 735 |
self.plot_wind_rose(epw_data)
|
| 736 |
|
| 737 |
else:
|
| 738 |
-
st.info("No climate data available. Please upload an EPW file to proceed.")
|
| 739 |
|
| 740 |
def display_design_conditions(self, location: ClimateLocation):
|
| 741 |
"""Display design conditions for HVAC calculations using styled HTML."""
|
|
@@ -798,6 +877,21 @@ class ClimateData:
|
|
| 798 |
</div>
|
| 799 |
""", unsafe_allow_html=True)
|
| 800 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
# Ground Temperatures (Table)
|
| 802 |
if location.ground_temperatures:
|
| 803 |
st.markdown('<div class="markdown-text"><h3>Ground Temperatures</h3></div>', unsafe_allow_html=True)
|
|
|
|
| 171 |
|
| 172 |
# Store hourly data with enhanced fields, including solar angles and ground-reflected radiation
|
| 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
|
|
|
|
| 182 |
solar_time = self._calculate_solar_time(timestamp)
|
| 183 |
solar_angles = self._calculate_solar_angles(timestamp)
|
| 184 |
|
| 185 |
+
# Initialize defaults for solar-related fields
|
| 186 |
+
dni = 0.0
|
| 187 |
+
dhi = 0.0
|
| 188 |
+
ground_radiation = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
+
# Perform solar calculations only if sun is above horizon (zenith < 90°)
|
| 191 |
+
if solar_angles['zenith'] < 90:
|
| 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]),
|
|
|
|
| 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 |
|
|
|
|
| 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
|
| 513 |
|
| 514 |
@staticmethod
|
|
|
|
| 540 |
return False
|
| 541 |
|
| 542 |
def display_climate_input(self, session_state: Dict[str, Any]):
|
| 543 |
+
"""Display Streamlit interface for EPW and JSON upload, and visualizations."""
|
| 544 |
st.title("Climate Data Analysis")
|
| 545 |
|
| 546 |
# Apply consistent styling
|
|
|
|
| 551 |
st.warning("Clearing invalid climate data from session state.")
|
| 552 |
del session_state["climate_data"]
|
| 553 |
|
| 554 |
+
# File uploaders
|
| 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 |
+
# Process JSON file if uploaded
|
| 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 = uploaded_epw_file.read().decode("utf-8")
|
| 612 |
epw_lines = epw_content.splitlines()
|
| 613 |
|
| 614 |
# Parse header
|
|
|
|
| 678 |
|
| 679 |
# Parse GROUND TEMPERATURES
|
| 680 |
ground_temperatures = {}
|
| 681 |
+
for line in epw_lines:
|
| 682 |
if line.startswith("GROUND TEMPERATURES"):
|
| 683 |
parts = line.strip().split(',')
|
| 684 |
try:
|
|
|
|
| 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 |
+
# Export to JSON automatically
|
| 743 |
+
json_file_path = os.path.join(DATA_DIR, f"{city}_climate_data.json")
|
| 744 |
+
self.export_to_json(json_file_path)
|
| 745 |
+
st.info(f"Climate data exported to {json_file_path}")
|
| 746 |
+
logger.info(f"Exported climate data with ground_reflected_radiation to {json_file_path}")
|
| 747 |
|
| 748 |
except Exception as e:
|
| 749 |
st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
|
|
|
|
| 814 |
self.plot_wind_rose(epw_data)
|
| 815 |
|
| 816 |
else:
|
| 817 |
+
st.info("No climate data available. Please upload an EPW or JSON file to proceed.")
|
| 818 |
|
| 819 |
def display_design_conditions(self, location: ClimateLocation):
|
| 820 |
"""Display design conditions for HVAC calculations using styled HTML."""
|
|
|
|
| 877 |
</div>
|
| 878 |
""", unsafe_allow_html=True)
|
| 879 |
|
| 880 |
+
# Ground Temperatures (Table)
|
| 881 |
+
if location.typical_extreme_periods:
|
| 882 |
+
period_items = [
|
| 883 |
+
f"<li><strong>{key.replace('_', ' ').title()}:</strong> {period['start']['month']}/{period['start']['day']} to {period['end']['month']}/{period['end']['day']}</li>"
|
| 884 |
+
for key, period in location.typical_extreme_periods.items()
|
| 885 |
+
]
|
| 886 |
+
st.markdown(f"""
|
| 887 |
+
<div class="markdown-text">
|
| 888 |
+
<h3>Typical/Extreme Periods</h3>
|
| 889 |
+
<ul>
|
| 890 |
+
{''.join(period_items)}
|
| 891 |
+
</ul>
|
| 892 |
+
</div>
|
| 893 |
+
""", unsafe_allow_html=True)
|
| 894 |
+
|
| 895 |
# Ground Temperatures (Table)
|
| 896 |
if location.ground_temperatures:
|
| 897 |
st.markdown('<div class="markdown-text"><h3>Ground Temperatures</h3></div>', unsafe_allow_html=True)
|