mabuseif commited on
Commit
db4b197
·
verified ·
1 Parent(s): f94a42b

Update app/component_selection.py

Browse files
Files changed (1) hide show
  1. app/component_selection.py +117 -138
app/component_selection.py CHANGED
@@ -4,6 +4,7 @@ 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
  """
@@ -39,11 +40,6 @@ class ComponentType(Enum):
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"
@@ -104,7 +100,6 @@ class Wall(BuildingComponent):
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
 
@@ -125,17 +120,13 @@ class Wall(BuildingComponent):
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,7 +136,6 @@ class Roof(BuildingComponent):
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):
@@ -161,17 +151,12 @@ class Roof(BuildingComponent):
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
 
@@ -206,13 +191,13 @@ class Window(BuildingComponent):
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__()
@@ -251,8 +236,8 @@ class Window(BuildingComponent):
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__()
@@ -277,13 +262,13 @@ 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__()
@@ -398,11 +383,6 @@ class ReferenceData:
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,
@@ -516,11 +496,10 @@ class ComponentSelectionInterface:
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()))
524
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(wall_options[selected_wall]["u_value"]), step=0.01)
525
  wall_group = st.selectbox("Wall Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G", "H"], index=0)
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)
@@ -536,7 +515,7 @@ class ComponentSelectionInterface:
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)
@@ -553,12 +532,12 @@ class ComponentSelectionInterface:
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",
@@ -577,7 +556,7 @@ class ComponentSelectionInterface:
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)
@@ -601,7 +580,6 @@ class ComponentSelectionInterface:
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"]
@@ -618,8 +596,7 @@ class ComponentSelectionInterface:
618
  new_roof = Roof(
619
  name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
620
  roof_type=selected_roof, roof_group=roof_group, slope=slope,
621
- absorptivity=absorptivity_value, surface_color=surface_color,
622
- roof_height=roof_height
623
  )
624
  self.component_library.add_component(new_roof)
625
  session_state.components['roofs'].append(new_roof)
@@ -651,11 +628,11 @@ class ComponentSelectionInterface:
651
  uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload")
652
  required_cols = [
653
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Roof Type", "Roof Group", "Slope",
654
- "Absorptivity", "Surface Color", "Roof Height (m)"
655
  ]
656
  template_data = pd.DataFrame(columns=required_cols)
657
  template_data.loc[0] = [
658
- "Example Roof", 10.0, 0.3, "Concrete Roof", "A", "Flat", 0.6, "Dark", 3.0
659
  ]
660
  st.download_button(
661
  label="Download Roof Template",
@@ -672,8 +649,7 @@ class ComponentSelectionInterface:
672
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
673
  orientation=Orientation.HORIZONTAL, roof_type=str(row["Roof Type"]),
674
  roof_group=str(row["Roof Group"]), slope=str(row["Slope"]),
675
- absorptivity=float(row["Absorptivity"]), surface_color=str(row["Surface Color"]),
676
- roof_height=float(row["Roof Height (m)"])
677
  )
678
  self.component_library.add_component(new_roof)
679
  session_state.components['roofs'].append(new_roof)
@@ -1092,11 +1068,11 @@ class ComponentSelectionInterface:
1092
  headers = {
1093
  ComponentType.WALL: [
1094
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group",
1095
- "Absorptivity", "Surface Color", "Crack Length (m)", "Crack Width (m)", "Edit", "Delete"
1096
  ],
1097
  ComponentType.ROOF: [
1098
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Roof Type", "Roof Group", "Slope",
1099
- "Absorptivity", "Surface Color", "Roof Height (m)", "Edit", "Delete"
1100
  ],
1101
  ComponentType.FLOOR: [
1102
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact",
@@ -1130,18 +1106,16 @@ class ComponentSelectionInterface:
1130
  cols[4].write(comp.wall_type)
1131
  cols[5].write(comp.wall_group)
1132
  cols[6].write(comp.absorptivity)
1133
- cols[7].write(comp.surface_color)
1134
- cols[8].write(comp.crack_length)
1135
- cols[9].write(comp.crack_width)
1136
- edit_col = 10
1137
  elif component_type == ComponentType.ROOF:
1138
  cols[3].write(comp.roof_type)
1139
  cols[4].write(comp.roof_group)
1140
  cols[5].write(comp.slope)
1141
  cols[6].write(comp.absorptivity)
1142
- cols[7].write(comp.surface_color)
1143
- cols[8].write(comp.roof_height)
1144
- edit_col = 9
1145
  elif component_type == ComponentType.FLOOR:
1146
  cols[3].write(comp.floor_type)
1147
  cols[4].write("Yes" if comp.ground_contact else "No")
@@ -1209,7 +1183,6 @@ class ComponentSelectionInterface:
1209
  name = st.text_input("Name", wall.name)
1210
  area = st.number_input("Area (m²)", min_value=0.0, value=wall.area, step=0.1)
1211
  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))
1212
- surface_color = st.selectbox("Surface Color", ["Dark", "Medium", "Light"], index=["Dark", "Medium", "Light"].index(wall.surface_color))
1213
  crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=wall.crack_length, step=0.1)
1214
  with col2:
1215
  wall_options = self.reference_data.data["wall_types"]
@@ -1221,30 +1194,32 @@ class ComponentSelectionInterface:
1221
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=wall.infiltration_rate_cfm, step=0.1)
1222
  crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=wall.crack_width, step=0.001)
1223
 
1224
- col1, col2 = st.columns([3, 1])
1225
- with col1:
1226
- submitted = st.form_submit_button("Update Wall")
1227
- with col2:
1228
- if st.form_submit_button("Cancel"):
1229
- session_state[f"edit_wall"] = None
1230
- st.rerun()
1231
 
1232
- if submitted:
1233
- try:
1234
- absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
1235
- updated_wall = Wall(
1236
- id=wall.id, name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
1237
- wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value,
1238
- shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate,
1239
- surface_color=surface_color, crack_length=crack_length, crack_width=crack_width
1240
- )
1241
- self.component_library.components[wall.id] = updated_wall
1242
- session_state.components['walls'][index] = updated_wall
1243
- st.success(f"Updated {updated_wall.name}")
 
 
 
 
 
 
1244
  session_state[f"edit_wall"] = None
1245
  st.rerun()
1246
- except ValueError as e:
1247
- st.error(f"Error: {str(e)}")
1248
 
1249
  def _display_edit_roof_form(self, session_state: Any, roof: Roof, index: int) -> None:
1250
  with st.form(f"edit_roof_form_{index}", clear_on_submit=True):
@@ -1252,7 +1227,6 @@ class ComponentSelectionInterface:
1252
  with col1:
1253
  name = st.text_input("Name", roof.name)
1254
  area = st.number_input("Area (m²)", min_value=0.0, value=roof.area, step=0.1)
1255
- surface_color = st.selectbox("Surface Color", ["Dark", "Medium", "Light"], index=["Dark", "Medium", "Light"].index(roof.surface_color))
1256
  roof_height = st.number_input("Roof Height (m)", min_value=0.0, max_value=100.0, value=roof.roof_height, step=0.1)
1257
  with col2:
1258
  roof_options = self.reference_data.data["roof_types"]
@@ -1262,13 +1236,11 @@ class ComponentSelectionInterface:
1262
  slope = st.selectbox("Roof Slope", ["Flat", "Low Slope", "Steep Slope"], index=["Flat", "Low Slope", "Steep Slope"].index(roof.slope))
1263
  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)']])]}"))
1264
 
1265
- col1, col2 = st.columns([3, 1])
1266
- with col1:
1267
  submitted = st.form_submit_button("Update Roof")
1268
- with col2:
1269
- if st.form_submit_button("Cancel"):
1270
- session_state[f"edit_roof"] = None
1271
- st.rerun()
1272
 
1273
  if submitted:
1274
  try:
@@ -1276,16 +1248,19 @@ class ComponentSelectionInterface:
1276
  updated_roof = Roof(
1277
  id=roof.id, name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
1278
  roof_type=selected_roof, roof_group=roof_group, slope=slope,
1279
- absorptivity=absorptivity_value, surface_color=surface_color,
1280
- roof_height=roof_height
1281
  )
1282
  self.component_library.components[roof.id] = updated_roof
1283
  session_state.components['roofs'][index] = updated_roof
1284
- st.success(f"Updated {updated_roof.name}")
1285
  session_state[f"edit_roof"] = None
1286
  st.rerun()
1287
  except ValueError as e:
1288
  st.error(f"Error: {str(e)}")
 
 
 
 
1289
 
1290
  def _display_edit_floor_form(self, session_state: Any, floor: Floor, index: int) -> None:
1291
  with st.form(f"edit_floor_form_{index}", clear_on_submit=True):
@@ -1302,13 +1277,11 @@ class ComponentSelectionInterface:
1302
  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
1303
  insulated = st.checkbox("Insulated Floor (e.g., R-10)", value=floor.insulated)
1304
 
1305
- col1, col2 = st.columns([3, 1])
1306
- with col1:
1307
  submitted = st.form_submit_button("Update Floor")
1308
- with col2:
1309
- if st.form_submit_button("Cancel"):
1310
- session_state[f"edit_floor"] = None
1311
- st.rerun()
1312
 
1313
  if submitted:
1314
  try:
@@ -1319,11 +1292,15 @@ class ComponentSelectionInterface:
1319
  )
1320
  self.component_library.components[floor.id] = updated_floor
1321
  session_state.components['floors'][index] = updated_floor
1322
- st.success(f"Updated {updated_floor.name}")
1323
  session_state[f"edit_floor"] = None
1324
  st.rerun()
1325
  except ValueError as e:
1326
  st.error(f"Error: {str(e)}")
 
 
 
 
1327
 
1328
  def _display_edit_window_form(self, session_state: Any, window: Window, index: int) -> None:
1329
  with st.form(f"edit_window_form_{index}", clear_on_submit=True):
@@ -1333,7 +1310,7 @@ class ComponentSelectionInterface:
1333
  area = st.number_input("Area (m²)", min_value=0.0, value=window.area, step=0.1)
1334
  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))
1335
  window_options = self.reference_data.data["window_types"]
1336
- 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)
1337
  glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=[g.value for g in GlazingType].index(window.glazing_type))
1338
  drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=[o.value for o in DraperyOpenness].index(window.drapery_openness))
1339
  with col2:
@@ -1343,19 +1320,17 @@ class ComponentSelectionInterface:
1343
  frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=window.frame_percentage)
1344
  shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
1345
  shading_options["Custom"] = "Custom"
1346
- 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)
1347
  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]]
1348
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=window.infiltration_rate_cfm, step=0.1)
1349
  drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=[c.value for c in DraperyColor].index(window.drapery_color))
1350
  drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=window.drapery_fullness, step=0.1)
1351
 
1352
- col1, col2 = st.columns([3, 1])
1353
- with col1:
1354
  submitted = st.form_submit_button("Update Window")
1355
- with col2:
1356
- if st.form_submit_button("Cancel"):
1357
- session_state[f"edit_window"] = None
1358
- st.rerun()
1359
 
1360
  if submitted:
1361
  try:
@@ -1368,11 +1343,15 @@ class ComponentSelectionInterface:
1368
  )
1369
  self.component_library.components[window.id] = updated_window
1370
  session_state.components['windows'][index] = updated_window
1371
- st.success(f"Updated {updated_window.name}")
1372
  session_state[f"edit_window"] = None
1373
  st.rerun()
1374
  except ValueError as e:
1375
  st.error(f"Error: {str(e)}")
 
 
 
 
1376
 
1377
  def _display_edit_door_form(self, session_state: Any, door: Door, index: int) -> None:
1378
  with st.form(f"edit_door_form_{index}", clear_on_submit=True):
@@ -1389,13 +1368,11 @@ class ComponentSelectionInterface:
1389
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=door.infiltration_rate_cfm, step=0.1)
1390
  crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=door.crack_width, step=0.001)
1391
 
1392
- col1, col2 = st.columns([3, 1])
1393
- with col1:
1394
  submitted = st.form_submit_button("Update Door")
1395
- with col2:
1396
- if st.form_submit_button("Cancel"):
1397
- session_state[f"edit_door"] = None
1398
- st.rerun()
1399
 
1400
  if submitted:
1401
  try:
@@ -1406,11 +1383,15 @@ class ComponentSelectionInterface:
1406
  )
1407
  self.component_library.components[door.id] = updated_door
1408
  session_state.components['doors'][index] = updated_door
1409
- st.success(f"Updated {updated_door.name}")
1410
  session_state[f"edit_door"] = None
1411
  st.rerun()
1412
  except ValueError as e:
1413
  st.error(f"Error: {str(e)}")
 
 
 
 
1414
 
1415
  def _display_edit_skylight_form(self, session_state: Any, skylight: Skylight, index: int) -> None:
1416
  with st.form(f"edit_skylight_form_{index}", clear_on_submit=True):
@@ -1419,7 +1400,7 @@ class ComponentSelectionInterface:
1419
  name = st.text_input("Name", skylight.name)
1420
  area = st.number_input("Area (m²)", min_value=0.0, value=skylight.area, step=0.1)
1421
  skylight_options = self.reference_data.data["skylight_types"]
1422
- 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)
1423
  glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=[g.value for g in GlazingType].index(skylight.glazing_type))
1424
  drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=[o.value for o in DraperyOpenness].index(skylight.drapery_openness))
1425
  with col2:
@@ -1429,19 +1410,17 @@ class ComponentSelectionInterface:
1429
  frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=skylight.frame_percentage)
1430
  shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
1431
  shading_options["Custom"] = "Custom"
1432
- 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)
1433
  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]]
1434
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=skylight.infiltration_rate_cfm, step=0.1)
1435
  drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=[c.value for c in DraperyColor].index(skylight.drapery_color))
1436
  drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=skylight.drapery_fullness, step=0.1)
1437
 
1438
- col1, col2 = st.columns([3, 1])
1439
- with col1:
1440
  submitted = st.form_submit_button("Update Skylight")
1441
- with col2:
1442
- if st.form_submit_button("Cancel"):
1443
- session_state[f"edit_skylight"] = None
1444
- st.rerun()
1445
 
1446
  if submitted:
1447
  try:
@@ -1454,38 +1433,38 @@ class ComponentSelectionInterface:
1454
  )
1455
  self.component_library.components[skylight.id] = updated_skylight
1456
  session_state.components['skylights'][index] = updated_skylight
1457
- st.success(f"Updated {updated_skylight.name}")
1458
  session_state[f"edit_skylight"] = None
1459
  st.rerun()
1460
  except ValueError as e:
1461
  st.error(f"Error: {str(e)}")
 
 
 
 
1462
 
1463
  def _save_components(self, session_state: Any) -> None:
1464
  components_data = {
1465
- "walls": [comp.to_dict() for comp in session_state.components['walls']],
1466
- "roofs": [comp.to_dict() for comp in session_state.components['roofs']],
1467
- "floors": [comp.to_dict() for comp in session_state.components['floors']],
1468
- "windows": [comp.to_dict() for comp in session_state.components['windows']],
1469
- "doors": [comp.to_dict() for comp in session_state.components['doors']],
1470
- "skylights": [comp.to_dict() for comp in session_state.components['skylights']],
1471
  "roof_air_volume_m3": session_state.roof_air_volume_m3,
1472
  "roof_ventilation_ach": session_state.roof_ventilation_ach
1473
  }
1474
- buffer = io.StringIO()
1475
- json.dump(components_data, buffer, indent=2)
1476
  st.download_button(
1477
  label="Download Components JSON",
1478
- data=buffer.getvalue(),
1479
  file_name="components.json",
1480
  mime="application/json"
1481
  )
1482
- st.success("Components saved! Download the JSON file.")
1483
-
1484
- # --- Main Application ---
1485
- def main():
1486
- st.set_page_config(page_title="HVAC Component Selection", layout="wide")
1487
- component_selection = ComponentSelectionInterface()
1488
- component_selection.display_component_selection(st.session_state)
1489
 
 
1490
  if __name__ == "__main__":
1491
- main()
 
 
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
+ Updated 2025-05-03: Removed surface_color field, using solar absorptivity (Light 0.3, Light to Medium 0.45, Medium 0.6, Medium to Dark 0.75, Dark 0.9) exclusively.
8
 
9
  Author: Dr Majed Abuseif
10
  """
 
