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

Update app/component_selection.py

Browse files
Files changed (1) hide show
  1. app/component_selection.py +385 -218
app/component_selection.py CHANGED
@@ -1,45 +1,98 @@
1
  """
2
- Component selection interface module for HVAC Load Calculator.
3
- This module implements the Streamlit interface for selecting building components.
4
  """
5
 
6
  import streamlit as st
7
  import pandas as pd
8
  import numpy as np
9
  import json
10
- from typing import Dict, List, Any
 
 
 
11
 
12
- # Import application modules
13
- from utils.component_library import component_library, ComponentLibrary
14
- from utils.u_value_calculator import u_value_calculator, UValueCalculator
15
- from data.building_components import (
16
- Wall, Roof, Floor, Window, Door, Orientation, ComponentType, BuildingComponent, MaterialLayer
17
- )
18
  from data.reference_data import reference_data
 
 
19
 
 
 
 
 
 
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  class ComponentSelectionInterface:
22
- """Class for managing the component selection interface."""
23
-
24
  def __init__(self):
25
- """Initialize component selection interface."""
26
- pass
27
-
 
28
  def display_component_selection(self, session_state: Any) -> None:
29
- """
30
- Display the component selection interface.
31
-
32
- Args:
33
- session_state: Streamlit session state object
34
- """
35
  st.title("Building Components")
36
 
37
  if 'components' not in session_state:
38
- session_state.components = {
39
- 'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []
40
- }
41
 
42
- tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors"])
43
 
44
  with tabs[0]:
45
  self._display_component_tab(session_state, ComponentType.WALL)
@@ -51,6 +104,8 @@ class ComponentSelectionInterface:
51
  self._display_component_tab(session_state, ComponentType.WINDOW)
52
  with tabs[4]:
53
  self._display_component_tab(session_state, ComponentType.DOOR)
 
 
54
 
55
  col1, col2 = st.columns(2)
56
  with col1:
@@ -60,7 +115,7 @@ class ComponentSelectionInterface:
60
  uploaded_file = st.file_uploader("Load Components", type=["json"])
61
  if uploaded_file and st.button("Load Uploaded Components"):
62
  self._load_components(session_state, uploaded_file)
63
-
64
  def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
65
  type_name = component_type.value.lower()
66
  st.subheader(f"{type_name.capitalize()} Components")
@@ -74,119 +129,166 @@ class ComponentSelectionInterface:
74
  self._display_components_table(session_state, component_type, components)
75
  else:
76
  st.info(f"No {type_name} components defined yet.")
77
-
78
  def _display_add_component_form(self, session_state: Any, component_type: ComponentType) -> None:
79
  type_name = component_type.value.lower()
80
- preset_components = component_library.get_preset_components_by_type(component_type)
81
 
82
  with st.form(f"add_{type_name}_form"):
83
  col1, col2 = st.columns(2)
84
-
85
  with col1:
86
- use_preset = st.checkbox("Use Preset", value=True)
87
- if use_preset and preset_components:
88
- preset_options = {comp.name: comp.id for comp in preset_components}
89
- selected_preset = st.selectbox(f"Select Preset {type_name.capitalize()}",
90
- options=list(preset_options.keys()))
91
  else:
92
- name = st.text_input("Name", f"Custom {type_name.capitalize()}")
93
 
94
  with col2:
95
- area = st.number_input("Area ()", min_value=0.1, value=1.0, step=0.1)
96
- if not use_preset:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=0.5, step=0.01)
98
-
99
- if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
100
- orientation = st.selectbox("Orientation",
101
- [o.value for o in Orientation],
102
- index=[o.value for o in Orientation].index("N/A"))
103
- else:
104
- orientation = Orientation.HORIZONTAL.value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  submitted = st.form_submit_button("Add Component")
107
-
108
  if submitted:
109
- try:
110
- if use_preset and preset_components:
111
- component_id = preset_options[selected_preset]
112
- component = component_library.get_component(component_id)
113
- if not component:
114
- raise ValueError(f"Preset component {selected_preset} not found.")
115
- new_component_id = component_library.clone_component(component_id, f"{component.name} (Copy)")
116
- component = component_library.get_component(new_component_id)
117
- component.area = area
118
- component.orientation = Orientation(orientation)
119
- else:
120
- component_dict = {
121
- "id": f"custom_{type_name}_{len(session_state.components[type_name + 's'])}",
122
- "name": name,
123
- "u_value": u_value,
124
- "area": area,
125
- "orientation": Orientation(orientation)
126
- }
127
- if component_type == ComponentType.WALL:
128
- component = Wall(**component_dict)
129
- elif component_type == ComponentType.ROOF:
130
- component = Roof(**component_dict)
131
- elif component_type == ComponentType.FLOOR:
132
- component = Floor(**component_dict)
133
- elif component_type == ComponentType.WINDOW:
134
- component = Window(**component_dict, shgc=0.7, vt=0.7)
135
- elif component_type == ComponentType.DOOR:
136
- component = Door(**component_dict, shgc=0.0, vt=0.0)
137
  else:
138
- raise ValueError(f"Unsupported component type: {component_type}")
139
- component_library.add_component(component)
140
-
141
- session_state.components[type_name + 's'].append(component)
142
- st.success(f"Added {component.name}")
143
- st.rerun()
144
- except ValueError as e:
145
- st.error(f"Error adding component: {str(e)}")
146
- except Exception as e:
147
- st.error(f"Unexpected error: {str(e)}")
148
-
149
- def _display_components_table(self, session_state: Any, component_type: ComponentType,
150
- components: List[BuildingComponent]) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  type_name = component_type.value.lower()
152
  data = []
153
  for comp in components:
154
- data.append({
155
- "Name": comp.name,
156
- "Area (m²)": comp.area,
157
- "U-Value (W/m²·K)": comp.u_value,
158
- "Orientation": comp.orientation.value,
159
- "ID": comp.id
160
- })
 
 
 
 
 
 
161
 
162
  df = pd.DataFrame(data)
163
- st.dataframe(df.drop(columns=["ID"]), use_container_width=True)
 
164
 
165
  for i, row in df.iterrows():
166
- col1, col2, col3 = st.columns([1, 1, 1])
167
  with col1:
168
  if st.button("Edit", key=f"edit_{row['ID']}"):
169
- self._display_edit_component_form(session_state, component_type, row["ID"])
 
170
  with col2:
171
  if st.button("Delete", key=f"delete_{row['ID']}"):
172
  if not row["ID"].startswith("preset_"):
173
- component_library.remove_component(row["ID"])
174
- session_state.components[type_name + 's'] = [
175
- c for c in components if c.id != row["ID"]
176
- ]
177
  st.success(f"Deleted {row['Name']}")
178
  st.rerun()
179
  else:
180
  st.warning("Cannot delete preset components.")
181
- with col3:
182
- if st.button("Calculate U-Value", key=f"u_calc_{row['ID']}"):
183
- self._display_u_value_calculator(session_state, component_type, row["ID"])
184
-
185
- def _display_edit_component_form(self, session_state: Any, component_type: ComponentType,
186
- component_id: str) -> None:
187
  type_name = component_type.value.lower()
188
  component = next((c for c in session_state.components[type_name + 's'] if c.id == component_id), None)
189
-
190
  if not component:
191
  st.error("Component not found.")
192
  return
@@ -195,140 +297,205 @@ class ComponentSelectionInterface:
195
  col1, col2 = st.columns(2)
196
  with col1:
197
  name = st.text_input("Name", value=component.name)
198
- with col2:
199
- area = st.number_input("Area (m²)", min_value=0.1, value=component.area, step=0.1)
 
 
 
 
200
 
201
- if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
202
- orientation = st.selectbox("Orientation",
203
- [o.value for o in Orientation],
204
- index=[o.value for o in Orientation].index(component.orientation.value))
205
- else:
206
- orientation = component.orientation.value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  submitted = st.form_submit_button("Update Component")
209
-
210
  if submitted:
211
- try:
 
 
 
 
 
 
212
  component.name = name
213
  component.area = area
214
- component.orientation = Orientation(orientation)
215
- component_library.update_component(component_id, component)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  st.success(f"Updated {name}")
217
  st.rerun()
218
- except ValueError as e:
219
- st.error(f"Error updating component: {str(e)}")
220
-
221
- def _display_u_value_calculator(self, session_state: Any, component_type: ComponentType,
222
- component_id: str) -> None:
223
- type_name = component_type.value.lower()
224
- component = next((c for c in session_state.components[type_name + 's'] if c.id == component_id), None)
 
 
 
 
 
 
 
225
 
 
 
226
  if not component:
227
  st.error("Component not found.")
228
  return
229
 
230
- st.subheader(f"U-Value Calculator for {component.name}")
231
-
232
- if component.material_layers:
233
- st.write("Current Material Layers:")
234
- layers_df = pd.DataFrame([{
235
- "Name": layer.name,
236
- "Thickness (m)": layer.thickness,
237
- "Conductivity (W/m·K)": layer.conductivity
238
- } for layer in component.material_layers])
239
- st.dataframe(layers_df, use_container_width=True)
240
- else:
241
- st.info("No material layers defined yet.")
242
-
243
- with st.form(f"u_value_form_{component_id}"):
244
- st.write("Add New Layer:")
245
  col1, col2, col3 = st.columns(3)
