mabuseif commited on
Commit
8312a99
·
verified ·
1 Parent(s): d4b3a08

Update app/component_selection.py

Browse files
Files changed (1) hide show
  1. app/component_selection.py +484 -599
app/component_selection.py CHANGED
@@ -34,12 +34,6 @@ class ComponentType(Enum):
34
  WINDOW = "Window"
35
  DOOR = "Door"
36
 
37
- class ShadingType(Enum):
38
- NONE = "None"
39
- INTERNAL = "Internal"
40
- EXTERNAL = "External"
41
- BETWEEN_GLASS = "Between-glass"
42
-
43
  # --- Data Models ---
44
  @dataclass
45
  class MaterialLayer:
@@ -55,208 +49,186 @@ class BuildingComponent:
55
  u_value: float = 0.0 # W/(m²·K)
56
  area: float = 0.0 # m²
57
  orientation: Orientation = Orientation.NOT_APPLICABLE
58
- material_layers: List[MaterialLayer] = field(default_factory=list)
59
 
60
  def __post_init__(self):
61
- if self.area < 0:
62
- raise ValueError("Area cannot be negative")
63
- if self.u_value < 0:
64
- raise ValueError("U-value cannot be negative")
65
 
66
  def to_dict(self) -> dict:
67
  return {
68
- "id": self.id,
69
- "name": self.name,
70
- "component_type": self.component_type.value,
71
- "u_value": self.u_value,
72
- "area": self.area,
73
- "orientation": self.orientation.value,
74
- "material_layers": [{"name": l.name, "thickness": l.thickness, "conductivity": l.conductivity} for l in self.material_layers]
75
  }
76
 
77
  @dataclass
78
  class Wall(BuildingComponent):
79
  wall_type: str = "Brick"
80
  wall_group: str = "Wall"
81
- shading_percentage: float = 0.0
 
 
82
 
83
  def __post_init__(self):
84
  super().__post_init__()
85
  self.component_type = ComponentType.WALL
86
- if not 0 <= self.shading_percentage <= 100:
87
- raise ValueError("Shading percentage must be between 0 and 100")
 
 
 
 
88
 
89
  def to_dict(self) -> dict:
90
  base_dict = super().to_dict()
91
- base_dict.update({"wall_type": self.wall_type, "wall_group": self.wall_group, "shading_percentage": self.shading_percentage})
 
 
 
92
  return base_dict
93
 
94
  @dataclass
95
  class Roof(BuildingComponent):
96
  roof_type: str = "Concrete"
97
  roof_group: str = "C"
 
 
98
 
99
  def __post_init__(self):
100
  super().__post_init__()
101
  self.component_type = ComponentType.ROOF
102
- self.orientation = Orientation.HORIZONTAL
 
 
 
103
 
104
  def to_dict(self) -> dict:
105
  base_dict = super().to_dict()
106
- base_dict.update({"roof_type": self.roof_type, "roof_group": self.roof_group})
 
 
107
  return base_dict
108
 
109
  @dataclass
110
  class Floor(BuildingComponent):
111
  floor_type: str = "Concrete"
 
 
112
 
113
  def __post_init__(self):
114
  super().__post_init__()
115
  self.component_type = ComponentType.FLOOR
116
- self.orientation = Orientation.HORIZONTAL
117
 
118
  def to_dict(self) -> dict:
119
  base_dict = super().to_dict()
120
- base_dict.update({"floor_type": self.floor_type})
 
 
121
  return base_dict
122
 
123
  @dataclass
124
  class Window(BuildingComponent):
125
  shgc: float = 0.7
126
- vt: float = 0.7
127
- window_type: str = "Double Glazed"
128
- glazing_layers: int = 2
129
- gas_fill: str = "Air"
130
- low_e_coating: bool = False
131
- shading_device_id: str = "preset_none"
132
 
133
  def __post_init__(self):
134
  super().__post_init__()
135
  self.component_type = ComponentType.WINDOW
136
  if not 0 <= self.shgc <= 1:
137
  raise ValueError("SHGC must be between 0 and 1")
138
- if not 0 <= self.vt <= 1:
139
- raise ValueError("VT must be between 0 and 1")
 
 
 
 
140
 
141
  def to_dict(self) -> dict:
142
  base_dict = super().to_dict()
143
  base_dict.update({
144
- "shgc": self.shgc, "vt": self.vt, "window_type": self.window_type,
145
- "glazing_layers": self.glazing_layers, "gas_fill": self.gas_fill,
146
- "low_e_coating": self.low_e_coating, "shading_device_id": self.shading_device_id
147
  })
148
  return base_dict
149
 
150
  @dataclass
151
  class Door(BuildingComponent):
152
  door_type: str = "Solid Wood"
 
153
 
154
  def __post_init__(self):
155
  super().__post_init__()
156
  self.component_type = ComponentType.DOOR
 
 
157
 
158
  def to_dict(self) -> dict:
159
  base_dict = super().to_dict()
160
- base_dict.update({"door_type": self.door_type})
161
  return base_dict
162
 
163
- # --- Shading System ---
164
- @dataclass
165
- class ShadingDevice:
166
- id: str
167
- name: str
168
- shading_type: ShadingType
169
- shading_coefficient: float
170
- coverage_percentage: float = 100.0
171
- description: str = ""
172
-
173
- def __post_init__(self):
174
- if self.shading_coefficient < 0 or self.shading_coefficient > 1:
175
- raise ValueError("Shading coefficient must be between 0 and 1")
176
- if self.coverage_percentage < 0 or self.coverage_percentage > 100:
177
- raise ValueError("Coverage percentage must be between 0 and 100")
178
-
179
- @property
180
- def effective_shading_coefficient(self) -> float:
181
- coverage_factor = self.coverage_percentage / 100.0
182
- return self.shading_coefficient * coverage_factor + 1.0 * (1 - coverage_factor)
183
-
184
- def to_dict(self) -> Dict[str, Any]:
185
- return {
186
- "id": self.id, "name": self.name, "shading_type": self.shading_type.value,
187
- "shading_coefficient": self.shading_coefficient, "coverage_percentage": self.coverage_percentage,
188
- "description": self.description, "effective_shading_coefficient": self.effective_shading_coefficient
189
- }
190
-
191
- class ShadingSystem:
192
- def __init__(self):
193
- self.shading_devices = {}
194
- self.load_preset_devices()
195
-
196
- def load_preset_devices(self) -> None:
197
- self.shading_devices["preset_none"] = ShadingDevice(
198
- id="preset_none", name="None", shading_type=ShadingType.NONE, shading_coefficient=1.0, description="No shading"
199
- )
200
- self.shading_devices["preset_venetian_blinds"] = ShadingDevice(
201
- id="preset_venetian_blinds", name="Venetian Blinds", shading_type=ShadingType.INTERNAL, shading_coefficient=0.6, description="Standard internal venetian blinds"
202
- )
203
- self.shading_devices["preset_overhang"] = ShadingDevice(
204
- id="preset_overhang", name="Overhang", shading_type=ShadingType.EXTERNAL, shading_coefficient=0.4, description="External overhang"
205
- )
206
-
207
- def get_device(self, device_id: str) -> Optional[ShadingDevice]:
208
- return self.shading_devices.get(device_id)
209
-
210
- def calculate_effective_shgc(self, base_shgc: float, device_id: str) -> float:
211
- device = self.get_device(device_id)
212
- if not device:
213
- return base_shgc
214
- return base_shgc * device.effective_shading_coefficient
215
-
216
- shading_system = ShadingSystem()
217
-
218
  # --- Reference Data ---
219
  class ReferenceData:
220
  def __init__(self):
