mabuseif commited on
Commit
3f76ae0
·
verified ·
1 Parent(s): de66edb

Update data/building_components.py

Browse files
Files changed (1) hide show
  1. data/building_components.py +376 -607
data/building_components.py CHANGED
@@ -1,19 +1,16 @@
1
  """
2
- HVAC Component Selection Module
3
- Combines all components into a single Streamlit UI with 6 tabs.
4
  """
5
 
6
- import streamlit as st
7
- import pandas as pd
8
- import numpy as np
9
- import json
10
- import uuid
11
  from dataclasses import dataclass, field
12
  from enum import Enum
13
- from typing import Dict, List, Any, Optional
 
 
14
 
15
- # --- Enums ---
16
  class Orientation(Enum):
 
17
  NORTH = "North"
18
  NORTHEAST = "Northeast"
19
  EAST = "East"
@@ -22,46 +19,130 @@ class Orientation(Enum):
22
  SOUTHWEST = "Southwest"
23
  WEST = "West"
24
  NORTHWEST = "Northwest"
25
- HORIZONTAL = "Horizontal"
26
- NOT_APPLICABLE = "N/A"
 
27
 
28
  class ComponentType(Enum):
 
29
  WALL = "Wall"
30
  ROOF = "Roof"
31
  FLOOR = "Floor"
32
  WINDOW = "Window"
33
  DOOR = "Door"
 
34
 
35
- class ShadingType(Enum):
36
- NONE = "None"
37
- INTERNAL = "Internal"
38
- EXTERNAL = "External"
39
- BETWEEN_GLASS = "Between-glass"
40
 
41
- # --- Data Models ---
42
- @dataclass
43
  class MaterialLayer:
44
- name: str
45
- thickness: float # in mm
46
- conductivity: float # W/(m·K)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  @dataclass
49
  class BuildingComponent:
50
- id: str = field(default_factory=lambda: str(uuid.uuid4()))
51
- name: str = "Unnamed Component"
52
- component_type: ComponentType = ComponentType.WALL
53
- u_value: float = 0.0 # W/(m²·K)
54
- area: float = 0.0 # m²
 
 
55
  orientation: Orientation = Orientation.NOT_APPLICABLE
 
56
  material_layers: List[MaterialLayer] = field(default_factory=list)
57
-
58
  def __post_init__(self):
59
- if self.area < 0:
60
- raise ValueError("Area cannot be negative")
 
61
  if self.u_value < 0:
62
  raise ValueError("U-value cannot be negative")
63
-
64
- def to_dict(self) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  return {
66
  "id": self.id,
67
  "name": self.name,
@@ -69,628 +150,316 @@ class BuildingComponent:
69
  "u_value": self.u_value,
70
  "area": self.area,
71
  "orientation": self.orientation.value,
72
- "material_layers": [{"name": l.name, "thickness": l.thickness, "conductivity": l.conductivity} for l in self.material_layers]
 
 
 
 
73
  }
74
 
 
75
  @dataclass
76
  class Wall(BuildingComponent):
77
- wall_type: str = "Brick"
78
- wall_group: str = "B"
79
-
 
 
 
 
 
 
 
80
  def __post_init__(self):
 
81
  super().__post_init__()
82
  self.component_type = ComponentType.WALL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- def to_dict(self) -> dict:
85
- base_dict = super().to_dict()
86
- base_dict.update({"wall_type": self.wall_type, "wall_group": self.wall_group})
87
- return base_dict
88
 
89
  @dataclass
90
  class Roof(BuildingComponent):
91
- roof_type: str = "Concrete"
92
- roof_group: str = "C"
93
-
 
 
 
 
 
94
  def __post_init__(self):
 
95
  super().__post_init__()
96
  self.component_type = ComponentType.ROOF
97
  self.orientation = Orientation.HORIZONTAL
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- def to_dict(self) -> dict:
100
- base_dict = super().to_dict()
101
- base_dict.update({"roof_type": self.roof_type, "roof_group": self.roof_group})
102
- return base_dict
103
 
104
  @dataclass
105
  class Floor(BuildingComponent):
106
- floor_type: str = "Concrete"
107
-
 
 
 
 
108
  def __post_init__(self):
 
109
  super().__post_init__()
110
  self.component_type = ComponentType.FLOOR
111
  self.orientation = Orientation.HORIZONTAL
 
 
 
 
 
 
 
 
 
 
112
 
113
- def to_dict(self) -> dict:
114
- base_dict = super().to_dict()
115
- base_dict.update({"floor_type": self.floor_type})
116
- return base_dict
117
 
118
  @dataclass
119
- class Window(BuildingComponent):
120
- shgc: float = 0.7
121
- vt: float = 0.7
122
- window_type: str = "Double Glazed"
123
- glazing_layers: int = 2
124
- gas_fill: str = "Air"
125
- low_e_coating: bool = False
126
- shading_device_id: str = "preset_none"
127
-
 
 
128
  def __post_init__(self):
 
129
  super().__post_init__()
130
- self.component_type = ComponentType.WINDOW
131
- if not 0 <= self.shgc <= 1:
132
  raise ValueError("SHGC must be between 0 and 1")
133
- if not 0 <= self.vt <= 1:
134
  raise ValueError("VT must be between 0 and 1")
135
-
136
- def to_dict(self) -> dict:
137
- base_dict = super().to_dict()
138
- base_dict.update({
139
- "shgc": self.shgc, "vt": self.vt, "window_type": self.window_type,
140
- "glazing_layers": self.glazing_layers, "gas_fill": self.gas_fill,
141
- "low_e_coating": self.low_e_coating, "shading_device_id": self.shading_device_id
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  })
143
- return base_dict
144
 
145
- @dataclass
146
- class Door(BuildingComponent):
147
- door_type: str = "Solid Wood"
148
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def __post_init__(self):
 
150
  super().__post_init__()
151
- self.component_type = ComponentType.DOOR
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- def to_dict(self) -> dict:
154
- base_dict = super().to_dict()
155
- base_dict.update({"door_type": self.door_type})
156
- return base_dict
157
 
158
- # --- Shading System ---
159
  @dataclass
160
- class ShadingDevice:
161
- id: str
162
- name: str
163
- shading_type: ShadingType
164
- shading_coefficient: float
165
- coverage_percentage: float = 100.0
166
- description: str = ""
167
-
 