40
  DOOR = "Door"
41
  SKYLIGHT = "Skylight"
42
 
 
 
 
 
 
43
  class GlazingType(Enum):
44
  SINGLE_CLEAR = "Single Clear"
45
  SINGLE_TINTED = "Single Tinted"
 
100
  absorptivity: float = 0.6
101
  shading_coefficient: float = 1.0
102
  infiltration_rate_cfm: float = 0.0
 
103
  crack_length: float = 0.0 # Added for infiltration (m)
104
  crack_width: float = 0.0 # Added for infiltration (m)
105
 
 
120
  if self.wall_group not in VALID_WALL_GROUPS:
121
  st.warning(f"Invalid wall_group '{self.wall_group}' for wall '{self.name}'. Defaulting to 'A'.")
122
  self.wall_group = "A"
 
 
 
 
123
 
124
  def to_dict(self) -> dict:
125
  base_dict = super().to_dict()
126
  base_dict.update({
127
  "wall_type": self.wall_type, "wall_group": self.wall_group, "absorptivity": self.absorptivity,
128
  "shading_coefficient": self.shading_coefficient, "infiltration_rate_cfm": self.infiltration_rate_cfm,
129
+ "crack_length": self.crack_length, "crack_width": self.crack_width
130
  })
131
  return base_dict