221
  self.data = {
222
  "materials": {
223
- "mat_001": {"name": "Concrete", "conductivity": 1.4},
224
- "mat_002": {"name": "Insulation", "conductivity": 0.04},
225
- "mat_003": {"name": "Brick", "conductivity": 0.8},
226
- "mat_004": {"name": "Glass", "conductivity": 1.0},
227
- "mat_005": {"name": "Wood", "conductivity": 0.15}
228
  },
229
  "wall_types": {
230
- "preset_wall_001": {"id": "preset_wall_001", "name": "Brick Wall (Single Layer) (U-value: 2.0 W/m²·K)", "component_type": "Wall", "u_value": 2.0, "area": 0.0, "orientation": "North", "wall_type": "Brick", "wall_group": "Wall", "shading_percentage": 0.0},
231
- "preset_wall_002": {"id": "preset_wall_002", "name": "Insulated Brick Wall (U-value: 0.5 W/m²·K)", "component_type": "Wall", "u_value": 0.5, "area": 0.0, "orientation": "North", "wall_type": "Insulated Brick", "wall_group": "Wall", "shading_percentage": 0.0},
232
- "preset_wall_003": {"id": "preset_wall_003", "name": "Concrete Block Wall (U-value: 1.8 W/m²·K)", "component_type": "Wall", "u_value": 1.8, "area": 0.0, "orientation": "North", "wall_type": "Concrete Block", "wall_group": "Wall", "shading_percentage": 0.0},
233
- "preset_wall_004": {"id": "preset_wall_004", "name": "Insulated Concrete Wall (U-value: 0.4 W/m²·K)", "component_type": "Wall", "u_value": 0.4, "area": 0.0, "orientation": "North", "wall_type": "Insulated Concrete", "wall_group": "Wall", "shading_percentage": 0.0},
234
- "preset_wall_005": {"id": "preset_wall_005", "name": "Timber Frame Wall (U-value: 0.3 W/m²·K)", "component_type": "Wall", "u_value": 0.3, "area": 0.0, "orientation": "North", "wall_type": "Timber Frame", "wall_group": "Wall", "shading_percentage": 0.0},
235
- "preset_wall_006": {"id": "preset_wall_006", "name": "Cavity Wall (Brick + Insulation) (U-value: 0.6 W/m²·K)", "component_type": "Wall", "u_value": 0.6, "area": 0.0, "orientation": "North", "wall_type": "Cavity Brick", "wall_group": "Wall", "shading_percentage": 0.0},
236
- "preset_wall_007": {"id": "preset_wall_007", "name": "Lightweight Panel Wall (U-value: 1.0 W/m²·K)", "component_type": "Wall", "u_value": 1.0, "area": 0.0, "orientation": "North", "wall_type": "Lightweight Panel", "wall_group": "Wall", "shading_percentage": 0.0},
237
- "preset_wall_008": {"id": "preset_wall_008", "name": "Reinforced Concrete Wall (U-value: 1.5 W/m²·K)", "component_type": "Wall", "u_value": 1.5, "area": 0.0, "orientation": "North", "wall_type": "Reinforced Concrete", "wall_group": "Wall", "shading_percentage": 0.0},
238
- "preset_wall_009": {"id": "preset_wall_009", "name": "SIP Wall (Structural Insulated Panel) (U-value: 0.25 W/m²·K)", "component_type": "Wall", "u_value": 0.25, "area": 0.0, "orientation": "North", "wall_type": "SIP", "wall_group": "Wall", "shading_percentage": 0.0},
239
- "preset_wall_010": {"id": "preset_wall_010", "name": "Custom Wall (U-value: 0.5 W/m²·K)", "component_type": "Wall", "u_value": 0.5, "area": 0.0, "orientation": "North", "wall_type": "Custom", "wall_group": "Wall", "shading_percentage": 0.0}
240
  },
241
  "roof_types": {
242
- "preset_roof_001": {"id": "preset_roof_001", "name": "Concrete Roof", "component_type": "Roof", "u_value": 0.3, "area": 0.0, "orientation": "Horizontal", "roof_type": "Concrete", "roof_group": "C"}
 
 
 
 
 
 
 
243
  },
244
  "floor_types": {
245
- "preset_floor_001": {"id": "preset_floor_001", "name": "Concrete Floor", "component_type": "Floor", "u_value": 0.4, "area": 0.0, "orientation": "Horizontal", "floor_type": "Concrete"}
 
246
  },
247
  "window_types": {
248
- "preset_window_001": {"id": "preset_window_001", "name": "Double Glazed Window", "component_type": "Window", "u_value": 2.8, "area": 0.0, "orientation": "North", "shgc": 0.7, "vt": 0.8, "window_type": "Double Glazed", "glazing_layers": 2, "gas_fill": "Air", "low_e_coating": False, "shading_device_id": "preset_none"}
 
 
 
 
 
 
 
 
249
  },
250
  "door_types": {
251
- "preset_door_001": {"id": "preset_door_001", "name": "Solid Wood Door", "component_type": "Door", "u_value": 2.0, "area": 0.0, "orientation": "North", "door_type": "Solid Wood"}
 
252
  }
253
  }
254
 
255
  def get_materials(self) -> List[Dict[str, Any]]:
256
- return list(self.data["materials"].values())
257
-
258
- def get_component_data(self, component_type: str) -> Dict[str, Any]:
259
- return self.data.get(f"{component_type.lower()}_types", {})
260
 
261
  reference_data = ReferenceData()
262
 
@@ -264,40 +236,12 @@ reference_data = ReferenceData()
264
  class ComponentLibrary:
265
  def __init__(self):
266
  self.components = {}
267
- self._load_preset_components()
268
-
269
- def _load_preset_components(self):
270
- for type_key in ["wall_types", "roof_types", "floor_types", "window_types", "door_types"]:
271
- for component_data in reference_data.data[type_key].values():
272
- component_data["orientation"] = Orientation(component_data["orientation"])
273
- if component_data["component_type"] == ComponentType.WALL.value:
274
- component = Wall(**component_data)
275
- elif component_data["component_type"] == ComponentType.ROOF.value:
276
- component = Roof(**component_data)
277
- elif component_data["component_type"] == ComponentType.FLOOR.value:
278
- component = Floor(**component_data)
279
- elif component_data["component_type"] == ComponentType.WINDOW.value:
280
- component = Window(**component_data)
281
- elif component_data["component_type"] == ComponentType.DOOR.value:
282
- component = Door(**component_data)
283
- self.components[component.id] = component
284
-
285
- def get_component(self, component_id: str) -> BuildingComponent:
286
- return self.components.get(component_id)
287
-
288
- def get_preset_components_by_type(self, component_type: ComponentType) -> List[BuildingComponent]:
289
- return [comp for comp in self.components.values() if comp.component_type == component_type and comp.id.startswith("preset_")]
290
 
291
  def add_component(self, component: BuildingComponent):
292
  self.components[component.id] = component
293
 
294
- def update_component(self, component_id: str, component: BuildingComponent):
295
- self.components[component_id] = component
296
-
297
  def remove_component(self, component_id: str):
298
- if component_id.startswith("preset_"):
299
- raise ValueError("Cannot remove preset components")
300
- if component_id in self.components:
301
  del self.components[component_id]
302
 
303
  component_library = ComponentLibrary()
@@ -307,14 +251,9 @@ class UValueCalculator:
307
  def __init__(self):
308
  self.materials = reference_data.get_materials()
309
 
310
- def get_preset_materials(self) -> List[Dict[str, Any]]:
311
- return self.materials
312
-
313
- def calculate_u_value(self, layers: List[Dict[str, float]]) -> float:
314
- r_outside = 0.04 # m²·K/W
315
- r_inside = 0.13 # m²·K/W
316
  r_layers = sum(layer["thickness"] / 1000 / layer["conductivity"] for layer in layers)
317
- r_total = r_outside + r_layers + r_inside
318
  return 1 / r_total if r_total > 0 else 0
319
 
320
  u_value_calculator = UValueCalculator()
@@ -324,7 +263,6 @@ class ComponentSelectionInterface:
324
  def __init__(self):
325
  self.component_library = component_library
326
  self.u_value_calculator = u_value_calculator
327
- self.shading_system = shading_system
328
  self.reference_data = reference_data
329
 
330
  def display_component_selection(self, session_state: Any) -> None:
@@ -332,7 +270,11 @@ class ComponentSelectionInterface:
332
 
333
  if 'components' not in session_state:
334
  session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
335
-
 
 
 
 
336
  tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
337
 
338
  with tabs[0]:
@@ -348,489 +290,451 @@ class ComponentSelectionInterface:
348
  with tabs[5]:
349
  self._display_u_value_calculator_tab(session_state)
350
 
351
- col1, col2 = st.columns(2)
352
- with col1:
353
- if st.button("Save Components"):
354
- self._save_components(session_state)
355
- with col2:
356
- uploaded_file = st.file_uploader("Load Components", type=["json"])
357
- if uploaded_file and st.button("Load Uploaded Components"):
358
- self._load_components(session_state, uploaded_file)
359
 
360
  def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
361
  type_name = component_type.value.lower()
362
  st.subheader(f"{type_name.capitalize()} Components")
363
 
364
- with st.expander(f"Add {type_name.capitalize()}", expanded=False):
365
  if component_type == ComponentType.WALL:
366
  self._display_add_wall_form(session_state)
367
- else:
368
- self._display_add_component_form(session_state, component_type)
 
 
 
 
 
 
369
 
370
  components = session_state.components.get(type_name + 's', [])
371
- if components:
372
  st.subheader(f"Existing {type_name.capitalize()} Components")
373
  self._display_components_table(session_state, component_type, components)
374
 
375
  def _display_add_wall_form(self, session_state: Any) -> None:
376
  st.write("Add walls manually or upload a file.")
377
- wall_method = st.radio("Add Wall Method", ["Manual Entry", "File Upload"])
378
-
379
- if wall_method == "Manual Entry":
380
- if "add_wall_submitted" not in session_state:
381
- session_state.add_wall_submitted = False
382
-
383
  with st.form("add_wall_form", clear_on_submit=True):
384
  col1, col2 = st.columns(2)
385
  with col1:
386
  name = st.text_input("Name", "New Wall")
387
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
388
- orientation = st.selectbox("Orientation", [o.value for o in Orientation], index=0)
389
  with col2:
390
- preset_walls = self.component_library.get_preset_components_by_type(ComponentType.WALL)
391
- wall_options = {comp.name: comp for comp in preset_walls}
392
- selected_wall = st.selectbox("Select Wall Type", options=list(wall_options.keys()), key="wall_type_select")
393
- component = wall_options[selected_wall]
394
- if "prev_selected_wall" not in session_state or session_state.prev_selected_wall != selected_wall:
395
- session_state.prev_selected_wall = selected_wall
396
- session_state.u_value = component.u_value
397
- u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, max_value=9.0, value=float(session_state.u_value), step=0.01, key=f"u_value_{selected_wall}")
398
- shading_percentage = st.slider("Shading Percentage (%)", min_value=0.0, max_value=100.0, value=0.0, step=5.0)
399
-
400
  submitted = st.form_submit_button("Add Wall")
401
  if submitted and not session_state.add_wall_submitted:
402
- if not name:
403
- st.error("Wall name is required!")
404
- elif area <= 0:
405
- st.error("Wall area must be greater than zero!")
406
- elif u_value <= 0 or u_value >= 9:
407
- st.error("Wall U-value must be between 0 and 9!")
408
- else:
409
  new_wall = Wall(
410
- id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
411
- orientation=Orientation(orientation), wall_type=component.wall_type,
412
- wall_group="Wall", shading_percentage=shading_percentage
413
  )
414
  self.component_library.add_component(new_wall)
415
  session_state.components['walls'].append(new_wall)
416
  st.success(f"Added {new_wall.name}")
417
  session_state.add_wall_submitted = True
418
  st.rerun()
419
-
 
 
420
  if session_state.add_wall_submitted:
421
  session_state.add_wall_submitted = False
422
-
423
- elif wall_method == "File Upload":
424
- st.info("Upload a CSV or Excel file with columns: Name, Area (m²), U-Value (W/m²·K), Orientation, Wall Type, Wall Group, Shading (%)")
425
- # Use a unique key for the file uploader to allow clearing
426
- if "file_uploaded" not in session_state:
427
- session_state.file_uploaded = False
428
- if "uploaded_file_key" not in session_state:
429
- session_state.uploaded_file_key = 0
430
-
431
- uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key=f"wall_upload_{session_state.uploaded_file_key}")
432
- required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Shading (%)"]
433
-
434
- if uploaded_file and st.button("Process File"):
435
- try:
436
- if uploaded_file.name.endswith('.csv'):
437
- encodings = ['utf-8', 'latin1', 'windows-1252']
438
- df = None
439
- for encoding in encodings:
440
- try:
441
- uploaded_file.seek(0)
442
- df = pd.read_csv(uploaded_file, encoding=encoding)
443
- break
444
- except UnicodeDecodeError:
445
- continue
446
- if df is None:
447
- raise UnicodeDecodeError("Unable to decode CSV file with available encodings.")
448
- else: # .xlsx
449
- df = pd.read_excel(uploaded_file)
450
-
451
- # Normalize column names
452
- df.columns = [col.strip() for col in df.columns]
453
- if "Group" in df.columns and "Wall Group" not in df.columns:
454
- df.rename(columns={"Group": "Wall Group"}, inplace=True)
455
- if not all(col in df.columns for col in required_cols):
456
- st.error(f"File must contain all required columns: {', '.join(required_cols)}")
457
- else:
458
- # Overwrite existing walls
459
- session_state.components['walls'] = []
460
- for comp_id in list(self.component_library.components.keys()):
461
- if not comp_id.startswith("preset_") and self.component_library.components[comp_id].component_type == ComponentType.WALL:
462
- self.component_library.remove_component(comp_id)
463
- for _, row in df.iterrows():
464
- try:
465
- new_wall = Wall(
466
- id=str(uuid.uuid4()), name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]),
467
- area=float(row["Area (m²)"]), orientation=Orientation(row["Orientation"]),
468
- wall_type=str(row["Wall Type"]), wall_group=str(row["Wall Group"]),
469
- shading_percentage=float(row["Shading (%)"])
470
- )
471
- self.component_library.add_component(new_wall)
472
- session_state.components['walls'].append(new_wall)
473
- except ValueError as e:
474
- st.error(f"Error in row: {row['Name']} - {str(e)}")
475
- st.success("Walls uploaded and overwritten successfully!")
476
- # Clear the file uploader by incrementing the key
477
- session_state.uploaded_file_key += 1
478
- session_state.file_uploaded = True
479
- except Exception as e:
480
- st.error(f"Error processing file: {str(e)}")
481
-
482
- # Provide a downloadable template
483
- template_df = pd.DataFrame(columns=required_cols)
484
- buffer = io.BytesIO()
485
- template_df.to_csv(buffer, index=False, encoding='utf-8')
486
- st.download_button(
487
- label="Download Wall Template (CSV)",
488
- data=buffer.getvalue(),
489
- file_name="wall_template.csv",
490
- mime="text/csv"
491
- )
492
-
493
- def _display_add_component_form(self, session_state: Any, component_type: ComponentType) -> None:
494
- type_name = component_type.value.lower()
495
- preset_components = self.component_library.get_preset_components_by_type(component_type)
496
-
497
- if f"add_{type_name}_submitted" not in session_state:
498
- session_state[f"add_{type_name}_submitted"] = False
499
-
500
- with st.form(f"add_{type_name}_form", clear_on_submit=True):
501
- col1, col2 = st.columns(2)
502
- with col1:
503
- name = st.text_input("Name", f"New {type_name.capitalize()}")
504
- area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
505
- if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
506
- orientation = st.selectbox("Orientation", [o.value for o in Orientation], index=0)
507
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  orientation = Orientation.HORIZONTAL.value
509
-
510
- with col2:
511
- selection_method = st.radio(f"{type_name.capitalize()} Selection Method", ["Select from Presets", "Custom Properties"])
512
- if selection_method == "Select from Presets" and preset_components:
513
- preset_options = {comp.name: comp.id for comp in preset_components}
514
- selected_preset = st.selectbox(f"Select Preset {type_name.capitalize()}", options=list(preset_options.keys()))
515
- component = self.component_library.get_component(preset_options[selected_preset])
516
- u_value = st.number_input("U-Value (W/m²·K)", value=component.u_value, disabled=True)
517
- if component_type == ComponentType.ROOF:
518
- st.text_input("Roof Type", value=component.roof_type, disabled=True)
519
- st.text_input("Roof Group", value=component.roof_group, disabled=True)
520
- elif component_type == ComponentType.FLOOR:
521
- st.text_input("Floor Type", value=component.floor_type, disabled=True)
522
- elif component_type == ComponentType.WINDOW:
523
- st.number_input("SHGC", value=component.shgc, disabled=True)
524
- st.number_input("VT", value=component.vt, disabled=True)
525
- st.text_input("Window Type", value=component.window_type, disabled=True)
526
- elif component_type == ComponentType.DOOR:
527
- st.text_input("Door Type", value=component.door_type, disabled=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  else:
529
- u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=0.5, step=0.01)
530
- if component_type == ComponentType.ROOF:
531
- roof_type = st.text_input("Roof Type", "Custom")
532
- roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"], index=2)
533
- elif component_type == ComponentType.FLOOR:
534
- floor_type = st.text_input("Floor Type", "Custom")
535
- elif component_type == ComponentType.WINDOW:
536
- shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
537
- vt = st.number_input("VT", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
538
- window_type = st.text_input("Window Type", "Custom")
539
- glazing_layers = st.selectbox("Glazing Layers", [1, 2, 3], index=1)
540
- gas_fill = st.selectbox("Gas Fill", ["Air", "Argon", "Krypton"], index=0)
541
- low_e_coating = st.checkbox("Low-E Coating")
542
- shading_options = {d.name: d.id for d in self.shading_system.shading_devices.values()}
543
- shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
544
- elif component_type == ComponentType.DOOR:
545
- door_type = st.text_input("Door Type", "Custom")
546
-
547
- submitted = st.form_submit_button("Add Component")
548
- if submitted and not session_state[f"add_{type_name}_submitted"]:
549
- if not name:
550
- st.error(f"{type_name.capitalize()} name is required!")
551
- elif area <= 0:
552
- st.error(f"{type_name.capitalize()} area must be greater than zero!")
553
- elif u_value <= 0:
554
- st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  try:
557
- if selection_method == "Select from Presets" and preset_components:
558
- component_id = preset_options[selected_preset]
559
- component = self.component_library.get_component(component_id)
560
- preset_attrs = component.__dict__.copy()
561
- for key in ['id', 'name', 'area', 'orientation']:
562
- preset_attrs.pop(key, None)
563
- new_component = component.__class__(
564
- id=str(uuid.uuid4()), name=name, area=area,
565
- orientation=Orientation(orientation), **preset_attrs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  )
567
- else:
568
- if component_type == ComponentType.ROOF:
569
- new_component = Roof(
570
- id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
571
- orientation=Orientation(orientation), roof_type=roof_type, roof_group=roof_group
572
- )
573
- elif component_type == ComponentType.FLOOR:
574
- new_component = Floor(
575
- id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
576
- orientation=Orientation(orientation), floor_type=floor_type
577
- )
578
- elif component_type == ComponentType.WINDOW:
579
- new_component = Window(
580
- id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
581
- orientation=Orientation(orientation), shgc=shgc, vt=vt, window_type=window_type,
582
- glazing_layers=glazing_layers, gas_fill=gas_fill, low_e_coating=low_e_coating,
583
- shading_device_id=shading_options[shading_device]
584
- )
585
- elif component_type == ComponentType.DOOR:
586
- new_component = Door(
587
- id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
588
- orientation=Orientation(orientation), door_type=door_type
589
- )
590
- self.component_library.add_component(new_component)
591
- session_state.components[type_name + 's'].append(new_component)
592
- st.success(f"Added {new_component.name}")
593
- session_state[f"add_{type_name}_submitted"] = True
 
 
 
 
 
 
 
 
 
 
 
 
594
  st.rerun()
595
  except ValueError as e:
596
  st.error(f"Error: {str(e)}")
597
-
598
- if session_state[f"add_{type_name}_submitted"]:
599
- session_state[f"add_{type_name}_submitted"] = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
 
601
  def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
602
  type_name = component_type.value.lower()
603
- data = []
604
- for comp in components:
605
- row = {"Name": comp.name, "Area (m²)": comp.area, "U-Value (W/m²·K)": comp.u_value, "Orientation": comp.orientation.value, "ID": comp.id}
606
- if component_type == ComponentType.WALL:
607
- row.update({"Wall Type": comp.wall_type, "Wall Group": comp.wall_group, "Shading (%)": comp.shading_percentage})
608
- elif component_type == ComponentType.ROOF:
609
- row.update({"Roof Type": comp.roof_type, "Roof Group": comp.roof_group})
610
- elif component_type == ComponentType.FLOOR:
611
- row.update({"Floor Type": comp.floor_type})
612
- elif component_type == ComponentType.WINDOW:
613
- row.update({"SHGC": comp.shgc, "VT": comp.vt, "Window Type": comp.window_type,
614
- "Effective SHGC": self.shading_system.calculate_effective_shgc(comp.shgc, comp.shading_device_id)})
615
- elif component_type == ComponentType.DOOR:
616
- row.update({"Door Type": comp.door_type})
617
- data.append(row)
618
-
619
- df = pd.DataFrame(data)
620
- display_cols = [col for col in df.columns if col != "ID"]
621
-
622
- if component_type == ComponentType.WALL:
623
- # Custom table rendering for walls with delete button in the row
624
- st.write("### Walls Table")
625
- # Display headers
626
- headers = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Shading (%)", "Delete"]
627
- cols = st.columns([1, 1, 1, 1, 1, 1, 1, 1])
628
  for i, header in enumerate(headers):
629
  cols[i].write(f"**{header}**")
630
-
631
- # Display rows
632
- for i, row in df.iterrows():
633
- cols = st.columns([1, 1, 1, 1, 1, 1, 1, 1])
634
- cols[0].write(row["Name"])
635
- cols[1].write(row["Area (m²)"])
636
- cols[2].write(row["U-Value (W/m²·K)"])
637
- cols[3].write(row["Orientation"])
638
- cols[4].write(row["Wall Type"])
639
- cols[5].write(row["Wall Group"])
640
- cols[6].write(row["Shading (%)"])
641
- if cols[7].button("Delete", key=f"delete_{row['ID']}"):
642
- if not row["ID"].startswith("preset_"):
643
- self.component_library.remove_component(row["ID"])
644
- session_state.components['walls'] = [c for c in components if c.id != row["ID"]]
645
- st.success(f"Deleted {row['Name']}")
646
- st.rerun()
647
- else:
648
- st.warning("Cannot delete preset components.")
649
-
650
- # Download walls table (excluding Delete column)
651
- csv_df = df[["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Shading (%)"]]
652
- csv_buffer = io.BytesIO()
653
- csv_df.to_csv(csv_buffer, index=False, encoding='utf-8')
654
- st.download_button(label="Download Walls Table", data=csv_buffer.getvalue(), file_name="walls_table.csv", mime="text/csv")
655
- else:
656
- st.dataframe(df[display_cols], use_container_width=True)
657
- for i, row in df.iterrows():
658
- col1, col2 = st.columns(2)
659
- with col1:
660
- if st.button("Edit", key=f"edit_{row['ID']}"):
661
- session_state[f"edit_{type_name}"] = row["ID"]
662
- st.rerun()
663
- with col2:
664
- if st.button("Delete", key=f"delete_{row['ID']}"):
665
- if not row["ID"].startswith("preset_"):
666
- self.component_library.remove_component(row["ID"])
667
- session_state.components[type_name + 's'] = [c for c in components if c.id != row["ID"]]
668
- st.success(f"Deleted {row['Name']}")
669
- st.rerun()
670
- else:
671
- st.warning("Cannot delete preset components.")
672
-
673
- if f"edit_{type_name}" in session_state and component_type != ComponentType.WALL:
674
- self._display_edit_component_form(session_state, component_type, session_state[f"edit_{type_name}"])
675
- del session_state[f"edit_{type_name}"]
676
 
677
- def _display_edit_component_form(self, session_state: Any, component_type: ComponentType, component_id: str) -> None:
678
- type_name = component_type.value.lower()
679
- component = next((c for c in session_state.components[type_name + 's'] if c.id == component_id), None)
680
- if not component:
681
- st.error("Component not found.")
682
- return
683
-
684
- with st.form(f"edit_{component_id}_form"):
685
- col1, col2 = st.columns(2)
686
- with col1:
687
- name = st.text_input("Name", value=component.name)
688
- area = st.number_input("Area (m²)", min_value=0.0, value=component.area, step=0.1)
689
- if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
690
- orientation = st.selectbox("Orientation", [o.value for o in Orientation],
691
- index=[o.value for o in Orientation].index(component.orientation.value))
692
- else:
693
- orientation = Orientation.HORIZONTAL.value
694
-
695
- with col2:
696
- u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=component.u_value, step=0.01)
697
- if component_type == ComponentType.ROOF:
698
- roof_type = st.text_input("Roof Type", value=component.roof_type)
699
- roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"],
700
- index=["A", "B", "C", "D", "E", "F", "G"].index(component.roof_group))
701
  elif component_type == ComponentType.FLOOR:
702
- floor_type = st.text_input("Floor Type", value=component.floor_type)
 
 
703
  elif component_type == ComponentType.WINDOW:
704
- shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=component.shgc, step=0.01)
705
- vt = st.number_input("VT", min_value=0.0, max_value=1.0, value=component.vt, step=0.01)
706
- window_type = st.text_input("Window Type", value=component.window_type)
707
- glazing_layers = st.selectbox("Glazing Layers", [1, 2, 3], index=[1, 2, 3].index(component.glazing_layers))
708
- gas_fill = st.selectbox("Gas Fill", ["Air", "Argon", "Krypton"],
709
- index=["Air", "Argon", "Krypton"].index(component.gas_fill))
710
- low_e_coating = st.checkbox("Low-E Coating", value=component.low_e_coating)
711
- shading_options = {d.name: d.id for d in self.shading_system.shading_devices.values()}
712
- shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()),
713
- index=list(shading_options.keys()).index(self.shading_system.get_device(component.shading_device_id).name))
714
  elif component_type == ComponentType.DOOR:
715
- door_type = st.text_input("Door Type", value=component.door_type)
716
-
717
- submitted = st.form_submit_button("Update Component")
718
- if submitted:
719
- if not name:
720
- st.error(f"{type_name.capitalize()} name is required!")
721
- elif area <= 0:
722
- st.error(f"{type_name.capitalize()} area must be greater than zero!")
723
- elif u_value <= 0:
724
- st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
725
- else:
726
- component.name = name
727
- component.area = area
728
- component.u_value = u_value
729
- component.orientation = Orientation(orientation)
730
- if component_type == ComponentType.ROOF:
731
- component.roof_type = roof_type
732
- component.roof_group = roof_group
733
- elif component_type == ComponentType.FLOOR:
734
- component.floor_type = floor_type
735
- elif component_type == ComponentType.WINDOW:
736
- component.shgc = shgc
737
- component.vt = vt
738
- component.window_type = window_type
739
- component.glazing_layers = glazing_layers
740
- component.gas_fill = gas_fill
741
- component.low_e_coating = low_e_coating
742
- component.shading_device_id = shading_options[shading_device]
743
- elif component_type == ComponentType.DOOR:
744
- component.door_type = door_type
745
- self.component_library.update_component(component_id, component)
746
- st.success(f"Updated {name}")
747
  st.rerun()
748
 
749
  def _display_u_value_calculator_tab(self, session_state: Any) -> None:
750
- st.subheader("U-Value Calculator")
751
-
752
- if "material_layers" not in session_state:
753
- session_state.material_layers = []
754
-
755
- if "u_value_applied" not in session_state:
756
- session_state.u_value_applied = False
757
-
758
- components = []
759
- for type_name in ['walls', 'roofs', 'floors', 'windows', 'doors']:
760
- components.extend(session_state.components[type_name])
761
-
762
- if not components:
763
- st.warning("No components available. Add components first.")
764
- return
765
-
766
- selected_component = st.selectbox("Select Component", options=[c.name for c in components])
767
- component = next((c for c in components if c.name == selected_component), None)
768
- if not component:
769
- st.error("Component not found.")
770
- return
771
-
772
- if session_state.material_layers:
773
- st.write("Current Material Layers (outside to inside):")
774
  layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
775
- "Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]}
776
- for i, l in enumerate(session_state.material_layers)]
777
- st.dataframe(pd.DataFrame(layer_data), use_container_width=True)
778
-
779
- u_value = self.u_value_calculator.calculate_u_value(session_state.material_layers)
780
-
781
- col1, col2, col3 = st.columns(3)
782
- with col1:
783
- st.metric("Total R-Value", f"{1/u_value if u_value > 0 else 0:.3f} m²·K/W")
784
- with col2:
785
- st.metric("U-Value", f"{u_value:.3f} W/m²·K")
786
- with col3:
787
- if st.button("Use This U-Value") and not session_state.u_value_applied:
788
- component.u_value = u_value
789
- self.component_library.update_component(component.id, component)
790
- session_state.material_layers = []
791
- st.success(f"U-Value set to {u_value:.3f} W/m²·K for {component.name}")
792
- session_state.u_value_applied = True
793
- st.rerun()
794
-
795
- if session_state.u_value_applied:
796
- session_state.u_value_applied = False
797
-
798
- with st.form("material_layer_form"):
799
  col1, col2 = st.columns(2)
800
  with col1:
801
- material_selection = st.radio("Material Selection", ["Select from Presets", "Custom Material"])
802
- if material_selection == "Select from Presets":
803
- preset_materials = self.u_value_calculator.get_preset_materials()
804
- material_options = {m["name"]: m for m in preset_materials}
805
- material_name = st.selectbox("Material", options=list(material_options.keys()))
806
- conductivity = material_options[material_name]["conductivity"]
807
- st.number_input("Conductivity (W/m·K)", value=conductivity, disabled=True)
808
- else:
809
- material_name = st.text_input("Material Name", "Custom Material")
810
- conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=1.0, step=0.01)
811
-
812
  with col2:
813
  thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
814
- if conductivity > 0:
815
- r_value = thickness / 1000 / conductivity
816
- st.metric("Layer R-Value", f"{r_value:.3f} m²·K/W")
817
-
818
  submitted = st.form_submit_button("Add Layer")
819
  if submitted:
820
- if not material_name:
821
- st.error("Material name is required!")
822
- elif thickness <= 0:
823
- st.error("Thickness must be greater than zero!")
824
- elif conductivity <= 0:
825
- st.error("Conductivity must be greater than zero!")
826
- else:
827
- session_state.material_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity})
828
- st.success(f"Added layer: {material_name}")
829
  st.rerun()
830
-
831
- if st.button("Remove Last Layer"):
832
- if session_state.material_layers:
833
- session_state.material_layers.pop()
834
  st.rerun()
835
 
836
  def _save_components(self, session_state: Any) -> None:
@@ -839,7 +743,9 @@ class ComponentSelectionInterface:
839
  "roofs": [c.to_dict() for c in session_state.components["roofs"]],
840
  "floors": [c.to_dict() for c in session_state.components["floors"]],
841
  "windows": [c.to_dict() for c in session_state.components["windows"]],
842
- "doors": [c.to_dict() for c in session_state.components["doors"]]
 
 
843
  }
844
  file_path = "components_export.json"
845
  with open(file_path, 'w') as f:
@@ -848,27 +754,6 @@ class ComponentSelectionInterface:
848
  st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
849
  st.success("Components saved successfully.")
850
 
851
- def _load_components(self, session_state: Any, uploaded_file: Any) -> None:
852
- data = json.load(uploaded_file)
853
- session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
854
- for type_name in data:
855
- for comp_dict in data[type_name]:
856
- comp_dict["orientation"] = Orientation(comp_dict["orientation"])
857
- if type_name == "walls":
858
- comp = Wall(**comp_dict)
859
- elif type_name == "roofs":
860
- comp = Roof(**comp_dict)
861
- elif type_name == "floors":
862
- comp = Floor(**comp_dict)
863
- elif type_name == "windows":
864
- comp = Window(**comp_dict)
865
- elif type_name == "doors":
866
- comp = Door(**comp_dict)
867
- self.component_library.add_component(comp)
868
- session_state.components[type_name].append(comp)
869
- st.success("Loaded components successfully.")
870
- st.rerun()
871
-
872
  # --- Main Execution ---
873
  if __name__ == "__main__":
874
  interface = ComponentSelectionInterface()
 
34
  WINDOW = "Window"
35
  DOOR = "Door"
36
 
 
 
 
 
 
 
37
  # --- Data Models ---
38
  @dataclass
39
  class MaterialLayer:
 
49
  u_value: float = 0.0 # W/(m²·K)
50
  area: float = 0.0 # m²
51
  orientation: Orientation = Orientation.NOT_APPLICABLE
 
52
 
53
  def __post_init__(self):
54
+ if self.area <= 0:
55
+ raise ValueError("Area must be greater than zero")
56
+ if self.u_value <= 0:
57
+ raise ValueError("U-value must be greater than zero")
58
 
59
  def to_dict(self) -> dict:
60
  return {
61
+ "id": self.id, "name": self.name, "component_type": self.component_type.value,
62
+ "u_value": self.u_value, "area": self.area, "orientation": self.orientation.value
 
 
 
 
 
63
  }
64
 
65
  @dataclass
66
  class Wall(BuildingComponent):
67
  wall_type: str = "Brick"
68
  wall_group: str = "Wall"
69
+ absorptivity: float = 0.6
70
+ shading_coefficient: float = 1.0
71
+ infiltration_rate_cfm: float = 0.0
72
 
73
  def __post_init__(self):
74
  super().__post_init__()
75
  self.component_type = ComponentType.WALL
76
+ if not 0 <= self.absorptivity <= 1:
77
+ raise ValueError("Absorptivity must be between 0 and 1")
78
+ if not 0 <= self.shading_coefficient <= 1:
79
+ raise ValueError("Shading coefficient must be between 0 and 1")
80
+ if self.infiltration_rate_cfm < 0:
81
+ raise ValueError("Infiltration rate cannot be negative")
82
 
83
  def to_dict(self) -> dict:
84
  base_dict = super().to_dict()
85
+ base_dict.update({
86
+ "wall_type": self.wall_type, "wall_group": self.wall_group, "absorptivity": self.absorptivity,
87
+ "shading_coefficient": self.shading_coefficient, "infiltration_rate_cfm": self.infiltration_rate_cfm
88
+ })
89
  return base_dict
90
 
91
  @dataclass
92
  class Roof(BuildingComponent):
93
  roof_type: str = "Concrete"
94
  roof_group: str = "C"
95
+ slope: str = "Flat"
96
+ absorptivity: float = 0.6
97
 
98
  def __post_init__(self):
99
  super().__post_init__()
100
  self.component_type = ComponentType.ROOF
101
+ if not self.orientation == Orientation.HORIZONTAL:
102
+ self.orientation = Orientation.HORIZONTAL
103
+ if not 0 <= self.absorptivity <= 1:
104
+ raise ValueError("Absorptivity must be between 0 and 1")
105
 
106
  def to_dict(self) -> dict:
107
  base_dict = super().to_dict()
108
+ base_dict.update({
109
+ "roof_type": self.roof_type, "roof_group": self.roof_group, "slope": self.slope, "absorptivity": self.absorptivity
110
+ })
111
  return base_dict
112
 
113
  @dataclass
114
  class Floor(BuildingComponent):
115
  floor_type: str = "Concrete"
116
+ ground_contact: bool = True
117
+ ground_temperature_c: float = 25.0
118
 
119
  def __post_init__(self):
120
  super().__post_init__()
121
  self.component_type = ComponentType.FLOOR
122
+ self.orientation = Orientation.NOT_APPLICABLE
123
 
124
  def to_dict(self) -> dict:
125
  base_dict = super().to_dict()
126
+ base_dict.update({
127
+ "floor_type": self.floor_type, "ground_contact": self.ground_contact, "ground_temperature_c": self.ground_temperature_c
128
+ })
129
  return base_dict
130
 
131
  @dataclass
132
  class Window(BuildingComponent):
133
  shgc: float = 0.7
134
+ shading_device: str = "None"
135
+ shading_coefficient: float = 1.0
136
+ frame_type: str = "Aluminum"
137
+ frame_percentage: float = 20.0
138
+ infiltration_rate_cfm: float = 0.0
 
139
 
140
  def __post_init__(self):
141
  super().__post_init__()
142
  self.component_type = ComponentType.WINDOW
143
  if not 0 <= self.shgc <= 1:
144
  raise ValueError("SHGC must be between 0 and 1")