168
  def __post_init__(self):
169
- if self.shading_coefficient < 0 or self.shading_coefficient > 1:
170
- raise ValueError("Shading coefficient must be between 0 and 1")
171
- if self.coverage_percentage < 0 or self.coverage_percentage > 100:
172
- raise ValueError("Coverage percentage must be between 0 and 100")
173
-
174
- @property
175
- def effective_shading_coefficient(self) -> float:
176
- coverage_factor = self.coverage_percentage / 100.0
177
- return self.shading_coefficient * coverage_factor + 1.0 * (1 - coverage_factor)
178
-
179
- def to_dict(self) -> Dict[str, Any]:
180
- return {
181
- "id": self.id, "name": self.name, "shading_type": self.shading_type.value,
182
- "shading_coefficient": self.shading_coefficient, "coverage_percentage": self.coverage_percentage,
183
- "description": self.description, "effective_shading_coefficient": self.effective_shading_coefficient
184
- }
185
-
186
- class ShadingSystem:
187
- def __init__(self):
188
- self.shading_devices = {}
189
- self.load_preset_devices()
190
-
191
- def load_preset_devices(self) -> None:
192
- self.shading_devices["preset_none"] = ShadingDevice(
193
- id="preset_none", name="None", shading_type=ShadingType.NONE, shading_coefficient=1.0, description="No shading"
194
- )
195
- self.shading_devices["preset_venetian_blinds"] = ShadingDevice(
196
- id="preset_venetian_blinds", name="Venetian Blinds", shading_type=ShadingType.INTERNAL, shading_coefficient=0.6, description="Standard internal venetian blinds"
197
- )
198
- self.shading_devices["preset_overhang"] = ShadingDevice(
199
- id="preset_overhang", name="Overhang", shading_type=ShadingType.EXTERNAL, shading_coefficient=0.4, description="External overhang"
200
- )
201
-
202
- def get_device(self, device_id: str) -> Optional[ShadingDevice]:
203
- return self.shading_devices.get(device_id)
204
-
205
- def calculate_effective_shgc(self, base_shgc: float, device_id: str) -> float:
206
- device = self.get_device(device_id)
207
- if not device:
208
- return base_shgc
209
- return base_shgc * device.effective_shading_coefficient
210
-
211
- shading_system = ShadingSystem()
212
-
213
- # --- Component Library ---
214
- class ComponentLibrary:
215
- def __init__(self):
216
- self.components = {}
217
- self._load_preset_components()
218
-
219
- def _load_preset_components(self):
220
- for data_set in [reference_data["wall_types"], reference_data["roof_types"],
221
- reference_data["floor_types"], reference_data["window_types"],
222
- reference_data["door_types"]]:
223
- for component_data in data_set.values():
224
- component_data["orientation"] = Orientation(component_data["orientation"])
225
- if component_data["component_type"] == ComponentType.WALL.value:
226
- component = Wall(**component_data)
227
- elif component_data["component_type"] == ComponentType.ROOF.value:
228
- component = Roof(**component_data)
229
- elif component_data["component_type"] == ComponentType.FLOOR.value:
230
- component = Floor(**component_data)
231
- elif component_data["component_type"] == ComponentType.WINDOW.value:
232
- component = Window(**component_data)
233
- elif component_data["component_type"] == ComponentType.DOOR.value:
234
- component = Door(**component_data)
235
- self.components[component.id] = component
236
-
237
- def get_component(self, component_id: str) -> BuildingComponent:
238
- return self.components.get(component_id)
239
-
240
- def get_preset_components_by_type(self, component_type: ComponentType) -> List[BuildingComponent]:
241
- return [comp for comp in self.components.values() if comp.component_type == component_type and comp.id.startswith("preset_")]
242
-
243
- def add_component(self, component: BuildingComponent):
244
- self.components[component.id] = component
245
-
246
- def update_component(self, component_id: str, component: BuildingComponent):
247
- self.components[component_id] = component
248
-
249
- def remove_component(self, component_id: str):
250
- if component_id.startswith("preset_"):
251
- raise ValueError("Cannot remove preset components")
252
- if component_id in self.components:
253
- del self.components[component_id]
254
-
255
- component_library = ComponentLibrary()
256
-
257
- # --- Reference Data ---
258
- reference_data = {
259
- "materials": {
260
- "mat_001": {"name": "Concrete", "conductivity": 1.4},
261
- "mat_002": {"name": "Insulation", "conductivity": 0.04},
262
- "mat_003": {"name": "Brick", "conductivity": 0.8},
263
- "mat_004": {"name": "Glass", "conductivity": 1.0},
264
- "mat_005": {"name": "Wood", "conductivity": 0.15}
265
- },
266
- "wall_types": {
267
- "preset_wall_001": {
268
- "id": "preset_wall_001", "name": "Standard Brick Wall", "component_type": "Wall",
269
- "u_value": 0.5, "area": 0.0, "orientation": "North", "wall_type": "Brick", "wall_group": "B"
270
- }
271
- },
272
- "roof_types": {
273
- "preset_roof_001": {
274
- "id": "preset_roof_001", "name": "Concrete Roof", "component_type": "Roof",
275
- "u_value": 0.3, "area": 0.0, "orientation": "Horizontal", "roof_type": "Concrete", "roof_group": "C"
276
- }
277
- },
278
- "floor_types": {
279
- "preset_floor_001": {
280
- "id": "preset_floor_001", "name": "Concrete Floor", "component_type": "Floor",
281
- "u_value": 0.4, "area": 0.0, "orientation": "Horizontal", "floor_type": "Concrete"
282
- }
283
- },
284
- "window_types": {
285
- "preset_window_001": {
286
- "id": "preset_window_001", "name": "Double Glazed Window", "component_type": "Window",
287
- "u_value": 2.8, "area": 0.0, "orientation": "North", "shgc": 0.7, "vt": 0.8,
288
- "window_type": "Double Glazed", "glazing_layers": 2, "gas_fill": "Air", "low_e_coating": False,
289
- "shading_device_id": "preset_none"
290
- }
291
- },
292
- "door_types": {
293
- "preset_door_001": {
294
- "id": "preset_door_001", "name": "Solid Wood Door", "component_type": "Door",
295
- "u_value": 2.0, "area": 0.0, "orientation": "North", "door_type": "Solid Wood"
296
- }
297
- }
298
- }
299
-
300
- # --- Component Selection Interface ---
301
- class ComponentSelectionInterface:
302
- def display_component_selection(self, session_state: Any) -> None:
303
- st.title("Building Components")
304
-
305
- if 'components' not in session_state:
306
- session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
307
-
308
- tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
309
-
310
- with tabs[0]:
311
- self._display_component_tab(session_state, ComponentType.WALL)
312
- with tabs[1]:
313
- self._display_component_tab(session_state, ComponentType.ROOF)
314
- with tabs[2]:
315
- self._display_component_tab(session_state, ComponentType.FLOOR)
316
- with tabs[3]:
317
- self._display_component_tab(session_state, ComponentType.WINDOW)
318
- with tabs[4]:
319
- self._display_component_tab(session_state, ComponentType.DOOR)
320
- with tabs[5]:
321
- self._display_u_value_calculator_tab(session_state)
322
-
323
- col1, col2 = st.columns(2)
324
- with col1:
325
- if st.button("Save Components"):
326
- self._save_components(session_state)
327
- with col2:
328
- uploaded_file = st.file_uploader("Load Components", type=["json"])
329
- if uploaded_file and st.button("Load Uploaded Components"):
330
- self._load_components(session_state, uploaded_file)
331
-
332
- def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
333
- type_name = component_type.value.lower()
334
- st.subheader(f"{type_name.capitalize()} Components")
335
 