132
 
 
136
  roof_group: str = "A" # ASHRAE group
137
  slope: str = "Flat"
138
  absorptivity: float = 0.6
 
139
  roof_height: float = 3.0 # Added for stack effect (m)
140
 
141
  def __post_init__(self):
 
151
  if self.roof_group not in VALID_ROOF_GROUPS:
152
  st.warning(f"Invalid roof_group '{self.roof_group}' for roof '{self.name}'. Defaulting to 'A'.")
153
  self.roof_group = "A"
 
 
 
 
154
 
155
  def to_dict(self) -> dict:
156
  base_dict = super().to_dict()
157
  base_dict.update({
158
  "roof_type": self.roof_type, "roof_group": self.roof_group, "slope": self.slope,
159
+ "absorptivity": self.absorptivity, "roof_height": self.roof_height
 
160
  })
161
  return base_dict
162
 
 
191
  shgc: float = 0.7
192
  shading_device: str = "None"
193
  shading_coefficient: float = 1.0
194
+ frame_type: str = "Aluminum without Thermal Break"
195
  frame_percentage: float = 20.0
196
  infiltration_rate_cfm: float = 0.0
197
+ glazing_type: str = "Double Clear"
198
+ drapery_openness: str = "Open"
199
+ drapery_color: str = "Light"
200
+ drapery_fullness: float = 1.5
201
 