145
+ if not 0 <= self.shading_coefficient <= 1:
146
+ raise ValueError("Shading coefficient must be between 0 and 1")
147
+ if not 0 <= self.frame_percentage <= 30:
148
+ raise ValueError("Frame percentage must be between 0 and 30")
149
+ if self.infiltration_rate_cfm < 0:
150
+ raise ValueError("Infiltration rate cannot be negative")
151
 
152
  def to_dict(self) -> dict:
153
  base_dict = super().to_dict()
154
  base_dict.update({
155
+ "shgc": self.shgc, "shading_device": self.shading_device, "shading_coefficient": self.shading_coefficient,
156
+ "frame_type": self.frame_type, "frame_percentage": self.frame_percentage, "infiltration_rate_cfm": self.infiltration_rate_cfm
 
157
  })
158
  return base_dict
159
 
160
  @dataclass
161
  class Door(BuildingComponent):
162
  door_type: str = "Solid Wood"
163
+ infiltration_rate_cfm: float = 0.0
164
 
165
  def __post_init__(self):
166
  super().__post_init__()
167
  self.component_type = ComponentType.DOOR
168
+ if self.infiltration_rate_cfm < 0:
169
+ raise ValueError("Infiltration rate cannot be negative")
170
 
171
  def to_dict(self) -> dict:
172
  base_dict = super().to_dict()
173
+ base_dict.update({"door_type": self.door_type, "infiltration_rate_cfm": self.infiltration_rate_cfm})
174
  return base_dict
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  # --- Reference Data ---
177
  class ReferenceData:
178
  def __init__(self):
179
  self.data = {
180
  "materials": {
181
+ "Concrete": {"conductivity": 1.4},
182
+ "Insulation": {"conductivity": 0.04},
183
+ "Brick": {"conductivity": 0.8},
184
+ "Glass": {"conductivity": 1.0},
185
+ "Wood": {"conductivity": 0.15}
186
  },
187
  "wall_types": {
188
+ "Brick Wall": {"u_value": 2.0, "absorptivity": 0.6},
189
+ "Insulated Brick": {"u_value": 0.5, "absorptivity": 0.6},
190
+ "Concrete Block": {"u_value": 1.8, "absorptivity": 0.6},
191
+ "Insulated Concrete": {"u_value": 0.4, "absorptivity": 0.6},
192
+ "Timber Frame": {"u_value": 0.3, "absorptivity": 0.6},
193
+ "Cavity Brick": {"u_value": 0.6, "absorptivity": 0.6},
194
+ "Lightweight Panel": {"u_value": 1.0, "absorptivity": 0.6},
195
+ "Reinforced Concrete": {"u_value": 1.5, "absorptivity": 0.6},
196
+ "SIP": {"u_value": 0.25, "absorptivity": 0.6},
197
+ "Custom": {"u_value": 0.5, "absorptivity": 0.6}
198
  },
199
  "roof_types": {
200
+ "Concrete Roof": {"u_value": 0.3, "absorptivity": 0.6, "group": "C"},
201
+ "Metal Roof": {"u_value": 1.0, "absorptivity": 0.75, "group": "B"}
202
+ },
203
+ "roof_ventilation_methods": {
204
+ "No Ventilation": 0.0,
205
+ "Natural Low": 0.1,
206
+ "Natural High": 0.5,
207
+ "Mechanical": 1.0
208
  },
209
  "floor_types": {
210
+ "Concrete Slab": {"u_value": 0.4, "ground_contact": True},
211
+ "Wood Floor": {"u_value": 0.8, "ground_contact": False}
212
  },
213
  "window_types": {
214
+ "Double Glazed": {"u_value": 2.8, "shgc": 0.7, "frame_type": "Aluminum"},
215
+ "Single Glazed": {"u_value": 5.0, "shgc": 0.9, "frame_type": "Wood"}
216
+ },
217
+ "shading_devices": {
218
+ "None": 1.0,
219
+ "Venetian Blinds": 0.6,
220
+ "Overhang": 0.4,
221
+ "Roller Shades": 0.5,
222
+ "Drapes": 0.7
223
  },
224
  "door_types": {
225
+ "Solid Wood": {"u_value": 2.0},
226
+ "Glass Door": {"u_value": 3.5}
227
  }
228
  }
229
 
230
  def get_materials(self) -> List[Dict[str, Any]]:
231
+ return [{"name": k, "conductivity": v["conductivity"]} for k, v in self.data["materials"].items()]
 
 
 
232
 
233
  reference_data = ReferenceData()
234
 
 
236
  class ComponentLibrary:
237
  def __init__(self):
238
  self.components = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  def add_component(self, component: BuildingComponent):
241
  self.components[component.id] = component
242
 
 
 
 
243
  def remove_component(self, component_id: str):
244
+ if not component_id.startswith("preset_") and component_id in self.components:
 
 
245
  del self.components[component_id]
246
 
247
  component_library = ComponentLibrary()
 
251
  def __init__(self):
252
  self.materials = reference_data.get_materials()
253
 
254
+ def calculate_u_value(self, layers: List[Dict[str, float]], outside_resistance: float, inside_resistance: float) -> float:
 
 
 
 
 
255
  r_layers = sum(layer["thickness"] / 1000 / layer["conductivity"] for layer in layers)
256
+ r_total = outside_resistance + r_layers + inside_resistance
257
  return 1 / r_total if r_total > 0 else 0
258
 
259
  u_value_calculator = UValueCalculator()
 
263
  def __init__(self):
264
  self.component_library = component_library
265
  self.u_value_calculator = u_value_calculator
 
266
  self.reference_data = reference_data
267
 
268
  def display_component_selection(self, session_state: Any) -> None:
 
270
 
271
  if 'components' not in session_state:
272
  session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
273
+ if 'roof_air_volume_m3' not in session_state:
274
+ session_state.roof_air_volume_m3 = 0.0
275
+ if 'roof_ventilation_ach' not in session_state:
276
+ session_state.roof_ventilation_ach = 0.0
277
+
278
  tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
279
 
280
  with tabs[0]:
 
290
  with tabs[5]:
291
  self._display_u_value_calculator_tab(session_state)
292
 
293
+ if st.button("Save Components"):
294
+ self._save_components(session_state)
 
 
 
 
 
 
295
 
296
  def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
297
  type_name = component_type.value.lower()
298
  st.subheader(f"{type_name.capitalize()} Components")
299
 
300
+ with st.expander(f"Add {type_name.capitalize()}", expanded=True):
301
  if component_type == ComponentType.WALL:
302
  self._display_add_wall_form(session_state)
303
+ elif component_type == ComponentType.ROOF:
304
+ self._display_add_roof_form(session_state)
305
+ elif component_type == ComponentType.FLOOR:
306
+ self._display_add_floor_form(session_state)
307
+ elif component_type == ComponentType.WINDOW:
308
+ self._display_add_window_form(session_state)
309
+ elif component_type == ComponentType.DOOR:
310
+ self._display_add_door_form(session_state)
311
 
312
  components = session_state.components.get(type_name + 's', [])
313
+ if components or component_type == ComponentType.ROOF:
314
  st.subheader(f"Existing {type_name.capitalize()} Components")
315
  self._display_components_table(session_state, component_type, components)
316
 
317
  def _display_add_wall_form(self, session_state: Any) -> None:
318
  st.write("Add walls manually or upload a file.")
319
+ method = st.radio("Add Wall Method", ["Manual Entry", "File Upload"])
320
+ if "add_wall_submitted" not in session_state:
321
+ session_state.add_wall_submitted = False
322
+
323
+ if method == "Manual Entry":
 
324
  with st.form("add_wall_form", clear_on_submit=True):
325
  col1, col2 = st.columns(2)
326
  with col1:
327
  name = st.text_input("Name", "New Wall")
328
  area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
329
+ orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
330
  with col2:
331
+ wall_options = self.reference_data.data["wall_types"]
332
+ selected_wall = st.selectbox("Wall Type", options=list(wall_options.keys()))
333
+ 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)
334
+ 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)
335
+ shading_coefficient = st.number_input("Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05)
336
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
337
+
 
 
 
338
  submitted = st.form_submit_button("Add Wall")
339
  if submitted and not session_state.add_wall_submitted:
340
+ try:
341
+ absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
 
 
 
 
 
342
  new_wall = Wall(
343
+ name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
344
+ wall_type=selected_wall, wall_group="Wall", absorptivity=absorptivity_value,
345
+ shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate
346
  )
347
  self.component_library.add_component(new_wall)
348
  session_state.components['walls'].append(new_wall)
349
  st.success(f"Added {new_wall.name}")
350
  session_state.add_wall_submitted = True
351
  st.rerun()
352
+ except ValueError as e:
353
+ st.error(f"Error: {str(e)}")
354
+
355
  if session_state.add_wall_submitted:
356
  session_state.add_wall_submitted = False
357
+
358
+ elif method == "File Upload":
359
+ uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key="wall_upload")
360
+ required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)"]
361
+ st.download_button(label="Download Wall Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="wall_template.csv", mime="text/csv")
362
+ if uploaded_file:
363
+ df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
364
+ if all(col in df.columns for col in required_cols):
365
+ for _, row in df.iterrows():
366
+ try:
367
+ new_wall = Wall(
368
+ name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
369
+ orientation=Orientation(row["Orientation"]), wall_type=str(row["Wall Type"]),
370
+ wall_group=str(row["Wall Group"]), absorptivity=float(row["Absorptivity"]),
371
+ shading_coefficient=float(row["Shading Coefficient"]), infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
372
+ )
373
+ self.component_library.add_component(new_wall)
374
+ session_state.components['walls'].append(new_wall)
375
+ except ValueError as e:
376
+ st.error(f"Error in row {row['Name']}: {str(e)}")
377
+ st.success("Walls uploaded successfully!")
378
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  else:
380
+ st.error(f"File must contain: {', '.join(required_cols)}")
381
+
382
+ def _display_add_roof_form(self, session_state: Any) -> None:
383
+ st.write("Add roofs manually or upload a file.")
384
+ method = st.radio("Add Roof Method", ["Manual Entry", "File Upload"])
385
+ if "add_roof_submitted" not in session_state:
386
+ session_state.add_roof_submitted = False
387
+
388
+ # Shared Roof-Level Fields
389
+ st.subheader("Roof System Ventilation")
390
+ 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")
391
+ vent_options = {f"{k} (ACH={v})": v for k, v in self.reference_data.data["roof_ventilation_methods"].items()}
392
+ vent_options["Custom"] = None
393
+ ventilation_method = st.selectbox("Ventilation Method", options=list(vent_options.keys()), index=0, help="Applies to entire roof system")
394
+ 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]
395
+ session_state.roof_air_volume_m3 = air_volume
396
+ session_state.roof_ventilation_ach = ventilation_ach
397
+
398
+ if method == "Manual Entry":
399
+ with st.form("add_roof_form", clear_on_submit=True):
400
+ col1, col2 = st.columns(2)
401
+ with col1:
402
+ name = st.text_input("Name", "New Roof")
403
+ area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
404
  orientation = Orientation.HORIZONTAL.value
405
+ with col2:
406
+ roof_options = self.reference_data.data["roof_types"]
407
+ selected_roof = st.selectbox("Roof Type", options=list(roof_options.keys()))
408
+ 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)
409
+ roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"], index=2)
410
+ slope = st.selectbox("Slope", ["Flat", "Pitched"], index=0)
411
+ 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)
412
+
413
+ submitted = st.form_submit_button("Add Roof")
414
+ if submitted and not session_state.add_roof_submitted:
415
+ try:
416
+ absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
417
+ new_roof = Roof(
418
+ name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
419
+ roof_type=selected_roof, roof_group=roof_group, slope=slope, absorptivity=absorptivity_value
420
+ )
421
+ self.component_library.add_component(new_roof)
422
+ session_state.components['roofs'].append(new_roof)
423
+ st.success(f"Added {new_roof.name}")
424
+ session_state.add_roof_submitted = True
425
+ st.rerun()
426
+ except ValueError as e:
427
+ st.error(f"Error: {str(e)}")
428
+
429
+ if session_state.add_roof_submitted:
430
+ session_state.add_roof_submitted = False
431
+
432
+ elif method == "File Upload":
433
+ uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload")
434
+ required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity"]
435
+ st.download_button(label="Download Roof Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="roof_template.csv", mime="text/csv")
436
+ if uploaded_file:
437
+ df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
438
+ if all(col in df.columns for col in required_cols):
439
+ for _, row in df.iterrows():
440
+ try:
441
+ new_roof = Roof(
442
+ name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
443
+ orientation=Orientation(row["Orientation"]), roof_type=str(row["Roof Type"]),
444
+ roof_group=str(row["Roof Group"]), slope=str(row["Slope"]), absorptivity=float(row["Absorptivity"])
445
+ )
446
+ self.component_library.add_component(new_roof)
447
+ session_state.components['roofs'].append(new_roof)
448
+ except ValueError as e:
449
+ st.error(f"Error in row {row['Name']}: {str(e)}")
450
+ st.success("Roofs uploaded successfully!")
451
+ st.rerun()
452
  else:
453
+ st.error(f"File must contain: {', '.join(required_cols)}")
454
+
455
+ def _display_add_floor_form(self, session_state: Any) -> None:
456
+ st.write("Add floors manually or upload a file.")
457
+ method = st.radio("Add Floor Method", ["Manual Entry", "File Upload"])
458
+ if "add_floor_submitted" not in session_state:
459
+ session_state.add_floor_submitted = False
460
+
461
+ if method == "Manual Entry":
462
+ with st.form("add_floor_form", clear_on_submit=True):
463
+ col1, col2 = st.columns(2)
464
+ with col1:
465
+ name = st.text_input("Name", "New Floor")
466
+ area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
467
+ with col2:
468
+ floor_options = self.reference_data.data["floor_types"]
469
+ selected_floor = st.selectbox("Floor Type", options=list(floor_options.keys()))
470
+ 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)
471
+ ground_contact = st.selectbox("Ground Contact", ["Yes", "No"], index=0 if floor_options[selected_floor]["ground_contact"] else 1)
472
+ ground_temp = st.number_input("Ground Temperature (°C)", value=25.0, step=0.1) if ground_contact == "Yes" else 25.0
473
+
474
+ submitted = st.form_submit_button("Add Floor")
475
+ if submitted and not session_state.add_floor_submitted:
476
+ try:
477
+ new_floor = Floor(
478
+ name=name, u_value=u_value, area=area, floor_type=selected_floor,
479
+ ground_contact=(ground_contact == "Yes"), ground_temperature_c=ground_temp
480
+ )
481
+ self.component_library.add_component(new_floor)
482
+ session_state.components['floors'].append(new_floor)
483
+ st.success(f"Added {new_floor.name}")
484
+ session_state.add_floor_submitted = True
485
+ st.rerun()
486
+ except ValueError as e:
487
+ st.error(f"Error: {str(e)}")
488
+
489
+ if session_state.add_floor_submitted:
490
+ session_state.add_floor_submitted = False
491
+
492
+ elif method == "File Upload":
493
+ uploaded_file = st.file_uploader("Upload Floors File", type=["csv", "xlsx"], key="floor_upload")
494
+ required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)"]
495
+ st.download_button(label="Download Floor Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="floor_template.csv", mime="text/csv")
496
+ if uploaded_file:
497
+ df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
498
+ if all(col in df.columns for col in required_cols):
499
+ for _, row in df.iterrows():
500
+ try:
501
+ new_floor = Floor(
502
+ name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
503
+ floor_type=str(row["Floor Type"]), ground_contact=(str(row["Ground Contact"]).lower() == "yes"),
504
+ ground_temperature_c=float(row["Ground Temperature (°C)"])
505
+ )
506
+ self.component_library.add_component(new_floor)
507
+ session_state.components['floors'].append(new_floor)
508
+ except ValueError as e:
509
+ st.error(f"Error in row {row['Name']}: {str(e)}")
510
+ st.success("Floors uploaded successfully!")
511
+ st.rerun()
512
  else:
513
+ st.error(f"File must contain: {', '.join(required_cols)}")
514
+
515
+ def _display_add_window_form(self, session_state: Any) -> None:
516
+ st.write("Add windows manually or upload a file.")
517
+ method = st.radio("Add Window Method", ["Manual Entry", "File Upload"])
518
+ if "add_window_submitted" not in session_state:
519
+ session_state.add_window_submitted = False
520
+
521
+ if method == "Manual Entry":
522
+ with st.form("add_window_form", clear_on_submit=True):
523
+ col1, col2 = st.columns(2)
524
+ with col1:
525
+ name = st.text_input("Name", "New Window")
526
+ area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
527
+ orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
528
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
529
+ with col2:
530
+ window_options = self.reference_data.data["window_types"]
531
+ selected_window = st.selectbox("Window Type", options=list(window_options.keys()))
532
+ 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)
533
+ shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
534
+ shading_options["Custom"] = "Custom"
535
+ shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
536
+ 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]]
537
+ frame_type = st.selectbox("Frame Type", ["Aluminum", "Wood", "Vinyl"], index=0)
538
+ frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=20.0)
539
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
540
+
541
+ submitted = st.form_submit_button("Add Window")
542
+ if submitted and not session_state.add_window_submitted:
543
  try:
544
+ new_window = Window(
545
+ name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
546
+ shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
547
+ frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate
548
+ )
549
+ self.component_library.add_component(new_window)
550
+ session_state.components['windows'].append(new_window)
551
+ st.success(f"Added {new_window.name}")
552
+ session_state.add_window_submitted = True
553
+ st.rerun()
554
+ except ValueError as e:
555
+ st.error(f"Error: {str(e)}")
556
+
557
+ if session_state.add_window_submitted:
558
+ session_state.add_window_submitted = False
559
+
560
+ elif method == "File Upload":
561
+ uploaded_file = st.file_uploader("Upload Windows File", type=["csv", "xlsx"], key="window_upload")
562
+ required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)"]
563
+ 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")
564
+ if uploaded_file:
565
+ df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
566
+ if all(col in df.columns for col in required_cols):
567
+ for _, row in df.iterrows():
568
+ try:
569
+ new_window = Window(
570
+ name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
571
+ orientation=Orientation(row["Orientation"]), shgc=float(row["SHGC"]),
572
+ shading_device=str(row["Shading Device"]), shading_coefficient=float(row["Shading Coefficient"]),
573
+ frame_type=str(row["Frame Type"]), frame_percentage=float(row["Frame Percentage"]),
574
+ infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
575
  )
576
+ self.component_library.add_component(new_window)
577
+ session_state.components['windows'].append(new_window)
578
+ except ValueError as e:
579
+ st.error(f"Error in row {row['Name']}: {str(e)}")
580
+ st.success("Windows uploaded successfully!")
581
+ st.rerun()
582
+ else:
583
+ st.error(f"File must contain: {', '.join(required_cols)}")
584
+
585
+ def _display_add_door_form(self, session_state: Any) -> None:
586
+ st.write("Add doors manually or upload a file.")
587
+ method = st.radio("Add Door Method", ["Manual Entry", "File Upload"])
588
+ if "add_door_submitted" not in session_state:
589
+ session_state.add_door_submitted = False
590
+
591
+ if method == "Manual Entry":
592
+ with st.form("add_door_form", clear_on_submit=True):
593
+ col1, col2 = st.columns(2)
594
+ with col1:
595
+ name = st.text_input("Name", "New Door")
596
+ area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
597
+ orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
598
+ with col2:
599
+ door_options = self.reference_data.data["door_types"]
600
+ selected_door = st.selectbox("Door Type", options=list(door_options.keys()))
601
+ 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)
602
+ infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
603
+
604
+ submitted = st.form_submit_button("Add Door")
605
+ if submitted and not session_state.add_door_submitted:
606
+ try:
607
+ new_door = Door(
608
+ name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
609
+ door_type=selected_door, infiltration_rate_cfm=infiltration_rate
610
+ )
611
+ self.component_library.add_component(new_door)
612
+ session_state.components['doors'].append(new_door)
613
+ st.success(f"Added {new_door.name}")
614
+ session_state.add_door_submitted = True
615
  st.rerun()
616
  except ValueError as e:
617
  st.error(f"Error: {str(e)}")
618
+
619
+ if session_state.add_door_submitted:
620
+ session_state.add_door_submitted = False
621
+
622
+ elif method == "File Upload":
623
+ uploaded_file = st.file_uploader("Upload Doors File", type=["csv", "xlsx"], key="door_upload")
624
+ required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)"]
625
+ 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")
626
+ if uploaded_file:
627
+ df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
628
+ if all(col in df.columns for col in required_cols):
629
+ for _, row in df.iterrows():
630
+ try:
631
+ new_door = Door(
632
+ name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
633
+ orientation=Orientation(row["Orientation"]), door_type=str(row["Door Type"]),
634
+ infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
635
+ )
636
+ self.component_library.add_component(new_door)
637
+ session_state.components['doors'].append(new_door)
638
+ except ValueError as e:
639
+ st.error(f"Error in row {row['Name']}: {str(e)}")
640
+ st.success("Doors uploaded successfully!")
641
+ st.rerun()
642
+ else:
643
+ st.error(f"File must contain: {', '.join(required_cols)}")
644
 
645
  def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
646
  type_name = component_type.value.lower()
647
+ if component_type == ComponentType.ROOF:
648
+ st.write(f"Roof Air Volume: {session_state.roof_air_volume_m3} m³, Ventilation Rate: {session_state.roof_ventilation_ach} ACH")
649
+
650
+ if components:
651
+ headers = {
652
+ ComponentType.WALL: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)", "Delete"],
653
+ ComponentType.ROOF: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity", "Delete"],
654
+ ComponentType.FLOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)", "Delete"],
655
+ ComponentType.WINDOW: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)", "Delete"],
656
+ ComponentType.DOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)", "Delete"]
657
+ }[component_type]
658
+ cols = st.columns([1] * len(headers))
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  for i, header in enumerate(headers):
660
  cols[i].write(f"**{header}**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
 
662
+ for comp in components:
663
+ cols = st.columns([1] * len(headers))
664
+ cols[0].write(comp.name)
665
+ cols[1].write(comp.area)
666
+ cols[2].write(comp.u_value)
667
+ cols[3].write(comp.orientation.value)
668
+ if component_type == ComponentType.WALL:
669
+ cols[4].write(comp.wall_type)
670
+ cols[5].write(comp.wall_group)
671
+ cols[6].write(comp.absorptivity)
672
+ cols[7].write(comp.shading_coefficient)
673
+ cols[8].write(comp.infiltration_rate_cfm)
674
+ elif component_type == ComponentType.ROOF:
675
+ cols[4].write(comp.roof_type)
676
+ cols[5].write(comp.roof_group)
677
+ cols[6].write(comp.slope)
678
+ cols[7].write(comp.absorptivity)
 
 
 
 
 
 
 
679
  elif component_type == ComponentType.FLOOR:
680
+ cols[4].write(comp.floor_type)
681
+ cols[5].write("Yes" if comp.ground_contact else "No")
682
+ cols[6].write(comp.ground_temperature_c if comp.ground_contact else "N/A")
683
  elif component_type == ComponentType.WINDOW:
684
+ cols[4].write(comp.shgc)
685
+ cols[5].write(comp.shading_device)
686
+ cols[6].write(comp.shading_coefficient)
687
+ cols[7].write(comp.frame_type)
688
+ cols[8].write(comp.frame_percentage)
689
+ cols[9].write(comp.infiltration_rate_cfm)
 
 
 
 
690
  elif component_type == ComponentType.DOOR:
691
+ cols[4].write(comp.door_type)
692
+ cols[5].write(comp.infiltration_rate_cfm)
693
+ if cols[-1].button("Delete", key=f"delete_{comp.id}"):
694
+ self.component_library.remove_component(comp.id)
695
+ session_state.components[type_name + 's'] = [c for c in components if c.id != comp.id]
696
+ st.success(f"Deleted {comp.name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  st.rerun()
698
 
699
  def _display_u_value_calculator_tab(self, session_state: Any) -> None:
700
+ st.subheader("U-Value Calculator (Standalone)")
701
+ if "u_value_layers" not in session_state:
702
+ session_state.u_value_layers = []
703
+
704
+ if session_state.u_value_layers:
705
+ st.write("Material Layers (Outside to Inside):")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
707
+ "Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]}
708
+ for i, l in enumerate(session_state.u_value_layers)]
709
+ st.dataframe(pd.DataFrame(layer_data))
710
+ outside_resistance = st.selectbox("Outside Resistance (m²·K/W)", ["Summer (0.04)", "Winter (0.03)", "Custom"], index=0)
711
+ 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)
712
+ inside_r = st.number_input("Inside Resistance (m²·K/W)", min_value=0.0, value=0.13, step=0.01)
713
+ u_value = self.u_value_calculator.calculate_u_value(session_state.u_value_layers, outside_r, inside_r)
714
+ st.metric("U-Value", f"{u_value:.3f} W/m²·K")
715
+
716
+ with st.form("u_value_form"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  col1, col2 = st.columns(2)
718
  with col1:
719
+ material_options = {m["name"]: m["conductivity"] for m in self.u_value_calculator.materials}
720
+ material_name = st.selectbox("Material", options=list(material_options.keys()))
721
+ conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=material_options[material_name], step=0.01)
 
 
 
 
 
 
 
 
722
  with col2:
723
  thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
 
 
 
 
724
  submitted = st.form_submit_button("Add Layer")
725
  if submitted:
726
+ session_state.u_value_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity})
727
+ st.rerun()
728
+
729
+ col1, col2 = st.columns(2)
730
+ with col1:
731
+ if st.button("Remove Last Layer"):
732
+ if session_state.u_value_layers:
733
+ session_state.u_value_layers.pop()
 
734
  st.rerun()
735
+ with col2:
736
+ if st.button("Reset"):
737
+ session_state.u_value_layers = []
 
738
  st.rerun()
739
 
740
  def _save_components(self, session_state: Any) -> None:
 
743
  "roofs": [c.to_dict() for c in session_state.components["roofs"]],
744
  "floors": [c.to_dict() for c in session_state.components["floors"]],
745
  "windows": [c.to_dict() for c in session_state.components["windows"]],
746
+ "doors": [c.to_dict() for c in session_state.components["doors"]],
747
+ "roof_air_volume_m3": session_state.roof_air_volume_m3,
748
+ "roof_ventilation_ach": session_state.roof_ventilation_ach
749
  }
750
  file_path = "components_export.json"
751
  with open(file_path, 'w') as f:
 
754
  st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
755
  st.success("Components saved successfully.")
756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  # --- Main Execution ---
758
  if __name__ == "__main__":
759
  interface = ComponentSelectionInterface()