336
- with st.expander(f"Add {type_name.capitalize()}", expanded=False):
337
- self._display_add_component_form(session_state, component_type)
 
338
 
339
- components = session_state.components.get(type_name + 's', [])
340
- if components:
341
- st.subheader(f"Existing {type_name.capitalize()} Components")
342
- self._display_components_table(session_state, component_type, components)
343
- else:
344
- st.info(f"No {type_name} components defined yet.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
- def _display_add_component_form(self, session_state: Any, component_type: ComponentType) -> None:
347
- type_name = component_type.value.lower()
348
- preset_components = component_library.get_preset_components_by_type(component_type)
349
-
350
- with st.form(f"add_{type_name}_form"):
351
- col1, col2 = st.columns(2)
352
- with col1:
353
- name = st.text_input("Name", f"New {type_name.capitalize()}")
354
- area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
355
- if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
356
- orientation = st.selectbox("Orientation", [o.value for o in Orientation], index=0)
357
- else:
358
- orientation = Orientation.HORIZONTAL.value
359
-
360
- with col2:
361
- selection_method = st.radio(f"{type_name.capitalize()} Selection Method", ["Select from Presets", "Custom Properties"])
362
- if selection_method == "Select from Presets" and preset_components:
363
- preset_options = {comp.name: comp.id for comp in preset_components}
364
- selected_preset = st.selectbox(f"Select Preset {type_name.capitalize()}", options=list(preset_options.keys()))
365
- component = component_library.get_component(preset_options[selected_preset])
366
- u_value = st.number_input("U-Value (W/m²·K)", value=component.u_value, disabled=True)
367
- if component_type == ComponentType.WALL:
368
- st.text_input("Wall Type", value=component.wall_type, disabled=True)
369
- st.text_input("Wall Group", value=component.wall_group, disabled=True)
370
- elif component_type == ComponentType.ROOF:
371
- st.text_input("Roof Type", value=component.roof_type, disabled=True)
372
- st.text_input("Roof Group", value=component.roof_group, disabled=True)
373
- elif component_type == ComponentType.FLOOR:
374
- st.text_input("Floor Type", value=component.floor_type, disabled=True)
375
- elif component_type == ComponentType.WINDOW:
376
- st.number_input("SHGC", value=component.shgc, disabled=True)
377
- st.number_input("VT", value=component.vt, disabled=True)
378
- st.text_input("Window Type", value=component.window_type, disabled=True)
379
- elif component_type == ComponentType.DOOR:
380
- st.text_input("Door Type", value=component.door_type, disabled=True)
381
- else:
382
- u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=0.5, step=0.01)
383
- if component_type == ComponentType.WALL:
384
- wall_type = st.text_input("Wall Type", "Custom")
385
- wall_group = st.selectbox("Wall Group", ["A", "B", "C", "D", "E", "F", "G", "H"], index=1)
386
- elif component_type == ComponentType.ROOF:
387
- roof_type = st.text_input("Roof Type", "Custom")
388
- roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"], index=2)
389
- elif component_type == ComponentType.FLOOR:
390
- floor_type = st.text_input("Floor Type", "Custom")
391
- elif component_type == ComponentType.WINDOW:
392
- shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
393
- vt = st.number_input("VT", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
394
- window_type = st.text_input("Window Type", "Custom")
395
- glazing_layers = st.selectbox("Glazing Layers", [1, 2, 3], index=1)
396
- gas_fill = st.selectbox("Gas Fill", ["Air", "Argon", "Krypton"], index=0)
397
- low_e_coating = st.checkbox("Low-E Coating")
398
- shading_options = {d.name: d.id for d in shading_system.shading_devices.values()}
399
- shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
400
- coverage = st.number_input("Coverage (%)", min_value=0.0, max_value=100.0, value=100.0, step=5.0)
401
- device = shading_system.get_device(shading_options[shading_device])
402
- device.coverage_percentage = coverage
403
- st.write(f"Effective SHGC: {shading_system.calculate_effective_shgc(shgc, shading_options[shading_device]):.2f}")
404
- elif component_type == ComponentType.DOOR:
405
- door_type = st.text_input("Door Type", "Custom")
406
-
407
- submitted = st.form_submit_button("Add Component")
408
- if submitted:
409
- if not name:
410
- st.error(f"{type_name.capitalize()} name is required!")
411
- elif area <= 0:
412
- st.error(f"{type_name.capitalize()} area must be greater than zero!")
413
- elif u_value <= 0:
414
- st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
415
- else:
416
- try:
417
- if selection_method == "Select from Presets" and preset_components:
418
- component_id = preset_options[selected_preset]
419
- component = component_library.get_component(component_id)
420
- new_component = component.__class__(id=str(uuid.uuid4()), name=name, area=area,
421
- orientation=Orientation(orientation), **component.__dict__)
422
- del new_component.__dict__["id"]
423
- else:
424
- if component_type == ComponentType.WALL:
425
- new_component = Wall(id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
426
- orientation=Orientation(orientation), wall_type=wall_type, wall_group=wall_group)
427
- elif component_type == ComponentType.ROOF:
428
- new_component = Roof(id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
429
- orientation=Orientation(orientation), roof_type=roof_type, roof_group=roof_group)
430
- elif component_type == ComponentType.FLOOR:
431
- new_component = Floor(id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
432
- orientation=Orientation(orientation), floor_type=floor_type)
433
- elif component_type == ComponentType.WINDOW:
434
- new_component = Window(id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
435
- orientation=Orientation(orientation), shgc=shgc, vt=vt, window_type=window_type,
436
- glazing_layers=glazing_layers, gas_fill=gas_fill, low_e_coating=low_e_coating,
437
- shading_device_id=shading_options[shading_device])
438
- elif component_type == ComponentType.DOOR:
439
- new_component = Door(id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
440
- orientation=Orientation(orientation), door_type=door_type)
441
- component_library.add_component(new_component)
442
- session_state.components[type_name + 's'].append(new_component)
443
- st.success(f"Added {new_component.name}")
444
- st.rerun()
445
- except ValueError as e:
446
- st.error(f"Error: {str(e)}")
447
 
448
- def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
449
- type_name = component_type.value.lower()
450
- data = []
451
- for comp in components:
452
- row = {"Name": comp.name, "Area (m²)": comp.area, "U-Value (W/m²·K)": comp.u_value, "Orientation": comp.orientation.value, "ID": comp.id}
453
- if component_type == ComponentType.WALL:
454
- row.update({"Wall Type": comp.wall_type, "Wall Group": comp.wall_group})
455
- elif component_type == ComponentType.ROOF:
456
- row.update({"Roof Type": comp.roof_type, "Roof Group": comp.roof_group})
457
- elif component_type == ComponentType.FLOOR:
458
- row.update({"Floor Type": comp.floor_type})
459
- elif component_type == ComponentType.WINDOW:
460
- row.update({"SHGC": comp.shgc, "VT": comp.vt, "Window Type": comp.window_type,
461
- "Effective SHGC": shading_system.calculate_effective_shgc(comp.shgc, comp.shading_device_id)})
462
- elif component_type == ComponentType.DOOR:
463
- row.update({"Door Type": comp.door_type})
464
- data.append(row)
465
-
466
- df = pd.DataFrame(data)
467
- display_cols = [col for col in df.columns if col != "ID"]
468
- st.dataframe(df[display_cols], use_container_width=True)
469
-
470
- for i, row in df.iterrows():
471
- col1, col2 = st.columns(2)
472
- with col1:
473
- if st.button("Edit", key=f"edit_{row['ID']}"):
474
- session_state[f"edit_{type_name}"] = row["ID"]
475
- st.rerun()
476
- with col2:
477
- if st.button("Delete", key=f"delete_{row['ID']}"):
478
- if not row["ID"].startswith("preset_"):
479
- component_library.remove_component(row["ID"])
480
- session_state.components[type_name + 's'] = [c for c in components if c.id != row["ID"]]
481
- st.success(f"Deleted {row['Name']}")
482
- st.rerun()
483
- else:
484
- st.warning("Cannot delete preset components.")
485
 
486
- if f"edit_{type_name}" in session_state:
487
- self._display_edit_component_form(session_state, component_type, session_state[f"edit_{type_name}"])
488
- del session_state[f"edit_{type_name}"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
- def _display_edit_component_form(self, session_state: Any, component_type: ComponentType, component_id: str) -> None:
491
- type_name = component_type.value.lower()
492
- component = next((c for c in session_state.components[type_name + 's'] if c.id == component_id), None)
493
- if not component:
494
- st.error("Component not found.")
495
- return
496
-
497
- with st.form(f"edit_{component_id}_form"):
498
- col1, col2 = st.columns(2)
499
- with col1:
500
- name = st.text_input("Name", value=component.name)
501
- area = st.number_input("Area (m²)", min_value=0.0, value=component.area, step=0.1)
502
- if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
503
- orientation = st.selectbox("Orientation", [o.value for o in Orientation],
504
- index=[o.value for o in Orientation].index(component.orientation.value))
505
- else:
506
- orientation = Orientation.HORIZONTAL.value
507
-
508
- with col2:
509
- u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=component.u_value, step=0.01)
510
- if component_type == ComponentType.WALL:
511
- wall_type = st.text_input("Wall Type", value=component.wall_type)
512
- wall_group = st.selectbox("Wall Group", ["A", "B", "C", "D", "E", "F", "G", "H"],
513
- index=["A", "B", "C", "D", "E", "F", "G", "H"].index(component.wall_group))
514
- elif component_type == ComponentType.ROOF:
515
- roof_type = st.text_input("Roof Type", value=component.roof_type)
516
- roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"],
517
- index=["A", "B", "C", "D", "E", "F", "G"].index(component.roof_group))
518
- elif component_type == ComponentType.FLOOR:
519
- floor_type = st.text_input("Floor Type", value=component.floor_type)
520
- elif component_type == ComponentType.WINDOW:
521
- shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=component.shgc, step=0.01)
522
- vt = st.number_input("VT", min_value=0.0, max_value=1.0, value=component.vt, step=0.01)
523
- window_type = st.text_input("Window Type", value=component.window_type)
524
- glazing_layers = st.selectbox("Glazing Layers", [1, 2, 3], index=[1, 2, 3].index(component.glazing_layers))
525
- gas_fill = st.selectbox("Gas Fill", ["Air", "Argon", "Krypton"],
526
- index=["Air", "Argon", "Krypton"].index(component.gas_fill))
527
- low_e_coating = st.checkbox("Low-E Coating", value=component.low_e_coating)
528
- shading_options = {d.name: d.id for d in shading_system.shading_devices.values()}
529
- shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()),
530
- index=list(shading_options.keys()).index(shading_system.get_device(component.shading_device_id).name))
531
- coverage = st.number_input("Coverage (%)", min_value=0.0, max_value=100.0,
532
- value=shading_system.get_device(component.shading_device_id).coverage_percentage, step=5.0)
533
- device = shading_system.get_device(shading_options[shading_device])
534
- device.coverage_percentage = coverage
535
- st.write(f"Effective SHGC: {shading_system.calculate_effective_shgc(shgc, shading_options[shading_device]):.2f}")
536
- elif component_type == ComponentType.DOOR:
537
- door_type = st.text_input("Door Type", value=component.door_type)
538
-
539
- submitted = st.form_submit_button("Update Component")
540
- if submitted:
541
- if not name:
542
- st.error(f"{type_name.capitalize()} name is required!")
543
- elif area <= 0:
544
- st.error(f"{type_name.capitalize()} area must be greater than zero!")
545
- elif u_value <= 0:
546
- st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
547
- else:
548
- component.name = name
549
- component.area = area
550
- component.u_value = u_value
551
- component.orientation = Orientation(orientation)
552
- if component_type == ComponentType.WALL:
553
- component.wall_type = wall_type
554
- component.wall_group = wall_group
555
- elif component_type == ComponentType.ROOF:
556
- component.roof_type = roof_type
557
- component.roof_group = roof_group
558
- elif component_type == ComponentType.FLOOR:
559
- component.floor_type = floor_type
560
- elif component_type == ComponentType.WINDOW:
561
- component.shgc = shgc
562
- component.vt = vt
563
- component.window_type = window_type
564
- component.glazing_layers = glazing_layers
565
- component.gas_fill = gas_fill
566
- component.low_e_coating = low_e_coating
567
- component.shading_device_id = shading_options[shading_device]
568
- elif component_type == ComponentType.DOOR:
569
- component.door_type = door_type
570
- component_library.update_component(component_id, component)
571
- st.success(f"Updated {name}")
572
- st.rerun()
573
 