202
  def __post_init__(self):
203
  super().__post_init__()
 
236
  class Door(BuildingComponent):
237
  door_type: str = "Solid Wood"
238
  infiltration_rate_cfm: float = 0.0
239
+ crack_length: float = 0.0
240
+ crack_width: float = 0.0
241
 
242
  def __post_init__(self):
243
  super().__post_init__()
 
262
  shgc: float = 0.7
263
  shading_device: str = "None"
264
  shading_coefficient: float = 1.0
265
+ frame_type: str = "Aluminum without Thermal Break"
266
  frame_percentage: float = 20.0
267
  infiltration_rate_cfm: float = 0.0
268
+ glazing_type: str = "Double Clear"
269
+ drapery_openness: str = "Open"
270
+ drapery_color: str = "Light"
271
+ drapery_fullness: float = 1.5
272
 
273
  def __post_init__(self):
274
  super().__post_init__()
 
383
  "Reflective": {"u_value": 2.4, "shgc": 0.38, "glazing_type": "Reflective", "frame_type": "Aluminum with Thermal Break"},
384
  "Custom": {"u_value": 3.0, "shgc": 0.7, "glazing_type": "Double Clear", "frame_type": "Aluminum with Thermal Break"}
385
  },
 
 
 
 
 
386
  "frame_types": {
387
  "Aluminum without Thermal Break": 1.0,
388
  "Aluminum with Thermal Break": 0.8,
 
496
  name = st.text_input("Name", "New Wall")
497
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
498
  orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
 
499
  crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=0.0, step=0.1)
