mabuseif commited on
Commit
c36de67
·
verified ·
1 Parent(s): 4e2f290

Update data/building_components.py

Browse files
Files changed (1) hide show
  1. data/building_components.py +607 -376
data/building_components.py CHANGED
@@ -1,16 +1,19 @@
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,130 +22,46 @@ class Orientation(Enum):
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,316 +69,628 @@ class BuildingComponent:
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}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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
  "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)