574
- def _display_u_value_calculator_tab(self, session_state: Any) -> None:
575
- st.subheader("U-Value Calculator")
576
-
577
- if "material_layers" not in session_state:
578
- session_state.material_layers = []
579
-
580
- components = []
581
- for type_name in ['walls', 'roofs', 'floors', 'windows', 'doors']:
582
- components.extend(session_state.components[type_name])
583
 
584
- if not components:
585
- st.warning("No components available. Add components first.")
586
- return
587
-
588
- selected_component = st.selectbox("Select Component", options=[c.name for c in components])
589
- component = next((c for c in components if c.name == selected_component), None)
590
- if not component:
591
- st.error("Component not found.")
592
- return
593
-
594
- if session_state.material_layers:
595
- st.write("Current Material Layers (outside to inside):")
596
- layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
597
- "Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]}
598
- for i, l in enumerate(session_state.material_layers)]
599
- st.dataframe(pd.DataFrame(layer_data), use_container_width=True)
600
-
601
- r_outside = 0.04 # m²·K/W
602
- r_inside = 0.13 # m²·K/W
603
- r_layers = sum([l["thickness"] / 1000 / l["conductivity"] for l in session_state.material_layers])
604
- r_total = r_outside + r_layers + r_inside
605
- u_value = 1 / r_total if r_total > 0 else 0
606
-
607
- col1, col2, col3 = st.columns(3)
608
- with col1:
609
- st.metric("Total R-Value", f"{r_total:.3f} m²·K/W")
610
- with col2:
611
- st.metric("U-Value", f"{u_value:.3f} W/m²·K")
612
- with col3:
613
- if st.button("Use This U-Value"):
614
- component.u_value = u_value
615
- component_library.update_component(component.id, component)
616
- session_state.material_layers = []
617
- st.success(f"U-Value set to {u_value:.3f} W/m²·K for {component.name}")
618
- st.rerun()
619
 