500
  with col2:
501
  wall_options = self.reference_data.data["wall_types"]
502
+ selected_wall = st seletbox("Wall Type", options=list(wall_options.keys()))
503
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(wall_options[selected_wall]["u_value"]), step=0.01)
504
  wall_group = st.selectbox("Wall Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G", "H"], index=0)
505
  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)
 
515
  name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
516
  wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value,
517
  shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate,
518
+ crack_length=crack_length, crack_width=crack_width
519
  )
520
  self.component_library.add_component(new_wall)
521
  session_state.components['walls'].append(new_wall)
 
532
  uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key="wall_upload")
533
  required_cols = [
534
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group",
535
+ "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)",
536
  "Crack Length (m)", "Crack Width (m)"
537
  ]
538
  template_data = pd.DataFrame(columns=required_cols)
539
  template_data.loc[0] = [
540
+ "Example Wall", 10.0, 0.5, "North", "Brick Wall", "A", 0.6, 1.0, 0.0, 0.0, 0.0
541
  ]
542
  st.download_button(
543
  label="Download Wall Template",
 
556
  wall_group=str(row["Wall Group"]), absorptivity=float(row["Absorptivity"]),
557
  shading_coefficient=float(row["Shading Coefficient"]),
558
  infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]),
559
+ crack_length=float(row["Crack Length (m)"]),
560
  crack_width=float(row["Crack Width (m)"])
561
  )
562
  self.component_library.add_component(new_wall)
 
580
  with col1:
581
  name = st.text_input("Name", "New Roof")
582
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
 
583
  roof_height = st.number_input("Roof Height (m)", min_value=0.0, max_value=100.0, value=3.0, step=0.1)
584
  with col2:
585
  roof_options = self.reference_data.data["roof_types"]
 
596
  new_roof = Roof(
597
  name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
598
  roof_type=selected_roof, roof_group=roof_group, slope=slope,
599
+ absorptivity=absorptivity_value, roof_height=roof_height
 
600
  )
601
  self.component_library.add_component(new_roof)
