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 |
|