620
- if st.button("Remove Last Layer"):
621
- if session_state.material_layers:
622
- session_state.material_layers.pop()
623
- st.rerun()
624
 
625
- with st.form("material_layer_form"):
626
- col1, col2 = st.columns(2)
627
- with col1:
628
- material_selection = st.radio("Material Selection", ["Select from Presets", "Custom Material"])
629
- if material_selection == "Select from Presets":
630
- material_options = {m["name"]: mid for mid, m in reference_data["materials"].items()}
631
- material_name = st.selectbox("Material", options=list(material_options.keys()))
632
- conductivity = reference_data["materials"][material_options[material_name]]["conductivity"]
633
- st.number_input("Conductivity (W/m·K)", value=conductivity, disabled=True)
634
- else:
635
- material_name = st.text_input("Material Name", "Custom Material")
636
- conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=1.0, step=0.01)
637
-
638
- with col2:
639
- thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
640
- if conductivity > 0:
641
- r_value = thickness / 1000 / conductivity
642
- st.metric("Layer R-Value", f"{r_value:.3f} m²·K/W")
643
-
644
- submitted = st.form_submit_button("Add Layer")
645
- if submitted:
646
- if not material_name:
647
- st.error("Material name is required!")
648
- elif thickness <= 0:
649
- st.error("Thickness must be greater than zero!")
650
- elif conductivity <= 0:
651
- st.error("Conductivity must be greater than zero!")
652
- else:
653
- session_state.material_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity})
654
- st.success(f"Added layer: {material_name}")
655
- st.rerun()
656
-
657
- def _save_components(self, session_state: Any) -> None:
658
- components_dict = {
659
- "walls": [c.to_dict() for c in session_state.components["walls"]],
660
- "roofs": [c.to_dict() for c in session_state.components["roofs"]],
661
- "floors": [c.to_dict() for c in session_state.components["floors"]],
662
- "windows": [c.to_dict() for c in session_state.components["windows"]],
663
- "doors": [c.to_dict() for c in session_state.components["doors"]]
664
- }
665
- file_path = "components_export.json"
666
- with open(file_path, 'w') as f:
667
- json.dump(components_dict, f, indent=4)
668
- with open(file_path, 'r') as f:
669
- st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
670
- st.success("Components saved successfully.")
671
-
672
- def _load_components(self, session_state: Any, uploaded_file: Any) -> None:
673
- data = json.load(uploaded_file)
674
- session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
675
- for type_name in data:
676
- for comp_dict in data[type_name]:
677
- comp_dict["orientation"] = Orientation(comp_dict["orientation"])
678
- if type_name == "walls":
679
- comp = Wall(**comp_dict)
680
- elif type_name == "roofs":
681
- comp = Roof(**comp_dict)
682
- elif type_name == "floors":
683
- comp = Floor(**comp_dict)
684
- elif type_name == "windows":
685
- comp = Window(**comp_dict)
686
- elif type_name == "doors":
687
- comp = Door(**comp_dict)
688
- component_library.add_component(comp)
689
- session_state.components[type_name].append(comp)
690
- st.success(f"Loaded components successfully.")
691
- st.rerun()
692
-
693
- # --- Main Execution ---
694
- if __name__ == "__main__":
695
- interface = ComponentSelectionInterface()
696
- interface.display_component_selection(st.session_state)
 