246
  with col1:
247
- material_options = {m["name"]: mid for mid, m in reference_data.materials.items()}
248
- material_name = st.selectbox("Material", options=list(material_options.keys()))
249
  with col2:
250
- thickness = st.number_input("Thickness (m)", min_value=0.001, value=0.1, step=0.01)
251
  with col3:
252
- custom_conductivity = st.checkbox("Custom Conductivity")
253
- if custom_conductivity:
254
- conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=0.5, step=0.01)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  else:
256
- conductivity = reference_data.materials[material_options[material_name]]["conductivity"]
 
257
 
258
- col4, col5 = st.columns(2)
259
- with col4:
260
- add_layer = st.form_submit_button("Add Layer")
261
- with col5:
262
- calculate = st.form_submit_button("Calculate U-Value")
263
 
264
- if add_layer:
265
- try:
266
- layer = MaterialLayer(
267
- name=material_name,
268
- thickness=thickness,
269
- conductivity=conductivity,
270
- density=reference_data.materials.get(material_options[material_name], {}).get("density"),
271
- specific_heat=reference_data.materials.get(material_options[material_name], {}).get("specific_heat")
272
- )
273
- component.material_layers.append(layer)
274
- component_library.update_component(component_id, component)
275
  st.success(f"Added layer: {material_name}")
276
  st.rerun()
277
- except ValueError as e:
278
- st.error(f"Error adding layer: {str(e)}")
279
-
280
- if calculate:
281
- if not component.material_layers:
282
- st.error("No layers defined for U-value calculation.")
283
- else:
284
- assembly_id = u_value_calculator.create_assembly(f"{component.name} Assembly")
285
- for layer in component.material_layers:
286
- u_value_calculator.add_custom_layer_to_assembly(
287
- assembly_id, layer.name, layer.thickness, layer.conductivity,
288
- layer.density, layer.specific_heat
289
- )
290
- u_value = u_value_calculator.calculate_u_value(assembly_id)
291
- if u_value is not None:
292
- component.u_value = u_value
293
- component_library.update_component(component_id, component)
294
- st.success(f"Calculated U-Value: {u_value:.3f} W/(m²·K)")
295
- st.rerun()
296
- else:
297
- st.error("Failed to calculate U-value.")
298
-
299
  def _save_components(self, session_state: Any) -> None:
300
- try:
301
- for type_name in ['walls', 'roofs', 'floors', 'windows', 'doors']:
302
- for comp in session_state.components[type_name]:
303
- component_library.update_component(comp.id, comp)
304
-
305
- file_path = "components_export.json"
306
- component_library.export_to_json(file_path)
307
- with open(file_path, 'r') as f:
308
- st.download_button(
309
- label="Download Components",
310
- data=f,
311
- file_name="components.json",
312
- mime="application/json"
313
- )
314
- st.success("Components saved successfully.")
315
- except Exception as e:
316
- st.error(f"Error saving components: {str(e)}")
317
-
318
  def _load_components(self, session_state: Any, uploaded_file: Any) -> None:
319
- try:
320
- data = json.load(uploaded_file)
321
- count = component_library.import_from_json(data)
322
-
323
- session_state.components = {
324
- 'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []
325
- }
326
-
327
- for comp_id, comp in component_library.components.items():
328
- type_name = comp.component_type.value.lower() + 's'
 
 
 
 
 
 
 
329
  session_state.components[type_name].append(comp)
330
-
331
- st.success(f"Loaded {count} components successfully.")
332
- st.rerun()
333
- except Exception as e:
334
- st.error(f"Error loading components: {str(e)}")
 
 
 
