mabuseif commited on
Commit
40c26e0
·
verified ·
1 Parent(s): 4f78072

Upload component_selection.py

Browse files
Files changed (1) hide show
  1. app/component_selection.py +837 -145
app/component_selection.py CHANGED
@@ -1,9 +1,11 @@
1
  """
2
- HVAC Component Selection Module
3
  Provides UI for selecting building components in the HVAC Load Calculator.
4
  All dependencies are included within this file for standalone operation.
5
- Updated 2025-04-28: Added perimeter field to Floor class for F-factor calculations.
6
- Updated 2025-04-29: Fixed Floors table headings, added insulated field for dynamic F-factor.
 
 
7
  """
8
 
9
  import streamlit as st
@@ -35,6 +37,37 @@ class ComponentType(Enum):
35
  FLOOR = "Floor"
36
  WINDOW = "Window"
37
  DOOR = "Door"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  # --- Data Models ---
40
  @dataclass
@@ -71,6 +104,9 @@ class Wall(BuildingComponent):
71
  absorptivity: float = 0.6
72
  shading_coefficient: float = 1.0
73
  infiltration_rate_cfm: float = 0.0
 
 
 
74
 
75
  def __post_init__(self):
76
  super().__post_init__()
@@ -81,16 +117,25 @@ class Wall(BuildingComponent):
81
  raise ValueError("Shading coefficient must be between 0 and 1")
82
  if self.infiltration_rate_cfm < 0:
83
  raise ValueError("Infiltration rate cannot be negative")
 
 
 
 
84
  VALID_WALL_GROUPS = {"A", "B", "C", "D", "E", "F", "G", "H"}
85
  if self.wall_group not in VALID_WALL_GROUPS:
86
  st.warning(f"Invalid wall_group '{self.wall_group}' for wall '{self.name}'. Defaulting to 'A'.")
87
  self.wall_group = "A"
 
 
 
 
88
 
89
  def to_dict(self) -> dict:
90
  base_dict = super().to_dict()
91
  base_dict.update({
92
  "wall_type": self.wall_type, "wall_group": self.wall_group, "absorptivity": self.absorptivity,
93
- "shading_coefficient": self.shading_coefficient, "infiltration_rate_cfm": self.infiltration_rate_cfm
 
94
  })
95
  return base_dict
96
 
@@ -100,6 +145,8 @@ class Roof(BuildingComponent):
100
  roof_group: str = "A" # ASHRAE group
101
  slope: str = "Flat"
102
  absorptivity: float = 0.6
 
 
103
 
104
  def __post_init__(self):
105
  super().__post_init__()
@@ -108,15 +155,23 @@ class Roof(BuildingComponent):
108
  self.orientation = Orientation.HORIZONTAL
109
  if not 0 <= self.absorptivity <= 1:
110
  raise ValueError("Absorptivity must be between 0 and 1")
 
 
111
  VALID_ROOF_GROUPS = {"A", "B", "C", "D", "E", "F", "G"}
112
  if self.roof_group not in VALID_ROOF_GROUPS:
113
  st.warning(f"Invalid roof_group '{self.roof_group}' for roof '{self.name}'. Defaulting to 'A'.")
114
  self.roof_group = "A"
 
 
 
 
115
 
116
  def to_dict(self) -> dict:
117
  base_dict = super().to_dict()
118
  base_dict.update({
119
- "roof_type": self.roof_type, "roof_group": self.roof_group, "slope": self.slope, "absorptivity": self.absorptivity
 
 
120
  })
121
  return base_dict
122
 
@@ -126,7 +181,7 @@ class Floor(BuildingComponent):
126
  ground_contact: bool = True
127
  ground_temperature_c: float = 25.0
128
  perimeter: float = 0.0
129
- insulated: bool = False # NEW: For dynamic F-factor
130
 
131
  def __post_init__(self):
132
  super().__post_init__()
@@ -151,9 +206,13 @@ class Window(BuildingComponent):
151
  shgc: float = 0.7
152
  shading_device: str = "None"
153
  shading_coefficient: float = 1.0
154
- frame_type: str = "Aluminum"
155
  frame_percentage: float = 20.0
156
  infiltration_rate_cfm: float = 0.0
 
 
 
 
157
 
158
  def __post_init__(self):
159
  super().__post_init__()
@@ -166,12 +225,25 @@ class Window(BuildingComponent):
166
  raise ValueError("Frame percentage must be between 0 and 30")
167
  if self.infiltration_rate_cfm < 0:
168
  raise ValueError("Infiltration rate cannot be negative")
 
 
 
 
 
 
 
 
 
 
169
 
170
  def to_dict(self) -> dict:
171
  base_dict = super().to_dict()
172
  base_dict.update({
173
  "shgc": self.shgc, "shading_device": self.shading_device, "shading_coefficient": self.shading_coefficient,
174
- "frame_type": self.frame_type, "frame_percentage": self.frame_percentage, "infiltration_rate_cfm": self.infiltration_rate_cfm
 
 
 
175
  })
176
  return base_dict
177
 
@@ -179,16 +251,72 @@ class Window(BuildingComponent):
179
  class Door(BuildingComponent):
180
  door_type: str = "Solid Wood"
181
  infiltration_rate_cfm: float = 0.0
 
 
182
 
183
  def __post_init__(self):
184
  super().__post_init__()
185
  self.component_type = ComponentType.DOOR
186
  if self.infiltration_rate_cfm < 0:
187
  raise ValueError("Infiltration rate cannot be negative")
 
 
 
 
188
 
189
  def to_dict(self) -> dict:
190
  base_dict = super().to_dict()
191
- base_dict.update({"door_type": self.door_type, "infiltration_rate_cfm": self.infiltration_rate_cfm})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  return base_dict
193
 
194
  # --- Reference Data ---
@@ -216,7 +344,11 @@ class ReferenceData:
216
  },
217
  "roof_types": {
218
  "Concrete Roof": {"u_value": 0.3, "absorptivity": 0.6, "group": "A"},
219
- "Metal Roof": {"u_value": 1.0, "absorptivity": 0.75, "group": "B"}
 
 
 
 
220
  },
221
  "roof_ventilation_methods": {
222
  "No Ventilation": 0.0,
@@ -226,22 +358,57 @@ class ReferenceData:
226
  },
227
  "floor_types": {
228
  "Concrete Slab": {"u_value": 0.4, "ground_contact": True},
229
- "Wood Floor": {"u_value": 0.8, "ground_contact": False}
 
 
 
 
230
  },
231
  "window_types": {
232
- "Double Glazed": {"u_value": 2.8, "shgc": 0.7, "frame_type": "Aluminum"},
233
- "Single Glazed": {"u_value": 5.0, "shgc": 0.9, "frame_type": "Wood"}
 
 
 
 
 
234
  },
235
  "shading_devices": {
236
  "None": 1.0,
237
  "Venetian Blinds": 0.6,
238
  "Overhang": 0.4,
239
  "Roller Shades": 0.5,
240
- "Drapes": 0.7
 
241
  },
242
  "door_types": {
243
  "Solid Wood": {"u_value": 2.0},
244
- "Glass Door": {"u_value": 3.5}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  }
246
  }
247
 
@@ -287,13 +454,13 @@ class ComponentSelectionInterface:
287
  st.title("Building Components")
288
 
289
  if 'components' not in session_state:
290
- session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
291
  if 'roof_air_volume_m3' not in session_state:
292
  session_state.roof_air_volume_m3 = 0.0
293
  if 'roof_ventilation_ach' not in session_state:
294
  session_state.roof_ventilation_ach = 0.0
295
 
296
- tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
297
 
298
  with tabs[0]:
299
  self._display_component_tab(session_state, ComponentType.WALL)
@@ -306,6 +473,8 @@ class ComponentSelectionInterface:
306
  with tabs[4]:
307
  self._display_component_tab(session_state, ComponentType.DOOR)
308
  with tabs[5]:
 
 
309
  self._display_u_value_calculator_tab(session_state)
310
 
311
  if st.button("Save Components"):
@@ -326,6 +495,8 @@ class ComponentSelectionInterface:
326
  self._display_add_window_form(session_state)
327
  elif component_type == ComponentType.DOOR:
328
  self._display_add_door_form(session_state)
 
 
329
 
330
  components = session_state.components.get(type_name + 's', [])
331
  if components or component_type == ComponentType.ROOF:
@@ -345,6 +516,8 @@ class ComponentSelectionInterface:
345
  name = st.text_input("Name", "New Wall")
346
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
347
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
 
 
348
  with col2:
349
  wall_options = self.reference_data.data["wall_types"]
350
  selected_wall = st.selectbox("Wall Type", options=list(wall_options.keys()))
@@ -353,6 +526,7 @@ class ComponentSelectionInterface:
353
  absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2)
354
  shading_coefficient = st.number_input("Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05)
355
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
 
356
 
357
  submitted = st.form_submit_button("Add Wall")
358
  if submitted and not session_state.add_wall_submitted:
@@ -361,7 +535,8 @@ class ComponentSelectionInterface:
361
  new_wall = Wall(
362
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
363
  wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value,
364
- shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate
 
365
  )
366
  self.component_library.add_component(new_wall)
367
  session_state.components['walls'].append(new_wall)
@@ -376,25 +551,34 @@ class ComponentSelectionInterface:
376
 
377
  elif method == "File Upload":
378
  uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key="wall_upload")
379
- required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)"]
 
 
 
 
380
  template_data = pd.DataFrame(columns=required_cols)
381
- template_data.loc[0] = ["Example Wall", 10.0, 2.0, "North", "Brick Wall", "A", 0.6, 1.0, 0.0]
382
- st.download_button(label="Download Wall Template", data=template_data.to_csv(index=False), file_name="wall_template.csv", mime="text/csv")
 
 
 
 
 
 
 
383
  if uploaded_file:
384
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
385
  if all(col in df.columns for col in required_cols):
386
- valid_wall_groups = {"A", "B", "C", "D", "E", "F", "G", "H"}
387
  for _, row in df.iterrows():
388
  try:
389
- wall_group = str(row["Wall Group"])
390
- if wall_group not in valid_wall_groups:
391
- st.warning(f"Invalid Wall Group '{wall_group}' in row '{row['Name']}'. Defaulting to 'A'.")
392
- wall_group = "A"
393
  new_wall = Wall(
394
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
395
  orientation=Orientation(row["Orientation"]), wall_type=str(row["Wall Type"]),
396
- wall_group=wall_group, absorptivity=float(row["Absorptivity"]),
397
- shading_coefficient=float(row["Shading Coefficient"]), infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
 
 
 
398
  )
399
  self.component_library.add_component(new_wall)
400
  session_state.components['walls'].append(new_wall)
@@ -411,37 +595,33 @@ class ComponentSelectionInterface:
411
  if "add_roof_submitted" not in session_state:
412
  session_state.add_roof_submitted = False
413
 