1
  """
2
+ Building component data models for HVAC Load Calculator.
3
+ This module defines the data structures for walls, roofs, floors, windows, doors, and other building components.
4
  """
5
 
 
 
 
 
 
6
  from dataclasses import dataclass, field
7
  from enum import Enum
8
+ from typing import List, Dict, Optional, Union
9
+ import numpy as np
10
+
11
 
 
12
  class Orientation(Enum):
13
+ """Enumeration for building component orientations."""
14
  NORTH = "North"
15
  NORTHEAST = "Northeast"
16
  EAST = "East"
 
19
  SOUTHWEST = "Southwest"
20
  WEST = "West"
21
  NORTHWEST = "Northwest"
22
+ HORIZONTAL = "Horizontal" # For roofs and floors
23
+ NOT_APPLICABLE = "N/A" # For components without orientation
24
+
25
 
26
  class ComponentType(Enum):
27
+ """Enumeration for building component types."""
28
  WALL = "Wall"
29
  ROOF = "Roof"
30
  FLOOR = "Floor"
31
  WINDOW = "Window"
32
  DOOR = "Door"
33
+ SKYLIGHT = "Skylight"
34
 
 
 
 
 
 
35
 
 
 
36
  class MaterialLayer:
37
+ """Class representing a single material layer in a building component."""
38
+
39
+ def __init__(self, name: str, thickness: float, conductivity: float,
40
+ density: float = None, specific_heat: float = None):
41
+ """
42
+ Initialize a material layer.
43
+
44
+ Args:
45
+ name: Name of the material
46
+ thickness: Thickness of the layer in meters
47
+ conductivity: Thermal conductivity in W/(m·K)
48
+ density: Density in kg/m³ (optional)
49
+ specific_heat: Specific heat capacity in J/(kg·K) (optional)
50
+ """
51
+ self.name = name
52
+ self.thickness = thickness # m
53
+ self.conductivity = conductivity # W/(m·K)
54
+ self.density = density # kg/m³
55
+ self.specific_heat = specific_heat # J/(kg·K)
56
+
57
+ @property
58
+ def r_value(self) -> float:
59
+ """Calculate the thermal resistance (R-value) of the layer in m²·K/W."""
60
+ if self.conductivity == 0:
61
+ return float('inf') # Avoid division by zero
62
+ return self.thickness / self.conductivity
63
+
64
+ @property
65
+ def thermal_mass(self) -> Optional[float]:
66
+ """Calculate the thermal mass of the layer in J/(m²·K)."""
67
+ if self.density is None or self.specific_heat is None:
68
+ return None
69
+ return self.thickness * self.density * self.specific_heat
70
+
71
+ def to_dict(self) -> Dict:
72
+ """Convert the material layer to a dictionary."""
73
+ return {
74
+ "name": self.name,
75
+ "thickness": self.thickness,
76
+ "conductivity": self.conductivity,
77
+ "density": self.density,
78
+ "specific_heat": self.specific_heat,
79
+ "r_value": self.r_value,
80
+ "thermal_mass": self.thermal_mass
81
+ }
82
+
83
 
84
  @dataclass
85
  class BuildingComponent:
86
+ """Base class for all building components."""
87
+
88
+ id: str
89
+ name: str
90
+ component_type: ComponentType
91
+ u_value: float # W/(m²·K)
92
+ area: float # m²
93
  orientation: Orientation = Orientation.NOT_APPLICABLE
94
+ color: str = "Medium" # Light, Medium, Dark
95
  material_layers: List[MaterialLayer] = field(default_factory=list)
96
+
97
  def __post_init__(self):
98
+ """Validate component data after initialization."""
99
+ if self.area <= 0:
100
+ raise ValueError("Area must be greater than zero")
101
  if self.u_value < 0:
102
  raise ValueError("U-value cannot be negative")
103
+
104
+ @property
105
+ def r_value(self) -> float:
106
+ """Calculate the total thermal resistance (R-value) in m²·K/W."""
107
+ return 1 / self.u_value if self.u_value > 0 else float('inf')
108
+
109
+ @property
110
+ def total_r_value_from_layers(self) -> Optional[float]:
111
+ """Calculate the total R-value from material layers if available."""
112
+ if not self.material_layers:
113
+ return None
114
+
115
+ # Add surface resistances (interior and exterior)
116
+ r_si = 0.13 # m²·K/W (interior surface resistance)
117
+ r_se = 0.04 # m²·K/W (exterior surface resistance)
118
+
119
+ # Sum the R-values of all layers
120
+ r_layers = sum(layer.r_value for layer in self.material_layers)
121
+
122
+ return r_si + r_layers + r_se
123
+
124
+ @property
125
+ def calculated_u_value(self) -> Optional[float]:
126
+ """Calculate U-value from material layers if available."""
127
+ total_r = self.total_r_value_from_layers
128
+ if total_r is None or total_r == 0:
129
+ return None
130
+ return 1 / total_r
131
+
132
+ def heat_transfer_rate(self, delta_t: float) -> float:
133
+ """
134
+ Calculate heat transfer rate through the component.
135
+
136
+ Args:
137
+ delta_t: Temperature difference across the component in K or °C
138
+
139
+ Returns:
140
+ Heat transfer rate in Watts
141
+ """
142
+ return self.u_value * self.area * delta_t
143
+
144
+ def to_dict(self) -> Dict:
145
+ """Convert the building component to a dictionary."""
146
  return {
147
  "id": self.id,
148
  "name": self.name,
 
150
  "u_value": self.u_value,
151
  "area": self.area,
152
  "orientation": self.orientation.value,
153
+ "color": self.color,
154
+ "r_value": self.r_value,
155
+ "material_layers": [layer.to_dict() for layer in self.material_layers],
156
+ "calculated_u_value": self.calculated_u_value,
157
+ "total_r_value_from_layers": self.total_r_value_from_layers
158
  }
