Spaces:
Sleeping
Sleeping
Upload 13 files
Browse files- cooling_load.py +75 -6
- heating_load.py +105 -4
- pages/cooling_calculator.py +167 -10
- pages/heating_calculator.py +394 -10
- reference_data.py +15 -0
- utils/validation.py +4 -4
cooling_load.py
CHANGED
|
@@ -36,6 +36,60 @@ class CoolingLoadCalculator:
|
|
| 36 |
float: Heat gain in Watts
|
| 37 |
"""
|
| 38 |
return area * u_value * temp_diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def calculate_solar_heat_gain(self, area, shgf, shade_factor=1.0):
|
| 41 |
"""
|
|
@@ -140,7 +194,7 @@ class CoolingLoadCalculator:
|
|
| 140 |
Calculate the total cooling load including latent load.
|
| 141 |
|
| 142 |
Args:
|
| 143 |
-
building_components (list): List of dicts with 'area', 'u_value', and '
|
| 144 |
windows (list): List of dicts with 'area', 'orientation', 'glass_type', 'shading', etc.
|
| 145 |
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
| 146 |
internal_gains (dict): Dict with 'num_people', 'has_kitchen', and 'equipment_watts'
|
|
@@ -149,10 +203,24 @@ class CoolingLoadCalculator:
|
|
| 149 |
dict: Dictionary with sensible load, latent load, and total cooling load in Watts
|
| 150 |
"""
|
| 151 |
# Calculate conduction heat gain through building components
|
| 152 |
-
conduction_gain =
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
# Calculate solar and conduction heat gain through windows
|
| 158 |
window_conduction_gain = 0
|
|
@@ -191,7 +259,7 @@ class CoolingLoadCalculator:
|
|
| 191 |
)
|
| 192 |
|
| 193 |
# Calculate sensible cooling load
|
| 194 |
-
sensible_load = conduction_gain + window_conduction_gain + window_solar_gain + infiltration_gain + internal_gain
|
| 195 |
|
| 196 |
# Calculate total cooling load (including latent load)
|
| 197 |
latent_load = sensible_load * 0.3 # 30% of sensible load for latent load
|
|
@@ -201,6 +269,7 @@ class CoolingLoadCalculator:
|
|
| 201 |
'conduction_gain': conduction_gain,
|
| 202 |
'window_conduction_gain': window_conduction_gain,
|
| 203 |
'window_solar_gain': window_solar_gain,
|
|
|
|
| 204 |
'infiltration_gain': infiltration_gain,
|
| 205 |
'internal_gain': internal_gain,
|
| 206 |
'sensible_load': sensible_load,
|
|
|
|
| 36 |
float: Heat gain in Watts
|
| 37 |
"""
|
| 38 |
return area * u_value * temp_diff
|
| 39 |
+
|
| 40 |
+
def calculate_wall_solar_heat_gain(self, area, u_value, orientation, daily_range='medium', latitude='medium'):
|
| 41 |
+
"""
|
| 42 |
+
Calculate solar heat gain through walls based on orientation.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
area (float): Area of the wall in m²
|
| 46 |
+
u_value (float): U-value of the wall in W/m²°C
|
| 47 |
+
orientation (str): Wall orientation ('north', 'east', 'south', 'west')
|
| 48 |
+
daily_range (str): Daily temperature range ('low', 'medium', 'high')
|
| 49 |
+
latitude (str): Latitude category ('low', 'medium', 'high')
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
float: Heat gain in Watts
|
| 53 |
+
"""
|
| 54 |
+
# Solar intensity factors based on orientation
|
| 55 |
+
# These are simplified factors for demonstration
|
| 56 |
+
orientation_factors = {
|
| 57 |
+
'north': 0.3,
|
| 58 |
+
'east': 0.7,
|
| 59 |
+
'south': 0.5,
|
| 60 |
+
'west': 0.8,
|
| 61 |
+
'horizontal': 1.0
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Adjustments for latitude
|
| 65 |
+
latitude_factors = {
|
| 66 |
+
'low': 1.1, # Closer to equator
|
| 67 |
+
'medium': 1.0, # Mid latitudes
|
| 68 |
+
'high': 0.9 # Closer to poles
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Adjustments for daily temperature range
|
| 72 |
+
range_factors = {
|
| 73 |
+
'low': 0.95, # Less than 8.5°C
|
| 74 |
+
'medium': 1.0, # Between 8.5°C and 14°C
|
| 75 |
+
'high': 1.05 # Over 14°C
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Base solar heat gain through walls (W/m²)
|
| 79 |
+
base_solar_gain = 15.0
|
| 80 |
+
|
| 81 |
+
# Get factors
|
| 82 |
+
orientation_factor = orientation_factors.get(orientation.lower(), 0.5) # Default to south if not found
|
| 83 |
+
latitude_factor = latitude_factors.get(latitude.lower(), 1.0)
|
| 84 |
+
range_factor = range_factors.get(daily_range.lower(), 1.0)
|
| 85 |
+
|
| 86 |
+
# Calculate solar heat gain
|
| 87 |
+
solar_gain = area * base_solar_gain * orientation_factor * latitude_factor * range_factor
|
| 88 |
+
|
| 89 |
+
# Factor in the U-value (walls with higher U-values transmit more solar heat)
|
| 90 |
+
u_value_factor = min(u_value / 0.5, 2.0) # Normalize against a typical U-value of 0.5
|
| 91 |
+
|
| 92 |
+
return solar_gain * u_value_factor
|
| 93 |
|
| 94 |
def calculate_solar_heat_gain(self, area, shgf, shade_factor=1.0):
|
| 95 |
"""
|
|
|
|
| 194 |
Calculate the total cooling load including latent load.
|
| 195 |
|
| 196 |
Args:
|
| 197 |
+
building_components (list): List of dicts with 'area', 'u_value', 'temp_diff', and 'orientation' for each component
|
| 198 |
windows (list): List of dicts with 'area', 'orientation', 'glass_type', 'shading', etc.
|
| 199 |
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
| 200 |
internal_gains (dict): Dict with 'num_people', 'has_kitchen', and 'equipment_watts'
|
|
|
|
| 203 |
dict: Dictionary with sensible load, latent load, and total cooling load in Watts
|
| 204 |
"""
|
| 205 |
# Calculate conduction heat gain through building components
|
| 206 |
+
conduction_gain = 0
|
| 207 |
+
wall_solar_gain = 0
|
| 208 |
+
|
| 209 |
+
for comp in building_components:
|
| 210 |
+
# Calculate conduction gain
|
| 211 |
+
conduction_gain += self.calculate_conduction_heat_gain(comp['area'], comp['u_value'], comp['temp_diff'])
|
| 212 |
+
|
| 213 |
+
# Calculate solar gain for walls based on orientation
|
| 214 |
+
if 'orientation' in comp:
|
| 215 |
+
daily_range = comp.get('daily_range', 'medium')
|
| 216 |
+
latitude = comp.get('latitude', 'medium')
|
| 217 |
+
wall_solar_gain += self.calculate_wall_solar_heat_gain(
|
| 218 |
+
comp['area'],
|
| 219 |
+
comp['u_value'],
|
| 220 |
+
comp['orientation'],
|
| 221 |
+
daily_range,
|
| 222 |
+
latitude
|
| 223 |
+
)
|
| 224 |
|
| 225 |
# Calculate solar and conduction heat gain through windows
|
| 226 |
window_conduction_gain = 0
|
|
|
|
| 259 |
)
|
| 260 |
|
| 261 |
# Calculate sensible cooling load
|
| 262 |
+
sensible_load = conduction_gain + window_conduction_gain + window_solar_gain + wall_solar_gain + infiltration_gain + internal_gain
|
| 263 |
|
| 264 |
# Calculate total cooling load (including latent load)
|
| 265 |
latent_load = sensible_load * 0.3 # 30% of sensible load for latent load
|
|
|
|
| 269 |
'conduction_gain': conduction_gain,
|
| 270 |
'window_conduction_gain': window_conduction_gain,
|
| 271 |
'window_solar_gain': window_solar_gain,
|
| 272 |
+
'wall_solar_gain': wall_solar_gain,
|
| 273 |
'infiltration_gain': infiltration_gain,
|
| 274 |
'internal_gain': internal_gain,
|
| 275 |
'sensible_load': sensible_load,
|
heating_load.py
CHANGED
|
@@ -18,6 +18,10 @@ class HeatingLoadCalculator:
|
|
| 18 |
"""Initialize the heating load calculator with default values."""
|
| 19 |
# Specific heat capacity of air × density of air
|
| 20 |
self.air_heat_factor = 0.33
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def calculate_conduction_heat_loss(self, area, u_value, temp_diff):
|
| 23 |
"""
|
|
@@ -32,6 +36,61 @@ class HeatingLoadCalculator:
|
|
| 32 |
float: Heat loss in Watts
|
| 33 |
"""
|
| 34 |
return area * u_value * temp_diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def calculate_infiltration_heat_loss(self, volume, air_changes, temp_diff):
|
| 37 |
"""
|
|
@@ -46,6 +105,22 @@ class HeatingLoadCalculator:
|
|
| 46 |
float: Heat loss in Watts
|
| 47 |
"""
|
| 48 |
return self.air_heat_factor * volume * air_changes * temp_diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
def calculate_annual_heating_energy(self, total_heat_loss, heating_degree_days, correction_factor=1.0):
|
| 51 |
"""
|
|
@@ -146,13 +221,14 @@ class HeatingLoadCalculator:
|
|
| 146 |
|
| 147 |
return factors.get(occupancy_type.lower(), 1.0) # Default to continuous if not found
|
| 148 |
|
| 149 |
-
def calculate_total_heating_load(self, building_components, infiltration):
|
| 150 |
"""
|
| 151 |
Calculate the total peak heating load.
|
| 152 |
|
| 153 |
Args:
|
| 154 |
-
building_components (list): List of dicts with 'area', 'u_value', and '
|
| 155 |
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
|
|
|
| 156 |
|
| 157 |
Returns:
|
| 158 |
dict: Dictionary with component heat losses and total heating load in Watts
|
|
@@ -160,25 +236,50 @@ class HeatingLoadCalculator:
|
|
| 160 |
# Calculate conduction heat loss through building components
|
| 161 |
component_losses = {}
|
| 162 |
total_conduction_loss = 0
|
|
|
|
| 163 |
|
| 164 |
for comp in building_components:
|
| 165 |
name = comp.get('name', f"Component {len(component_losses) + 1}")
|
| 166 |
loss = self.calculate_conduction_heat_loss(comp['area'], comp['u_value'], comp['temp_diff'])
|
| 167 |
component_losses[name] = loss
|
| 168 |
total_conduction_loss += loss
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
# Calculate infiltration heat loss
|
| 171 |
infiltration_loss = self.calculate_infiltration_heat_loss(
|
| 172 |
infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
|
| 173 |
)
|
| 174 |
|
| 175 |
-
# Calculate
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
return {
|
| 179 |
'component_losses': component_losses,
|
| 180 |
'total_conduction_loss': total_conduction_loss,
|
| 181 |
'infiltration_loss': infiltration_loss,
|
|
|
|
|
|
|
| 182 |
'total_load': total_load
|
| 183 |
}
|
| 184 |
|
|
|
|
| 18 |
"""Initialize the heating load calculator with default values."""
|
| 19 |
# Specific heat capacity of air × density of air
|
| 20 |
self.air_heat_factor = 0.33
|
| 21 |
+
|
| 22 |
+
# Default values for internal heat gains (W)
|
| 23 |
+
self.heat_gain_per_person = 75
|
| 24 |
+
self.heat_gain_kitchen = 1000
|
| 25 |
|
| 26 |
def calculate_conduction_heat_loss(self, area, u_value, temp_diff):
|
| 27 |
"""
|
|
|
|
| 36 |
float: Heat loss in Watts
|
| 37 |
"""
|
| 38 |
return area * u_value * temp_diff
|
| 39 |
+
|
| 40 |
+
def calculate_wall_solar_heat_gain(self, area, u_value, orientation, daily_range='medium', latitude='medium'):
|
| 41 |
+
"""
|
| 42 |
+
Calculate solar heat gain through walls based on orientation.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
area (float): Area of the wall in m²
|
| 46 |
+
u_value (float): U-value of the wall in W/m²°C
|
| 47 |
+
orientation (str): Wall orientation ('north', 'east', 'south', 'west')
|
| 48 |
+
daily_range (str): Daily temperature range ('low', 'medium', 'high')
|
| 49 |
+
latitude (str): Latitude category ('low', 'medium', 'high')
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
float: Heat gain in Watts
|
| 53 |
+
"""
|
| 54 |
+
# Solar intensity factors based on orientation - for heating, south-facing walls (northern hemisphere)
|
| 55 |
+
# or north-facing walls (southern hemisphere) receive more solar gain in winter
|
| 56 |
+
# These are simplified factors for demonstration
|
| 57 |
+
orientation_factors = {
|
| 58 |
+
'north': 0.6, # Higher in southern hemisphere during winter
|
| 59 |
+
'east': 0.4,
|
| 60 |
+
'south': 0.2, # Lower in southern hemisphere during winter
|
| 61 |
+
'west': 0.4,
|
| 62 |
+
'horizontal': 0.3
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
# Adjustments for latitude
|
| 66 |
+
latitude_factors = {
|
| 67 |
+
'low': 0.9, # Closer to equator - less winter sun angle
|
| 68 |
+
'medium': 1.0, # Mid latitudes
|
| 69 |
+
'high': 1.1 # Closer to poles - more winter sun angle variation
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# Adjustments for daily temperature range
|
| 73 |
+
range_factors = {
|
| 74 |
+
'low': 0.95, # Less than 8.5°C
|
| 75 |
+
'medium': 1.0, # Between 8.5°C and 14°C
|
| 76 |
+
'high': 1.05 # Over 14°C
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# Base solar heat gain through walls (W/m²) - lower in winter
|
| 80 |
+
base_solar_gain = 10.0
|
| 81 |
+
|
| 82 |
+
# Get factors
|
| 83 |
+
orientation_factor = orientation_factors.get(orientation.lower(), 0.5) # Default to south if not found
|
| 84 |
+
latitude_factor = latitude_factors.get(latitude.lower(), 1.0)
|
| 85 |
+
range_factor = range_factors.get(daily_range.lower(), 1.0)
|
| 86 |
+
|
| 87 |
+
# Calculate solar heat gain
|
| 88 |
+
solar_gain = area * base_solar_gain * orientation_factor * latitude_factor * range_factor
|
| 89 |
+
|
| 90 |
+
# Factor in the U-value (walls with higher U-values transmit more solar heat)
|
| 91 |
+
u_value_factor = min(u_value / 0.5, 2.0) # Normalize against a typical U-value of 0.5
|
| 92 |
+
|
| 93 |
+
return solar_gain * u_value_factor
|
| 94 |
|
| 95 |
def calculate_infiltration_heat_loss(self, volume, air_changes, temp_diff):
|
| 96 |
"""
|
|
|
|
| 105 |
float: Heat loss in Watts
|
| 106 |
"""
|
| 107 |
return self.air_heat_factor * volume * air_changes * temp_diff
|
| 108 |
+
|
| 109 |
+
def calculate_internal_heat_gain(self, num_people, has_kitchen=False, equipment_watts=0):
|
| 110 |
+
"""
|
| 111 |
+
Calculate internal heat gain from people, kitchen, and equipment.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
num_people (int): Number of occupants
|
| 115 |
+
has_kitchen (bool): Whether the space includes a kitchen
|
| 116 |
+
equipment_watts (float): Additional equipment heat gain in Watts
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
float: Heat gain in Watts
|
| 120 |
+
"""
|
| 121 |
+
people_gain = num_people * self.heat_gain_per_person
|
| 122 |
+
kitchen_gain = self.heat_gain_kitchen if has_kitchen else 0
|
| 123 |
+
return people_gain + kitchen_gain + equipment_watts
|
| 124 |
|
| 125 |
def calculate_annual_heating_energy(self, total_heat_loss, heating_degree_days, correction_factor=1.0):
|
| 126 |
"""
|
|
|
|
| 221 |
|
| 222 |
return factors.get(occupancy_type.lower(), 1.0) # Default to continuous if not found
|
| 223 |
|
| 224 |
+
def calculate_total_heating_load(self, building_components, infiltration, internal_gains=None):
|
| 225 |
"""
|
| 226 |
Calculate the total peak heating load.
|
| 227 |
|
| 228 |
Args:
|
| 229 |
+
building_components (list): List of dicts with 'area', 'u_value', 'temp_diff', and 'orientation' for each component
|
| 230 |
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
| 231 |
+
internal_gains (dict): Dict with 'num_people', 'has_kitchen', and 'equipment_watts'
|
| 232 |
|
| 233 |
Returns:
|
| 234 |
dict: Dictionary with component heat losses and total heating load in Watts
|
|
|
|
| 236 |
# Calculate conduction heat loss through building components
|
| 237 |
component_losses = {}
|
| 238 |
total_conduction_loss = 0
|
| 239 |
+
wall_solar_gain = 0
|
| 240 |
|
| 241 |
for comp in building_components:
|
| 242 |
name = comp.get('name', f"Component {len(component_losses) + 1}")
|
| 243 |
loss = self.calculate_conduction_heat_loss(comp['area'], comp['u_value'], comp['temp_diff'])
|
| 244 |
component_losses[name] = loss
|
| 245 |
total_conduction_loss += loss
|
| 246 |
+
|
| 247 |
+
# Calculate solar gain for walls based on orientation
|
| 248 |
+
if 'orientation' in comp:
|
| 249 |
+
daily_range = comp.get('daily_range', 'medium')
|
| 250 |
+
latitude = comp.get('latitude', 'medium')
|
| 251 |
+
solar_gain = self.calculate_wall_solar_heat_gain(
|
| 252 |
+
comp['area'],
|
| 253 |
+
comp['u_value'],
|
| 254 |
+
comp['orientation'],
|
| 255 |
+
daily_range,
|
| 256 |
+
latitude
|
| 257 |
+
)
|
| 258 |
+
wall_solar_gain += solar_gain
|
| 259 |
|
| 260 |
# Calculate infiltration heat loss
|
| 261 |
infiltration_loss = self.calculate_infiltration_heat_loss(
|
| 262 |
infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
|
| 263 |
)
|
| 264 |
|
| 265 |
+
# Calculate internal heat gain if provided
|
| 266 |
+
internal_gain = 0
|
| 267 |
+
if internal_gains:
|
| 268 |
+
internal_gain = self.calculate_internal_heat_gain(
|
| 269 |
+
internal_gains.get('num_people', 0),
|
| 270 |
+
internal_gains.get('has_kitchen', False),
|
| 271 |
+
internal_gains.get('equipment_watts', 0)
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Calculate total heating load (subtract solar gain and internal gains as they reduce heating load)
|
| 275 |
+
total_load = total_conduction_loss + infiltration_loss - wall_solar_gain - internal_gain
|
| 276 |
|
| 277 |
return {
|
| 278 |
'component_losses': component_losses,
|
| 279 |
'total_conduction_loss': total_conduction_loss,
|
| 280 |
'infiltration_loss': infiltration_loss,
|
| 281 |
+
'wall_solar_gain': wall_solar_gain,
|
| 282 |
+
'internal_gain': internal_gain,
|
| 283 |
'total_load': total_load
|
| 284 |
}
|
| 285 |
|
pages/cooling_calculator.py
CHANGED
|
@@ -210,7 +210,7 @@ def building_info_form(ref_data):
|
|
| 210 |
warnings.append(ValidationWarning(
|
| 211 |
"Invalid temperature difference",
|
| 212 |
"Outdoor temperature should be higher than indoor temperature for cooling load calculation",
|
| 213 |
-
is_critical=
|
| 214 |
))
|
| 215 |
|
| 216 |
# Check if dimensions are reasonable
|
|
@@ -289,14 +289,18 @@ def building_envelope_form(ref_data):
|
|
| 289 |
|
| 290 |
# Get wall material options from reference data
|
| 291 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
|
|
|
|
|
|
| 292 |
|
| 293 |
# Display existing wall entries
|
| 294 |
if st.session_state.cooling_form_data['building_envelope']['walls']:
|
| 295 |
st.write("Current walls:")
|
| 296 |
walls_df = pd.DataFrame(st.session_state.cooling_form_data['building_envelope']['walls'])
|
| 297 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 298 |
-
|
| 299 |
-
walls_df
|
|
|
|
|
|
|
| 300 |
st.dataframe(walls_df)
|
| 301 |
|
| 302 |
# Add new wall form
|
|
@@ -313,10 +317,39 @@ def building_envelope_form(ref_data):
|
|
| 313 |
key="new_wall_material"
|
| 314 |
)
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
# Get material properties
|
| 317 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 318 |
u_value = material_data['u_value']
|
| 319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
with col2:
|
| 321 |
wall_area = st.number_input(
|
| 322 |
"Wall Area (m²)",
|
|
@@ -336,7 +369,8 @@ def building_envelope_form(ref_data):
|
|
| 336 |
'material_id': wall_material,
|
| 337 |
'area': wall_area,
|
| 338 |
'u_value': u_value,
|
| 339 |
-
'temp_diff': temp_diff
|
|
|
|
| 340 |
}
|
| 341 |
st.session_state.cooling_form_data['building_envelope']['walls'].append(new_wall)
|
| 342 |
st.experimental_rerun()
|
|
@@ -346,6 +380,8 @@ def building_envelope_form(ref_data):
|
|
| 346 |
|
| 347 |
# Get roof material options from reference data
|
| 348 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
|
|
|
|
|
|
| 349 |
|
| 350 |
col1, col2 = st.columns(2)
|
| 351 |
|
|
@@ -361,6 +397,28 @@ def building_envelope_form(ref_data):
|
|
| 361 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 362 |
roof_u_value = material_data['u_value']
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
with col2:
|
| 365 |
roof_area = st.number_input(
|
| 366 |
"Roof Area (m²)",
|
|
@@ -385,6 +443,8 @@ def building_envelope_form(ref_data):
|
|
| 385 |
|
| 386 |
# Get floor material options from reference data
|
| 387 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
|
|
|
|
|
|
| 388 |
|
| 389 |
col1, col2 = st.columns(2)
|
| 390 |
|
|
@@ -400,6 +460,28 @@ def building_envelope_form(ref_data):
|
|
| 400 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 401 |
floor_u_value = material_data['u_value']
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
with col2:
|
| 404 |
floor_area = st.number_input(
|
| 405 |
"Floor Area (m²)",
|
|
@@ -427,7 +509,7 @@ def building_envelope_form(ref_data):
|
|
| 427 |
warnings.append(ValidationWarning(
|
| 428 |
"No walls defined",
|
| 429 |
"Add at least one wall to continue",
|
| 430 |
-
is_critical=
|
| 431 |
))
|
| 432 |
|
| 433 |
# Check if total wall area is reasonable
|
|
@@ -1249,7 +1331,26 @@ def results_page():
|
|
| 1249 |
values=list(load_components.values()),
|
| 1250 |
names=list(load_components.keys()),
|
| 1251 |
title="Cooling Load Components",
|
| 1252 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
)
|
| 1254 |
|
| 1255 |
st.plotly_chart(fig)
|
|
@@ -1261,10 +1362,13 @@ def results_page():
|
|
| 1261 |
'Percentage (%)': [value / results['sensible_load'] * 100 for value in load_components.values()]
|
| 1262 |
})
|
| 1263 |
|
|
|
|
|
|
|
|
|
|
| 1264 |
st.dataframe(load_df.style.format({
|
| 1265 |
'Load (W)': '{:.2f}',
|
| 1266 |
'Percentage (%)': '{:.2f}'
|
| 1267 |
-
}))
|
| 1268 |
|
| 1269 |
# Display detailed results
|
| 1270 |
st.write("### Detailed Results")
|
|
@@ -1384,21 +1488,38 @@ def results_page():
|
|
| 1384 |
x=windows_df['Component'],
|
| 1385 |
y=windows_df['Conduction Heat Gain (W)'],
|
| 1386 |
name='Conduction Heat Gain',
|
| 1387 |
-
marker_color='
|
|
|
|
|
|
|
|
|
|
| 1388 |
))
|
| 1389 |
|
| 1390 |
fig.add_trace(go.Bar(
|
| 1391 |
x=windows_df['Component'],
|
| 1392 |
y=windows_df['Solar Heat Gain (W)'],
|
| 1393 |
name='Solar Heat Gain',
|
| 1394 |
-
marker_color='
|
|
|
|
|
|
|
|
|
|
| 1395 |
))
|
| 1396 |
|
| 1397 |
fig.update_layout(
|
| 1398 |
title="Window Heat Gains",
|
| 1399 |
xaxis_title="Window",
|
| 1400 |
yaxis_title="Heat Gain (W)",
|
| 1401 |
-
barmode='stack'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1402 |
)
|
| 1403 |
|
| 1404 |
st.plotly_chart(fig)
|
|
@@ -1606,6 +1727,42 @@ def cooling_calculator():
|
|
| 1606 |
"6. Results"
|
| 1607 |
])
|
| 1608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1609 |
# Display the active tab
|
| 1610 |
with tabs[0]:
|
| 1611 |
if st.session_state.cooling_active_tab == "building_info":
|
|
|
|
| 210 |
warnings.append(ValidationWarning(
|
| 211 |
"Invalid temperature difference",
|
| 212 |
"Outdoor temperature should be higher than indoor temperature for cooling load calculation",
|
| 213 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 214 |
))
|
| 215 |
|
| 216 |
# Check if dimensions are reasonable
|
|
|
|
| 289 |
|
| 290 |
# Get wall material options from reference data
|
| 291 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
| 292 |
+
# Add custom option
|
| 293 |
+
wall_material_options["custom_walls"] = "Custom Wall (User-defined)"
|
| 294 |
|
| 295 |
# Display existing wall entries
|
| 296 |
if st.session_state.cooling_form_data['building_envelope']['walls']:
|
| 297 |
st.write("Current walls:")
|
| 298 |
walls_df = pd.DataFrame(st.session_state.cooling_form_data['building_envelope']['walls'])
|
| 299 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 300 |
+
# Add orientation column with default value if not present
|
| 301 |
+
walls_df['orientation'] = walls_df['orientation'].fillna('not specified')
|
| 302 |
+
walls_df = walls_df[['name', 'Material', 'area', 'u_value', 'orientation']]
|
| 303 |
+
walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)', 'Orientation']
|
| 304 |
st.dataframe(walls_df)
|
| 305 |
|
| 306 |
# Add new wall form
|
|
|
|
| 317 |
key="new_wall_material"
|
| 318 |
)
|
| 319 |
|
| 320 |
+
# Add wall orientation selection
|
| 321 |
+
wall_orientation = st.selectbox(
|
| 322 |
+
"Wall Orientation",
|
| 323 |
+
options=["north", "east", "south", "west"],
|
| 324 |
+
key="new_wall_orientation"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
# Get material properties
|
| 328 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 329 |
u_value = material_data['u_value']
|
| 330 |
|
| 331 |
+
# Add custom U-value input if custom material is selected
|
| 332 |
+
if wall_material == "custom_walls":
|
| 333 |
+
u_value = st.number_input(
|
| 334 |
+
"Custom U-Value (W/m²°C)",
|
| 335 |
+
value=1.0,
|
| 336 |
+
min_value=0.1,
|
| 337 |
+
max_value=5.0,
|
| 338 |
+
step=0.1,
|
| 339 |
+
key="custom_wall_u_value"
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# Store custom material in session state
|
| 343 |
+
if "custom_materials" not in st.session_state:
|
| 344 |
+
st.session_state.custom_materials = {}
|
| 345 |
+
|
| 346 |
+
st.session_state.custom_materials["walls"] = {
|
| 347 |
+
"name": "Custom Wall",
|
| 348 |
+
"u_value": u_value,
|
| 349 |
+
"r_value": 1.0 / u_value if u_value > 0 else 1.0,
|
| 350 |
+
"description": "Custom wall with user-defined properties"
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
with col2:
|
| 354 |
wall_area = st.number_input(
|
| 355 |
"Wall Area (m²)",
|
|
|
|
| 369 |
'material_id': wall_material,
|
| 370 |
'area': wall_area,
|
| 371 |
'u_value': u_value,
|
| 372 |
+
'temp_diff': temp_diff,
|
| 373 |
+
'orientation': wall_orientation # Add orientation to wall data
|
| 374 |
}
|
| 375 |
st.session_state.cooling_form_data['building_envelope']['walls'].append(new_wall)
|
| 376 |
st.experimental_rerun()
|
|
|
|
| 380 |
|
| 381 |
# Get roof material options from reference data
|
| 382 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
| 383 |
+
# Add custom option
|
| 384 |
+
roof_material_options["custom_roofs"] = "Custom Roof (User-defined)"
|
| 385 |
|
| 386 |
col1, col2 = st.columns(2)
|
| 387 |
|
|
|
|
| 397 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 398 |
roof_u_value = material_data['u_value']
|
| 399 |
|
| 400 |
+
# Add custom U-value input if custom material is selected
|
| 401 |
+
if roof_material == "custom_roofs":
|
| 402 |
+
roof_u_value = st.number_input(
|
| 403 |
+
"Custom Roof U-Value (W/m²°C)",
|
| 404 |
+
value=1.0,
|
| 405 |
+
min_value=0.1,
|
| 406 |
+
max_value=5.0,
|
| 407 |
+
step=0.1,
|
| 408 |
+
key="custom_roof_u_value"
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Store custom material in session state
|
| 412 |
+
if "custom_materials" not in st.session_state:
|
| 413 |
+
st.session_state.custom_materials = {}
|
| 414 |
+
|
| 415 |
+
st.session_state.custom_materials["roofs"] = {
|
| 416 |
+
"name": "Custom Roof",
|
| 417 |
+
"u_value": roof_u_value,
|
| 418 |
+
"r_value": 1.0 / roof_u_value if roof_u_value > 0 else 1.0,
|
| 419 |
+
"description": "Custom roof with user-defined properties"
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
with col2:
|
| 423 |
roof_area = st.number_input(
|
| 424 |
"Roof Area (m²)",
|
|
|
|
| 443 |
|
| 444 |
# Get floor material options from reference data
|
| 445 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
| 446 |
+
# Add custom option
|
| 447 |
+
floor_material_options["custom_floors"] = "Custom Floor (User-defined)"
|
| 448 |
|
| 449 |
col1, col2 = st.columns(2)
|
| 450 |
|
|
|
|
| 460 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 461 |
floor_u_value = material_data['u_value']
|
| 462 |
|
| 463 |
+
# Add custom U-value input if custom material is selected
|
| 464 |
+
if floor_material == "custom_floors":
|
| 465 |
+
floor_u_value = st.number_input(
|
| 466 |
+
"Custom Floor U-Value (W/m²°C)",
|
| 467 |
+
value=1.0,
|
| 468 |
+
min_value=0.1,
|
| 469 |
+
max_value=5.0,
|
| 470 |
+
step=0.1,
|
| 471 |
+
key="custom_floor_u_value"
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
# Store custom material in session state
|
| 475 |
+
if "custom_materials" not in st.session_state:
|
| 476 |
+
st.session_state.custom_materials = {}
|
| 477 |
+
|
| 478 |
+
st.session_state.custom_materials["floors"] = {
|
| 479 |
+
"name": "Custom Floor",
|
| 480 |
+
"u_value": floor_u_value,
|
| 481 |
+
"r_value": 1.0 / floor_u_value if floor_u_value > 0 else 1.0,
|
| 482 |
+
"description": "Custom floor with user-defined properties"
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
with col2:
|
| 486 |
floor_area = st.number_input(
|
| 487 |
"Floor Area (m²)",
|
|
|
|
| 509 |
warnings.append(ValidationWarning(
|
| 510 |
"No walls defined",
|
| 511 |
"Add at least one wall to continue",
|
| 512 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 513 |
))
|
| 514 |
|
| 515 |
# Check if total wall area is reasonable
|
|
|
|
| 1331 |
values=list(load_components.values()),
|
| 1332 |
names=list(load_components.keys()),
|
| 1333 |
title="Cooling Load Components",
|
| 1334 |
+
color_discrete_sequence=px.colors.sequential.Turbo,
|
| 1335 |
+
hole=0.4, # Create a donut chart for better readability
|
| 1336 |
+
labels={'label': 'Component', 'value': 'Heat Gain (W)'}
|
| 1337 |
+
)
|
| 1338 |
+
|
| 1339 |
+
# Improve layout and formatting
|
| 1340 |
+
fig.update_traces(
|
| 1341 |
+
textposition='inside',
|
| 1342 |
+
textinfo='percent+label',
|
| 1343 |
+
hoverinfo='label+percent+value',
|
| 1344 |
+
marker=dict(line=dict(color='#FFFFFF', width=2))
|
| 1345 |
+
)
|
| 1346 |
+
|
| 1347 |
+
# Improve layout
|
| 1348 |
+
fig.update_layout(
|
| 1349 |
+
legend_title_text='Load Components',
|
| 1350 |
+
font=dict(size=14),
|
| 1351 |
+
title_font=dict(size=18),
|
| 1352 |
+
title_x=0.5, # Center the title
|
| 1353 |
+
margin=dict(t=50, b=50, l=50, r=50)
|
| 1354 |
)
|
| 1355 |
|
| 1356 |
st.plotly_chart(fig)
|
|
|
|
| 1362 |
'Percentage (%)': [value / results['sensible_load'] * 100 for value in load_components.values()]
|
| 1363 |
})
|
| 1364 |
|
| 1365 |
+
# Sort by load value for better readability
|
| 1366 |
+
load_df = load_df.sort_values(by='Load (W)', ascending=False).reset_index(drop=True)
|
| 1367 |
+
|
| 1368 |
st.dataframe(load_df.style.format({
|
| 1369 |
'Load (W)': '{:.2f}',
|
| 1370 |
'Percentage (%)': '{:.2f}'
|
| 1371 |
+
}).background_gradient(cmap='Blues', subset=['Percentage (%)']))
|
| 1372 |
|
| 1373 |
# Display detailed results
|
| 1374 |
st.write("### Detailed Results")
|
|
|
|
| 1488 |
x=windows_df['Component'],
|
| 1489 |
y=windows_df['Conduction Heat Gain (W)'],
|
| 1490 |
name='Conduction Heat Gain',
|
| 1491 |
+
marker_color='#1f77b4',
|
| 1492 |
+
text=windows_df['Conduction Heat Gain (W)'].round(1),
|
| 1493 |
+
textposition='auto',
|
| 1494 |
+
hovertemplate='<b>%{x}</b><br>Conduction Heat Gain: %{y:.1f} W<extra></extra>'
|
| 1495 |
))
|
| 1496 |
|
| 1497 |
fig.add_trace(go.Bar(
|
| 1498 |
x=windows_df['Component'],
|
| 1499 |
y=windows_df['Solar Heat Gain (W)'],
|
| 1500 |
name='Solar Heat Gain',
|
| 1501 |
+
marker_color='#ff7f0e',
|
| 1502 |
+
text=windows_df['Solar Heat Gain (W)'].round(1),
|
| 1503 |
+
textposition='auto',
|
| 1504 |
+
hovertemplate='<b>%{x}</b><br>Solar Heat Gain: %{y:.1f} W<extra></extra>'
|
| 1505 |
))
|
| 1506 |
|
| 1507 |
fig.update_layout(
|
| 1508 |
title="Window Heat Gains",
|
| 1509 |
xaxis_title="Window",
|
| 1510 |
yaxis_title="Heat Gain (W)",
|
| 1511 |
+
barmode='stack',
|
| 1512 |
+
font=dict(size=14),
|
| 1513 |
+
title_font=dict(size=18),
|
| 1514 |
+
title_x=0.5, # Center the title
|
| 1515 |
+
margin=dict(t=50, b=50, l=50, r=50),
|
| 1516 |
+
legend=dict(
|
| 1517 |
+
orientation="h",
|
| 1518 |
+
yanchor="bottom",
|
| 1519 |
+
y=1.02,
|
| 1520 |
+
xanchor="right",
|
| 1521 |
+
x=1
|
| 1522 |
+
)
|
| 1523 |
)
|
| 1524 |
|
| 1525 |
st.plotly_chart(fig)
|
|
|
|
| 1727 |
"6. Results"
|
| 1728 |
])
|
| 1729 |
|
| 1730 |
+
# Add direct navigation buttons at the top
|
| 1731 |
+
st.write("### Navigation")
|
| 1732 |
+
st.write("Click on any button below to navigate directly to that section:")
|
| 1733 |
+
|
| 1734 |
+
col1, col2, col3 = st.columns(3)
|
| 1735 |
+
with col1:
|
| 1736 |
+
if st.button("1. Building Information", key="direct_nav_building_info"):
|
| 1737 |
+
st.session_state.cooling_active_tab = "building_info"
|
| 1738 |
+
st.experimental_rerun()
|
| 1739 |
+
|
| 1740 |
+
if st.button("2. Building Envelope", key="direct_nav_building_envelope"):
|
| 1741 |
+
st.session_state.cooling_active_tab = "building_envelope"
|
| 1742 |
+
st.experimental_rerun()
|
| 1743 |
+
|
| 1744 |
+
with col2:
|
| 1745 |
+
if st.button("3. Windows & Doors", key="direct_nav_windows"):
|
| 1746 |
+
st.session_state.cooling_active_tab = "windows"
|
| 1747 |
+
st.experimental_rerun()
|
| 1748 |
+
|
| 1749 |
+
if st.button("4. Internal Loads", key="direct_nav_internal_loads"):
|
| 1750 |
+
st.session_state.cooling_active_tab = "internal_loads"
|
| 1751 |
+
st.experimental_rerun()
|
| 1752 |
+
|
| 1753 |
+
with col3:
|
| 1754 |
+
if st.button("5. Ventilation", key="direct_nav_ventilation"):
|
| 1755 |
+
st.session_state.cooling_active_tab = "ventilation"
|
| 1756 |
+
st.experimental_rerun()
|
| 1757 |
+
|
| 1758 |
+
if st.button("6. Results", key="direct_nav_results"):
|
| 1759 |
+
# Only enable if all previous steps are completed
|
| 1760 |
+
if all(st.session_state.cooling_completed.values()):
|
| 1761 |
+
st.session_state.cooling_active_tab = "results"
|
| 1762 |
+
st.experimental_rerun()
|
| 1763 |
+
else:
|
| 1764 |
+
st.warning("Please complete all previous steps before viewing results.")
|
| 1765 |
+
|
| 1766 |
# Display the active tab
|
| 1767 |
with tabs[0]:
|
| 1768 |
if st.session_state.cooling_active_tab == "building_info":
|
pages/heating_calculator.py
CHANGED
|
@@ -195,7 +195,7 @@ def building_info_form(ref_data):
|
|
| 195 |
warnings.append(ValidationWarning(
|
| 196 |
"Invalid temperature difference",
|
| 197 |
"Indoor temperature should be higher than outdoor temperature for heating load calculation",
|
| 198 |
-
is_critical=
|
| 199 |
))
|
| 200 |
|
| 201 |
# Check if dimensions are reasonable
|
|
@@ -274,14 +274,18 @@ def building_envelope_form(ref_data):
|
|
| 274 |
|
| 275 |
# Get wall material options from reference data
|
| 276 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
|
|
|
|
|
|
| 277 |
|
| 278 |
# Display existing wall entries
|
| 279 |
if st.session_state.heating_form_data['building_envelope']['walls']:
|
| 280 |
st.write("Current walls:")
|
| 281 |
walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls'])
|
| 282 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 283 |
-
|
| 284 |
-
walls_df
|
|
|
|
|
|
|
| 285 |
st.dataframe(walls_df)
|
| 286 |
|
| 287 |
# Add new wall form
|
|
@@ -298,10 +302,39 @@ def building_envelope_form(ref_data):
|
|
| 298 |
key="new_wall_material_heating"
|
| 299 |
)
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
# Get material properties
|
| 302 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 303 |
u_value = material_data['u_value']
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
with col2:
|
| 306 |
wall_area = st.number_input(
|
| 307 |
"Wall Area (m²)",
|
|
@@ -321,7 +354,8 @@ def building_envelope_form(ref_data):
|
|
| 321 |
'material_id': wall_material,
|
| 322 |
'area': wall_area,
|
| 323 |
'u_value': u_value,
|
| 324 |
-
'temp_diff': temp_diff
|
|
|
|
| 325 |
}
|
| 326 |
st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall)
|
| 327 |
st.experimental_rerun()
|
|
@@ -331,6 +365,8 @@ def building_envelope_form(ref_data):
|
|
| 331 |
|
| 332 |
# Get roof material options from reference data
|
| 333 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
|
|
|
|
|
|
| 334 |
|
| 335 |
col1, col2 = st.columns(2)
|
| 336 |
|
|
@@ -346,6 +382,28 @@ def building_envelope_form(ref_data):
|
|
| 346 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 347 |
roof_u_value = material_data['u_value']
|
| 348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
with col2:
|
| 350 |
roof_area = st.number_input(
|
| 351 |
"Roof Area (m²)",
|
|
@@ -371,6 +429,8 @@ def building_envelope_form(ref_data):
|
|
| 371 |
|
| 372 |
# Get floor material options from reference data
|
| 373 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
|
|
|
|
|
|
| 374 |
|
| 375 |
col1, col2 = st.columns(2)
|
| 376 |
|
|
@@ -386,6 +446,28 @@ def building_envelope_form(ref_data):
|
|
| 386 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 387 |
floor_u_value = material_data['u_value']
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
with col2:
|
| 390 |
floor_area = st.number_input(
|
| 391 |
"Floor Area (m²)",
|
|
@@ -414,7 +496,7 @@ def building_envelope_form(ref_data):
|
|
| 414 |
warnings.append(ValidationWarning(
|
| 415 |
"No walls defined",
|
| 416 |
"Add at least one wall to continue",
|
| 417 |
-
is_critical=
|
| 418 |
))
|
| 419 |
|
| 420 |
# Check if total wall area is reasonable
|
|
@@ -684,6 +766,30 @@ def ventilation_form(ref_data):
|
|
| 684 |
'air_changes': 0.0
|
| 685 |
}
|
| 686 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
# Infiltration section
|
| 688 |
st.write("### Infiltration")
|
| 689 |
st.write("Infiltration is the unintended air leakage through the building envelope.")
|
|
@@ -715,6 +821,172 @@ def ventilation_form(ref_data):
|
|
| 715 |
st.write("### Ventilation")
|
| 716 |
st.write("Ventilation is the intentional introduction of outside air into the building.")
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
col1, col2 = st.columns(2)
|
| 719 |
|
| 720 |
with col1:
|
|
@@ -1016,10 +1288,20 @@ def calculate_heating_load():
|
|
| 1016 |
'temp_diff': infiltration.get('temp_diff', 0)
|
| 1017 |
}
|
| 1018 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
# Calculate heating load
|
| 1020 |
results = calculator.calculate_total_heating_load(
|
| 1021 |
building_components=building_components,
|
| 1022 |
-
infiltration=infiltration_data
|
|
|
|
| 1023 |
)
|
| 1024 |
|
| 1025 |
# Calculate annual heating requirement
|
|
@@ -1102,7 +1384,26 @@ def results_page():
|
|
| 1102 |
values=list(component_losses.values()),
|
| 1103 |
names=list(component_losses.keys()),
|
| 1104 |
title="Heating Load Components",
|
| 1105 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
)
|
| 1107 |
|
| 1108 |
st.plotly_chart(fig)
|
|
@@ -1113,10 +1414,17 @@ def results_page():
|
|
| 1113 |
'Infiltration & Ventilation': results.get('infiltration_loss', 0)
|
| 1114 |
}
|
| 1115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
load_df = pd.DataFrame({
|
| 1117 |
'Component': list(load_components.keys()),
|
| 1118 |
'Load (W)': list(load_components.values()),
|
| 1119 |
-
'Percentage (%)': [value / results['total_load'] * 100 for value in load_components.values()]
|
| 1120 |
})
|
| 1121 |
|
| 1122 |
st.dataframe(load_df.style.format({
|
|
@@ -1200,7 +1508,26 @@ def results_page():
|
|
| 1200 |
y='Heat Loss (W)',
|
| 1201 |
title="Heat Loss by Building Component",
|
| 1202 |
color='Component',
|
| 1203 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
)
|
| 1205 |
|
| 1206 |
st.plotly_chart(fig)
|
|
@@ -1244,7 +1571,28 @@ def results_page():
|
|
| 1244 |
y='Heat Loss (W)',
|
| 1245 |
title="Ventilation & Infiltration Heat Losses",
|
| 1246 |
color='Source',
|
| 1247 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
)
|
| 1249 |
|
| 1250 |
st.plotly_chart(fig)
|
|
@@ -1405,6 +1753,42 @@ def heating_calculator():
|
|
| 1405 |
"6. Results"
|
| 1406 |
])
|
| 1407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1408 |
# Display the active tab
|
| 1409 |
with tabs[0]:
|
| 1410 |
if st.session_state.heating_active_tab == "building_info":
|
|
|
|
| 195 |
warnings.append(ValidationWarning(
|
| 196 |
"Invalid temperature difference",
|
| 197 |
"Indoor temperature should be higher than outdoor temperature for heating load calculation",
|
| 198 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 199 |
))
|
| 200 |
|
| 201 |
# Check if dimensions are reasonable
|
|
|
|
| 274 |
|
| 275 |
# Get wall material options from reference data
|
| 276 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
| 277 |
+
# Add custom option
|
| 278 |
+
wall_material_options["custom_walls"] = "Custom Wall (User-defined)"
|
| 279 |
|
| 280 |
# Display existing wall entries
|
| 281 |
if st.session_state.heating_form_data['building_envelope']['walls']:
|
| 282 |
st.write("Current walls:")
|
| 283 |
walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls'])
|
| 284 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 285 |
+
# Add orientation column with default value if not present
|
| 286 |
+
walls_df['orientation'] = walls_df['orientation'].fillna('not specified')
|
| 287 |
+
walls_df = walls_df[['name', 'Material', 'area', 'u_value', 'orientation']]
|
| 288 |
+
walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)', 'Orientation']
|
| 289 |
st.dataframe(walls_df)
|
| 290 |
|
| 291 |
# Add new wall form
|
|
|
|
| 302 |
key="new_wall_material_heating"
|
| 303 |
)
|
| 304 |
|
| 305 |
+
# Add wall orientation selection
|
| 306 |
+
wall_orientation = st.selectbox(
|
| 307 |
+
"Wall Orientation",
|
| 308 |
+
options=["north", "east", "south", "west"],
|
| 309 |
+
key="new_wall_orientation_heating"
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
# Get material properties
|
| 313 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 314 |
u_value = material_data['u_value']
|
| 315 |
|
| 316 |
+
# Add custom U-value input if custom material is selected
|
| 317 |
+
if wall_material == "custom_walls":
|
| 318 |
+
u_value = st.number_input(
|
| 319 |
+
"Custom U-Value (W/m²°C)",
|
| 320 |
+
value=1.0,
|
| 321 |
+
min_value=0.1,
|
| 322 |
+
max_value=5.0,
|
| 323 |
+
step=0.1,
|
| 324 |
+
key="custom_wall_u_value_heating"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# Store custom material in session state
|
| 328 |
+
if "custom_materials" not in st.session_state:
|
| 329 |
+
st.session_state.custom_materials = {}
|
| 330 |
+
|
| 331 |
+
st.session_state.custom_materials["walls"] = {
|
| 332 |
+
"name": "Custom Wall",
|
| 333 |
+
"u_value": u_value,
|
| 334 |
+
"r_value": 1.0 / u_value if u_value > 0 else 1.0,
|
| 335 |
+
"description": "Custom wall with user-defined properties"
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
with col2:
|
| 339 |
wall_area = st.number_input(
|
| 340 |
"Wall Area (m²)",
|
|
|
|
| 354 |
'material_id': wall_material,
|
| 355 |
'area': wall_area,
|
| 356 |
'u_value': u_value,
|
| 357 |
+
'temp_diff': temp_diff,
|
| 358 |
+
'orientation': wall_orientation # Add orientation to wall data
|
| 359 |
}
|
| 360 |
st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall)
|
| 361 |
st.experimental_rerun()
|
|
|
|
| 365 |
|
| 366 |
# Get roof material options from reference data
|
| 367 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
| 368 |
+
# Add custom option
|
| 369 |
+
roof_material_options["custom_roofs"] = "Custom Roof (User-defined)"
|
| 370 |
|
| 371 |
col1, col2 = st.columns(2)
|
| 372 |
|
|
|
|
| 382 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 383 |
roof_u_value = material_data['u_value']
|
| 384 |
|
| 385 |
+
# Add custom U-value input if custom material is selected
|
| 386 |
+
if roof_material == "custom_roofs":
|
| 387 |
+
roof_u_value = st.number_input(
|
| 388 |
+
"Custom Roof U-Value (W/m²°C)",
|
| 389 |
+
value=1.0,
|
| 390 |
+
min_value=0.1,
|
| 391 |
+
max_value=5.0,
|
| 392 |
+
step=0.1,
|
| 393 |
+
key="custom_roof_u_value_heating"
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
# Store custom material in session state
|
| 397 |
+
if "custom_materials" not in st.session_state:
|
| 398 |
+
st.session_state.custom_materials = {}
|
| 399 |
+
|
| 400 |
+
st.session_state.custom_materials["roofs"] = {
|
| 401 |
+
"name": "Custom Roof",
|
| 402 |
+
"u_value": roof_u_value,
|
| 403 |
+
"r_value": 1.0 / roof_u_value if roof_u_value > 0 else 1.0,
|
| 404 |
+
"description": "Custom roof with user-defined properties"
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
with col2:
|
| 408 |
roof_area = st.number_input(
|
| 409 |
"Roof Area (m²)",
|
|
|
|
| 429 |
|
| 430 |
# Get floor material options from reference data
|
| 431 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
| 432 |
+
# Add custom option
|
| 433 |
+
floor_material_options["custom_floors"] = "Custom Floor (User-defined)"
|
| 434 |
|
| 435 |
col1, col2 = st.columns(2)
|
| 436 |
|
|
|
|
| 446 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 447 |
floor_u_value = material_data['u_value']
|
| 448 |
|
| 449 |
+
# Add custom U-value input if custom material is selected
|
| 450 |
+
if floor_material == "custom_floors":
|
| 451 |
+
floor_u_value = st.number_input(
|
| 452 |
+
"Custom Floor U-Value (W/m²°C)",
|
| 453 |
+
value=1.0,
|
| 454 |
+
min_value=0.1,
|
| 455 |
+
max_value=5.0,
|
| 456 |
+
step=0.1,
|
| 457 |
+
key="custom_floor_u_value_heating"
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Store custom material in session state
|
| 461 |
+
if "custom_materials" not in st.session_state:
|
| 462 |
+
st.session_state.custom_materials = {}
|
| 463 |
+
|
| 464 |
+
st.session_state.custom_materials["floors"] = {
|
| 465 |
+
"name": "Custom Floor",
|
| 466 |
+
"u_value": floor_u_value,
|
| 467 |
+
"r_value": 1.0 / floor_u_value if floor_u_value > 0 else 1.0,
|
| 468 |
+
"description": "Custom floor with user-defined properties"
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
with col2:
|
| 472 |
floor_area = st.number_input(
|
| 473 |
"Floor Area (m²)",
|
|
|
|
| 496 |
warnings.append(ValidationWarning(
|
| 497 |
"No walls defined",
|
| 498 |
"Add at least one wall to continue",
|
| 499 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 500 |
))
|
| 501 |
|
| 502 |
# Check if total wall area is reasonable
|
|
|
|
| 766 |
'air_changes': 0.0
|
| 767 |
}
|
| 768 |
|
| 769 |
+
# Initialize internal loads data if not already in session state
|
| 770 |
+
if 'internal_loads' not in st.session_state.heating_form_data:
|
| 771 |
+
st.session_state.heating_form_data['internal_loads'] = {}
|
| 772 |
+
|
| 773 |
+
if 'occupants' not in st.session_state.heating_form_data['internal_loads']:
|
| 774 |
+
st.session_state.heating_form_data['internal_loads']['occupants'] = {
|
| 775 |
+
'count': 4,
|
| 776 |
+
'activity_level': 'seated_resting'
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
if 'lighting' not in st.session_state.heating_form_data['internal_loads']:
|
| 780 |
+
st.session_state.heating_form_data['internal_loads']['lighting'] = {
|
| 781 |
+
'type': 'led',
|
| 782 |
+
'power_density': 5.0 # W/m²
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
if 'appliances' not in st.session_state.heating_form_data['internal_loads']:
|
| 786 |
+
st.session_state.heating_form_data['internal_loads']['appliances'] = {
|
| 787 |
+
'kitchen': True,
|
| 788 |
+
'living_room': True,
|
| 789 |
+
'bedroom': True,
|
| 790 |
+
'office': False
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
# Infiltration section
|
| 794 |
st.write("### Infiltration")
|
| 795 |
st.write("Infiltration is the unintended air leakage through the building envelope.")
|
|
|
|
| 821 |
st.write("### Ventilation")
|
| 822 |
st.write("Ventilation is the intentional introduction of outside air into the building.")
|
| 823 |
|
| 824 |
+
# Internal Loads section
|
| 825 |
+
st.write("### Internal Loads")
|
| 826 |
+
st.write("Internal loads are heat sources inside the building that reduce heating requirements.")
|
| 827 |
+
|
| 828 |
+
# Occupants section
|
| 829 |
+
st.write("#### Occupants")
|
| 830 |
+
|
| 831 |
+
col1, col2 = st.columns(2)
|
| 832 |
+
|
| 833 |
+
with col1:
|
| 834 |
+
occupant_count = st.number_input(
|
| 835 |
+
"Number of Occupants",
|
| 836 |
+
value=int(st.session_state.heating_form_data['internal_loads']['occupants'].get('count', 4)),
|
| 837 |
+
min_value=1,
|
| 838 |
+
step=1,
|
| 839 |
+
key="occupant_count_heating"
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
with col2:
|
| 843 |
+
# Get activity level options from reference data
|
| 844 |
+
activity_options = {act_id: act_data['name'] for act_id, act_data in ref_data.internal_loads['people'].items()}
|
| 845 |
+
|
| 846 |
+
activity_level = st.selectbox(
|
| 847 |
+
"Activity Level",
|
| 848 |
+
options=list(activity_options.keys()),
|
| 849 |
+
format_func=lambda x: activity_options[x],
|
| 850 |
+
index=list(activity_options.keys()).index(st.session_state.heating_form_data['internal_loads']['occupants'].get('activity_level', 'seated_resting')) if st.session_state.heating_form_data['internal_loads']['occupants'].get('activity_level') in activity_options else 0,
|
| 851 |
+
key="activity_level_heating"
|
| 852 |
+
)
|
| 853 |
+
|
| 854 |
+
# Get heat gain per person
|
| 855 |
+
activity_data = ref_data.get_internal_load('people', activity_level)
|
| 856 |
+
sensible_heat_pp = activity_data['sensible_heat']
|
| 857 |
+
latent_heat_pp = activity_data['latent_heat']
|
| 858 |
+
total_heat_pp = sensible_heat_pp + latent_heat_pp
|
| 859 |
+
|
| 860 |
+
st.write(f"Heat gain per person: {total_heat_pp} W ({sensible_heat_pp} W sensible + {latent_heat_pp} W latent)")
|
| 861 |
+
st.write(f"Total occupant heat gain: {total_heat_pp * occupant_count} W")
|
| 862 |
+
|
| 863 |
+
# Save occupants data
|
| 864 |
+
st.session_state.heating_form_data['internal_loads']['occupants'] = {
|
| 865 |
+
'count': occupant_count,
|
| 866 |
+
'activity_level': activity_level,
|
| 867 |
+
'sensible_heat_pp': sensible_heat_pp,
|
| 868 |
+
'latent_heat_pp': latent_heat_pp,
|
| 869 |
+
'total_heat_gain': total_heat_pp * occupant_count
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
# Lighting section
|
| 873 |
+
st.write("#### Lighting")
|
| 874 |
+
|
| 875 |
+
col1, col2 = st.columns(2)
|
| 876 |
+
|
| 877 |
+
with col1:
|
| 878 |
+
# Get lighting type options from reference data
|
| 879 |
+
lighting_options = {light_id: light_data['name'] for light_id, light_data in ref_data.internal_loads['lighting'].items()}
|
| 880 |
+
|
| 881 |
+
lighting_type = st.selectbox(
|
| 882 |
+
"Lighting Type",
|
| 883 |
+
options=list(lighting_options.keys()),
|
| 884 |
+
format_func=lambda x: lighting_options[x],
|
| 885 |
+
index=list(lighting_options.keys()).index(st.session_state.heating_form_data['internal_loads']['lighting'].get('type', 'led')) if st.session_state.heating_form_data['internal_loads']['lighting'].get('type') in lighting_options else 0,
|
| 886 |
+
key="lighting_type_heating"
|
| 887 |
+
)
|
| 888 |
+
|
| 889 |
+
with col2:
|
| 890 |
+
lighting_power_density = st.number_input(
|
| 891 |
+
"Lighting Power Density (W/m²)",
|
| 892 |
+
value=float(st.session_state.heating_form_data['internal_loads']['lighting'].get('power_density', 5.0)),
|
| 893 |
+
min_value=1.0,
|
| 894 |
+
max_value=20.0,
|
| 895 |
+
step=0.5,
|
| 896 |
+
help="Typical values: Residential 5-10 W/m², Office 10-15 W/m²",
|
| 897 |
+
key="lighting_power_density_heating"
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
+
# Get lighting heat factor
|
| 901 |
+
lighting_data = ref_data.get_internal_load('lighting', lighting_type)
|
| 902 |
+
lighting_heat_factor = lighting_data['heat_factor']
|
| 903 |
+
|
| 904 |
+
# Calculate lighting heat gain
|
| 905 |
+
floor_area = st.session_state.heating_form_data['building_info'].get('floor_area', 80.0)
|
| 906 |
+
lighting_heat_gain = lighting_power_density * floor_area * lighting_heat_factor
|
| 907 |
+
|
| 908 |
+
st.write(f"Lighting heat factor: {lighting_heat_factor}")
|
| 909 |
+
st.write(f"Total lighting heat gain: {lighting_heat_gain:.2f} W")
|
| 910 |
+
|
| 911 |
+
# Save lighting data
|
| 912 |
+
st.session_state.heating_form_data['internal_loads']['lighting'] = {
|
| 913 |
+
'type': lighting_type,
|
| 914 |
+
'power_density': lighting_power_density,
|
| 915 |
+
'heat_factor': lighting_heat_factor,
|
| 916 |
+
'total_heat_gain': lighting_heat_gain
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
# Equipment section
|
| 920 |
+
st.write("#### Equipment")
|
| 921 |
+
st.write("Select the equipment present in your space:")
|
| 922 |
+
|
| 923 |
+
col1, col2 = st.columns(2)
|
| 924 |
+
|
| 925 |
+
with col1:
|
| 926 |
+
has_kitchen = st.checkbox(
|
| 927 |
+
"Kitchen Appliances",
|
| 928 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('kitchen', True),
|
| 929 |
+
help="Refrigerator, stove, microwave, etc.",
|
| 930 |
+
key="has_kitchen_heating"
|
| 931 |
+
)
|
| 932 |
+
|
| 933 |
+
has_living_room = st.checkbox(
|
| 934 |
+
"Living Room Equipment",
|
| 935 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('living_room', True),
|
| 936 |
+
help="TV, audio equipment, etc.",
|
| 937 |
+
key="has_living_room_heating"
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
with col2:
|
| 941 |
+
has_bedroom = st.checkbox(
|
| 942 |
+
"Bedroom Equipment",
|
| 943 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('bedroom', True),
|
| 944 |
+
help="TV, chargers, etc.",
|
| 945 |
+
key="has_bedroom_heating"
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
has_office = st.checkbox(
|
| 949 |
+
"Office Equipment",
|
| 950 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('office', False),
|
| 951 |
+
help="Computer, printer, etc.",
|
| 952 |
+
key="has_office_heating"
|
| 953 |
+
)
|
| 954 |
+
|
| 955 |
+
# Calculate equipment heat gain
|
| 956 |
+
equipment_watts = 0
|
| 957 |
+
|
| 958 |
+
if has_kitchen:
|
| 959 |
+
equipment_watts += 1000 # Kitchen appliances
|
| 960 |
+
if has_living_room:
|
| 961 |
+
equipment_watts += 300 # Living room equipment
|
| 962 |
+
if has_bedroom:
|
| 963 |
+
equipment_watts += 150 # Bedroom equipment
|
| 964 |
+
if has_office:
|
| 965 |
+
equipment_watts += 450 # Office equipment
|
| 966 |
+
|
| 967 |
+
st.write(f"Total equipment heat gain: {equipment_watts} W")
|
| 968 |
+
|
| 969 |
+
# Save appliances data
|
| 970 |
+
st.session_state.heating_form_data['internal_loads']['appliances'] = {
|
| 971 |
+
'kitchen': has_kitchen,
|
| 972 |
+
'living_room': has_living_room,
|
| 973 |
+
'bedroom': has_bedroom,
|
| 974 |
+
'office': has_office,
|
| 975 |
+
'total_heat_gain': equipment_watts
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
# Calculate total internal heat gain
|
| 979 |
+
total_internal_gain = (
|
| 980 |
+
st.session_state.heating_form_data['internal_loads']['occupants']['total_heat_gain'] +
|
| 981 |
+
st.session_state.heating_form_data['internal_loads']['lighting']['total_heat_gain'] +
|
| 982 |
+
st.session_state.heating_form_data['internal_loads']['appliances']['total_heat_gain']
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
st.write(f"Total internal heat gain: {total_internal_gain:.2f} W")
|
| 986 |
+
|
| 987 |
+
# Save total internal gain
|
| 988 |
+
st.session_state.heating_form_data['internal_loads']['total_heat_gain'] = total_internal_gain
|
| 989 |
+
|
| 990 |
col1, col2 = st.columns(2)
|
| 991 |
|
| 992 |
with col1:
|
|
|
|
| 1288 |
'temp_diff': infiltration.get('temp_diff', 0)
|
| 1289 |
}
|
| 1290 |
|
| 1291 |
+
# Prepare internal loads data
|
| 1292 |
+
internal_loads = None
|
| 1293 |
+
if 'internal_loads' in form_data:
|
| 1294 |
+
internal_loads = {
|
| 1295 |
+
'num_people': form_data['internal_loads']['occupants'].get('count', 0),
|
| 1296 |
+
'has_kitchen': form_data['internal_loads']['appliances'].get('kitchen', False),
|
| 1297 |
+
'equipment_watts': form_data['internal_loads']['appliances'].get('total_heat_gain', 0)
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
# Calculate heating load
|
| 1301 |
results = calculator.calculate_total_heating_load(
|
| 1302 |
building_components=building_components,
|
| 1303 |
+
infiltration=infiltration_data,
|
| 1304 |
+
internal_gains=internal_loads
|
| 1305 |
)
|
| 1306 |
|
| 1307 |
# Calculate annual heating requirement
|
|
|
|
| 1384 |
values=list(component_losses.values()),
|
| 1385 |
names=list(component_losses.keys()),
|
| 1386 |
title="Heating Load Components",
|
| 1387 |
+
color_discrete_sequence=px.colors.sequential.Viridis,
|
| 1388 |
+
hole=0.4, # Create a donut chart for better readability
|
| 1389 |
+
labels={'label': 'Component', 'value': 'Heat Loss (W)'}
|
| 1390 |
+
)
|
| 1391 |
+
|
| 1392 |
+
# Improve layout and formatting
|
| 1393 |
+
fig.update_traces(
|
| 1394 |
+
textposition='inside',
|
| 1395 |
+
textinfo='percent+label',
|
| 1396 |
+
hoverinfo='label+percent+value',
|
| 1397 |
+
marker=dict(line=dict(color='#FFFFFF', width=2))
|
| 1398 |
+
)
|
| 1399 |
+
|
| 1400 |
+
# Improve layout
|
| 1401 |
+
fig.update_layout(
|
| 1402 |
+
legend_title_text='Building Components',
|
| 1403 |
+
font=dict(size=14),
|
| 1404 |
+
title_font=dict(size=18),
|
| 1405 |
+
title_x=0.5, # Center the title
|
| 1406 |
+
margin=dict(t=50, b=50, l=50, r=50)
|
| 1407 |
)
|
| 1408 |
|
| 1409 |
st.plotly_chart(fig)
|
|
|
|
| 1414 |
'Infiltration & Ventilation': results.get('infiltration_loss', 0)
|
| 1415 |
}
|
| 1416 |
|
| 1417 |
+
# Add internal gains and solar gains if available
|
| 1418 |
+
if 'internal_gain' in results and results['internal_gain'] > 0:
|
| 1419 |
+
load_components['Internal Gains (reduction)'] = -results['internal_gain']
|
| 1420 |
+
|
| 1421 |
+
if 'wall_solar_gain' in results and results['wall_solar_gain'] > 0:
|
| 1422 |
+
load_components['Solar Gains (reduction)'] = -results['wall_solar_gain']
|
| 1423 |
+
|
| 1424 |
load_df = pd.DataFrame({
|
| 1425 |
'Component': list(load_components.keys()),
|
| 1426 |
'Load (W)': list(load_components.values()),
|
| 1427 |
+
'Percentage (%)': [abs(value) / results['total_load'] * 100 for value in load_components.values()]
|
| 1428 |
})
|
| 1429 |
|
| 1430 |
st.dataframe(load_df.style.format({
|
|
|
|
| 1508 |
y='Heat Loss (W)',
|
| 1509 |
title="Heat Loss by Building Component",
|
| 1510 |
color='Component',
|
| 1511 |
+
color_discrete_sequence=px.colors.sequential.Viridis,
|
| 1512 |
+
text='Heat Loss (W)'
|
| 1513 |
+
)
|
| 1514 |
+
|
| 1515 |
+
# Improve layout and formatting
|
| 1516 |
+
fig.update_traces(
|
| 1517 |
+
texttemplate='%{text:.1f} W',
|
| 1518 |
+
textposition='outside',
|
| 1519 |
+
hovertemplate='<b>%{x}</b><br>Heat Loss: %{y:.1f} W<extra></extra>'
|
| 1520 |
+
)
|
| 1521 |
+
|
| 1522 |
+
# Improve layout
|
| 1523 |
+
fig.update_layout(
|
| 1524 |
+
xaxis_title="Building Component",
|
| 1525 |
+
yaxis_title="Heat Loss (W)",
|
| 1526 |
+
font=dict(size=14),
|
| 1527 |
+
title_font=dict(size=18),
|
| 1528 |
+
title_x=0.5, # Center the title
|
| 1529 |
+
margin=dict(t=50, b=50, l=50, r=50),
|
| 1530 |
+
xaxis={'categoryorder':'total descending'} # Sort by highest heat loss
|
| 1531 |
)
|
| 1532 |
|
| 1533 |
st.plotly_chart(fig)
|
|
|
|
| 1571 |
y='Heat Loss (W)',
|
| 1572 |
title="Ventilation & Infiltration Heat Losses",
|
| 1573 |
color='Source',
|
| 1574 |
+
color_discrete_sequence=px.colors.sequential.Plasma,
|
| 1575 |
+
text='Heat Loss (W)'
|
| 1576 |
+
)
|
| 1577 |
+
|
| 1578 |
+
# Improve layout and formatting
|
| 1579 |
+
fig.update_traces(
|
| 1580 |
+
texttemplate='%{text:.1f} W',
|
| 1581 |
+
textposition='outside',
|
| 1582 |
+
hovertemplate='<b>%{x}</b><br>Heat Loss: %{y:.1f} W<br>Air Changes: %{customdata[0]:.2f} ACH<extra></extra>'
|
| 1583 |
+
)
|
| 1584 |
+
|
| 1585 |
+
# Add custom data for hover
|
| 1586 |
+
fig.update_traces(customdata=ventilation_df[['Air Changes per Hour']])
|
| 1587 |
+
|
| 1588 |
+
# Improve layout
|
| 1589 |
+
fig.update_layout(
|
| 1590 |
+
xaxis_title="Ventilation Source",
|
| 1591 |
+
yaxis_title="Heat Loss (W)",
|
| 1592 |
+
font=dict(size=14),
|
| 1593 |
+
title_font=dict(size=18),
|
| 1594 |
+
title_x=0.5, # Center the title
|
| 1595 |
+
margin=dict(t=50, b=50, l=50, r=50)
|
| 1596 |
)
|
| 1597 |
|
| 1598 |
st.plotly_chart(fig)
|
|
|
|
| 1753 |
"6. Results"
|
| 1754 |
])
|
| 1755 |
|
| 1756 |
+
# Add direct navigation buttons at the top
|
| 1757 |
+
st.write("### Navigation")
|
| 1758 |
+
st.write("Click on any button below to navigate directly to that section:")
|
| 1759 |
+
|
| 1760 |
+
col1, col2, col3 = st.columns(3)
|
| 1761 |
+
with col1:
|
| 1762 |
+
if st.button("1. Building Information", key="direct_nav_heating_info"):
|
| 1763 |
+
st.session_state.heating_active_tab = "building_info"
|
| 1764 |
+
st.experimental_rerun()
|
| 1765 |
+
|
| 1766 |
+
if st.button("2. Building Envelope", key="direct_nav_heating_envelope"):
|
| 1767 |
+
st.session_state.heating_active_tab = "building_envelope"
|
| 1768 |
+
st.experimental_rerun()
|
| 1769 |
+
|
| 1770 |
+
with col2:
|
| 1771 |
+
if st.button("3. Windows & Doors", key="direct_nav_heating_windows"):
|
| 1772 |
+
st.session_state.heating_active_tab = "windows"
|
| 1773 |
+
st.experimental_rerun()
|
| 1774 |
+
|
| 1775 |
+
if st.button("4. Ventilation", key="direct_nav_heating_ventilation"):
|
| 1776 |
+
st.session_state.heating_active_tab = "ventilation"
|
| 1777 |
+
st.experimental_rerun()
|
| 1778 |
+
|
| 1779 |
+
with col3:
|
| 1780 |
+
if st.button("5. Occupancy", key="direct_nav_heating_occupancy"):
|
| 1781 |
+
st.session_state.heating_active_tab = "occupancy"
|
| 1782 |
+
st.experimental_rerun()
|
| 1783 |
+
|
| 1784 |
+
if st.button("6. Results", key="direct_nav_heating_results"):
|
| 1785 |
+
# Only enable if all previous steps are completed
|
| 1786 |
+
if all(st.session_state.heating_completed.values()):
|
| 1787 |
+
st.session_state.heating_active_tab = "results"
|
| 1788 |
+
st.experimental_rerun()
|
| 1789 |
+
else:
|
| 1790 |
+
st.warning("Please complete all previous steps before viewing results.")
|
| 1791 |
+
|
| 1792 |
# Display the active tab
|
| 1793 |
with tabs[0]:
|
| 1794 |
if st.session_state.heating_active_tab == "building_info":
|
reference_data.py
CHANGED
|
@@ -481,6 +481,21 @@ class ReferenceData:
|
|
| 481 |
Returns:
|
| 482 |
dict: Material properties
|
| 483 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
if material_type in self.materials and material_id in self.materials[material_type]:
|
| 485 |
return self.materials[material_type][material_id]
|
| 486 |
return None
|
|
|
|
| 481 |
Returns:
|
| 482 |
dict: Material properties
|
| 483 |
"""
|
| 484 |
+
# Check if this is a custom material (custom_[type])
|
| 485 |
+
if material_id == f"custom_{material_type}":
|
| 486 |
+
# Return the custom material from session state if available
|
| 487 |
+
import streamlit as st
|
| 488 |
+
if "custom_materials" in st.session_state and material_type in st.session_state.custom_materials:
|
| 489 |
+
return st.session_state.custom_materials[material_type]
|
| 490 |
+
# Return a default custom material template if not in session state
|
| 491 |
+
return {
|
| 492 |
+
"name": f"Custom {material_type[:-1]}", # Remove 's' from end
|
| 493 |
+
"u_value": 1.0, # Default U-value
|
| 494 |
+
"r_value": 1.0, # Default R-value
|
| 495 |
+
"description": f"Custom {material_type[:-1]} with user-defined properties"
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
# Return predefined material
|
| 499 |
if material_type in self.materials and material_id in self.materials[material_type]:
|
| 500 |
return self.materials[material_type][material_id]
|
| 501 |
return None
|
utils/validation.py
CHANGED
|
@@ -45,7 +45,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 45 |
warnings.append(ValidationWarning(
|
| 46 |
"Required field is empty",
|
| 47 |
"Please provide a value for this field",
|
| 48 |
-
is_critical=True
|
| 49 |
))
|
| 50 |
is_valid = False
|
| 51 |
|
|
@@ -65,7 +65,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 65 |
warnings.append(ValidationWarning(
|
| 66 |
f"Value is below minimum ({min_value})",
|
| 67 |
f"Please enter a value greater than or equal to {min_value}",
|
| 68 |
-
is_critical=
|
| 69 |
))
|
| 70 |
is_valid = False
|
| 71 |
|
|
@@ -74,7 +74,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 74 |
warnings.append(ValidationWarning(
|
| 75 |
f"Value exceeds maximum ({max_value})",
|
| 76 |
f"Please enter a value less than or equal to {max_value}",
|
| 77 |
-
is_critical=
|
| 78 |
))
|
| 79 |
is_valid = False
|
| 80 |
|
|
@@ -82,7 +82,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 82 |
warnings.append(ValidationWarning(
|
| 83 |
"Invalid number format",
|
| 84 |
"Please enter a valid number",
|
| 85 |
-
is_critical=True
|
| 86 |
))
|
| 87 |
is_valid = False
|
| 88 |
|
|
|
|
| 45 |
warnings.append(ValidationWarning(
|
| 46 |
"Required field is empty",
|
| 47 |
"Please provide a value for this field",
|
| 48 |
+
is_critical=True # Keep required fields as critical
|
| 49 |
))
|
| 50 |
is_valid = False
|
| 51 |
|
|
|
|
| 65 |
warnings.append(ValidationWarning(
|
| 66 |
f"Value is below minimum ({min_value})",
|
| 67 |
f"Please enter a value greater than or equal to {min_value}",
|
| 68 |
+
is_critical=False # Changed to non-critical
|
| 69 |
))
|
| 70 |
is_valid = False
|
| 71 |
|
|
|
|
| 74 |
warnings.append(ValidationWarning(
|
| 75 |
f"Value exceeds maximum ({max_value})",
|
| 76 |
f"Please enter a value less than or equal to {max_value}",
|
| 77 |
+
is_critical=False # Changed to non-critical
|
| 78 |
))
|
| 79 |
is_valid = False
|
| 80 |
|
|
|
|
| 82 |
warnings.append(ValidationWarning(
|
| 83 |
"Invalid number format",
|
| 84 |
"Please enter a valid number",
|
| 85 |
+
is_critical=True # Keep format validation as critical
|
| 86 |
))
|
| 87 |
is_valid = False
|
| 88 |
|