414
- st.subheader("Roof System Ventilation")
415
- air_volume = st.number_input("Air Volume (m³)", min_value=0.0, value=session_state.roof_air_volume_m3, step=1.0, help="Total volume between roof and ceiling")
416
- vent_options = {f"{k} (ACH={v})": v for k, v in self.reference_data.data["roof_ventilation_methods"].items()}
417
- vent_options["Custom"] = None
418
- ventilation_method = st.selectbox("Ventilation Method", options=list(vent_options.keys()), index=0, help="Applies to entire roof system")
419
- ventilation_ach = st.number_input("Custom Ventilation Rate (ACH)", min_value=0.0, max_value=10.0, value=0.0, step=0.1) if ventilation_method == "Custom" else vent_options[ventilation_method]
420
- session_state.roof_air_volume_m3 = air_volume
421
- session_state.roof_ventilation_ach = ventilation_ach
422
-
423
  if method == "Manual Entry":
424
  with st.form("add_roof_form", clear_on_submit=True):
425
  col1, col2 = st.columns(2)
426
  with col1:
427
  name = st.text_input("Name", "New Roof")
428
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
429
- orientation = Orientation.HORIZONTAL.value
 
430
  with col2:
431
  roof_options = self.reference_data.data["roof_types"]
432
  selected_roof = st.selectbox("Roof Type", options=list(roof_options.keys()))
433
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(roof_options[selected_roof]["u_value"]), step=0.01)
434
  roof_group = st.selectbox("Roof Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G"], index=0)
435
- slope = st.selectbox("Slope", ["Flat", "Pitched"], index=0)
436
- absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2)
 
 
437
 
438
  submitted = st.form_submit_button("Add Roof")
439
  if submitted and not session_state.add_roof_submitted:
440
  try:
441
  absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
442
  new_roof = Roof(
443
- name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
444
- roof_type=selected_roof, roof_group=roof_group, slope=slope, absorptivity=absorptivity_value
 
 
445
  )
446
  self.component_library.add_component(new_roof)
447
  session_state.components['roofs'].append(new_roof)
@@ -454,26 +634,48 @@ class ComponentSelectionInterface:
454
  if session_state.add_roof_submitted:
455
  session_state.add_roof_submitted = False
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  elif method == "File Upload":
458
  uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload")
459
- required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity"]
 
 
 
460
  template_data = pd.DataFrame(columns=required_cols)
461
- template_data.loc[0] = ["Example Roof", 10.0, 0.3, "Horizontal", "Concrete Roof", "A", "Flat", 0.6]
462
- st.download_button(label="Download Roof Template", data=template_data.to_csv(index=False), file_name="roof_template.csv", mime="text/csv")
 
 
 
 
 
 
 
463
  if uploaded_file:
464
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
465
  if all(col in df.columns for col in required_cols):
466
- valid_roof_groups = {"A", "B", "C", "D", "E", "F", "G"}
467
  for _, row in df.iterrows():
468
  try:
469
- roof_group = str(row["Roof Group"])
470
- if roof_group not in valid_roof_groups:
471
- st.warning(f"Invalid Roof Group '{roof_group}' in row '{row['Name']}'. Defaulting to 'A'.")
472
- roof_group = "A"
473
  new_roof = Roof(
474
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
475
- orientation=Orientation(row["Orientation"]), roof_type=str(row["Roof Type"]),
476
- roof_group=roof_group, slope=str(row["Slope"]), absorptivity=float(row["Absorptivity"])
 
 
477
  )
478
  self.component_library.add_component(new_roof)
479
  session_state.components['roofs'].append(new_roof)
@@ -496,14 +698,14 @@ class ComponentSelectionInterface:
496
  with col1:
497
  name = st.text_input("Name", "New Floor")
498
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
499
- perimeter = st.number_input("Perimeter (m)", min_value=0.0, value=0.0, step=0.1)
500
  with col2:
501
  floor_options = self.reference_data.data["floor_types"]
502
  selected_floor = st.selectbox("Floor Type", options=list(floor_options.keys()))
503
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(floor_options[selected_floor]["u_value"]), step=0.01)
504
  ground_contact = st.selectbox("Ground Contact", ["Yes", "No"], index=0 if floor_options[selected_floor]["ground_contact"] else 1)
505
  ground_temp = st.number_input("Ground Temperature (°C)", min_value=-10.0, max_value=40.0, value=25.0, step=0.1) if ground_contact == "Yes" else 25.0
506
- insulated = st.checkbox("Insulated Floor (e.g., R-10)", value=False) # NEW: Insulation option
507
 
508
  submitted = st.form_submit_button("Add Floor")
509
  if submitted and not session_state.add_floor_submitted:
@@ -526,10 +728,18 @@ class ComponentSelectionInterface:
526
 
527
  elif method == "File Upload":
528
  uploaded_file = st.file_uploader("Upload Floors File", type=["csv", "xlsx"], key="floor_upload")
529
- required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)", "Perimeter (m)", "Insulated"] # NEW: Added Insulated
 
 
 
530
  template_data = pd.DataFrame(columns=required_cols)
531
  template_data.loc[0] = ["Example Floor", 10.0, 0.4, "Concrete Slab", "Yes", 25.0, 12.0, "No"]
532
- st.download_button(label="Download Floor Template", data=template_data.to_csv(index=False), file_name="floor_template.csv", mime="text/csv")
 
 
 
 
 
533
  if uploaded_file:
534
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
535
  if all(col in df.columns for col in required_cols):
@@ -564,18 +774,22 @@ class ComponentSelectionInterface:
564
  name = st.text_input("Name", "New Window")
565
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
566
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
567
- shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
568
- with col2:
569
  window_options = self.reference_data.data["window_types"]
570
  selected_window = st.selectbox("Window Type", options=list(window_options.keys()))
 
 
 
571
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(window_options[selected_window]["u_value"]), step=0.01)
 
 
 
572
  shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
573
  shading_options["Custom"] = "Custom"
574
  shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
575
  shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]]
576
- frame_type = st.selectbox("Frame Type", ["Aluminum", "Wood", "Vinyl"], index=0)
577
- frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=20.0)
578
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
 
 
579
 
580
  submitted = st.form_submit_button("Add Window")
581
  if submitted and not session_state.add_window_submitted:
@@ -583,7 +797,9 @@ class ComponentSelectionInterface:
583
  new_window = Window(
584
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
585
  shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
586
- frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate
 
 
587
  )
588
  self.component_library.add_component(new_window)
589
  session_state.components['windows'].append(new_window)
@@ -598,8 +814,22 @@ class ComponentSelectionInterface:
598
 
599
  elif method == "File Upload":
600
  uploaded_file = st.file_uploader("Upload Windows File", type=["csv", "xlsx"], key="window_upload")
601
- required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)"]
602
- st.download_button(label="Download Window Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="window_template.csv", mime="text/csv")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  if uploaded_file:
604
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
605
  if all(col in df.columns for col in required_cols):
@@ -610,7 +840,9 @@ class ComponentSelectionInterface:
610
  orientation=Orientation(row["Orientation"]), shgc=float(row["SHGC"]),
611
  shading_device=str(row["Shading Device"]), shading_coefficient=float(row["Shading Coefficient"]),
612
  frame_type=str(row["Frame Type"]), frame_percentage=float(row["Frame Percentage"]),
613
- infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
 
 
614
  )
615
  self.component_library.add_component(new_window)
616
  session_state.components['windows'].append(new_window)
@@ -634,18 +866,21 @@ class ComponentSelectionInterface:
634
  name = st.text_input("Name", "New Door")
635
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
636
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
 
637
  with col2:
638
  door_options = self.reference_data.data["door_types"]
639
  selected_door = st.selectbox("Door Type", options=list(door_options.keys()))
640
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(door_options[selected_door]["u_value"]), step=0.01)
641
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
 
642
 
643
  submitted = st.form_submit_button("Add Door")
644
  if submitted and not session_state.add_door_submitted:
645
  try:
646
  new_door = Door(
647
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
648
- door_type=selected_door, infiltration_rate_cfm=infiltration_rate
 
649
  )
650
  self.component_library.add_component(new_door)
651
  session_state.components['doors'].append(new_door)
@@ -660,8 +895,18 @@ class ComponentSelectionInterface:
660
 
661
  elif method == "File Upload":
662
  uploaded_file = st.file_uploader("Upload Doors File", type=["csv", "xlsx"], key="door_upload")
663
- required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)"]
664
- st.download_button(label="Download Door Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="door_template.csv", mime="text/csv")
 
 
 
 
 
 
 
 
 
 
665
  if uploaded_file:
666
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
667
  if all(col in df.columns for col in required_cols):
@@ -670,7 +915,8 @@ class ComponentSelectionInterface:
670
  new_door = Door(
671
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
672
  orientation=Orientation(row["Orientation"]), door_type=str(row["Door Type"]),
673
- infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
 
674
  )
675
  self.component_library.add_component(new_door)
676
  session_state.components['doors'].append(new_door)
@@ -681,6 +927,164 @@ class ComponentSelectionInterface:
681
  else:
682
  st.error(f"File must contain: {', '.join(required_cols)}")
683
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
685
  type_name = component_type.value.lower()
686
  if component_type == ComponentType.ROOF:
@@ -688,114 +1092,402 @@ class ComponentSelectionInterface:
688
 
689
  if components:
690
  headers = {
691
- ComponentType.WALL: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)", "Delete"],
692
- ComponentType.ROOF: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity", "Delete"],
693
- ComponentType.FLOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)", "Perimeter (m)", "Insulated", "Delete"], # NEW: Added Insulated
694
- ComponentType.WINDOW: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)", "Delete"],
695
- ComponentType.DOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)", "Delete"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  }[component_type]
697
  cols = st.columns([1] * len(headers))
698
  for i, header in enumerate(headers):
699
  cols[i].write(f"**{header}**")
700
 
701
- for comp in components:
702
  cols = st.columns([1] * len(headers))
703
  cols[0].write(comp.name)
704
  cols[1].write(comp.area)
705
  cols[2].write(comp.u_value)
706
- cols[3].write(comp.orientation.value)
707
  if component_type == ComponentType.WALL:
 
708
  cols[4].write(comp.wall_type)
709
  cols[5].write(comp.wall_group)
710
  cols[6].write(comp.absorptivity)
711
- cols[7].write(comp.shading_coefficient)
712
- cols[8].write(comp.infiltration_rate_cfm)
 
 
713
  elif component_type == ComponentType.ROOF:
714
- cols[4].write(comp.roof_type)
715
- cols[5].write(comp.roof_group)
716
- cols[6].write(comp.slope)
717
- cols[7].write(comp.absorptivity)
 
 
 
718
  elif component_type == ComponentType.FLOOR:
719
- cols[4].write(comp.floor_type)
720
- cols[5].write("Yes" if comp.ground_contact else "No")
721
- cols[6].write(comp.ground_temperature_c if comp.ground_contact else "N/A")
722
- cols[7].write(comp.perimeter)
723
- cols[8].write("Yes" if comp.insulated else "No") # NEW: Display insulated
 
724
  elif component_type == ComponentType.WINDOW:
725
- cols[4].write(comp.shgc)
726
- cols[5].write(comp.shading_device)
727
- cols[6].write(comp.shading_coefficient)
728
- cols[7].write(comp.frame_type)
729
- cols[8].write(comp.frame_percentage)
730
- cols[9].write(comp.infiltration_rate_cfm)
 
731
  elif component_type == ComponentType.DOOR:
 