159
 
160
+
161
  @dataclass
162
  class Wall(BuildingComponent):
163
+ """Class representing a wall component."""
164
+
165
+ has_sun_exposure: bool = True
166
+ wall_type: str = "Custom" # Brick, Concrete, Wood Frame, etc.
167
+ wall_group: str = "A" # ASHRAE wall group (A, B, C, D, E, F, G, H)
168
+ gross_area: float = None # m² (before subtracting windows/doors)
169
+ net_area: float = None # m² (after subtracting windows/doors)
170
+ windows: List[str] = field(default_factory=list) # List of window IDs
171
+ doors: List[str] = field(default_factory=list) # List of door IDs
172
+
173
  def __post_init__(self):
174
+ """Initialize wall-specific attributes."""
175
  super().__post_init__()
176
  self.component_type = ComponentType.WALL
177
+
178
+ # Set net area equal to area if not specified
179
+ if self.net_area is None:
180
+ self.net_area = self.area
181
+
182
+ # Set gross area equal to net area if not specified
183
+ if self.gross_area is None:
184
+ self.gross_area = self.net_area
185
+
186
+ def update_net_area(self, window_areas: Dict[str, float], door_areas: Dict[str, float]):
187
+ """
188
+ Update the net wall area by subtracting windows and doors.
189
+
190
+ Args:
191
+ window_areas: Dictionary mapping window IDs to areas
192
+ door_areas: Dictionary mapping door IDs to areas
193
+ """
194
+ total_window_area = sum(window_areas.get(window_id, 0) for window_id in self.windows)
195
+ total_door_area = sum(door_areas.get(door_id, 0) for door_id in self.doors)
196
+
197
+ self.net_area = self.gross_area - total_window_area - total_door_area
198
+ self.area = self.net_area # Update the main area property
199
+
200
+ if self.net_area <= 0:
201
+ raise ValueError("Net wall area cannot be negative or zero")
202
+
203
+ def to_dict(self) -> Dict:
204
+ """Convert the wall to a dictionary."""
205
+ wall_dict = super().to_dict()
206
+ wall_dict.update({
207
+ "has_sun_exposure": self.has_sun_exposure,
208
+ "wall_type": self.wall_type,
209
+ "wall_group": self.wall_group,
210
+ "gross_area": self.gross_area,
211
+ "net_area": self.net_area,
212
+ "windows": self.windows,
213
+ "doors": self.doors
214
+ })
215
+ return wall_dict
216
 
 
 
 
 
217
 
218
  @dataclass
219
  class Roof(BuildingComponent):
220
+ """Class representing a roof component."""
221
+
222
+ roof_type: str = "Custom" # Flat, Pitched, etc.
223
+ roof_group: str = "A" # ASHRAE roof group
224
+ pitch: float = 0.0 # Roof pitch in degrees
225
+ has_suspended_ceiling: bool = False
226
+ ceiling_plenum_height: float = 0.0 # m
227
+
228
  def __post_init__(self):
229
+ """Initialize roof-specific attributes."""
230
  super().__post_init__()
231
  self.component_type = ComponentType.ROOF
232
  self.orientation = Orientation.HORIZONTAL
233
+
234
+ def to_dict(self) -> Dict:
235
+ """Convert the roof to a dictionary."""
236
+ roof_dict = super().to_dict()
237
+ roof_dict.update({
238
+ "roof_type": self.roof_type,
239
+ "roof_group": self.roof_group,
240
+ "pitch": self.pitch,
241
+ "has_suspended_ceiling": self.has_suspended_ceiling,
242
+ "ceiling_plenum_height": self.ceiling_plenum_height
243
+ })
244
+ return roof_dict
245
 
 
 
 
 
246
 
247
  @dataclass
248
  class Floor(BuildingComponent):
249
+ """Class representing a floor component."""
250
+
251
+ floor_type: str = "Custom" # Slab-on-grade, Raised, etc.
252
+ is_ground_contact: bool = False
253
+ perimeter_length: float = 0.0 # m (for slab-on-grade floors)
254
+
255
  def __post_init__(self):
256
+ """Initialize floor-specific attributes."""
257
  super().__post_init__()
258
  self.component_type = ComponentType.FLOOR
259
  self.orientation = Orientation.HORIZONTAL
260
+
261
+ def to_dict(self) -> Dict:
262
+ """Convert the floor to a dictionary."""
263
+ floor_dict = super().to_dict()
264
+ floor_dict.update({
265
+ "floor_type": self.floor_type,
266
+ "is_ground_contact": self.is_ground_contact,
267
+ "perimeter_length": self.perimeter_length
268
+ })
269
+ return floor_dict
270
 
 
 
 
 
271
 
272
  @dataclass
273
+ class Fenestration(BuildingComponent):
274
+ """Base class for fenestration components (windows, doors, skylights)."""
275
+
276
+ shgc: float = 0.7 # Solar Heat Gain Coefficient
277
+ vt: float = 0.7 # Visible Transmittance
278
+ frame_type: str = "Aluminum" # Aluminum, Wood, Vinyl, etc.
279
+ frame_width: float = 0.05 # m
280
+ has_shading: bool = False
281
+ shading_type: str = None # Internal, External, Between-glass
282
+ shading_coefficient: float = 1.0 # 0-1 (1 = no shading)
283
+
284
  def __post_init__(self):
285
+ """Initialize fenestration-specific attributes."""
286
  super().__post_init__()
287
+
288
+ if self.shgc < 0 or self.shgc > 1:
289
  raise ValueError("SHGC must be between 0 and 1")
290
+ if self.vt < 0 or self.vt > 1:
291
  raise ValueError("VT must be between 0 and 1")
292
+ if self.shading_coefficient < 0 or self.shading_coefficient > 1:
293
+ raise ValueError("Shading coefficient must be between 0 and 1")
294
+
295
+ @property
296
+ def effective_shgc(self) -> float:
297
+ """Calculate the effective SHGC considering shading."""
298
+ return self.shgc * self.shading_coefficient
299
+
300
+ def to_dict(self) -> Dict:
301
+ """Convert the fenestration to a dictionary."""
302
+ fenestration_dict = super().to_dict()
303
+ fenestration_dict.update({
304
+ "shgc": self.shgc,
305
+ "vt": self.vt,
306
+ "frame_type": self.frame_type,
307
+ "frame_width": self.frame_width,
308
+ "has_shading": self.has_shading,
309
+ "shading_type": self.shading_type,
310
+ "shading_coefficient": self.shading_coefficient,
311
+ "effective_shgc": self.effective_shgc
312
  })
