mabuseif commited on
Commit
a1d7129
·
verified ·
1 Parent(s): 60cca7c

Upload 13 files

Browse files
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 'temp_diff' for each component
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 = sum(
153
- self.calculate_conduction_heat_gain(comp['area'], comp['u_value'], comp['temp_diff'])
154
- for comp in building_components
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 'temp_diff' for each component
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 total heating load
176
- total_load = total_conduction_loss + infiltration_loss
 
 
 
 
 
 
 
 
 
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=True
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
- walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
299
- walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
 
 
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=True
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.qualitative.Set2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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='indianred'
 
 
 
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='lightsalmon'
 
 
 
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=True
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
- walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
284
- walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
 
 
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=True
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.qualitative.Set2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.qualitative.Set3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.qualitative.Pastel2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=True
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=True
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