732
  cols[4].write(comp.door_type)
733
- cols[5].write(comp.infiltration_rate_cfm)
734
- if cols[-1].button("Delete", key=f"delete_{comp.id}"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  self.component_library.remove_component(comp.id)
736
- session_state.components[type_name + 's'] = [c for c in components if c.id != comp.id]
737
- st.success(f"Deleted {comp.name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  st.rerun()
739
 
740
- def _display_u_value_calculator_tab(self, session_state: Any) -> None:
741
- st.subheader("U-Value Calculator (Standalone)")
742
- if "u_value_layers" not in session_state:
743
- session_state.u_value_layers = []
 
 
 
 
 
 
 
 
 
 
 
 
744
 
745
- if session_state.u_value_layers:
746
- st.write("Material Layers (Outside to Inside):")
747
- layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
748
- "Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]}
749
- for i, l in enumerate(session_state.u_value_layers)]
750
- st.dataframe(pd.DataFrame(layer_data))
751
- outside_resistance = st.selectbox("Outside Resistance (m²·K/W)", ["Summer (0.04)", "Winter (0.03)", "Custom"], index=0)
752
- outside_r = float(st.number_input("Custom Outside Resistance", min_value=0.0, value=0.04, step=0.01)) if outside_resistance == "Custom" else (0.04 if outside_resistance.startswith("Summer") else 0.03)
753
- inside_r = st.number_input("Inside Resistance (m²·K/W)", min_value=0.0, value=0.13, step=0.01)
754
- u_value = self.u_value_calculator.calculate_u_value(session_state.u_value_layers, outside_r, inside_r)
755
- st.metric("U-Value", f"{u_value:.3f} W/m²·K")
756
-
757
- with st.form("u_value_form"):
758
  col1, col2 = st.columns(2)
759
  with col1:
760
- material_options = {m["name"]: m["conductivity"] for m in self.u_value_calculator.materials}
761
- material_name = st.selectbox("Material", options=list(material_options.keys()))
762
- conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=material_options[material_name], step=0.01)
 
763
  with col2:
764
- thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
765
- submitted = st.form_submit_button("Add Layer")
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  if submitted:
767
- session_state.u_value_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity})
768
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
769
 
770
- col1, col2 = st.columns(2)
771
- with col1:
772
- if st.button("Remove Last Layer"):
773
- if session_state.u_value_layers:
774
- session_state.u_value_layers.pop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
  st.rerun()
776
- with col2:
777
- if st.button("Reset"):
778
- session_state.u_value_layers = []
779
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
 
781
  def _save_components(self, session_state: Any) -> None:
782
- components_dict = {
783
- "walls": [c.to_dict() for c in session_state.components["walls"]],
784
- "roofs": [c.to_dict() for c in session_state.components["roofs"]],
785
- "floors": [c.to_dict() for c in session_state.components["floors"]],
786
- "windows": [c.to_dict() for c in session_state.components["windows"]],
787
- "doors": [c.to_dict() for c in session_state.components["doors"]],
 
788
  "roof_air_volume_m3": session_state.roof_air_volume_m3,
789
  "roof_ventilation_ach": session_state.roof_ventilation_ach
790
  }
791
- file_path = "components_export.json"
792
- with open(file_path, 'w') as f:
793
- json.dump(components_dict, f, indent=4)
794
- with open(file_path, 'r') as f:
795
- st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
796
- st.success("Components saved successfully.")
797
-
798
- # --- Main Execution ---
 
 
 
 
 
 
 
 
799
  if __name__ == "__main__":
800
- interface = ComponentSelectionInterface()
801
- interface.display_component_selection(st.session_state)
 
1
  """
2
+ Enhanced HVAC Component Selection Module
3
  Provides UI for selecting building components in the HVAC Load Calculator.
4
  All dependencies are included within this file for standalone operation.
5
+ Updated 2025-04-28: Added surface color selection, expanded component types, and skylight support.
6
+ Updated 2025-05-02: Added crack dimensions for walls/doors, drapery properties for windows/skylights, and roof height per enhancement plan.
7
+
8
+ Author: Dr Majed Abuseif
9
  """
10
 
11
  import streamlit as st
 
37
  FLOOR = "Floor"
38
  WINDOW = "Window"
39
  DOOR = "Door"
40
+ SKYLIGHT = "Skylight"
41
+
42
+ class SurfaceColor(Enum):
43
+ DARK = "Dark"
44
+ MEDIUM = "Medium"
45
+ LIGHT = "Light"
46
+
47
+ class GlazingType(Enum):
48
+ SINGLE_CLEAR = "Single Clear"
49
+ SINGLE_TINTED = "Single Tinted"
50
+ DOUBLE_CLEAR = "Double Clear"
51
+ DOUBLE_TINTED = "Double Tinted"
52
+ LOW_E = "Low-E"
53
+ REFLECTIVE = "Reflective"
54
+
55
+ class FrameType(Enum):
56
+ ALUMINUM = "Aluminum without Thermal Break"
57
+ ALUMINUM_THERMAL_BREAK = "Aluminum with Thermal Break"
58
+ VINYL = "Vinyl/Fiberglass"
59
+ WOOD = "Wood/Vinyl-Clad Wood"
60
+ INSULATED = "Insulated"
61
+
62
+ class DraperyOpenness(Enum):
63
+ CLOSED = "Closed"
64
+ SEMI_OPEN = "Semi-Open"
65
+ OPEN = "Open"
66
+
67
+ class DraperyColor(Enum):
68
+ LIGHT = "Light"
69
+ MEDIUM = "Medium"
70
+ DARK = "Dark"
71
 
72
  # --- Data Models ---
73
  @dataclass
 
104
  absorptivity: float = 0.6
105
  shading_coefficient: float = 1.0
106
  infiltration_rate_cfm: float = 0.0
107
+ surface_color: str = "Dark" # Added surface color
108
+ crack_length: float = 0.0 # Added for infiltration (m)
109
+ crack_width: float = 0.0 # Added for infiltration (m)
110
 
111
  def __post_init__(self):
112
  super().__post_init__()
 
117
  raise ValueError("Shading coefficient must be between 0 and 1")
118
  if self.infiltration_rate_cfm < 0:
119
  raise ValueError("Infiltration rate cannot be negative")
120
+ if not 0 <= self.crack_length <= 100:
121
+ raise ValueError("Crack length must be between 0 and 100 meters")
122
+ if not 0 <= self.crack_width <= 0.1:
123
+ raise ValueError("Crack width must be between 0 and 0.1 meters")
124
  VALID_WALL_GROUPS = {"A", "B", "C", "D", "E", "F", "G", "H"}
125
  if self.wall_group not in VALID_WALL_GROUPS:
126
  st.warning(f"Invalid wall_group '{self.wall_group}' for wall '{self.name}'. Defaulting to 'A'.")
127
  self.wall_group = "A"
128
+ VALID_SURFACE_COLORS = {"Dark", "Medium", "Light"}
129
+ if self.surface_color not in VALID_SURFACE_COLORS:
130
+ st.warning(f"Invalid surface_color '{self.surface_color}' for wall '{self.name}'. Defaulting to 'Dark'.")
131
+ self.surface_color = "Dark"
132
 
133
  def to_dict(self) -> dict:
134
  base_dict = super().to_dict()
135
  base_dict.update({
136
  "wall_type": self.wall_type, "wall_group": self.wall_group, "absorptivity": self.absorptivity,
137
+ "shading_coefficient": self.shading_coefficient, "infiltration_rate_cfm": self.infiltration_rate_cfm,
138
+ "surface_color": self.surface_color, "crack_length": self.crack_length, "crack_width": self.crack_width
139
  })
140
  return base_dict
141
 
 
145
  roof_group: str = "A" # ASHRAE group
146
  slope: str = "Flat"
147
  absorptivity: float = 0.6
148
+ surface_color: str = "Dark" # Added surface color
149
+ roof_height: float = 3.0 # Added for stack effect (m)
150
 
151
  def __post_init__(self):
152
  super().__post_init__()
 
155
  self.orientation = Orientation.HORIZONTAL
156
  if not 0 <= self.absorptivity <= 1:
157
  raise ValueError("Absorptivity must be between 0 and 1")
158
+ if not 0 <= self.roof_height <= 100:
159
+ raise ValueError("Roof height must be between 0 and 100 meters")
160
  VALID_ROOF_GROUPS = {"A", "B", "C", "D", "E", "F", "G"}
161
  if self.roof_group not in VALID_ROOF_GROUPS:
162
  st.warning(f"Invalid roof_group '{self.roof_group}' for roof '{self.name}'. Defaulting to 'A'.")
163
  self.roof_group = "A"
164
+ VALID_SURFACE_COLORS = {"Dark", "Medium", "Light"}
165
+ if self.surface_color not in VALID_SURFACE_COLORS:
166
+ st.warning(f"Invalid surface_color '{self.surface_color}' for roof '{self.name}'. Defaulting to 'Dark'.")
167
+ self.surface_color = "Dark"
168
 
169
  def to_dict(self) -> dict:
170
  base_dict = super().to_dict()
171
  base_dict.update({
172
+ "roof_type": self.roof_type, "roof_group": self.roof_group, "slope": self.slope,
173
+ "absorptivity": self.absorptivity, "surface_color": self.surface_color,
174
+ "roof_height": self.roof_height
175
  })
176
  return base_dict
177
 
 
181
  ground_contact: bool = True
182
  ground_temperature_c: float = 25.0
183
  perimeter: float = 0.0
184
+ insulated: bool = False # For dynamic F-factor
185
 
186
  def __post_init__(self):
187
  super().__post_init__()
 
206
  shgc: float = 0.7
207
  shading_device: str = "None"
208
  shading_coefficient: float = 1.0
209
+ frame_type: str = "Aluminum without Thermal Break" # Updated to match FrameType enum
210
  frame_percentage: float = 20.0
211
  infiltration_rate_cfm: float = 0.0
212
+ glazing_type: str = "Double Clear" # Added glazing type
213
+ drapery_openness: str = "Open" # Added for drapery properties
214
+ drapery_color: str = "Light" # Added for drapery properties
215
+ drapery_fullness: float = 1.5 # Added for drapery properties
216
 
217
  def __post_init__(self):
218
  super().__post_init__()
 
225
  raise ValueError("Frame percentage must be between 0 and 30")
226
  if self.infiltration_rate_cfm < 0:
227
  raise ValueError("Infiltration rate cannot be negative")
228
+ VALID_DRAPERY_OPENNESS = {"Closed", "Semi-Open", "Open"}
229
+ if self.drapery_openness not in VALID_DRAPERY_OPENNESS:
230
+ st.warning(f"Invalid drapery_openness '{self.drapery_openness}' for window '{self.name}'. Defaulting to 'Open'.")
231
+ self.drapery_openness = "Open"
232
+ VALID_DRAPERY_COLORS = {"Light", "Medium", "Dark"}
233
+ if self.drapery_color not in VALID_DRAPERY_COLORS:
234
+ st.warning(f"Invalid drapery_color '{self.drapery_color}' for window '{self.name}'. Defaulting to 'Light'.")
235
+ self.drapery_color = "Light"
236
+ if not 1.0 <= self.drapery_fullness <= 2.0:
237
+ raise ValueError("Drapery fullness must be between 1.0 and 2.0")
238
 