1
  """
2
+ HVAC Component Selection Module
3
+ Provides UI for selecting building components in the HVAC Load Calculator.
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
+ # Imports from your project structure
16
+ from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType
 
 
 
 
17
  from data.reference_data import reference_data
18
+ from utils.component_library import ComponentLibrary
19
+ from utils.u_value_calculator import UValueCalculator
20
 
21
+ # --- Shading System (New Addition) ---
22
+ class ShadingType(Enum):
23
+ NONE = "None"
24
+ INTERNAL = "Internal"
25
+ EXTERNAL = "External"
26
+ BETWEEN_GLASS = "Between-glass"
27
 
28
+ @dataclass
29
+ class ShadingDevice:
30
+ id: str
31
+ name: str
32
+ shading_type: ShadingType
33
+ shading_coefficient: float
34
+ coverage_percentage: float = 100.0
35
+ description: str = ""
36
+
37
+ def __post_init__(self):
38
+ if self.shading_coefficient < 0 or self.shading_coefficient > 1:
39
+ raise ValueError("Shading coefficient must be between 0 and 1")
40
+ if self.coverage_percentage < 0 or self.coverage_percentage > 100:
41
+ raise ValueError("Coverage percentage must be between 0 and 100")
42
+
43
+ @property
44
+ def effective_shading_coefficient(self) -> float:
45
+ coverage_factor = self.coverage_percentage / 100.0
46
+ return self.shading_coefficient * coverage_factor + 1.0 * (1 - coverage_factor)
47
+
48
+ def to_dict(self) -> Dict[str, Any]:
49
+ return {
50
+ "id": self.id, "name": self.name, "shading_type": self.shading_type.value,
51
+ "shading_coefficient": self.shading_coefficient, "coverage_percentage": self.coverage_percentage,
52
+ "description": self.description, "effective_shading_coefficient": self.effective_shading_coefficient
53
+ }
54
+
55
+ class ShadingSystem:
56
+ def __init__(self):
57
+ self.shading_devices = {}
58
+ self.load_preset_devices()
59
+
60
+ def load_preset_devices(self) -> None:
61
+ self.shading_devices["preset_none"] = ShadingDevice(
62
+ id="preset_none", name="None", shading_type=ShadingType.NONE, shading_coefficient=1.0, description="No shading"
63
+ )
64
+ self.shading_devices["preset_venetian_blinds"] = ShadingDevice(
65
+ id="preset_venetian_blinds", name="Venetian Blinds", shading_type=ShadingType.INTERNAL, shading_coefficient=0.6, description="Standard internal venetian blinds"
66
+ )
67
+ self.shading_devices["preset_overhang"] = ShadingDevice(
68
+ id="preset_overhang", name="Overhang", shading_type=ShadingType.EXTERNAL, shading_coefficient=0.4, description="External overhang"
69
+ )
70
+
71
+ def get_device(self, device_id: str) -> Optional[ShadingDevice]:
72
+ return self.shading_devices.get(device_id)
73
+
74
+ def calculate_effective_shgc(self, base_shgc: float, device_id: str) -> float:
75
+ device = self.get_device(device_id)
76
+ if not device:
77
+ return base_shgc
78
+ return base_shgc * device.effective_shading_coefficient
79
+
80
+ shading_system = ShadingSystem()
81
+
82
+ # --- Component Selection Interface ---
83
  class ComponentSelectionInterface:
 
 
84
  def __init__(self):
85
+ self.component_library = ComponentLibrary()
86
+ self.u_value_calculator = UValueCalculator()
87
+ self.shading_system = shading_system
88
+
89
  def display_component_selection(self, session_state: Any) -> None:
 
 
 
 
 
 
90
  st.title("Building Components")
91
 
92
  if 'components' not in session_state:
93
+ session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
 
 
94
 
95
+ tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
96
 
97
  with tabs[0]:
98
  self._display_component_tab(session_state, ComponentType.WALL)
 
104
  self._display_component_tab(session_state, ComponentType.WINDOW)
105
  with tabs[4]:
106
  self._display_component_tab(session_state, ComponentType.DOOR)
107
+ with tabs[5]:
108
+ self._display_u_value_calculator_tab(session_state)
109
 
110
  col1, col2 = st.columns(2)
111
  with col1:
 
115
  uploaded_file = st.file_uploader("Load Components", type=["json"])
116
  if uploaded_file and st.button("Load Uploaded Components"):
117
  self._load_components(session_state, uploaded_file)
118
+
119
  def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
120
  type_name = component_type.value.lower()
121
  st.subheader(f"{type_name.capitalize()} Components")
 
129
  self._display_components_table(session_state, component_type, components)
130
  else:
131
  st.info(f"No {type_name} components defined yet.")
132
+
133
  def _display_add_component_form(self, session_state: Any, component_type: ComponentType) -> None:
134
  type_name = component_type.value.lower()
135
+ preset_components = self.component_library.get_preset_components_by_type(component_type)
136
 
137
  with st.form(f"add_{type_name}_form"):
138
  col1, col2 = st.columns(2)
 
139
  with col1:
140
+ name = st.text_input("Name", f"New {type_name.capitalize()}")
141
+ area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
142
+ if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
143
+ orientation = st.selectbox("Orientation", [o.name for o in Orientation], index=0)
 
144
  else:
145
+ orientation = Orientation.HORIZONTAL.name
146
 
147
  with col2:
148
+ selection_method = st.radio(f"{type_name.capitalize()} Selection Method", ["Select from Presets", "Custom Properties"])
149
+ if selection_method == "Select from Presets" and preset_components:
150
+ preset_options = {comp.name: comp.id for comp in preset_components}
151
+ selected_preset = st.selectbox(f"Select Preset {type_name.capitalize()}", options=list(preset_options.keys()))
152
+ component = self.component_library.get_component(preset_options[selected_preset])
153
+ u_value = st.number_input("U-Value (W/m²·K)", value=component.u_value, disabled=True)
154
+ if component_type == ComponentType.WALL:
155
+ st.text_input("Wall Type", value=component.wall_type, disabled=True)
156
+ st.text_input("Wall Group", value=component.wall_group, disabled=True)
157
+ elif component_type == ComponentType.ROOF:
158
+ st.text_input("Roof Type", value=component.roof_type, disabled=True)
159
+ st.text_input("Roof Group", value=component.roof_group, disabled=True)
160
+ elif component_type == ComponentType.FLOOR:
161
+ st.text_input("Floor Type", value=component.floor_type, disabled=True)
162
+ elif component_type == ComponentType.WINDOW:
163
+ st.number_input("SHGC", value=component.shgc, disabled=True)
164
+ st.number_input("VT", value=component.vt, disabled=True)
165
+ st.text_input("Window Type", value=component.window_type, disabled=True)
166
+ elif component_type == ComponentType.DOOR:
167
+ st.text_input("Door Type", value=component.door_type, disabled=True)
168
+ else:
169
  u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=0.5, step=0.01)
170
+ if component_type == ComponentType.WALL:
171
+ wall_type = st.text_input("Wall Type", "Custom")
172
+ wall_group = st.selectbox("Wall Group", ["A", "B", "C", "D", "E", "F", "G", "H"], index=1)
173
+ elif component_type == ComponentType.ROOF:
174
+ roof_type = st.text_input("Roof Type", "Custom")
175
+ roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"], index=2)
176
+ elif component_type == ComponentType.FLOOR:
177
+ floor_type = st.text_input("Floor Type", "Custom")
178
+ elif component_type == ComponentType.WINDOW:
179
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
180
+ vt = st.number_input("VT", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
181
+ window_type = st.text_input("Window Type", "Custom")
182
+ glazing_layers = st.selectbox("Glazing Layers", [1, 2, 3], index=1)
183
+ gas_fill = st.selectbox("Gas Fill", ["Air", "Argon", "Krypton"], index=0)
184
+ low_e_coating = st.checkbox("Low-E Coating")
185
+ shading_options = {d.name: d.id for d in self.shading_system.shading_devices.values()}
186
+ shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
187
+ coverage = st.number_input("Coverage (%)", min_value=0.0, max_value=100.0, value=100.0, step=5.0)
188
+ device = self.shading_system.get_device(shading_options[shading_device])
189
+ device.coverage_percentage = coverage
190
+ st.write(f"Effective SHGC: {self.shading_system.calculate_effective_shgc(shgc, shading_options[shading_device]):.2f}")
191
+ elif component_type == ComponentType.DOOR:
192
+ door_type = st.text_input("Door Type", "Custom")
193
 
194
  submitted = st.form_submit_button("Add Component")
 
195
  if submitted:
196
+ if not name:
197
+ st.error(f"{type_name.capitalize()} name is required!")
198
+ elif area <= 0:
199
+ st.error(f"{type_name.capitalize()} area must be greater than zero!")
200
+ elif u_value <= 0:
201
+ st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
202
+ else:
203
+ try:
204
+ if selection_method == "Select from Presets" and preset_components:
205
+ component_id = preset_options[selected_preset]
206
+ component = self.component_library.get_component(component_id)
207
+ new_component = component.__class__(
208
+ id=str(uuid.uuid4()), name=name, area=area,
209
+ orientation=Orientation[orientation], **vars(component)
210
+ )
211
+ del new_component.__dict__["id"]
 
 
 
 
 
 
 
 
 
 
 
 
212
  else:
213
+ if component_type == ComponentType.WALL:
214
+ new_component = Wall(
215
+ id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
216
+ orientation=Orientation[orientation], wall_type=wall_type, wall_group=wall_group
217
+ )
218
+ elif component_type == ComponentType.ROOF:
219
+ new_component = Roof(
220
+ id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
221
+ orientation=Orientation[orientation], roof_type=roof_type, roof_group=roof_group
222
+ )
223
+ elif component_type == ComponentType.FLOOR:
224
+ new_component = Floor(
225
+ id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
226
+ orientation=Orientation[orientation], floor_type=floor_type
227
+ )
228
+ elif component_type == ComponentType.WINDOW:
229
+ new_component = Window(
230
+ id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
231
+ orientation=Orientation[orientation], shgc=shgc, vt=vt, window_type=window_type,
232
+ glazing_layers=glazing_layers, gas_fill=gas_fill, low_e_coating=low_e_coating,
233
+ shading_device_id=shading_options[shading_device]
234
+ )
235
+ elif component_type == ComponentType.DOOR:
236
+ new_component = Door(
237
+ id=str(uuid.uuid4()), name=name, u_value=u_value, area=area,
238
+ orientation=Orientation[orientation], door_type=door_type
239
+ )
240
+ self.component_library.add_component(new_component)
241
+ session_state.components[type_name + 's'].append(new_component)
242
+ st.success(f"Added {new_component.name}")
243
+ st.rerun()
244
+ except ValueError as e:
245
+ st.error(f"Error: {str(e)}")
246
+
247
+ def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[Any]) -> None:
248
  type_name = component_type.value.lower()
249
  data = []
250
  for comp in components:
251
+ row = {"Name": comp.name, "Area (m²)": comp.area, "U-Value (W/m²·K)": comp.u_value, "Orientation": comp.orientation.name, "ID": comp.id}
252
+ if component_type == ComponentType.WALL:
253
+ row.update({"Wall Type": comp.wall_type, "Wall Group": comp.wall_group})
254
+ elif component_type == ComponentType.ROOF:
255
+ row.update({"Roof Type": comp.roof_type, "Roof Group": comp.roof_group})
256
+ elif component_type == ComponentType.FLOOR:
257
+ row.update({"Floor Type": comp.floor_type})
258
+ elif component_type == ComponentType.WINDOW:
259
+ row.update({"SHGC": comp.shgc, "VT": comp.vt, "Window Type": comp.window_type,
260
+ "Effective SHGC": self.shading_system.calculate_effective_shgc(comp.shgc, comp.shading_device_id)})
261
+ elif component_type == ComponentType.DOOR:
262
+ row.update({"Door Type": comp.door_type})
263
+ data.append(row)
264
 
265
  df = pd.DataFrame(data)
266
+ display_cols = [col for col in df.columns if col != "ID"]
267
+ st.dataframe(df[display_cols], use_container_width=True)
268
 
269
  for i, row in df.iterrows():
270
+ col1, col2 = st.columns(2)
271
  with col1:
272
  if st.button("Edit", key=f"edit_{row['ID']}"):
273
+ session_state[f"edit_{type_name}"] = row["ID"]
274
+ st.rerun()
275
  with col2:
276
  if st.button("Delete", key=f"delete_{row['ID']}"):
277
  if not row["ID"].startswith("preset_"):
278
+ self.component_library.remove_component(row["ID"])
279
+ session_state.components[type_name + 's'] = [c for c in components if c.id != row["ID"]]
 
 
280
  st.success(f"Deleted {row['Name']}")
281
  st.rerun()
282
  else:
283
  st.warning("Cannot delete preset components.")
284
+
285
+ if f"edit_{type_name}" in session_state:
286
+ self._display_edit_component_form(session_state, component_type, session_state[f"edit_{type_name}"])
287
+ del session_state[f"edit_{type_name}"]
288
+
289
+ def _display_edit_component_form(self, session_state: Any, component_type: ComponentType, component_id: str) -> None:
290
  type_name = component_type.value.lower()
291
  component = next((c for c in session_state.components[type_name + 's'] if c.id == component_id), None)
 
292
  if not component:
293
  st.error("Component not found.")
294
  return
 
297
  col1, col2 = st.columns(2)
298
  with col1:
299
  name = st.text_input("Name", value=component.name)
300
+ area = st.number_input("Area (m²)", min_value=0.0, value=component.area, step=0.1)
301
+ if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
302
+ orientation = st.selectbox("Orientation", [o.name for o in Orientation],
303
+ index=[o.name for o in Orientation].index(component.orientation.name))
304
+ else:
305
+ orientation = Orientation.HORIZONTAL.name
306
 
307
+ with col2:
308
+ u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=component.u_value, step=0.01)
309
+ if component_type == ComponentType.WALL:
310
+ wall_type = st.text_input("Wall Type", value=component.wall_type)
311
+ wall_group = st.selectbox("Wall Group", ["A", "B", "C", "D", "E", "F", "G", "H"],
312
+ index=["A", "B", "C", "D", "E", "F", "G", "H"].index(component.wall_group))
313
+ elif component_type == ComponentType.ROOF:
314
+ roof_type = st.text_input("Roof Type", value=component.roof_type)
315
+ roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"],
316
+ index=["A", "B", "C", "D", "E", "F", "G"].index(component.roof_group))
317
+ elif component_type == ComponentType.FLOOR:
318
+ floor_type = st.text_input("Floor Type", value=component.floor_type)
319
+ elif component_type == ComponentType.WINDOW:
320
+ shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=component.shgc, step=0.01)
321
+ vt = st.number_input("VT", min_value=0.0, max_value=1.0, value=component.vt, step=0.01)
322
+ window_type = st.text_input("Window Type", value=component.window_type)
323
+ glazing_layers = st.selectbox("Glazing Layers", [1, 2, 3], index=[1, 2, 3].index(component.glazing_layers))
324
+ gas_fill = st.selectbox("Gas Fill", ["Air", "Argon", "Krypton"],
325
+ index=["Air", "Argon", "Krypton"].index(component.gas_fill))
326
+ low_e_coating = st.checkbox("Low-E Coating", value=component.low_e_coating)
327
+ shading_options = {d.name: d.id for d in self.shading_system.shading_devices.values()}
328
+ shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()),
329
+ index=list(shading_options.keys()).index(self.shading_system.get_device(component.shading_device_id).name))
330
+ coverage = st.number_input("Coverage (%)", min_value=0.0, max_value=100.0,
331
+ value=self.shading_system.get_device(component.shading_device_id).coverage_percentage, step=5.0)
332
+ device = self.shading_system.get_device(shading_options[shading_device])
333
+ device.coverage_percentage = coverage
334
+ st.write(f"Effective SHGC: {self.shading_system.calculate_effective_shgc(shgc, shading_options[shading_device]):.2f}")
335
+ elif component_type == ComponentType.DOOR:
336
+ door_type = st.text_input("Door Type", value=component.door_type)
337
 
338
  submitted = st.form_submit_button("Update Component")
 
339
  if submitted:
340
+ if not name:
341
+ st.error(f"{type_name.capitalize()} name is required!")
342
+ elif area <= 0:
343
+ st.error(f"{type_name.capitalize()} area must be greater than zero!")
344
+ elif u_value <= 0:
345
+ st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
346
+ else:
347
  component.name = name
348
  component.area = area
349
+ component.u_value = u_value
350
+ component.orientation = Orientation[orientation]
351
+ if component_type == ComponentType.WALL:
352
+ component.wall_type = wall_type
353
+ component.wall_group = wall_group
354
+ elif component_type == ComponentType.ROOF:
355
+ component.roof_type = roof_type
356
+ component.roof_group = roof_group
357
+ elif component_type == ComponentType.FLOOR:
358
+ component.floor_type = floor_type
359
+ elif component_type == ComponentType.WINDOW:
360
+ component.shgc = shgc
361
+ component.vt = vt
362
+ component.window_type = window_type
363
+ component.glazing_layers = glazing_layers
364
+ component.gas_fill = gas_fill
365
+ component.low_e_coating = low_e_coating
366
+ component.shading_device_id = shading_options[shading_device]
367
+ elif component_type == ComponentType.DOOR:
368
+ component.door_type = door_type
369
+ self.component_library.update_component(component_id, component)
370
  st.success(f"Updated {name}")
371
  st.rerun()
372
+
373
+ def _display_u_value_calculator_tab(self, session_state: Any) -> None:
374
+ st.subheader("U-Value Calculator")
375
+
376
+ if "material_layers" not in session_state:
377
+ session_state.material_layers = []
378
+
379
+ components = []
380
+ for type_name in ['walls', 'roofs', 'floors', 'windows', 'doors']:
381
+ components.extend(session_state.components[type_name])
382
+
383
+ if not components:
384
+ st.warning("No components available. Add components first.")
385
+ return
386
 
387
+ selected_component = st.selectbox("Select Component", options=[c.name for c in components])
388
+ component = next((c for c in components if c.name == selected_component), None)
389
  if not component:
390
  st.error("Component not found.")
391
  return
392
 
393
+ if session_state.material_layers:
394
+ st.write("Current Material Layers (outside to inside):")
395
+ layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
396
+ "Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]}
397
+ for i, l in enumerate(session_state.material_layers)]
398
+ st.dataframe(pd.DataFrame(layer_data), use_container_width=True)
399
+
400
+ r_outside = 0.04 # m²·K/W
401
+ r_inside = 0.13 # m²·K/W
402
+ r_layers = sum([l["thickness"] / 1000 / l["conductivity"] for l in session_state.material_layers])
403
+ r_total = r_outside + r_layers + r_inside
404
+ u_value = 1 / r_total if r_total > 0 else 0
405
+
 
 
406
  col1, col2, col3 = st.columns(3)
407
  with col1:
408
+ st.metric("Total R-Value", f"{r_total:.3f} m²·K/W")
 
409
  with col2:
410
+ st.metric("U-Value", f"{u_value:.3f} W/m²·K")
411
  with col3:
412
+ if st.button("Use This U-Value"):
413
+ component.u_value = u_value
414
+ self.component_library.update_component(component.id, component)
415
+ session_state.material_layers = []
416
+ st.success(f"U-Value set to {u_value:.3f} W/m²·K for {component.name}")
417
+ st.rerun()
418
+
419
+ if st.button("Remove Last Layer"):
420
+ if session_state.material_layers:
421
+ session_state.material_layers.pop()
422
+ st.rerun()
423
+
424
+ with st.form("material_layer_form"):
425
+ col1, col2 = st.columns(2)
426
+ with col1:
427
+ material_selection = st.radio("Material Selection", ["Select from Presets", "Custom Material"])
428
+ if material_selection == "Select from Presets":
429
+ preset_materials = self.u_value_calculator.get_preset_materials()
430
+ material_options = {m["name"]: m for m in preset_materials}
431
+ material_name = st.selectbox("Material", options=list(material_options.keys()))
432
+ conductivity = material_options[material_name]["conductivity"]
433
+ st.number_input("Conductivity (W/m·K)", value=conductivity, disabled=True)
434
  else:
435
+ material_name = st.text_input("Material Name", "Custom Material")
436
+ conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=1.0, step=0.01)
437
 
438
+ with col2:
439
+ thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
440
+ if conductivity > 0:
441
+ r_value = thickness / 1000 / conductivity
442
+ st.metric("Layer R-Value", f"{r_value:.3f} m²·K/W")
443
 
444
+ submitted = st.form_submit_button("Add Layer")
445
+ if submitted:
446
+ if not material_name:
447
+ st.error("Material name is required!")
448
+ elif thickness <= 0:
449
+ st.error("Thickness must be greater than zero!")
450
+ elif conductivity <= 0:
451
+ st.error("Conductivity must be greater than zero!")
452
+ else:
453
+ session_state.material_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity})
 
454
  st.success(f"Added layer: {material_name}")
455
  st.rerun()
456
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  def _save_components(self, session_state: Any) -> None:
458
+ components_dict = {
459
+ "walls": [c.__dict__ for c in session_state.components["walls"]],
460
+ "roofs": [c.__dict__ for c in session_state.components["roofs"]],
461
+ "floors": [c.__dict__ for c in session_state.components["floors"]],
462
+ "windows": [c.__dict__ for c in session_state.components["windows"]],
463
+ "doors": [c.__dict__ for c in session_state.components["doors"]]
464
+ }
465
+ for type_name in components_dict:
466
+ for comp in components_dict[type_name]:
467
+ comp["orientation"] = comp["orientation"].name
468
+ comp["component_type"] = comp["component_type"].name
469
+ file_path = "components_export.json"
470
+ with open(file_path, 'w') as f:
471
+ json.dump(components_dict, f, indent=4)
472
+ with open(file_path, 'r') as f:
473
+ st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
474
+ st.success("Components saved successfully.")
475
+
476
  def _load_components(self, session_state: Any, uploaded_file: Any) -> None:
477
+ data = json.load(uploaded_file)
478
+ session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
479
+ for type_name in data:
480
+ for comp_dict in data[type_name]:
481
+ comp_dict["orientation"] = Orientation[comp_dict["orientation"]]
482
+ comp_dict["component_type"] = ComponentType[comp_dict["component_type"]]
483
+ if type_name == "walls":
484
+ comp = Wall(**comp_dict)
485
+ elif type_name == "roofs":
486
+ comp = Roof(**comp_dict)
487
+ elif type_name == "floors":
488
+ comp = Floor(**comp_dict)
489
+ elif type_name == "windows":
490
+ comp = Window(**comp_dict)
491
+ elif type_name == "doors":
492
+ comp = Door(**comp_dict)
493
+ self.component_library.add_component(comp)
494
  session_state.components[type_name].append(comp)
495
+ st.success("Loaded components successfully.")
496
+ st.rerun()
497
+
498
+ # --- Main Execution ---
499
+ if __name__ == "__main__":
500
+ interface = ComponentSelectionInterface()
501
+ interface.display_component_selection(st.session_state)