602
  session_state.components['roofs'].append(new_roof)
 
628
  uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload")
629
  required_cols = [
630
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Roof Type", "Roof Group", "Slope",
631
+ "Absorptivity", "Roof Height (m)"
632
  ]
633
  template_data = pd.DataFrame(columns=required_cols)
634
  template_data.loc[0] = [
635
+ "Example Roof", 10.0, 0.3, "Concrete Roof", "A", "Flat", 0.6, 3.0
636
  ]
637
  st.download_button(
638
  label="Download Roof Template",
 
649
  name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
650
  orientation=Orientation.HORIZONTAL, roof_type=str(row["Roof Type"]),
651
  roof_group=str(row["Roof Group"]), slope=str(row["Slope"]),
652
+ absorptivity=float(row["Absorptivity"]), roof_height=float(row["Roof Height (m)"])
 
653
  )
654
  self.component_library.add_component(new_roof)
655
  session_state.components['roofs'].append(new_roof)
 
1068
  headers = {
1069
  ComponentType.WALL: [
1070
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group",
1071
+ "Absorptivity", "Crack Length (m)", "Crack Width (m)", "Edit", "Delete"
1072
  ],
1073
  ComponentType.ROOF: [
1074
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Roof Type", "Roof Group", "Slope",
1075
+ "Absorptivity", "Roof Height (m)", "Edit", "Delete"
1076
  ],
1077
  ComponentType.FLOOR: [
1078
  "Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact",
 
1106
  cols[4].write(comp.wall_type)
1107
  cols[5].write(comp.wall_group)
1108
  cols[6].write(comp.absorptivity)
1109
+ cols[7].write(comp.crack_length)
1110
+ cols[8].write(comp.crack_width)
1111
+ edit_col = 9
 
1112
  elif component_type == ComponentType.ROOF:
1113
  cols[3].write(comp.roof_type)
1114
  cols[4].write(comp.roof_group)
1115
  cols[5].write(comp.slope)
1116
  cols[6].write(comp.absorptivity)
1117
+ cols[7].write(comp.roof_height)
1118
+ edit_col = 8
 
1119
  elif component_type == ComponentType.FLOOR:
1120
  cols[3].write(comp.floor_type)
1121
  cols[4].write("Yes" if comp.ground_contact else "No")
 
1183
  name = st.text_input("Name", wall.name)
1184
  area = st.number_input("Area (m²)", min_value=0.0, value=wall.area, step=0.1)
1185
  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))
 
1186
  crack_length = st.number_input("Crack Length (m)", min_value=0.0, max_value=100.0, value=wall.crack_length, step=0.1)
1187
  with col2:
1188
  wall_options = self.reference_data.data["wall_types"]
 
1194
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=wall.infiltration_rate_cfm, step=0.1)
1195
  crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=wall.crack_width, step=0.001)
1196
 
1197
+ col3, col4 = st.columns(2)
1198
+ with col3:
1199
+ submitted = st.form_submit_button("Update Wall")
1200
+ with col4:
1201
+ cancelled = st.form_submit_button("Cancel")
 
 
1202
 
1203
+ if submitted:
1204
+ try:
1205
+ absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
1206
+ updated_wall = Wall(
1207
+ id=wall.id, name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
1208
+ wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value,
1209
+ shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate,
1210
+ crack_length=crack_length, crack_width=crack_width
1211
+ )
1212
+ self.component_library.components[wall.id] = updated_wall
1213
+ session_state.components['walls'][index] = updated_wall
1214
+ st.success(f"Updated {name}")
1215
+ session_state[f"edit_wall"] = None
1216
+ st.rerun()
1217
+ except ValueError as e:
1218
+ st.error(f"Error: {str(e)}")
1219
+
1220
+ if cancelled:
1221
  session_state[f"edit_wall"] = None
1222
  st.rerun()
 
 
1223
 
1224
  def _display_edit_roof_form(self, session_state: Any, roof: Roof, index: int) -> None:
1225
  with st.form(f"edit_roof_form_{index}", clear_on_submit=True):
 
1227
  with col1:
1228
  name = st.text_input("Name", roof.name)
1229
  area = st.number_input("Area (m²)", min_value=0.0, value=roof.area, step=0.1)
 
1230
  roof_height = st.number_input("Roof Height (m)", min_value=0.0, max_value=100.0, value=roof.roof_height, step=0.1)
1231
  with col2:
1232
  roof_options = self.reference_data.data["roof_types"]
 
1236
  slope = st.selectbox("Roof Slope", ["Flat", "Low Slope", "Steep Slope"], index=["Flat", "Low Slope", "Steep Slope"].index(roof.slope))