239
  def to_dict(self) -> dict:
240
  base_dict = super().to_dict()
241
  base_dict.update({
242
  "shgc": self.shgc, "shading_device": self.shading_device, "shading_coefficient": self.shading_coefficient,
243
+ "frame_type": self.frame_type, "frame_percentage": self.frame_percentage,
244
+ "infiltration_rate_cfm": self.infiltration_rate_cfm, "glazing_type": self.glazing_type,
245
+ "drapery_openness": self.drapery_openness, "drapery_color": self.drapery_color,
246
+ "drapery_fullness": self.drapery_fullness
247
  })
248
  return base_dict
249
 
 
251
  class Door(BuildingComponent):
252
  door_type: str = "Solid Wood"
253
  infiltration_rate_cfm: float = 0.0
254
+ crack_length: float = 0.0 # Added for infiltration (m)
255
+ crack_width: float = 0.0 # Added for infiltration (m)
256
 
257
  def __post_init__(self):
258
  super().__post_init__()
259
  self.component_type = ComponentType.DOOR
260
  if self.infiltration_rate_cfm < 0:
261
  raise ValueError("Infiltration rate cannot be negative")
262
+ if not 0 <= self.crack_length <= 100:
263
+ raise ValueError("Crack length must be between 0 and 100 meters")
264
+ if not 0 <= self.crack_width <= 0.1:
265
+ raise ValueError("Crack width must be between 0 and 0.1 meters")
266
 
267
  def to_dict(self) -> dict:
268
  base_dict = super().to_dict()
269
+ base_dict.update({
270
+ "door_type": self.door_type, "infiltration_rate_cfm": self.infiltration_rate_cfm,
271
+ "crack_length": self.crack_length, "crack_width": self.crack_width
272
+ })
273
+ return base_dict
274
+
275
+ @dataclass
276
+ class Skylight(BuildingComponent):
277
+ shgc: float = 0.7
278
+ shading_device: str = "None"
279
+ shading_coefficient: float = 1.0
280
+ frame_type: str = "Aluminum without Thermal Break" # Match FrameType enum
281
+ frame_percentage: float = 20.0
282
+ infiltration_rate_cfm: float = 0.0
283
+ glazing_type: str = "Double Clear" # Added glazing type
284
+ drapery_openness: str = "Open" # Added for drapery properties
285
+ drapery_color: str = "Light" # Added for drapery properties
286
+ drapery_fullness: float = 1.5 # Added for drapery properties
287
+
288
+ def __post_init__(self):
289
+ super().__post_init__()
290
+ self.component_type = ComponentType.SKYLIGHT
291
+ self.orientation = Orientation.HORIZONTAL
292
+ if not 0 <= self.shgc <= 1:
293
+ raise ValueError("SHGC must be between 0 and 1")
294
+ if not 0 <= self.shading_coefficient <= 1:
295
+ raise ValueError("Shading coefficient must be between 0 and 1")
296
+ if not 0 <= self.frame_percentage <= 30:
297
+ raise ValueError("Frame percentage must be between 0 and 30")
298
+ if self.infiltration_rate_cfm < 0:
299
+ raise ValueError("Infiltration rate cannot be negative")
300
+ VALID_DRAPERY_OPENNESS = {"Closed", "Semi-Open", "Open"}
301
+ if self.drapery_openness not in VALID_DRAPERY_OPENNESS:
302
+ st.warning(f"Invalid drapery_openness '{self.drapery_openness}' for skylight '{self.name}'. Defaulting to 'Open'.")
303
+ self.drapery_openness = "Open"
304
+ VALID_DRAPERY_COLORS = {"Light", "Medium", "Dark"}
305
+ if self.drapery_color not in VALID_DRAPERY_COLORS:
306
+ st.warning(f"Invalid drapery_color '{self.drapery_color}' for skylight '{self.name}'. Defaulting to 'Light'.")
307
+ self.drapery_color = "Light"
308
+ if not 1.0 <= self.drapery_fullness <= 2.0:
309
+ raise ValueError("Drapery fullness must be between 1.0 and 2.0")
310
+
311
+ def to_dict(self) -> dict:
312
+ base_dict = super().to_dict()
313
+ base_dict.update({
314
+ "shgc": self.shgc, "shading_device": self.shading_device, "shading_coefficient": self.shading_coefficient,
315
+ "frame_type": self.frame_type, "frame_percentage": self.frame_percentage,
316
+ "infiltration_rate_cfm": self.infiltration_rate_cfm, "glazing_type": self.glazing_type,
317
+ "drapery_openness": self.drapery_openness, "drapery_color": self.drapery_color,
318
+ "drapery_fullness": self.drapery_fullness
319
+ })
320
  return base_dict
321
 
322
  # --- Reference Data ---
 
344
  },
345
  "roof_types": {
346
  "Concrete Roof": {"u_value": 0.3, "absorptivity": 0.6, "group": "A"},
347
+ "Metal Roof": {"u_value": 1.0, "absorptivity": 0.75, "group": "B"},
348
+ "Built-up Roof": {"u_value": 0.5, "absorptivity": 0.8, "group": "C"},
349
+ "Insulated Metal Deck": {"u_value": 0.4, "absorptivity": 0.7, "group": "D"},
350
+ "Wood Shingle": {"u_value": 0.6, "absorptivity": 0.5, "group": "E"},
351
+ "Custom": {"u_value": 0.5, "absorptivity": 0.6, "group": "A"}
352
  },
353
  "roof_ventilation_methods": {
354
  "No Ventilation": 0.0,
 
358
  },
359
  "floor_types": {
360
  "Concrete Slab": {"u_value": 0.4, "ground_contact": True},
361
+ "Wood Floor": {"u_value": 0.8, "ground_contact": False},
362
+ "Insulated Concrete Slab": {"u_value": 0.2, "ground_contact": True},
363
+ "Raised Floor": {"u_value": 0.5, "ground_contact": False},
364
+ "Tile on Concrete": {"u_value": 0.45, "ground_contact": True},
365
+ "Custom": {"u_value": 0.5, "ground_contact": True}
366
  },
367
  "window_types": {
368
+ "Single Clear": {"u_value": 5.0, "shgc": 0.86, "glazing_type": "Single Clear", "frame_type": "Aluminum without Thermal Break"},
369
+ "Single Tinted": {"u_value": 5.0, "shgc": 0.73, "glazing_type": "Single Tinted", "frame_type": "Aluminum without Thermal Break"},
370
+ "Double Clear": {"u_value": 2.8, "shgc": 0.76, "glazing_type": "Double Clear", "frame_type": "Aluminum with Thermal Break"},
371
+ "Double Tinted": {"u_value": 2.8, "shgc": 0.62, "glazing_type": "Double Tinted", "frame_type": "Aluminum with Thermal Break"},
372
+ "Low-E": {"u_value": 1.8, "shgc": 0.48, "glazing_type": "Low-E", "frame_type": "Vinyl/Fiberglass"},
373
+ "Reflective": {"u_value": 2.0, "shgc": 0.35, "glazing_type": "Reflective", "frame_type": "Aluminum with Thermal Break"},
374
+ "Custom": {"u_value": 2.5, "shgc": 0.7, "glazing_type": "Double Clear", "frame_type": "Aluminum with Thermal Break"}
375
  },
376
  "shading_devices": {
377
  "None": 1.0,
378
  "Venetian Blinds": 0.6,
379
  "Overhang": 0.4,
380
  "Roller Shades": 0.5,
381
+ "Drapes": 0.7,
382
+ "Reflective Film": 0.3
383
  },
384
  "door_types": {
385
  "Solid Wood": {"u_value": 2.0},
386
+ "Glass Door": {"u_value": 3.5},
387
+ "Metal Door": {"u_value": 3.0},
388
+ "Insulated Metal": {"u_value": 1.2},
389
+ "Insulated Wood": {"u_value": 1.0},
390
+ "Custom": {"u_value": 2.0}
391
+ },
392
+ "skylight_types": {
393
+ "Single Clear": {"u_value": 5.5, "shgc": 0.83, "glazing_type": "Single Clear", "frame_type": "Aluminum without Thermal Break"},
394
+ "Single Tinted": {"u_value": 5.5, "shgc": 0.70, "glazing_type": "Single Tinted", "frame_type": "Aluminum without Thermal Break"},
395
+ "Double Clear": {"u_value": 3.2, "shgc": 0.70, "glazing_type": "Double Clear", "frame_type": "Aluminum with Thermal Break"},
396
+ "Double Tinted": {"u_value": 3.2, "shgc": 0.58, "glazing_type": "Double Tinted", "frame_type": "Aluminum with Thermal Break"},
397
+ "Low-E": {"u_value": 2.2, "shgc": 0.51, "glazing_type": "Low-E", "frame_type": "Vinyl/Fiberglass"},
398
+ "Reflective": {"u_value": 2.4, "shgc": 0.38, "glazing_type": "Reflective", "frame_type": "Aluminum with Thermal Break"},
399
+ "Custom": {"u_value": 3.0, "shgc": 0.7, "glazing_type": "Double Clear", "frame_type": "Aluminum with Thermal Break"}
400
+ },
401
+ "surface_colors": {
402
+ "Dark": 0.9,
403
+ "Medium": 0.6,
404
+ "Light": 0.3
405
+ },
406
+ "frame_types": {
407
+ "Aluminum without Thermal Break": 1.0,
408
+ "Aluminum with Thermal Break": 0.8,
409
+ "Vinyl/Fiberglass": 0.6,
410
+ "Wood/Vinyl-Clad Wood": 0.5,
411
+ "Insulated": 0.4
412
  }
413
  }
414
 
 
454
  st.title("Building Components")
455
 
456
  if 'components' not in session_state:
457
+ session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': [], 'skylights': []}
458
  if 'roof_air_volume_m3' not in session_state:
459
  session_state.roof_air_volume_m3 = 0.0
460
  if 'roof_ventilation_ach' not in session_state:
461
  session_state.roof_ventilation_ach = 0.0
462
 
463
+ tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "Skylights", "U-Value Calculator"])
464
 
465
  with tabs[0]:
466
  self._display_component_tab(session_state, ComponentType.WALL)
 
473
  with tabs[4]:
474
  self._display_component_tab(session_state, ComponentType.DOOR)
475
  with tabs[5]:
476
+ self._display_component_tab(session_state, ComponentType.SKYLIGHT)
477
+ with tabs[6]:
478
  self._display_u_value_calculator_tab(session_state)
479
 
480
  if st.button("Save Components"):
 
495
  self._display_add_window_form(session_state)
496
  elif component_type == ComponentType.DOOR:
497
  self._display_add_door_form(session_state)
498
+ elif component_type == ComponentType.SKYLIGHT:
499
+ self._display_add_skylight_form(session_state)
500
 
501
  components = session_state.components.get(type_name + 's', [])
502
  if components or component_type == ComponentType.ROOF:
 
516
  name = st.text_input("Name", "New Wall")
517
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
518
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
519
+ surface_color = st.selectbox("Surface Color", ["Dark", "Medium", "Light"], index=0)
520
+ crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=0.0, step=0.1)
521
  with col2:
522
  wall_options = self.reference_data.data["wall_types"]
523
  selected_wall = st.selectbox("Wall Type", options=list(wall_options.keys()))
 
526
  absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2)
527
  shading_coefficient = st.number_input("Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05)
528
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
529
+ crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=0.0, step=0.001)
530
 
531
  submitted = st.form_submit_button("Add Wall")
532
  if submitted and not session_state.add_wall_submitted:
 
535
  new_wall = Wall(
536
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
537
  wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value,
538
+ shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate,
539
+ surface_color=surface_color, crack_length=crack_length, crack_width=crack_width
540
  )
541
  self.component_library.add_component(new_wall)
542
  session_state.components['walls'].append(new_wall)
 
551
 
552
  elif method == "File Upload":
553
  uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key="wall_upload")
554
+ required_cols = [
555
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group",
556
+ "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)", "Surface Color",
557
+ "Crack Length (m)", "Crack Width (m)"
558
+ ]
559
  template_data = pd.DataFrame(columns=required_cols)
560
+ template_data.loc[0] = [
561
+ "Example Wall", 10.0, 0.5, "North", "Brick Wall", "A", 0.6, 1.0, 0.0, "Dark", 0.0, 0.0
562
+ ]
563
+ st.download_button(
564
+ label="Download Wall Template",
565
+ data=template_data.to_csv(index=False),
566
+ file_name="wall_template.csv",
567
+ mime="text/csv"
568
+ )
569
  if uploaded_file:
570
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
571
  if all(col in df.columns for col in required_cols):
 
572
  for _, row in df.iterrows():
573
  try:
 
 
 
 
574
  new_wall = Wall(
575
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
576
  orientation=Orientation(row["Orientation"]), wall_type=str(row["Wall Type"]),
577
+ wall_group=str(row["Wall Group"]), absorptivity=float(row["Absorptivity"]),
578
+ shading_coefficient=float(row["Shading Coefficient"]),
579
+ infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]),
580
+ surface_color=str(row["Surface Color"]), crack_length=float(row["Crack Length (m)"]),
581
+ crack_width=float(row["Crack Width (m)"])
582
  )
583
  self.component_library.add_component(new_wall)
584
  session_state.components['walls'].append(new_wall)
 
595
  if "add_roof_submitted" not in session_state:
596
  session_state.add_roof_submitted = False
597
 
 
 
 
 
 
 
 
 
 
598
  if method == "Manual Entry":
599
  with st.form("add_roof_form", clear_on_submit=True):
600
  col1, col2 = st.columns(2)
601
  with col1:
602
  name = st.text_input("Name", "New Roof")
603
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
604
+ surface_color = st.selectbox("Surface Color", ["Dark", "Medium", "Light"], index=0)
605
+ roof_height = st.number_input("Roof Height (m)", min_value=0.0, max_value=100.0, value=3.0, step=0.1)
606
  with col2:
607
  roof_options = self.reference_data.data["roof_types"]
608
  selected_roof = st.selectbox("Roof Type", options=list(roof_options.keys()))
609
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(roof_options[selected_roof]["u_value"]), step=0.01)
610
  roof_group = st.selectbox("Roof Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G"], index=0)
611
+ slope = st.selectbox("Roof Slope", ["Flat", "Low Slope", "Steep Slope"], index=0)
612
+ absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark
613
+
614
+ (0.9)"], index=2)
615
 
616
  submitted = st.form_submit_button("Add Roof")
617
  if submitted and not session_state.add_roof_submitted:
618
  try:
619
  absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
620
  new_roof = Roof(
621
+ name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
622
+ roof_type=selected_roof, roof_group=roof_group, slope=slope,
623
+ absorptivity=absorptivity_value, surface_color=surface_color,
624
+ roof_height=roof_height
625
  )
626
  self.component_library.add_component(new_roof)
627
  session_state.components['roofs'].append(new_roof)
 
634
  if session_state.add_roof_submitted:
635
  session_state.add_roof_submitted = False
636
 
637
+ # Roof air volume and ventilation
638
+ st.subheader("Roof Air Volume and Ventilation")
639
+ col1, col2 = st.columns(2)
640
+ with col1:
641
+ roof_air_volume = st.number_input("Roof Air Volume (m³)", min_value=0.0, value=session_state.roof_air_volume_m3, step=1.0)
642
+ with col2:
643
+ ventilation_method = st.selectbox("Ventilation Method", list(self.reference_data.data["roof_ventilation_methods"].keys()))
644
+ ventilation_ach = self.reference_data.data["roof_ventilation_methods"][ventilation_method]
645
+
646
+ if st.button("Update Roof Air Volume and Ventilation"):
647
+ session_state.roof_air_volume_m3 = roof_air_volume
648
+ session_state.roof_ventilation_ach = ventilation_ach
649
+ st.success("Updated roof air volume and ventilation")
650
+ st.rerun()
651
+
652
  elif method == "File Upload":
653
  uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload")
654
+ required_cols = [
655
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Roof Type", "Roof Group", "Slope",
656
+ "Absorptivity", "Surface Color", "Roof Height (m)"
657
+ ]
658
  template_data = pd.DataFrame(columns=required_cols)
659
+ template_data.loc[0] = [
660
+ "Example Roof", 10.0, 0.3, "Concrete Roof", "A", "Flat", 0.6, "Dark", 3.0
661
+ ]
662
+ st.download_button(
663
+ label="Download Roof Template",
664
+ data=template_data.to_csv(index=False),
665
+ file_name="roof_template.csv",
666
+ mime="text/csv"
667
+ )
668
  if uploaded_file:
669
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
670
  if all(col in df.columns for col in required_cols):
 
671
  for _, row in df.iterrows():
672
  try:
 
 
 
 
673
  new_roof = Roof(
674
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
675
+ orientation=Orientation.HORIZONTAL, roof_type=str(row["Roof Type"]),
676
+ roof_group=str(row["Roof Group"]), slope=str(row["Slope"]),
677
+ absorptivity=float(row["Absorptivity"]), surface_color=str(row["Surface Color"]),
678
+ roof_height=float(row["Roof Height (m)"])
679
  )
680
  self.component_library.add_component(new_roof)
681
  session_state.components['roofs'].append(new_roof)
 
698
  with col1:
699
  name = st.text_input("Name", "New Floor")
700
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
701
+ perimeter = st.number_input("Perimeter (m)", min_value=0.0, value=4.0, step=0.1)
702
  with col2:
703
  floor_options = self.reference_data.data["floor_types"]
704
  selected_floor = st.selectbox("Floor Type", options=list(floor_options.keys()))
705
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(floor_options[selected_floor]["u_value"]), step=0.01)
706
  ground_contact = st.selectbox("Ground Contact", ["Yes", "No"], index=0 if floor_options[selected_floor]["ground_contact"] else 1)
707
  ground_temp = st.number_input("Ground Temperature (°C)", min_value=-10.0, max_value=40.0, value=25.0, step=0.1) if ground_contact == "Yes" else 25.0
708
+ insulated = st.checkbox("Insulated Floor (e.g., R-10)", value=False)
709
 
710
  submitted = st.form_submit_button("Add Floor")
711
  if submitted and not session_state.add_floor_submitted:
 
728
 
729
  elif method == "File Upload":
730
  uploaded_file = st.file_uploader("Upload Floors File", type=["csv", "xlsx"], key="floor_upload")
731
+ required_cols = [
732
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact",
733
+ "Ground Temperature (°C)", "Perimeter (m)", "Insulated"
734
+ ]
735
  template_data = pd.DataFrame(columns=required_cols)
736
  template_data.loc[0] = ["Example Floor", 10.0, 0.4, "Concrete Slab", "Yes", 25.0, 12.0, "No"]
737
+ st.download_button(
738
+ label="Download Floor Template",
739
+ data=template_data.to_csv(index=False),
740
+ file_name="floor_template.csv",
741
+ mime="text/csv"
742
+ )
743
  if uploaded_file:
744
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
745
  if all(col in df.columns for col in required_cols):
 
774
  name = st.text_input("Name", "New Window")
775
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
776
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
 
 
777
  window_options = self.reference_data.data["window_types"]
778
  selected_window = st.selectbox("Window Type", options=list(window_options.keys()))
779
+ glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=2)
780
+ drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=2)
781
+ with col2:
782
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(window_options[selected_window]["u_value"]), step=0.01)
783
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=window_options[selected_window]["shgc"], step=0.01)
784
+ frame_type = st.selectbox("Frame Type", [f.value for f in FrameType], index=1)
785
+ frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=20.0)
786
  shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
787
  shading_options["Custom"] = "Custom"
788
  shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
789
  shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]]
 
 
790
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
791
+ drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=0)
792
+ drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=1.5, step=0.1)
793
 
794
  submitted = st.form_submit_button("Add Window")
795
  if submitted and not session_state.add_window_submitted:
 
797
  new_window = Window(
798
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
799
  shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
800
+ frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate,
801
+ glazing_type=glazing_type, drapery_openness=drapery_openness, drapery_color=drapery_color,
802
+ drapery_fullness=drapery_fullness
803
  )
804
  self.component_library.add_component(new_window)
805
  session_state.components['windows'].append(new_window)
 
814
 
815
  elif method == "File Upload":
816
  uploaded_file = st.file_uploader("Upload Windows File", type=["csv", "xlsx"], key="window_upload")
817
+ required_cols = [
818
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device",
819
+ "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)",
820
+ "Glazing Type", "Drapery Openness", "Drapery Color", "Drapery Fullness"
821
+ ]
822
+ template_data = pd.DataFrame(columns=required_cols)
823
+ template_data.loc[0] = [
824
+ "Example Window", 2.0, 2.8, "North", 0.7, "None", 1.0, "Aluminum with Thermal Break",
825
+ 20.0, 0.0, "Double Clear", "Open", "Light", 1.5
826
+ ]
827
+ st.download_button(
828
+ label="Download Window Template",
829
+ data=template_data.to_csv(index=False),
830
+ file_name="window_template.csv",
831
+ mime="text/csv"
832
+ )
833
  if uploaded_file:
834
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
835
  if all(col in df.columns for col in required_cols):
 
840
  orientation=Orientation(row["Orientation"]), shgc=float(row["SHGC"]),
841
  shading_device=str(row["Shading Device"]), shading_coefficient=float(row["Shading Coefficient"]),
842
  frame_type=str(row["Frame Type"]), frame_percentage=float(row["Frame Percentage"]),
843
+ infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]), glazing_type=str(row["Glazing Type"]),
844
+ drapery_openness=str(row["Drapery Openness"]), drapery_color=str(row["Drapery Color"]),
845
+ drapery_fullness=float(row["Drapery Fullness"])
846
  )
847
  self.component_library.add_component(new_window)
848
  session_state.components['windows'].append(new_window)
 
866
  name = st.text_input("Name", "New Door")
867
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
868
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
869
+ crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=0.0, step=0.1)
870
  with col2:
871
  door_options = self.reference_data.data["door_types"]
872
  selected_door = st.selectbox("Door Type", options=list(door_options.keys()))