313
+ return fenestration_dict
314
 
 
 
 
315
 
316
+ @dataclass
317
+ class Window(Fenestration):
318
+ """Class representing a window component."""
319
+
320
+ window_type: str = "Custom" # Single, Double, Triple glazed, etc.
321
+ glazing_layers: int = 2 # Number of glazing layers
322
+ gas_fill: str = "Air" # Air, Argon, Krypton, etc.
323
+ low_e_coating: bool = False
324
+ width: float = 1.0 # m
325
+ height: float = 1.0 # m
326
+ wall_id: str = None # ID of the wall containing this window
327
+
328
  def __post_init__(self):
329
+ """Initialize window-specific attributes."""
330
  super().__post_init__()
331
+ self.component_type = ComponentType.WINDOW
332
+
333
+ # Calculate area from width and height if not provided
334
+ if self.area <= 0 and self.width > 0 and self.height > 0:
335
+ self.area = self.width * self.height
336
+
337
+ def to_dict(self) -> Dict:
338
+ """Convert the window to a dictionary."""
339
+ window_dict = super().to_dict()
340
+ window_dict.update({
341
+ "window_type": self.window_type,
342
+ "glazing_layers": self.glazing_layers,
343
+ "gas_fill": self.gas_fill,
344
+ "low_e_coating": self.low_e_coating,
345
+ "width": self.width,
346
+ "height": self.height,
347
+ "wall_id": self.wall_id
348
+ })
349
+ return window_dict
350
 
 
 
 
 
351
 
 
352
  @dataclass
353
+ class Door(Fenestration):
354
+ """Class representing a door component."""
355
+
356
+ door_type: str = "Custom" # Solid, Partially glazed, etc.
357
+ glazing_percentage: float = 0.0 # Percentage of door area that is glazed (0-100)
358
+ width: float = 0.9 # m
359
+ height: float = 2.1 # m
360
+ wall_id: str = None # ID of the wall containing this door
361
+
362
  def __post_init__(self):
363
+ """Initialize door-specific attributes."""
364
+ super().__post_init__()
365
+ self.component_type = ComponentType.DOOR
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ # Calculate area from width and height if not provided
368
+ if self.area <= 0 and self.width > 0 and self.height > 0:
369
+ self.area = self.width * self.height
370
 
371
+ if self.glazing_percentage < 0 or self.glazing_percentage > 100:
372
+ raise ValueError("Glazing percentage must be between 0 and 100")
373
+
374
+ @property
375
+ def glazing_area(self) -> float:
376
+ """Calculate the glazed area of the door in m²."""
377
+ return self.area * (self.glazing_percentage / 100)
378
+
379
+ @property
380
+ def opaque_area(self) -> float:
381
+ """Calculate the opaque area of the door in m²."""
382
+ return self.area - self.glazing_area
383
+
384
+ def to_dict(self) -> Dict:
385
+ """Convert the door to a dictionary."""
386
+ door_dict = super().to_dict()
387
+ door_dict.update({
388
+ "door_type": self.door_type,
389
+ "glazing_percentage": self.glazing_percentage,
390
+ "width": self.width,
391
+ "height": self.height,
392
+ "wall_id": self.wall_id,
393
+ "glazing_area": self.glazing_area,
394
+ "opaque_area": self.opaque_area
395
+ })
396
+ return door_dict
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
 
399
+ @dataclass
400
+ class Skylight(Fenestration):
401
+ """Class representing a skylight component."""
402
+
403
+ skylight_type: str = "Custom" # Flat, Domed, etc.
404
+ glazing_layers: int = 2 # Number of glazing layers
405
+ gas_fill: str = "Air" # Air, Argon, Krypton, etc.
406
+ low_e_coating: bool = False
407
+ width: float = 1.0 # m
408
+ length: float = 1.0 # m
409
+ roof_id: str = None # ID of the roof containing this skylight
410
+
411
+ def __post_init__(self):
412
+ """Initialize skylight-specific attributes."""
413
+ super().__post_init__()
414
+ self.component_type = ComponentType.SKYLIGHT
415
+ self.orientation = Orientation.HORIZONTAL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
+ # Calculate area from width and length if not provided
418
+ if self.area <= 0 and self.width > 0 and self.length > 0:
419
+ self.area = self.width * self.length
420
+
421
+ def to_dict(self) -> Dict:
422
+ """Convert the skylight to a dictionary."""
423
+ skylight_dict = super().to_dict()
424
+ skylight_dict.update({
425
+ "skylight_type": self.skylight_type,
426
+ "glazing_layers": self.glazing_layers,
427
+ "gas_fill": self.gas_fill,
428
+ "low_e_coating": self.low_e_coating,
429
+ "width": self.width,
430
+ "length": self.length,
431
+ "roof_id": self.roof_id
432
+ })
433
+ return skylight_dict
434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
+ class BuildingComponentFactory:
437
+ """Factory class for creating building components."""
438
+
439
+ @staticmethod
440
+ def create_component(component_data: Dict) -> BuildingComponent:
441
+ """
442
+ Create a building component from a dictionary of data.
 
 
443
 
444
+ Args:
445
+ component_data: Dictionary containing component data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
+ Returns:
448
+ A BuildingComponent object of the appropriate type
449
+ """
450
+ component_type = component_data.get("component_type")
451
 
452
+ if component_type == ComponentType.WALL.value:
453
+ return Wall(**component_data)
454
+ elif component_type == ComponentType.ROOF.value:
455
+ return Roof(**component_data)
456
+ elif component_type == ComponentType.FLOOR.value:
457
+ return Floor(**component_data)
458
+ elif component_type == ComponentType.WINDOW.value:
459
+ return Window(**component_data)
460
+ elif component_type == ComponentType.DOOR.value:
461
+ return Door(**component_data)
462
+ elif component_type == ComponentType.SKYLIGHT.value:
463
+ return Skylight(**component_data)
464
+ else:
465
+ raise ValueError(f"Unknown component type: {component_type}")