1237
  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)']])]}"))
1238
 
1239
+ col3, col4 = st.columns(2)
1240
+ with col3:
1241
  submitted = st.form_submit_button("Update Roof")
1242
+ with col4:
1243
+ cancelled = st.form_submit_button("Cancel")
 
 
1244
 
1245
  if submitted:
1246
  try:
 
1248
  updated_roof = Roof(
1249
  id=roof.id, name=name, u_value=u_value, area=area, orientation=Orientation.HORIZONTAL,
1250
  roof_type=selected_roof, roof_group=roof_group, slope=slope,
1251
+ absorptivity=absorptivity_value, roof_height=roof_height
 
1252
  )
1253
  self.component_library.components[roof.id] = updated_roof
1254
  session_state.components['roofs'][index] = updated_roof
1255
+ st.success(f"Updated {name}")
1256
  session_state[f"edit_roof"] = None
1257
  st.rerun()
1258
  except ValueError as e:
1259
  st.error(f"Error: {str(e)}")
1260
+
1261
+ if cancelled:
1262
+ session_state[f"edit_roof"] = None
1263
+ st.rerun()
1264
 
1265
  def _display_edit_floor_form(self, session_state: Any, floor: Floor, index: int) -> None:
1266
  with st.form(f"edit_floor_form_{index}", clear_on_submit=True):
 
1277
  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
1278
  insulated = st.checkbox("Insulated Floor (e.g., R-10)", value=floor.insulated)
1279
 
1280
+ col3, col4 = st.columns(2)
1281
+ with col3:
1282
  submitted = st.form_submit_button("Update Floor")
1283
+ with col4:
1284
+ cancelled = st.form_submit_button("Cancel")
 
 
1285
 
1286
  if submitted:
1287
  try:
 
1292
  )
1293
  self.component_library.components[floor.id] = updated_floor
1294
  session_state.components['floors'][index] = updated_floor
1295
+ st.success(f"Updated {name}")
1296
  session_state[f"edit_floor"] = None
1297
  st.rerun()
1298
  except ValueError as e:
1299
  st.error(f"Error: {str(e)}")
1300
+
1301
+ if cancelled:
1302
+ session_state[f"edit_floor"] = None
1303
+ st.rerun()
1304
 
1305
  def _display_edit_window_form(self, session_state: Any, window: Window, index: int) -> None:
1306
  with st.form(f"edit_window_form_{index}", clear_on_submit=True):
 
1310
  area = st.number_input("Area (m²)", min_value=0.0, value=window.area, step=0.1)
1311
  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))
1312
  window_options = self.reference_data.data["window_types"]
1313
+ selected_window = st.selectbox("Window Type", options=list(window_options.keys()), index=list(window_options.keys()).index(window.glazing_type) if window.glazing_type in window_options else 0)
1314
  glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=[g.value for g in GlazingType].index(window.glazing_type))
1315
  drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=[o.value for o in DraperyOpenness].index(window.drapery_openness))
1316
  with col2:
 
1320
  frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=window.frame_percentage)
1321
  shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
1322
  shading_options["Custom"] = "Custom"
1323
+ 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 0)
1324
  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]]
1325
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=window.infiltration_rate_cfm, step=0.1)
1326
  drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=[c.value for c in DraperyColor].index(window.drapery_color))
1327
  drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=window.drapery_fullness, step=0.1)
1328
 
1329
+ col3, col4 = st.columns(2)
1330
+ with col3:
1331
  submitted = st.form_submit_button("Update Window")
1332
+ with col4:
1333
+ cancelled = st.form_submit_button("Cancel")
 
 
1334
 
1335
  if submitted:
1336
  try:
 
1343
  )
1344
  self.component_library.components[window.id] = updated_window
1345
  session_state.components['windows'][index] = updated_window
1346
+ st.success(f"Updated {name}")
1347
  session_state[f"edit_window"] = None
1348
  st.rerun()
1349
  except ValueError as e:
1350
  st.error(f"Error: {str(e)}")
1351
+
1352
+ if cancelled:
1353
+ session_state[f"edit_window"] = None
1354
+ st.rerun()
1355
 
1356
  def _display_edit_door_form(self, session_state: Any, door: Door, index: int) -> None:
1357
  with st.form(f"edit_door_form_{index}", clear_on_submit=True):
 
1368
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=door.infiltration_rate_cfm, step=0.1)
1369
  crack_width = st.number_input("Crack Width (m)", min_value=0.0, max_value=0.1, value=door.crack_width, step=0.001)