873
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(door_options[selected_door]["u_value"]), step=0.01)
874
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
875
+ crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=0.0, step=0.001)
876
 
877
  submitted = st.form_submit_button("Add Door")
878
  if submitted and not session_state.add_door_submitted:
879
  try:
880
  new_door = Door(
881
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
882
+ door_type=selected_door, infiltration_rate_cfm=infiltration_rate,
883
+ crack_length=crack_length, crack_width=crack_width
884
  )
885
  self.component_library.add_component(new_door)
886
  session_state.components['doors'].append(new_door)
 
895
 
896
  elif method == "File Upload":
897
  uploaded_file = st.file_uploader("Upload Doors File", type=["csv", "xlsx"], key="door_upload")
898
+ required_cols = [
899
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type",
900
+ "Infiltration Rate (CFM)", "Crack Length (m)", "Crack Width (m)"
901
+ ]
902
+ template_data = pd.DataFrame(columns=required_cols)
903
+ template_data.loc[0] = ["Example Door", 2.0, 2.0, "North", "Solid Wood", 0.0, 0.0, 0.0]
904
+ st.download_button(
905
+ label="Download Door Template",
906
+ data=template_data.to_csv(index=False),
907
+ file_name="door_template.csv",
908
+ mime="text/csv"
909
+ )
910
  if uploaded_file:
911
  df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
912
  if all(col in df.columns for col in required_cols):
 
915
  new_door = Door(
916
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
917
  orientation=Orientation(row["Orientation"]), door_type=str(row["Door Type"]),
918
+ infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]),
919
+ crack_length=float(row["Crack Length (m)"]), crack_width=float(row["Crack Width (m)"])
920
  )
921
  self.component_library.add_component(new_door)
922
  session_state.components['doors'].append(new_door)
 
927
  else:
928
  st.error(f"File must contain: {', '.join(required_cols)}")
929
 
930
+ def _display_add_skylight_form(self, session_state: Any) -> None:
931
+ st.write("Add skylights manually or upload a file.")
932
+ method = st.radio("Add Skylight Method", ["Manual Entry", "File Upload"])
933
+ if "add_skylight_submitted" not in session_state:
934
+ session_state.add_skylight_submitted = False
935
+
936
+ if method == "Manual Entry":
937
+ with st.form("add_skylight_form", clear_on_submit=True):
938
+ col1, col2 = st.columns(2)
939
+ with col1:
940
+ name = st.text_input("Name", "New Skylight")
941
+ area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
942
+ skylight_options = self.reference_data.data["skylight_types"]
943
+ selected_skylight = st.selectbox("Skylight Type", options=list(skylight_options.keys()))
944
+ glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=2)
945
+ drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=2)
946
+ with col2:
947
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(skylight_options[selected_skylight]["u_value"]), step=0.01)
948
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=skylight_options[selected_skylight]["shgc"], step=0.01)
949
+ frame_type = st.selectbox("Frame Type", [f.value for f in FrameType], index=1)
950
+ frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=20.0)
951
+ shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
952
+ shading_options["Custom"] = "Custom"
953
+ shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
954
+ shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]]
955
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
956
+ drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=0)
957
+ drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=1.5, step=0.1)
958
+
959
+ submitted = st.form_submit_button("Add Skylight")
960
+ if submitted and not session_state.add_skylight_submitted:
961
+ try:
962
+ new_skylight = Skylight(
963
+ name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
964
+ shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
965
+ frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate,
966
+ glazing_type=glazing_type, drapery_openness=drapery_openness, drapery_color=drapery_color,
967
+ drapery_fullness=drapery_fullness
968
+ )
969
+ self.component_library.add_component(new_skylight)
970
+ session_state.components['skylights'].append(new_skylight)
971
+ st.success(f"Added {new_skylight.name}")
972
+ session_state.add_skylight_submitted = True
973
+ st.rerun()
974
+ except ValueError as e:
975
+ st.error(f"Error: {str(e)}")
976
+
977
+ if session_state.add_skylight_submitted:
978
+ session_state.add_skylight_submitted = False
979
+
980
+ elif method == "File Upload":
981
+ uploaded_file = st.file_uploader("Upload Skylights File", type=["csv", "xlsx"], key="skylight_upload")
982
+ required_cols = [
983
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "SHGC", "Shading Device", "Shading Coefficient",
984
+ "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)", "Glazing Type",
985
+ "Drapery Openness", "Drapery Color", "Drapery Fullness"
986
+ ]
987
+ template_data = pd.DataFrame(columns=required_cols)
988
+ template_data.loc[0] = [
989
+ "Example Skylight", 1.0, 3.2, 0.7, "None", 1.0, "Aluminum with Thermal Break",
990
+ 20.0, 0.0, "Double Clear", "Open", "Light", 1.5
991
+ ]
992
+ st.download_button(
993
+ label="Download Skylight Template",
994
+ data=template_data.to_csv(index=False),
995
+ file_name="skylight_template.csv",
996
+ mime="text/csv"
997
+ )
998
+ if uploaded_file:
999
+ df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
1000
+ if all(col in df.columns for col in required_cols):
1001
+ for _, row in df.iterrows():
1002
+ try:
1003
+ new_skylight = Skylight(
1004
+ name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
1005
+ orientation=Orientation.HORIZONTAL, shgc=float(row["SHGC"]),
1006
+ shading_device=str(row["Shading Device"]), shading_coefficient=float(row["Shading Coefficient"]),
1007
+ frame_type=str(row["Frame Type"]), frame_percentage=float(row["Frame Percentage"]),
1008
+ infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]), glazing_type=str(row["Glazing Type"]),
1009
+ drapery_openness=str(row["Drapery Openness"]), drapery_color=str(row["Drapery Color"]),
1010
+ drapery_fullness=float(row["Drapery Fullness"])
1011
+ )
1012
+ self.component_library.add_component(new_skylight)
1013
+ session_state.components['skylights'].append(new_skylight)
1014
+ except ValueError as e:
1015
+ st.error(f"Error in row {row['Name']}: {str(e)}")
1016
+ st.success("Skylights uploaded successfully!")
1017
+ st.rerun()
1018
+ else:
1019
+ st.error(f"File must contain: {', '.join(required_cols)}")
1020
+
1021
+ def _display_u_value_calculator_tab(self, session_state: Any) -> None:
1022
+ st.subheader("U-Value Calculator")
1023
+ st.write("Calculate U-value based on material layers.")
1024
+
1025
+ if "u_value_layers" not in session_state:
1026
+ session_state.u_value_layers = []
1027
+
1028
+ col1, col2 = st.columns(2)
1029
+ with col1:
1030
+ outside_resistance = st.number_input("Outside Surface Resistance (m²·K/W)", min_value=0.0, value=0.04, step=0.01)
1031
+ with col2:
1032
+ inside_resistance = st.number_input("Inside Surface Resistance (m²·K/W)", min_value=0.0, value=0.13, step=0.01)
1033
+
1034
+ st.subheader("Material Layers")
1035
+ for i, layer in enumerate(session_state.u_value_layers):
1036
+ col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
1037
+ with col1:
1038
+ material_options = [m["name"] for m in self.u_value_calculator.materials]
1039
+ material_index = material_options.index(layer["name"]) if layer["name"] in material_options else 0
1040
+ layer["name"] = st.selectbox(f"Material {i+1}", material_options, index=material_index, key=f"material_{i}")
1041
+ with col2:
1042
+ layer["thickness"] = st.number_input(f"Thickness {i+1} (mm)", min_value=0.1, value=layer["thickness"], step=1.0, key=f"thickness_{i}")
1043
+ with col3:
1044
+ material_conductivity = next((m["conductivity"] for m in self.u_value_calculator.materials if m["name"] == layer["name"]), 1.0)
1045
+ layer["conductivity"] = st.number_input(f"Conductivity {i+1} (W/m·K)", min_value=0.001, value=material_conductivity, step=0.01, key=f"conductivity_{i}")
1046
+ with col4:
1047
+ if st.button("Remove", key=f"remove_{i}"):
1048
+ session_state.u_value_layers.pop(i)
1049
+ st.rerun()
1050
+
1051
+ col1, col2 = st.columns([3, 1])
1052
+ with col1:
1053
+ new_material = st.selectbox("Add Material", [m["name"] for m in self.u_value_calculator.materials])
1054
+ new_thickness = st.number_input("Thickness (mm)", min_value=0.1, value=100.0, step=1.0)
1055
+ new_conductivity = next((m["conductivity"] for m in self.u_value_calculator.materials if m["name"] == new_material), 1.0)
1056
+ with col2:
1057
+ if st.button("Add Layer"):
1058
+ session_state.u_value_layers.append({
1059
+ "name": new_material,
1060
+ "thickness": new_thickness,
1061
+ "conductivity": new_conductivity
1062
+ })
1063
+ st.rerun()
1064
+
1065
+ if session_state.u_value_layers:
1066
+ u_value = self.u_value_calculator.calculate_u_value(session_state.u_value_layers, outside_resistance, inside_resistance)
1067
+ st.success(f"Calculated U-Value: {u_value:.3f} W/m²·K")
1068
+
1069
+ # Display R-value breakdown
1070
+ st.subheader("R-Value Breakdown")
1071
+ data = []
1072
+ data.append({"Layer": "Outside Surface", "Thickness (mm)": "-", "Conductivity (W/m·K)": "-", "R-Value (m²·K/W)": outside_resistance})
1073
+ for layer in session_state.u_value_layers:
1074
+ r_value = layer["thickness"] / 1000 / layer["conductivity"]
1075
+ data.append({
1076
+ "Layer": layer["name"],
1077
+ "Thickness (mm)": f"{layer['thickness']:.1f}",
1078
+ "Conductivity (W/m·K)": f"{layer['conductivity']:.3f}",
1079
+ "R-Value (m²·K/W)": f"{r_value:.3f}"
1080
+ })
1081
+ data.append({"Layer": "Inside Surface", "Thickness (mm)": "-", "Conductivity (W/m·K)": "-", "R-Value (m²·K/W)": inside_resistance})
1082
+
1083
+ total_r_value = sum(layer["thickness"] / 1000 / layer["conductivity"] for layer in session_state.u_value_layers) + outside_resistance + inside_resistance
1084
+ data.append({"Layer": "Total", "Thickness (mm)": f"{sum(layer['thickness'] for layer in session_state.u_value_layers):.1f}", "Conductivity (W/m·K)": "-", "R-Value (m²·K/W)": f"{total_r_value:.3f}"})
1085
+
1086
+ st.table(pd.DataFrame(data))
1087
+
1088
  def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
1089
  type_name = component_type.value.lower()
1090
  if component_type == ComponentType.ROOF:
 
1092
 
1093
  if components:
1094
  headers = {
1095
+ ComponentType.WALL: [
1096
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group",
1097
+ "Absorptivity", "Surface Color", "Crack Length (m)", "Crack Width (m)", "Edit", "Delete"
1098
+ ],
1099
+ ComponentType.ROOF: [
1100
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Roof Type", "Roof Group", "Slope",
1101
+ "Absorptivity", "Surface Color", "Roof Height (m)", "Edit", "Delete"
1102
+ ],
1103
+ ComponentType.FLOOR: [
1104
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact",
1105
+ "Ground Temperature (°C)", "Perimeter (m)", "Insulated", "Edit", "Delete"
1106
+ ],
1107
+ ComponentType.WINDOW: [
1108
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Glazing Type", "SHGC",
1109
+ "Frame Type", "Drapery Openness", "Drapery Color", "Edit", "Delete"
1110
+ ],
1111
+ ComponentType.DOOR: [
1112
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type",
1113
+ "Crack Length (m)", "Crack Width (m)", "Edit", "Delete"
1114
+ ],
1115
+ ComponentType.SKYLIGHT: [
1116
+ "Name", "Area (m²)", "U-Value (W/m²·K)", "Glazing Type", "SHGC", "Frame Type",
1117
+ "Drapery Openness", "Drapery Color", "Edit", "Delete"
1118
+ ]
1119
  }[component_type]
1120
  cols = st.columns([1] * len(headers))
1121
  for i, header in enumerate(headers):
1122
  cols[i].write(f"**{header}**")
1123
 
1124
+ for i, comp in enumerate(components):
1125
  cols = st.columns([1] * len(headers))
1126
  cols[0].write(comp.name)
1127
  cols[1].write(comp.area)
1128
  cols[2].write(comp.u_value)
1129
+
1130
  if component_type == ComponentType.WALL:
1131
+ cols[3].write(comp.orientation.value)
1132
  cols[4].write(comp.wall_type)
1133
  cols[5].write(comp.wall_group)
1134
  cols[6].write(comp.absorptivity)
1135
+ cols[7].write(comp.surface_color)
1136
+ cols[8].write(comp.crack_length)
1137
+ cols[9].write(comp.crack_width)
1138
+ edit_col = 10
1139
  elif component_type == ComponentType.ROOF:
1140
+ cols[3].write(comp.roof_type)
1141
+ cols[4].write(comp.roof_group)
1142
+ cols[5].write(comp.slope)
1143
+ cols[6].write(comp.absorptivity)
1144
+ cols[7].write(comp.surface_color)
1145
+ cols[8].write(comp.roof_height)
1146
+ edit_col = 9
1147
  elif component_type == ComponentType.FLOOR:
1148
+ cols[3].write(comp.floor_type)
1149
+ cols[4].write("Yes" if comp.ground_contact else "No")
1150
+ cols[5].write(comp.ground_temperature_c if comp.ground_contact else "N/A")
1151
+ cols[6].write(comp.perimeter)
1152
+ cols[7].write("Yes" if comp.insulated else "No")
1153
+ edit_col = 8
1154
  elif component_type == ComponentType.WINDOW:
1155
+ cols[3].write(comp.orientation.value)
1156
+ cols[4].write(comp.glazing_type)
1157
+ cols[5].write(comp.shgc)
1158
+ cols[6].write(comp.frame_type)
1159
+ cols[7].write(comp.drapery_openness)
1160
+ cols[8].write(comp.drapery_color)
1161
+ edit_col = 9
1162
  elif component_type == ComponentType.DOOR:
1163
+ cols[3].write(comp.orientation.value)
1164
  cols[4].write(comp.door_type)
1165
+ cols[5].write(comp.crack_length)
1166
+ cols[6].write(comp.crack_width)
1167
+ edit_col = 7
1168
+ elif component_type == ComponentType.SKYLIGHT:
1169
+ cols[3].write(comp.glazing_type)
1170
+ cols[4].write(comp.shgc)
1171
+ cols[5].write(comp.frame_type)
1172
+ cols[6].write(comp.drapery_openness)
1173
+ cols[7].write(comp.drapery_color)
1174
+ edit_col = 8
1175
+
1176
+ # Edit button
1177
+ if cols[edit_col].button("✏️", key=f"edit_{type_name}_{i}"):
1178
+ session_state[f"edit_{type_name}"] = i
1179
+ st.rerun()
1180
+
1181
+ # Delete button
1182
+ if cols[edit_col + 1].button("🗑️", key=f"delete_{type_name}_{i}"):
1183
  self.component_library.remove_component(comp.id)
1184
+ session_state.components[type_name + 's'].pop(i)
1185
+ st.rerun()
1186
+
1187
+ # Edit form
1188
+ if f"edit_{type_name}" in session_state and session_state[f"edit_{type_name}"] is not None:
1189
+ edit_index = session_state[f"edit_{type_name}"]
1190
+ if 0 <= edit_index < len(components):
1191
+ comp = components[edit_index]
1192
+ st.subheader(f"Edit {type_name.capitalize()}: {comp.name}")
1193
+
1194
+ if component_type == ComponentType.WALL:
1195
+ self._display_edit_wall_form(session_state, comp, edit_index)
1196
+ elif component_type == ComponentType.ROOF:
1197
+ self._display_edit_roof_form(session_state, comp, edit_index)
1198
+ elif component_type == ComponentType.FLOOR:
1199
+ self._display_edit_floor_form(session_state, comp, edit_index)
1200
+ elif component_type == ComponentType.WINDOW:
1201
+ self._display_edit_window_form(session_state, comp, edit_index)
1202
+ elif component_type == ComponentType.DOOR:
1203
+ self._display_edit_door_form(session_state, comp, edit_index)
1204
+ elif component_type == ComponentType.SKYLIGHT:
1205
+ self._display_edit_skylight_form(session_state, comp, edit_index)
1206
+
1207
+ def _display_edit_wall_form(self, session_state: Any, wall: Wall, index: int) -> None:
1208
+ with st.form(f"edit_wall_form_{index}", clear_on_submit=True):
1209
+ col1, col2 = st.columns(2)
1210
+ with col1:
1211
+ name = st.text_input("Name", wall.name)
1212
+ area = st.number_input("Area (m²)", min_value=0.0, value=wall.area, step=0.1)
1213
+ orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=[o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE].index(wall.orientation.value))
1214
+ surface_color = st.selectbox("Surface Color", ["Dark", "Medium", "Light"], index=["Dark", "Medium", "Light"].index(wall.surface_color))
1215
+ crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=wall.crack_length, step=0.1)
1216
+ with col2:
1217
+ wall_options = self.reference_data.data["wall_types"]
1218
+ selected_wall = st.selectbox("Wall Type", options=list(wall_options.keys()), index=list(wall_options.keys()).index(wall.wall_type) if wall.wall_type in wall_options else 0)
1219
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=wall.u_value, step=0.01)
1220
+ wall_group = st.selectbox("Wall Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G", "H"], index=["A", "B", "C", "D", "E", "F", "G", "H"].index(wall.wall_group))
1221
+ absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"].index(f"{['Light (0.3)', 'Light to Medium (0.45)', 'Medium (0.6)', 'Medium to Dark (0.75)', 'Dark (0.9)'][np.argmin([abs(float(opt.split('(')[1].strip(')')) - wall.absorptivity) for opt in ['Light (0.3)', 'Light to Medium (0.45)', 'Medium (0.6)', 'Medium to Dark (0.75)', 'Dark (0.9)']])]}"))
1222
+ shading_coefficient = st.number_input("Shading Coefficient", min_value=0.0, max_value=1.0, value=wall.shading_coefficient, step=0.05)
1223
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=wall.infiltration_rate_cfm, step=0.1)
1224
+ crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=wall.crack_width, step=0.001)
1225
+
1226
+ col1, col2 = st.columns([3, 1])
1227
+ with col1:
1228
+ submitted = st.form_submit_button("Update Wall")
1229
+ with col2:
1230
+ if st.form_submit_button("Cancel"):
1231
+ session_state[f"edit_wall"] = None
1232
  st.rerun()
1233
 
1234
+ if submitted:
1235
+ try:
1236
+ absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
1237
+ updated_wall = Wall(
1238
+ id=wall.id, name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
1239
+ wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value,
1240
+ shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate,
1241
+ surface_color=surface_color, crack_length=crack_length, crack_width=crack_width
1242
+ )
1243
+ self.component_library.components[wall.id] = updated_wall
1244
+ session_state.components['walls'][index] = updated_wall
1245
+ st.success(f"Updated {updated_wall.name}")
1246
+ session_state[f"edit_wall"] = None
1247
+ st.rerun()
1248
+ except ValueError as e:
1249
+ st.error(f"Error: {str(e)}")
1250
 
1251
+ def _display_edit_roof_form(self, session_state: Any, roof: Roof, index: int) -> None:
1252
+ with st.form(f"edit_roof_form_{index}", clear_on_submit=True):
 
 
 
 
 
 
 
 
 
 
 
1253
  col1, col2 = st.columns(2)
1254
  with col1:
1255
+ name = st.text_input("Name", roof.name)
1256
+ area = st.number_input("Area (m²)", min_value=0.0, value=roof.area, step=0.1)
1257
+ surface_color = st.selectbox("Surface Color", ["Dark", "Medium", "Light"], index=["Dark", "Medium", "Light"].index(roof.surface_color))
1258
+ roof_height = st.number_input("Roof Height (m)", min_value=0.0, max_value=100.0, value=roof.roof_height, step=0.1)
1259
  with col2:
1260
+ roof_options = self.reference_data.data["roof_types"]
1261
+ selected_roof = st.selectbox("Roof Type", options=list(roof_options.keys()), index=list(roof_options.keys()).index(roof.roof_type) if roof.roof_type in roof_options else 0)
1262
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=roof.u_value, step=0.01)
1263
+ roof_group = st.selectbox("Roof Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G"], index=["A", "B", "C", "D", "E", "F", "G"].index(roof.roof_group))
1264
+ slope = st.selectbox("Roof Slope", ["Flat", "Low Slope", "Steep Slope"], index=["Flat", "Low Slope", "Steep Slope"].index(roof.slope))
1265
+ absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"].index(f"{['Light (0.3)', 'Light to Medium (0.45)', 'Medium (0.6)', 'Medium to Dark (0.75)', 'Dark (0.9)'][np.argmin([abs(float(opt.split('(')[1].strip(')')) - roof.absorptivity) for opt in ['Light (0.3)', 'Light to Medium (0.45)', 'Medium (0.6)', 'Medium to Dark (0.75)', 'Dark (0.9)']])]}"))
1266
+
1267
+ col1, col2 = st.columns([3, 1])
1268
+ with col1:
1269
+ submitted = st.form_submit_button("Update Roof")
1270
+ with col2:
1271
+ if st.form_submit_button("Cancel"):
1272
+ session_state[f"edit_roof"] = None
1273
+ st.rerun()
1274
+
1275
  if submitted:
1276
+ try:
1277
+ absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
1278
+ updated_roof = Roof(
1279
+ id=roof.id, name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
1280
+ roof_type=selected_roof, roof_group=roof_group, slope=slope,
1281
+ absorptivity=absorptivity_value, surface_color=surface_color,
1282
+ roof_height=roof_height
1283
+ )
1284
+ self.component_library.components[roof.id] = updated_roof
1285
+ session_state.components['roofs'][index] = updated_roof
1286
+ st.success(f"Updated {updated_roof.name}")
1287
+ session_state[f"edit_roof"] = None
1288
+ st.rerun()
1289
+ except ValueError as e:
1290
+ st.error(f"Error: {str(e)}")
1291
 
