Update data/climate_data.py
Browse files- data/climate_data.py +84 -65
data/climate_data.py
CHANGED
|
@@ -4,7 +4,7 @@ Extracts climate data from EPW files and provides visualizations inspired by Cli
|
|
| 4 |
|
| 5 |
Author: Dr Majed Abuseif
|
| 6 |
Date: May 2025
|
| 7 |
-
Version: 2.
|
| 8 |
"""
|
| 9 |
|
| 10 |
from typing import Dict, List, Any, Optional
|
|
@@ -18,7 +18,6 @@ import plotly.graph_objects as go
|
|
| 18 |
from io import StringIO
|
| 19 |
import pvlib
|
| 20 |
from datetime import datetime, timedelta
|
| 21 |
-
from plotly.subplots import make_subplots
|
| 22 |
|
| 23 |
# Define paths
|
| 24 |
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -263,7 +262,7 @@ class ClimateData:
|
|
| 263 |
|
| 264 |
location = ClimateLocation(
|
| 265 |
epw_file=epw_data,
|
| 266 |
-
id=f"{country[:
|
| 267 |
country=country,
|
| 268 |
state_province=state_province,
|
| 269 |
city=city,
|
|
@@ -312,10 +311,6 @@ class ClimateData:
|
|
| 312 |
st.button("Continue to Building Components", on_click=lambda: setattr(session_state, "page", "Building Components"))
|
| 313 |
else:
|
| 314 |
st.button("Continue to Building Components", disabled=True)
|
| 315 |
-
|
| 316 |
-
if "climate_data" in session_state and session_state["climate_data"]:
|
| 317 |
-
st.subheader("Saved Climate Data")
|
| 318 |
-
st.json(session_state["climate_data"])
|
| 319 |
|
| 320 |
def display_design_conditions(self, location: ClimateLocation):
|
| 321 |
"""Display design conditions for HVAC calculations using Markdown."""
|
|
@@ -365,7 +360,7 @@ class ClimateData:
|
|
| 365 |
return "8"
|
| 366 |
|
| 367 |
def plot_psychrometric_chart(self, location: ClimateLocation, epw_data: pd.DataFrame):
|
| 368 |
-
"""Plot psychrometric chart with ASHRAE 55 comfort zone."""
|
| 369 |
st.subheader("Psychrometric Chart")
|
| 370 |
|
| 371 |
dry_bulb = pd.to_numeric(epw_data[6], errors='coerce').values
|
|
@@ -378,24 +373,27 @@ class ClimateData:
|
|
| 378 |
pressure = location.pressure / 1000 # kPa
|
| 379 |
saturation_pressure = 6.1078 * 10 ** (7.5 * dry_bulb / (dry_bulb + 237.3))
|
| 380 |
vapor_pressure = humidity / 100 * saturation_pressure
|
| 381 |
-
humidity_ratio = 0.62198 * vapor_pressure / (pressure - vapor_pressure)
|
| 382 |
|
| 383 |
fig = go.Figure()
|
|
|
|
|
|
|
| 384 |
fig.add_trace(go.Scatter(
|
| 385 |
x=dry_bulb,
|
| 386 |
-
y=humidity_ratio
|
| 387 |
mode='markers',
|
| 388 |
-
marker=dict(size=5, opacity=0.5),
|
| 389 |
name='Hourly Conditions'
|
| 390 |
))
|
| 391 |
|
| 392 |
-
# ASHRAE 55 comfort zone (simplified: 20-26°C,
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
| 396 |
fig.add_trace(go.Scatter(
|
| 397 |
-
x=
|
| 398 |
-
y=comfort_hr
|
| 399 |
mode='lines',
|
| 400 |
line=dict(color='green', width=2),
|
| 401 |
fill='toself',
|
|
@@ -403,38 +401,58 @@ class ClimateData:
|
|
| 403 |
name='ASHRAE 55 Comfort Zone'
|
| 404 |
))
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
fig.update_layout(
|
| 407 |
title="Psychrometric Chart",
|
| 408 |
xaxis_title="Dry-Bulb Temperature (°C)",
|
| 409 |
yaxis_title="Humidity Ratio (g/kg dry air)",
|
| 410 |
-
xaxis=dict(range=[-
|
| 411 |
-
yaxis=dict(range=[0,
|
| 412 |
-
showlegend=True
|
|
|
|
| 413 |
)
|
| 414 |
st.plotly_chart(fig, use_container_width=True)
|
| 415 |
-
|
| 416 |
-
# HVAC strategy annotations
|
| 417 |
-
outside_comfort = np.sum((dry_bulb < 20) | (dry_bulb > 26) | (humidity < 30) | (humidity > 60))
|
| 418 |
-
st.markdown(f"**Hours outside comfort zone**: {outside_comfort} ({outside_comfort/8760*100:.1f}%)")
|
| 419 |
-
if outside_comfort > 0:
|
| 420 |
-
st.markdown("**HVAC Strategies**:")
|
| 421 |
-
if np.any(dry_bulb < 20):
|
| 422 |
-
st.markdown("- Heating required in colder periods.")
|
| 423 |
-
if np.any(dry_bulb > 26):
|
| 424 |
-
st.markdown("- Cooling required in warmer periods.")
|
| 425 |
-
if np.any(humidity > 60):
|
| 426 |
-
st.markdown("- Dehumidification may be needed in humid conditions.")
|
| 427 |
-
if np.any(humidity < 30):
|
| 428 |
-
st.markdown("- Humidification may be needed in dry conditions.")
|
| 429 |
|
| 430 |
def plot_sun_shading_chart(self, location: ClimateLocation):
|
| 431 |
-
"""Plot sun path chart for
|
| 432 |
st.subheader("Sun Shading Chart")
|
| 433 |
|
| 434 |
dates = [
|
| 435 |
-
datetime(2025, 6, 21), #
|
| 436 |
-
datetime(2025, 12, 21)
|
| 437 |
-
datetime(2025, 3, 20) # Spring equinox
|
| 438 |
]
|
| 439 |
times = pd.date_range(start="2025-01-01 00:00", end="2025-01-01 23:00", freq='H')
|
| 440 |
solar_data = []
|
|
@@ -453,13 +471,18 @@ class ClimateData:
|
|
| 453 |
})
|
| 454 |
|
| 455 |
fig = go.Figure()
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
| 457 |
fig.add_trace(go.Scatterpolar(
|
| 458 |
r=data['altitude'],
|
| 459 |
theta=data['azimuth'],
|
| 460 |
-
mode='lines',
|
| 461 |
-
name=
|
| 462 |
-
line=dict(width=2)
|
|
|
|
|
|
|
| 463 |
))
|
| 464 |
|
| 465 |
fig.update_layout(
|
|
@@ -478,16 +501,10 @@ class ClimateData:
|
|
| 478 |
ticktext=["N", "E", "S", "W"]
|
| 479 |
)
|
| 480 |
),
|
| 481 |
-
showlegend=True
|
|
|
|
| 482 |
)
|
| 483 |
st.plotly_chart(fig, use_container_width=True)
|
| 484 |
-
|
| 485 |
-
# Shading recommendations
|
| 486 |
-
max_altitude = max([max(data['altitude']) for data in solar_data])
|
| 487 |
-
st.markdown(f"**Maximum solar altitude**: {max_altitude:.1f}°")
|
| 488 |
-
st.markdown("**Shading Recommendations**:")
|
| 489 |
-
st.markdown(f"- Use overhangs or louvers angled to block sun above {max_altitude-10:.1f}° in summer.")
|
| 490 |
-
st.markdown("- Allow low-angle winter sun for passive heating.")
|
| 491 |
|
| 492 |
def plot_temperature_range(self, location: ClimateLocation, epw_data: pd.DataFrame):
|
| 493 |
"""Plot monthly temperature ranges with design conditions."""
|
|
@@ -544,12 +561,13 @@ class ClimateData:
|
|
| 544 |
yaxis_title="Temperature (°C)",
|
| 545 |
xaxis=dict(tickmode='array', tickvals=list(range(1, 13)), ticktext=month_names),
|
| 546 |
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
|
| 547 |
-
showlegend=True
|
|
|
|
| 548 |
)
|
| 549 |
st.plotly_chart(fig, use_container_width=True)
|
| 550 |
|
| 551 |
def plot_wind_rose(self, epw_data: pd.DataFrame):
|
| 552 |
-
"""Plot wind rose diagram."""
|
| 553 |
st.subheader("Wind Rose")
|
| 554 |
|
| 555 |
wind_speed = pd.to_numeric(epw_data[21], errors='coerce').values
|
|
@@ -558,10 +576,11 @@ class ClimateData:
|
|
| 558 |
wind_speed = wind_speed[valid_mask]
|
| 559 |
wind_direction = wind_direction[valid_mask]
|
| 560 |
|
| 561 |
-
# Bin data
|
| 562 |
-
speed_bins = [0, 2, 4, 6, 8,
|
| 563 |
-
direction_bins = np.linspace(0, 360,
|
| 564 |
-
speed_labels = ['0-2', '2-4', '4-6', '6-8', '8
|
|
|
|
| 565 |
|
| 566 |
hist = np.histogram2d(
|
| 567 |
wind_direction, wind_speed,
|
|
@@ -571,16 +590,15 @@ class ClimateData:
|
|
| 571 |
hist = hist * 100 # Convert to percentage
|
| 572 |
|
| 573 |
fig = go.Figure()
|
|
|
|
|
|
|
| 574 |
for i, speed_label in enumerate(speed_labels):
|
| 575 |
fig.add_trace(go.Barpolar(
|
| 576 |
r=hist[:, i],
|
| 577 |
theta=direction_bins,
|
| 578 |
-
width=
|
| 579 |
name=speed_label,
|
| 580 |
-
marker=dict(
|
| 581 |
-
colorscale='Viridis',
|
| 582 |
-
showscale=True
|
| 583 |
-
),
|
| 584 |
opacity=0.8
|
| 585 |
))
|
| 586 |
|
|
@@ -595,11 +613,12 @@ class ClimateData:
|
|
| 595 |
angularaxis=dict(
|
| 596 |
direction="clockwise",
|
| 597 |
rotation=90,
|
| 598 |
-
tickvals=
|
| 599 |
-
ticktext=
|
| 600 |
)
|
| 601 |
),
|
| 602 |
-
showlegend=True
|
|
|
|
| 603 |
)
|
| 604 |
st.plotly_chart(fig, use_container_width=True)
|
| 605 |
|
|
@@ -622,5 +641,5 @@ class ClimateData:
|
|
| 622 |
|
| 623 |
if __name__ == "__main__":
|
| 624 |
climate_data = ClimateData()
|
| 625 |
-
session_state = {"building_info": {"country": "
|
| 626 |
climate_data.display_climate_input(session_state)
|
|
|
|
| 4 |
|
| 5 |
Author: Dr Majed Abuseif
|
| 6 |
Date: May 2025
|
| 7 |
+
Version: 2.1.0
|
| 8 |
"""
|
| 9 |
|
| 10 |
from typing import Dict, List, Any, Optional
|
|
|
|
| 18 |
from io import StringIO
|
| 19 |
import pvlib
|
| 20 |
from datetime import datetime, timedelta
|
|
|
|
| 21 |
|
| 22 |
# Define paths
|
| 23 |
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 262 |
|
| 263 |
location = ClimateLocation(
|
| 264 |
epw_file=epw_data,
|
| 265 |
+
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 266 |
country=country,
|
| 267 |
state_province=state_province,
|
| 268 |
city=city,
|
|
|
|
| 311 |
st.button("Continue to Building Components", on_click=lambda: setattr(session_state, "page", "Building Components"))
|
| 312 |
else:
|
| 313 |
st.button("Continue to Building Components", disabled=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
def display_design_conditions(self, location: ClimateLocation):
|
| 316 |
"""Display design conditions for HVAC calculations using Markdown."""
|
|
|
|
| 360 |
return "8"
|
| 361 |
|
| 362 |
def plot_psychrometric_chart(self, location: ClimateLocation, epw_data: pd.DataFrame):
|
| 363 |
+
"""Plot psychrometric chart with ASHRAE 55 comfort zone and psychrometric lines."""
|
| 364 |
st.subheader("Psychrometric Chart")
|
| 365 |
|
| 366 |
dry_bulb = pd.to_numeric(epw_data[6], errors='coerce').values
|
|
|
|
| 373 |
pressure = location.pressure / 1000 # kPa
|
| 374 |
saturation_pressure = 6.1078 * 10 ** (7.5 * dry_bulb / (dry_bulb + 237.3))
|
| 375 |
vapor_pressure = humidity / 100 * saturation_pressure
|
| 376 |
+
humidity_ratio = 0.62198 * vapor_pressure / (pressure - vapor_pressure) * 1000 # Convert to g/kg
|
| 377 |
|
| 378 |
fig = go.Figure()
|
| 379 |
+
|
| 380 |
+
# Hourly data points
|
| 381 |
fig.add_trace(go.Scatter(
|
| 382 |
x=dry_bulb,
|
| 383 |
+
y=humidity_ratio,
|
| 384 |
mode='markers',
|
| 385 |
+
marker=dict(size=5, opacity=0.5, color='blue'),
|
| 386 |
name='Hourly Conditions'
|
| 387 |
))
|
| 388 |
|
| 389 |
+
# ASHRAE 55 comfort zone (simplified: 20-26°C, adjusted for humidity ratio)
|
| 390 |
+
comfort_db = [20, 26, 26, 20, 20]
|
| 391 |
+
comfort_rh = [30, 30, 60, 60, 30]
|
| 392 |
+
comfort_vp = np.array(comfort_rh) / 100 * 6.1078 * 10 ** (7.5 * np.array(comfort_db) / (np.array(comfort_db) + 237.3))
|
| 393 |
+
comfort_hr = 0.62198 * comfort_vp / (pressure - comfort_vp) * 1000
|
| 394 |
fig.add_trace(go.Scatter(
|
| 395 |
+
x=comfort_db,
|
| 396 |
+
y=comfort_hr,
|
| 397 |
mode='lines',
|
| 398 |
line=dict(color='green', width=2),
|
| 399 |
fill='toself',
|
|
|
|
| 401 |
name='ASHRAE 55 Comfort Zone'
|
| 402 |
))
|
| 403 |
|
| 404 |
+
# Constant humidity ratio lines (inspired by Climate Consultant)
|
| 405 |
+
for hr in [5, 10, 15]: # g/kg
|
| 406 |
+
db_range = np.linspace(0, 40, 100)
|
| 407 |
+
vp = (hr / 1000 * pressure) / (0.62198 + hr / 1000)
|
| 408 |
+
rh = vp / (6.1078 * 10 ** (7.5 * db_range / (db_range + 237.3))) * 100
|
| 409 |
+
hr_line = np.full_like(db_range, hr)
|
| 410 |
+
fig.add_trace(go.Scatter(
|
| 411 |
+
x=db_range,
|
| 412 |
+
y=hr_line,
|
| 413 |
+
mode='lines',
|
| 414 |
+
line=dict(color='gray', width=1, dash='dash'),
|
| 415 |
+
name=f'{hr} g/kg',
|
| 416 |
+
showlegend=True
|
| 417 |
+
))
|
| 418 |
+
|
| 419 |
+
# Constant wet-bulb temperature lines
|
| 420 |
+
wet_bulb_temps = [10, 15, 20]
|
| 421 |
+
for wbt in wet_bulb_temps:
|
| 422 |
+
db_range = np.linspace(0, 40, 100)
|
| 423 |
+
rh_range = np.linspace(5, 95, 100)
|
| 424 |
+
wb_values = self.calculate_wet_bulb(db_range, rh_range)
|
| 425 |
+
vp = rh_range / 100 * (6.1078 * 10 ** (7.5 * db_range / (db_range + 237.3)))
|
| 426 |
+
hr_values = 0.62198 * vp / (pressure - vp) * 1000
|
| 427 |
+
mask = (wb_values >= wbt - 0.5) & (wb_values <= wbt + 0.5)
|
| 428 |
+
if np.any(mask):
|
| 429 |
+
fig.add_trace(go.Scatter(
|
| 430 |
+
x=db_range[mask],
|
| 431 |
+
y=hr_values[mask],
|
| 432 |
+
mode='lines',
|
| 433 |
+
line=dict(color='purple', width=1, dash='dot'),
|
| 434 |
+
name=f'Wet-Bulb {wbt}°C',
|
| 435 |
+
showlegend=True
|
| 436 |
+
))
|
| 437 |
+
|
| 438 |
fig.update_layout(
|
| 439 |
title="Psychrometric Chart",
|
| 440 |
xaxis_title="Dry-Bulb Temperature (°C)",
|
| 441 |
yaxis_title="Humidity Ratio (g/kg dry air)",
|
| 442 |
+
xaxis=dict(range=[-5, 40]), # Adjusted for Geelong (3.1°C to 33.0°C)
|
| 443 |
+
yaxis=dict(range=[0, 25]), # Adjusted for typical humidity ratios
|
| 444 |
+
showlegend=True,
|
| 445 |
+
template='plotly_white'
|
| 446 |
)
|
| 447 |
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
def plot_sun_shading_chart(self, location: ClimateLocation):
|
| 450 |
+
"""Plot sun path chart for summer and winter solstices, inspired by Climate Consultant."""
|
| 451 |
st.subheader("Sun Shading Chart")
|
| 452 |
|
| 453 |
dates = [
|
| 454 |
+
datetime(2025, 6, 21), # Winter solstice (Southern Hemisphere)
|
| 455 |
+
datetime(2025, 12, 21) # Summer solstice (Southern Hemisphere)
|
|
|
|
| 456 |
]
|
| 457 |
times = pd.date_range(start="2025-01-01 00:00", end="2025-01-01 23:00", freq='H')
|
| 458 |
solar_data = []
|
|
|
|
| 471 |
})
|
| 472 |
|
| 473 |
fig = go.Figure()
|
| 474 |
+
colors = ['orange', 'blue'] # Summer = orange, Winter = blue
|
| 475 |
+
labels = ['Summer Solstice (Dec 21)', 'Winter Solstice (Jun 21)']
|
| 476 |
+
|
| 477 |
+
for i, data in enumerate(solar_data):
|
| 478 |
fig.add_trace(go.Scatterpolar(
|
| 479 |
r=data['altitude'],
|
| 480 |
theta=data['azimuth'],
|
| 481 |
+
mode='lines+markers',
|
| 482 |
+
name=labels[i],
|
| 483 |
+
line=dict(color=colors[i], width=2),
|
| 484 |
+
marker=dict(size=6, color=colors[i]),
|
| 485 |
+
opacity=0.8
|
| 486 |
))
|
| 487 |
|
| 488 |
fig.update_layout(
|
|
|
|
| 501 |
ticktext=["N", "E", "S", "W"]
|
| 502 |
)
|
| 503 |
),
|
| 504 |
+
showlegend=True,
|
| 505 |
+
template='plotly_white'
|
| 506 |
)
|
| 507 |
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
def plot_temperature_range(self, location: ClimateLocation, epw_data: pd.DataFrame):
|
| 510 |
"""Plot monthly temperature ranges with design conditions."""
|
|
|
|
| 561 |
yaxis_title="Temperature (°C)",
|
| 562 |
xaxis=dict(tickmode='array', tickvals=list(range(1, 13)), ticktext=month_names),
|
| 563 |
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
|
| 564 |
+
showlegend=True,
|
| 565 |
+
template='plotly_white'
|
| 566 |
)
|
| 567 |
st.plotly_chart(fig, use_container_width=True)
|
| 568 |
|
| 569 |
def plot_wind_rose(self, epw_data: pd.DataFrame):
|
| 570 |
+
"""Plot wind rose diagram with improved clarity, inspired by Climate Consultant."""
|
| 571 |
st.subheader("Wind Rose")
|
| 572 |
|
| 573 |
wind_speed = pd.to_numeric(epw_data[21], errors='coerce').values
|
|
|
|
| 576 |
wind_speed = wind_speed[valid_mask]
|
| 577 |
wind_direction = wind_direction[valid_mask]
|
| 578 |
|
| 579 |
+
# Bin data with 8 directions and tailored speed bins (based on Geelong’s mean wind speed of 4.0 m/s)
|
| 580 |
+
speed_bins = [0, 2, 4, 6, 8, np.inf]
|
| 581 |
+
direction_bins = np.linspace(0, 360, 9)[:-1]
|
| 582 |
+
speed_labels = ['0-2 m/s', '2-4 m/s', '4-6 m/s', '6-8 m/s', '8+ m/s']
|
| 583 |
+
direction_labels = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
|
| 584 |
|
| 585 |
hist = np.histogram2d(
|
| 586 |
wind_direction, wind_speed,
|
|
|
|
| 590 |
hist = hist * 100 # Convert to percentage
|
| 591 |
|
| 592 |
fig = go.Figure()
|
| 593 |
+
colors = ['#E6F0FF', '#B3D1FF', '#80B2FF', '#4D94FF', '#1A75FF'] # Light to dark blue gradient
|
| 594 |
+
|
| 595 |
for i, speed_label in enumerate(speed_labels):
|
| 596 |
fig.add_trace(go.Barpolar(
|
| 597 |
r=hist[:, i],
|
| 598 |
theta=direction_bins,
|
| 599 |
+
width=45,
|
| 600 |
name=speed_label,
|
| 601 |
+
marker=dict(color=colors[i]),
|
|
|
|
|
|
|
|
|
|
| 602 |
opacity=0.8
|
| 603 |
))
|
| 604 |
|
|
|
|
| 613 |
angularaxis=dict(
|
| 614 |
direction="clockwise",
|
| 615 |
rotation=90,
|
| 616 |
+
tickvals=direction_bins,
|
| 617 |
+
ticktext=direction_labels
|
| 618 |
)
|
| 619 |
),
|
| 620 |
+
showlegend=True,
|
| 621 |
+
template='plotly_white'
|
| 622 |
)
|
| 623 |
st.plotly_chart(fig, use_container_width=True)
|
| 624 |
|
|
|
|
| 641 |
|
| 642 |
if __name__ == "__main__":
|
| 643 |
climate_data = ClimateData()
|
| 644 |
+
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
| 645 |
climate_data.display_climate_input(session_state)
|