1370
 
1371
+ col3, col4 = st.columns(2)
1372
+ with col3:
1373
  submitted = st.form_submit_button("Update Door")
1374
+ with col4:
1375
+ cancelled = st.form_submit_button("Cancel")
 
 
1376
 
1377
  if submitted:
1378
  try:
 
1383
  )
1384
  self.component_library.components[door.id] = updated_door
1385
  session_state.components['doors'][index] = updated_door
1386
+ st.success(f"Updated {name}")
1387
  session_state[f"edit_door"] = None
1388
  st.rerun()
1389
  except ValueError as e:
1390
  st.error(f"Error: {str(e)}")
1391
+
1392
+ if cancelled:
1393
+ session_state[f"edit_door"] = None
1394
+ st.rerun()
1395
 
1396
  def _display_edit_skylight_form(self, session_state: Any, skylight: Skylight, index: int) -> None:
1397
  with st.form(f"edit_skylight_form_{index}", clear_on_submit=True):
 
1400
  name = st.text_input("Name", skylight.name)
1401
  area = st.number_input("Area (m²)", min_value=0.0, value=skylight.area, step=0.1)
1402
  skylight_options = self.reference_data.data["skylight_types"]
1403
+ selected_skylight = st.selectbox("Skylight Type", options=list(skylight_options.keys()), index=list(skylight_options.keys()).index(skylight.glazing_type) if skylight.glazing_type in skylight_options else 0)
1404
  glazing_type = st.selectbox("Glazing Type", [g.value for g in GlazingType], index=[g.value for g in GlazingType].index(skylight.glazing_type))
1405
  drapery_openness = st.selectbox("Drapery Openness", [o.value for o in DraperyOpenness], index=[o.value for o in DraperyOpenness].index(skylight.drapery_openness))
1406
  with col2:
 
1410
  frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=skylight.frame_percentage)
1411
  shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
1412
  shading_options["Custom"] = "Custom"
1413
+ 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 0)
1414
  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]]
1415
  infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=skylight.infiltration_rate_cfm, step=0.1)
1416
  drapery_color = st.selectbox("Drapery Color", [c.value for c in DraperyColor], index=[c.value for c in DraperyColor].index(skylight.drapery_color))
1417
  drapery_fullness = st.number_input("Drapery Fullness", min_value=1.0, max_value=2.0, value=skylight.drapery_fullness, step=0.1)
1418
 
1419
+ col3, col4 = st.columns(2)
1420
+ with col3:
1421
  submitted = st.form_submit_button("Update Skylight")
1422
+ with col4:
1423
+ cancelled = st.form_submit_button("Cancel")
 
 
1424
 
1425
  if submitted:
1426
  try:
 
1433
  )
1434
  self.component_library.components[skylight.id] = updated_skylight
1435
  session_state.components['skylights'][index] = updated_skylight
1436
+ st.success(f"Updated {name}")
1437
  session_state[f"edit_skylight"] = None
1438
  st.rerun()
1439
  except ValueError as e:
1440
  st.error(f"Error: {str(e)}")
1441
+
1442
+ if cancelled:
1443
+ session_state[f"edit_skylight"] = None
1444
+ st.rerun()
1445
 
1446
  def _save_components(self, session_state: Any) -> None:
1447
  components_data = {
1448
+ "walls": [comp.to_dict() for comp in session_state.components.get('walls', [])],
1449
+ "roofs": [comp.to_dict() for comp in session_state.components.get('roofs', [])],
1450
+ "floors": [comp.to_dict() for comp in session_state.components.get('floors', [])],
1451
+ "windows": [comp.to_dict() for comp in session_state.components.get('windows', [])],
1452
+ "doors": [comp.to_dict() for comp in session_state.components.get('doors', [])],
1453
+ "skylights": [comp.to_dict() for comp in session_state.components.get('skylights', [])],
1454
  "roof_air_volume_m3": session_state.roof_air_volume_m3,
1455
  "roof_ventilation_ach": session_state.roof_ventilation_ach
1456
  }
1457
+ output = io.StringIO()
1458
+ json.dump(components_data, output, indent=4)
1459
  st.download_button(
1460
  label="Download Components JSON",
1461
+ data=output.getvalue(),
1462
  file_name="components.json",
1463
  mime="application/json"
1464
  )
1465
+ st.success("Components saved! Click the button to download the JSON file.")
 
 
 
 
 
 
1466
 
1467
+ # --- Main Execution ---
1468
  if __name__ == "__main__":
1469
+ interface = ComponentSelectionInterface()
1470
+ interface.display_component_selection(st.session_state)