1292
+ def _display_edit_floor_form(self, session_state: Any, floor: Floor, index: int) -> None:
1293
+ with st.form(f"edit_floor_form_{index}", clear_on_submit=True):
1294
+ col1, col2 = st.columns(2)
1295
+ with col1:
1296
+ name = st.text_input("Name", floor.name)
1297
+ area = st.number_input("Area (m²)", min_value=0.0, value=floor.area, step=0.1)
1298
+ perimeter = st.number_input("Perimeter (m)", min_value=0.0, value=floor.perimeter, step=0.1)
1299
+ with col2:
1300
+ floor_options = self.reference_data.data["floor_types"]
1301
+ selected_floor = st.selectbox("Floor Type", options=list(floor_options.keys()), index=list(floor_options.keys()).index(floor.floor_type) if floor.floor_type in floor_options else 0)
1302
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=floor.u_value, step=0.01)
1303
+ ground_contact = st.selectbox("Ground Contact", ["Yes", "No"], index=0 if floor.ground_contact else 1)
1304
+ ground_temp = st.number_input("Ground Temperature (°C)", min_value=-10.0, max_value=40.0, value=floor.ground_temperature_c, step=0.1) if ground_contact == "Yes" else floor.ground_temperature_c
1305
+ insulated = st.checkbox("Insulated Floor (e.g., R-10)", value=floor.insulated)
1306
+
1307
+ col1, col2 = st.columns([3, 1])
1308
+ with col1:
1309
+ submitted = st.form_submit_button("Update Floor")
1310
+ with col2:
1311
+ if st.form_submit_button("Cancel"):
1312
+ session_state[f"edit_floor"] = None
1313
  st.rerun()
1314
+
1315
+ if submitted:
1316
+ try:
1317
+ updated_floor = Floor(
1318
+ id=floor.id, name=name, u_value=u_value, area=area, floor_type=selected_floor,
1319
+ ground_contact=(ground_contact == "Yes"), ground_temperature_c=ground_temp,
1320
+ perimeter=perimeter, insulated=insulated
1321
+ )
1322
+ self.component_library.components[floor.id] = updated_floor
1323
+ session_state.components['floors'][index] = updated_floor
1324
+ st.success(f"Updated {updated_floor.name}")
1325
+ session_state[f"edit_floor"] = None
1326
+ st.rerun()
1327
+ except ValueError as e:
1328
+ st.error(f"Error: {str(e)}")
1329
+
1330
+ def _display_edit_window_form(self, session_state: Any, window: Window, index: int) -> None:
1331
+ with st.form(f"edit_window_form_{index}", clear_on_submit=True):
1332
+ col1, col2 = st.columns(2)
1333
+ with col1:
1334
+ name = st.text_input("Name", window.name)
1335
+ area = st.number_input("Area (m²)", min_value=0.0, value=window.area, step=0.1)
1336
+ orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=[o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE].index(window.orientation.value))
1337
+ window_options = self.reference_data.data["window_types"]
1338
+ selected_window = st.selectbox("Window Type", options=list(window_options.keys()), index=list(window_options.keys()).index(window.shading_device) if window.shading_device in window_options else 0)
1339
+ glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=[g.value for g in GlazingType].index(window.glazing_type))
1340
+ drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=[o.value for o in DraperyOpenness].index(window.drapery_openness))
1341
+ with col2:
1342
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=window.u_value, step=0.01)
1343
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=window.shgc, step=0.01)
1344
+ frame_type = st.selectbox("Frame Type", [f.value for f in FrameType], index=[f.value for f in FrameType].index(window.frame_type))
1345
+ frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=window.frame_percentage)
1346
+ shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
1347
+ shading_options["Custom"] = "Custom"
1348
+ shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=list(shading_options.values()).index(window.shading_device) if window.shading_device in shading_options.values() else len(shading_options) - 1)
1349
+ shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=window.shading_coefficient, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]]
1350
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=window.infiltration_rate_cfm, step=0.1)
1351
+ drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=[c.value for c in DraperyColor].index(window.drapery_color))
1352
+ drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=window.drapery_fullness, step=0.1)
1353
+
1354
+ col1, col2 = st.columns([3, 1])
1355
+ with col1:
1356
+ submitted = st.form_submit_button("Update Window")
1357
+ with col2:
1358
+ if st.form_submit_button("Cancel"):
1359
+ session_state[f"edit_window"] = None
1360
+ st.rerun()
1361
+
1362
+ if submitted:
1363
+ try:
1364
+ updated_window = Window(
1365
+ id=window.id, name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
1366
+ shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
1367
+ frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate,
1368
+ glazing_type=glazing_type, drapery_openness=drapery_openness, drapery_color=drapery_color,
1369
+ drapery_fullness=drapery_fullness
1370
+ )
1371
+ self.component_library.components[window.id] = updated_window
1372
+ session_state.components['windows'][index] = updated_window
1373
+ st.success(f"Updated {updated_window.name}")
1374
+ session_state[f"edit_window"] = None
1375
+ st.rerun()
1376
+ except ValueError as e:
1377
+ st.error(f"Error: {str(e)}")
1378
+
1379
+ def _display_edit_door_form(self, session_state: Any, door: Door, index: int) -> None:
1380
+ with st.form(f"edit_door_form_{index}", clear_on_submit=True):
1381
+ col1, col2 = st.columns(2)
1382
+ with col1:
1383
+ name = st.text_input("Name", door.name)
1384
+ area = st.number_input("Area (m²)", min_value=0.0, value=door.area, step=0.1)
1385
+ orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=[o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE].index(door.orientation.value))
1386
+ crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=door.crack_length, step=0.1)
1387
+ with col2:
1388
+ door_options = self.reference_data.data["door_types"]
1389
+ selected_door = st.selectbox("Door Type", options=list(door_options.keys()), index=list(door_options.keys()).index(door.door_type) if door.door_type in door_options else 0)
1390
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=door.u_value, step=0.01)
1391
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=door.infiltration_rate_cfm, step=0.1)
1392
+ crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=door.crack_width, step=0.001)
1393
+
1394
+ col1, col2 = st.columns([3, 1])
1395
+ with col1:
1396
+ submitted = st.form_submit_button("Update Door")
1397
+ with col2:
1398
+ if st.form_submit_button("Cancel"):
1399
+ session_state[f"edit_door"] = None
1400
+ st.rerun()
1401
+
1402
+ if submitted:
1403
+ try:
1404
+ updated_door = Door(
1405
+ id=door.id, name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
1406
+ door_type=selected_door, infiltration_rate_cfm=infiltration_rate,
1407
+ crack_length=crack_length, crack_width=crack_width
1408
+ )
1409
+ self.component_library.components[door.id] = updated_door
1410
+ session_state.components['doors'][index] = updated_door
1411
+ st.success(f"Updated {updated_door.name}")
1412
+ session_state[f"edit_door"] = None
1413
+ st.rerun()
1414
+ except ValueError as e:
1415
+ st.error(f"Error: {str(e)}")
1416
+
1417
+ def _display_edit_skylight_form(self, session_state: Any, skylight: Skylight, index: int) -> None:
1418
+ with st.form(f"edit_skylight_form_{index}", clear_on_submit=True):
1419
+ col1, col2 = st.columns(2)
1420
+ with col1:
1421
+ name = st.text_input("Name", skylight.name)
1422
+ area = st.number_input("Area (m²)", min_value=0.0, value=skylight.area, step=0.1)
1423
+ skylight_options = self.reference_data.data["skylight_types"]
1424
+ selected_skylight = st.selectbox("Skylight Type", options=list(skylight_options.keys()), index=list(skylight_options.keys()).index(skylight.shading_device) if skylight.shading_device in skylight_options else 0)
1425
+ glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=[g.value for g in GlazingType].index(skylight.glazing_type))
1426
+ drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=[o.value for o in DraperyOpenness].index(skylight.drapery_openness))
1427
+ with col2:
1428
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=skylight.u_value, step=0.01)
1429
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=skylight.shgc, step=0.01)
1430
+ frame_type = st.selectbox("Frame Type", [f.value for f in FrameType], index=[f.value for f in FrameType].index(skylight.frame_type))
1431
+ frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=skylight.frame_percentage)
1432
+ shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
1433
+ shading_options["Custom"] = "Custom"
1434
+ shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=list(shading_options.values()).index(skylight.shading_device) if skylight.shading_device in shading_options.values() else len(shading_options) - 1)
1435
+ shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=skylight.shading_coefficient, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]]
1436
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=skylight.infiltration_rate_cfm, step=0.1)
1437
+ drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=[c.value for c in DraperyColor].index(skylight.drapery_color))
1438
+ drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=skylight.drapery_fullness, step=0.1)
1439
+
1440
+ col1, col2 = st.columns([3, 1])
1441
+ with col1:
1442
+ submitted = st.form_submit_button("Update Skylight")
1443
+ with col2:
1444
+ if st.form_submit_button("Cancel"):
1445
+ session_state[f"edit_skylight"] = None
1446
+ st.rerun()
1447
+
1448
+ if submitted:
1449
+ try:
1450
+ updated_skylight = Skylight(
1451
+ id=skylight.id, name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
1452
+ shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
1453
+ frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate,
1454
+ glazing_type=glazing_type, drapery_openness=drapery_openness, drapery_color=drapery_color,
1455
+ drapery_fullness=drapery_fullness
1456
+ )
1457
+ self.component_library.components[skylight.id] = updated_skylight
1458
+ session_state.components['skylights'][index] = updated_skylight
1459
+ st.success(f"Updated {updated_skylight.name}")
1460
+ session_state[f"edit_skylight"] = None
1461
+ st.rerun()
1462
+ except ValueError as e:
1463
+ st.error(f"Error: {str(e)}")
1464
 
1465
  def _save_components(self, session_state: Any) -> None:
1466
+ components_data = {
1467
+ "walls": [comp.to_dict() for comp in session_state.components['walls']],
1468
+ "roofs": [comp.to_dict() for comp in session_state.components['roofs']],
1469
+ "floors": [comp.to_dict() for comp in session_state.components['floors']],
1470
+ "windows": [comp.to_dict() for comp in session_state.components['windows']],
1471
+ "doors": [comp.to_dict() for comp in session_state.components['doors']],
1472
+ "skylights": [comp.to_dict() for comp in session_state.components['skylights']],
1473
  "roof_air_volume_m3": session_state.roof_air_volume_m3,
1474
  "roof_ventilation_ach": session_state.roof_ventilation_ach
1475
  }
1476
+ buffer = io.StringIO()
1477
+ json.dump(components_data, buffer, indent=2)
1478
+ st.download_button(
1479
+ label="Download Components JSON",
1480
+ data=buffer.getvalue(),
1481
+ file_name="components.json",
1482
+ mime="application/json"
1483
+ )
1484
+ st.success("Components saved! Download the JSON file.")
1485
+
1486
+ # --- Main Application ---
1487
+ def main():
1488
+ st.set_page_config(page_title="HVAC Component Selection", layout="wide")
1489
+ component_selection = ComponentSelectionInterface()
1490
+ component_selection.display_component_selection(st.session_state)
1491
+
1492
  if __name__ == "__main__":
1493
+ main()