mabuseif commited on
Commit
2f47b8d
verified
1 Parent(s): 96aef11

Update app/construction.py

Browse files
Files changed (1) hide show
  1. app/construction.py +474 -477
app/construction.py CHANGED
@@ -1,10 +1,10 @@
1
  """
2
- BuildSustain - Construction Module
3
 
4
  This module handles the construction assembly functionality of the BuildSustain application,
5
  allowing users to create and manage multi-layer constructions for walls, roofs, and floors.
6
  It integrates with the material library to select materials for each layer and calculates
7
- overall thermal properties.
8
 
9
  Developed by: Dr Majed Abuseif, Deakin University
10
  漏 2025
@@ -18,8 +18,9 @@ import logging
18
  import uuid
19
  from typing import Dict, List, Any, Optional, Tuple, Union
20
 
21
- # Import the centralized data module
22
  from app.m_c_data import get_default_constructions
 
23
 
24
  # Configure logging
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -28,522 +29,520 @@ logger = logging.getLogger(__name__)
28
  # Define constants
29
  CONSTRUCTION_TYPES = ["Wall", "Roof", "Floor"]
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  def display_construction_page():
32
  """
33
- Display the construction page.
34
- This is the main function called by main.py when the Construction page is selected.
35
  """
36
  st.title("Construction Library")
37
  st.write("Define multi-layer constructions for walls, roofs, and floors based on ASHRAE 2005 Handbook of Fundamentals.")
38
-
39
- # Display help information in an expandable section
40
  with st.expander("Help & Information"):
41
  display_construction_help()
42
-
43
- # Initialize constructions in session state if not present
44
- initialize_constructions()
45
-
46
- # Check if rerun is pending
47
- if 'construction_rerun_pending' not in st.session_state:
48
- st.session_state.construction_rerun_pending = False
49
-
50
- if st.session_state.construction_rerun_pending:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  st.session_state.construction_rerun_pending = False
52
  st.rerun()
53
-
54
- # Split the display into two columns
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  col1, col2 = st.columns([3, 2])
56
-
57
  with col1:
58
- st.subheader("Saved Constructions")
59
- display_constructions_table()
60
-
61
  with col2:
62
- st.subheader("Construction Editor/Creator")
63
- display_construction_editor()
64
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # Navigation buttons
66
  col1, col2 = st.columns(2)
67
-
68
  with col1:
69
  if st.button("Back to Material Library", key="back_to_materials"):
70
  st.session_state.current_page = "Material Library"
71
  st.rerun()
72
-
73
  with col2:
74
  if st.button("Continue to Building Components", key="continue_to_components"):
75
  st.session_state.current_page = "Building Components"
76
  st.rerun()
77
 
78
- def initialize_constructions():
79
  """Initialize constructions in session state if not present."""
 
 
80
  if "constructions" not in st.session_state.project_data:
81
  st.session_state.project_data["constructions"] = {
82
- "library": {},
83
  "project": {}
84
  }
85
-
86
- # Initialize library constructions if empty
87
- if not st.session_state.project_data["constructions"]["library"]:
88
- # Get default constructions from the centralized data module
89
- default_constructions = get_default_constructions()
90
- st.session_state.project_data["constructions"]["library"] = default_constructions.copy()
91
-
92
- # Initialize construction editor state
93
- if "construction_editor" not in st.session_state:
94
- st.session_state.construction_editor = {}
95
-
96
- # Initialize construction action state
97
- if "construction_action" not in st.session_state:
98
- st.session_state.construction_action = {"action": None, "id": None}
99
 
100
- def display_constructions_table():
101
- """Display the constructions in a table format."""
102
- # Filter options
103
- col1, col2 = st.columns(2)
104
-
105
- with col1:
106
- source_filter = st.selectbox(
107
- "Source",
108
- ["All", "Library", "Project"],
109
- key="construction_source_filter"
110
- )
111
-
112
- with col2:
113
- type_filter = st.selectbox(
114
- "Type",
115
- ["All"] + CONSTRUCTION_TYPES,
116
- key="construction_type_filter"
117
- )
118
-
119
- # Get constructions based on filters
120
- constructions = {}
121
-
122
- if source_filter in ["All", "Library"]:
123
- for name, props in st.session_state.project_data["constructions"]["library"].items():
124
- if type_filter == "All" or props["type"] == type_filter:
125
- constructions[name] = {**props, "source": "Library"}
126
-
127
- if source_filter in ["All", "Project"]:
128
- for name, props in st.session_state.project_data["constructions"]["project"].items():
129
- if type_filter == "All" or props["type"] == type_filter:
130
- constructions[name] = {**props, "source": "Project"}
131
-
132
- # Display constructions in a table
133
- if constructions:
134
- # Create column headers
135
- cols = st.columns([3, 1, 1, 1, 1, 1, 1])
136
  cols[0].write("**Name**")
137
- cols[1].write("**Type**")
138
- cols[2].write("**U-Value**")
139
- cols[3].write("**Layers**")
140
- cols[4].write("**Source**")
141
- cols[5].write("**Edit**")
142
- cols[6].write("**Delete**")
143
-
144
- # Display each construction
145
- for idx, (name, props) in enumerate(constructions.items()):
146
- cols = st.columns([3, 1, 1, 1, 1, 1, 1])
147
- cols[0].write(name)
148
- cols[1].write(props["type"])
149
- cols[2].write(f"{props.get('u_value', 0.0):.3f}")
150
- cols[3].write(str(len(props.get("layers", []))))
151
- cols[4].write(props["source"])
152
-
153
- # Edit button (only for project constructions)
154
- with cols[5].container():
155
- if props["source"] == "Project":
156
- edit_key = f"edit_construction_{name}_{idx}"
157
- if st.button("Edit", key=edit_key):
158
- # Load construction data into editor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  st.session_state.construction_editor = {
160
- "name": name,
161
- "type": props["type"],
162
- "layers": props.get("layers", []).copy(),
163
- "is_edit": True
 
 
 
 
 
 
 
164
  }
165
- st.session_state.construction_action = {"action": "edit", "id": str(uuid.uuid4())}
166
  st.session_state.construction_rerun_pending = True
167
- else:
168
- st.write("N/A")
169
-
170
- # Delete button (only for project constructions)
171
- with cols[6].container():
172
- if props["source"] == "Project":
173
- delete_key = f"delete_construction_{name}_{idx}"
174
- if st.button("Delete", key=delete_key):
175
- # Check if construction is in use
176
- is_in_use = check_construction_in_use(name)
177
-
178
- if is_in_use:
179
- st.error(f"Cannot delete construction '{name}' because it is in use in building components.")
180
- else:
181
- # Delete construction
182
- del st.session_state.project_data["constructions"]["project"][name]
183
- st.success(f"Construction '{name}' deleted from your project.")
184
- logger.info(f"Deleted construction '{name}' from project")
185
- st.session_state.construction_action = {"action": "delete", "id": str(uuid.uuid4())}
186
- st.session_state.construction_rerun_pending = True
187
- else:
188
- st.write("N/A")
189
-
190
- # Display details of selected construction
191
- st.subheader("Construction Details")
192
- selected_construction = st.selectbox(
193
- "Select Construction to View Details",
194
- list(constructions.keys()),
195
- key="construction_details_selector"
196
- )
197
-
198
- if selected_construction:
199
- display_construction_details(constructions[selected_construction])
200
-
201
- # Add to project button for library constructions
202
- if constructions[selected_construction]["source"] == "Library":
203
- if st.button("Add to Project", key="add_to_project"):
204
- # Check if construction already exists in project
205
- if selected_construction in st.session_state.project_data["constructions"]["project"]:
206
- st.warning(f"Construction '{selected_construction}' already exists in your project.")
207
- else:
208
- # Add to project constructions
209
- construction_data = st.session_state.project_data["constructions"]["library"][selected_construction].copy()
210
- st.session_state.project_data["constructions"]["project"][selected_construction] = construction_data
211
- st.success(f"Construction '{selected_construction}' added to your project.")
212
- logger.info(f"Added library construction '{selected_construction}' to project")
213
- st.session_state.construction_action = {"action": "add", "id": str(uuid.uuid4())}
214
  st.session_state.construction_rerun_pending = True
215
- else:
216
- st.info("No constructions found with the selected filters.")
217
-
218
- def display_construction_details(construction: Dict[str, Any]):
219
- """
220
- Display detailed information about a construction.
221
-
222
- Args:
223
- construction: Construction data dictionary
224
- """
225
- # Display basic properties
226
- col1, col2, col3 = st.columns(3)
227
-
228
- with col1:
229
- st.write(f"**Type:** {construction['type']}")
230
- st.write(f"**U-Value:** {construction.get('u_value', 0.0):.3f} W/m虏路K")
231
-
232
- with col2:
233
- st.write(f"**R-Value:** {construction.get('r_value', 0.0):.3f} m虏路K/W")
234
- st.write(f"**Thermal Mass:** {construction.get('thermal_mass', 0.0):.1f} kJ/m虏路K")
235
-
236
- with col3:
237
- st.write(f"**Embodied Carbon:** {construction.get('embodied_carbon', 0.0):.1f} kg CO鈧俥/m虏")
238
- st.write(f"**Cost:** ${construction.get('cost', 0.0):.2f}/m虏")
239
-
240
- # Display layers
241
- st.write("**Layers (from outside to inside):**")
242
-
243
- # Create a DataFrame for layers
244
- layers_data = []
245
- for i, layer in enumerate(construction.get("layers", [])):
246
- layers_data.append({
247
- "Layer": i + 1,
248
- "Material": layer.get("material", ""),
249
- "Thickness (m)": layer.get("thickness", 0.0)
250
- })
251
-
252
- if layers_data:
253
- layers_df = pd.DataFrame(layers_data)
254
- st.dataframe(layers_df, use_container_width=True, hide_index=True)
255
- else:
256
- st.info("No layers defined for this construction.")
257
 
258
- def display_construction_editor():
259
  """Display the construction editor form."""
260
- # Get available materials
261
- available_materials = get_available_materials()
262
-
263
- # Get editor state
264
- editor_state = st.session_state.construction_editor
265
- is_edit = editor_state.get("is_edit", False)
266
-
267
- # Create the editor form
268
- with st.form("construction_editor_form", clear_on_submit=True):
269
- # Construction name
270
- name = st.text_input(
271
- "Construction Name",
272
- value=editor_state.get("name", ""),
273
- help="Enter a unique name for this construction."
274
- )
275
-
276
- # Construction type
277
- construction_type = st.selectbox(
278
- "Construction Type",
279
- CONSTRUCTION_TYPES,
280
- index=CONSTRUCTION_TYPES.index(editor_state.get("type", CONSTRUCTION_TYPES[0])) if editor_state.get("type") in CONSTRUCTION_TYPES else 0,
281
- help="Select the type of construction (wall, roof, or floor)."
282
- )
283
-
284
- # Layers
285
- st.subheader("Layers (from outside to inside)")
286
-
287
- # Display existing layers
288
- layers = editor_state.get("layers", [])
289
-
290
- if layers:
291
- for i, layer in enumerate(layers):
292
- col1, col2, col3 = st.columns([3, 2, 1])
293
-
294
- with col1:
295
- st.write(f"**Layer {i+1}:** {layer.get('material', '')}")
296
-
297
- with col2:
298
- st.write(f"**Thickness:** {layer.get('thickness', 0.0):.3f} m")
299
-
300
- with col3:
301
- if st.form_submit_button(f"Remove Layer {i+1}"):
302
- layers.pop(i)
303
- st.session_state.construction_editor["layers"] = layers
304
- st.session_state.construction_action = {"action": "update_layers", "id": str(uuid.uuid4())}
305
- st.session_state.construction_rerun_pending = True
306
- else:
307
- st.info("No layers defined. Add layers below.")
308
-
309
- # Add new layer
310
- st.subheader("Add New Layer")
311
-
312
- col1, col2 = st.columns(2)
313
-
314
- with col1:
315
- new_material = st.selectbox(
316
- "Material",
317
- list(available_materials.keys()),
318
- help="Select a material for the new layer."
319
  )
320
-
321
- with col2:
322
- # Get thickness range for selected material
323
- thickness_range = available_materials[new_material].get("thickness_range", {"min": 0.01, "max": 0.5, "default": 0.1})
324
- min_thickness = thickness_range.get("min", 0.01)
325
- max_thickness = thickness_range.get("max", 0.5)
326
- default_thickness = thickness_range.get("default", 0.1)
327
-
328
- new_thickness = st.number_input(
329
- "Thickness (m)",
330
- min_value=min_thickness,
331
- max_value=max_thickness,
332
- value=default_thickness,
333
- format="%.3f",
334
- help=f"Enter the thickness of the layer (between {min_thickness} and {max_thickness} m)."
335
  )
336
-
337
- # Add layer button
338
- add_layer = st.form_submit_button("Add Layer")
339
-
340
- # Calculate button
341
- calculate = st.form_submit_button("Calculate Properties")
342
-
343
- # Submit buttons
344
- col1, col2 = st.columns(2)
345
-
346
- with col1:
347
- submit_label = "Update Construction" if is_edit else "Create Construction"
348
- submit = st.form_submit_button(submit_label)
349
-
350
- with col2:
351
- cancel = st.form_submit_button("Cancel")
352
-
353
- # Handle form actions
354
- if add_layer:
355
- # Add new layer to the list
356
- new_layer = {
357
- "material": new_material,
358
- "thickness": new_thickness
359
- }
360
-
361
- layers = editor_state.get("layers", [])
362
- layers.append(new_layer)
363
-
364
- # Update editor state
365
- st.session_state.construction_editor = {
366
- **editor_state,
367
- "layers": layers
368
- }
369
-
370
- st.success(f"Layer '{new_material}' added to construction.")
371
- st.session_state.construction_action = {"action": "add_layer", "id": str(uuid.uuid4())}
372
- st.session_state.construction_rerun_pending = True
373
-
374
- elif calculate:
375
- # Calculate construction properties
376
- if not layers:
377
- st.error("Cannot calculate properties for a construction with no layers.")
378
- else:
379
- properties = calculate_construction_properties(layers, available_materials)
380
-
381
- # Update editor state with calculated properties
382
- st.session_state.construction_editor = {
383
- **editor_state,
384
- "u_value": properties["u_value"],
385
- "r_value": properties["r_value"],
386
- "thermal_mass": properties["thermal_mass"],
387
- "embodied_carbon": properties["embodied_carbon"],
388
- "cost": properties["cost"]
389
- }
390
-
391
- # Display calculated properties
392
- st.success("Properties calculated successfully.")
393
- st.write(f"**U-Value:** {properties['u_value']:.3f} W/m虏路K")
394
- st.write(f"**R-Value:** {properties['r_value']:.3f} m虏路K/W")
395
- st.write(f"**Thermal Mass:** {properties['thermal_mass']:.1f} kJ/m虏路K")
396
- st.write(f"**Embodied Carbon:** {properties['embodied_carbon']:.1f} kg CO鈧俥/m虏")
397
- st.write(f"**Cost:** ${properties['cost']:.2f}/m虏")
398
-
399
- st.session_state.construction_action = {"action": "calculate", "id": str(uuid.uuid4())}
400
- st.session_state.construction_rerun_pending = True
401
-
402
- elif submit:
403
- # Validate inputs
404
- if not name:
405
- st.error("Construction name is required.")
406
- elif not layers:
407
- st.error("At least one layer is required.")
408
- else:
409
- # Calculate properties if not already calculated
410
- if "u_value" not in editor_state:
411
- properties = calculate_construction_properties(layers, available_materials)
412
- else:
413
- properties = {
414
- "u_value": editor_state.get("u_value", 0.0),
415
- "r_value": editor_state.get("r_value", 0.0),
416
- "thermal_mass": editor_state.get("thermal_mass", 0.0),
417
- "embodied_carbon": editor_state.get("embodied_carbon", 0.0),
418
- "cost": editor_state.get("cost", 0.0)
419
- }
420
-
421
- # Create construction data
422
- construction_data = {
423
- "type": construction_type,
424
- "layers": layers,
425
- "u_value": properties["u_value"],
426
- "r_value": properties["r_value"],
427
- "thermal_mass": properties["thermal_mass"],
428
- "embodied_carbon": properties["embodied_carbon"],
429
- "cost": properties["cost"]
430
- }
431
-
432
- # Handle edit mode
433
- if is_edit:
434
- # Check if name has changed
435
- original_name = editor_state.get("name", "")
436
-
437
- if name != original_name and name in st.session_state.project_data["constructions"]["project"]:
438
- st.error(f"Construction name '{name}' already exists in your project. Please choose a different name.")
439
- else:
440
- # If name changed, delete old entry and add new one
441
- if name != original_name:
442
- del st.session_state.project_data["constructions"]["project"][original_name]
443
-
444
- # Update construction
445
- st.session_state.project_data["constructions"]["project"][name] = construction_data
446
- st.success(f"Construction '{name}' updated successfully.")
447
- logger.info(f"Updated construction '{name}' in project")
448
-
449
- # Clear editor state
450
- st.session_state.construction_editor = {}
451
- st.session_state.construction_action = {"action": "update", "id": str(uuid.uuid4())}
452
- st.session_state.construction_rerun_pending = True
453
- else:
454
- # Check if name already exists
455
- if name in st.session_state.project_data["constructions"]["project"]:
456
- st.error(f"Construction name '{name}' already exists in your project. Please choose a different name.")
457
- else:
458
- # Add new construction
459
- st.session_state.project_data["constructions"]["project"][name] = construction_data
460
- st.success(f"Construction '{name}' created successfully.")
461
- logger.info(f"Created new construction '{name}' in project")
462
-
463
- # Clear editor state
464
- st.session_state.construction_editor = {}
465
- st.session_state.construction_action = {"action": "create", "id": str(uuid.uuid4())}
466
- st.session_state.construction_rerun_pending = True
467
-
468
- elif cancel:
469
- # Clear editor state
470
- st.session_state.construction_editor = {}
471
- st.session_state.construction_action = {"action": "cancel", "id": str(uuid.uuid4())}
472
- st.session_state.construction_rerun_pending = True
473
 
474
- def get_available_materials() -> Dict[str, Any]:
475
- """
476
- Get all available materials from both library and project.
477
-
478
- Returns:
479
- Dict of material name to material properties
480
- """
481
- available_materials = {}
482
-
483
- # Add materials from session state
484
- if "materials" in st.session_state.project_data:
485
- # Add library materials
486
- if "library" in st.session_state.project_data["materials"]:
487
- for name, material in st.session_state.project_data["materials"]["library"].items():
488
- if material.get("type") == "opaque":
489
- available_materials[name] = material
490
-
491
- # Add project materials
492
- if "project" in st.session_state.project_data["materials"]:
493
- for name, material in st.session_state.project_data["materials"]["project"].items():
494
- if material.get("type") == "opaque":
495
- available_materials[name] = material
496
-
497
- return available_materials
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
  def calculate_construction_properties(layers: List[Dict[str, Any]], materials: Dict[str, Any]) -> Dict[str, float]:
500
  """
501
  Calculate the thermal properties of a construction based on its layers.
502
-
503
- Args:
504
- layers: List of layer dictionaries with material and thickness
505
- materials: Dictionary of available materials
506
-
507
- Returns:
508
- Dictionary of calculated properties
509
  """
510
- # Initialize properties
511
- r_value = 0.0 # m虏路K/W
512
  thermal_mass = 0.0 # kJ/m虏路K
513
  embodied_carbon = 0.0 # kg CO鈧俥/m虏
514
  cost = 0.0 # $/m虏
515
-
516
- # Add surface resistances (inside and outside)
517
- r_value += 0.17 # m虏路K/W (typical combined surface resistance)
518
-
519
- # Calculate properties for each layer
520
  for layer in layers:
521
  material_name = layer.get("material", "")
522
  thickness = layer.get("thickness", 0.0)
523
-
524
  if material_name in materials:
525
  material = materials[material_name]
526
-
527
  # Thermal resistance
528
- if material.get("thermal_conductivity", 0.0) > 0:
529
- r_value += thickness / material.get("thermal_conductivity", 1.0)
530
-
531
  # Thermal mass
532
- thermal_mass += material.get("density", 0.0) * material.get("specific_heat", 0.0) * thickness / 1000.0
533
-
534
  # Embodied carbon
535
- embodied_carbon += material.get("embodied_carbon", 0.0) * material.get("density", 0.0) * thickness
536
-
537
  # Cost
538
- material_cost = material.get("cost", {})
539
- if isinstance(material_cost, dict):
540
- cost += material_cost.get("material", 0.0) * thickness + material_cost.get("labor", 0.0)
541
- else:
542
- cost += material_cost * thickness
543
-
544
- # Calculate U-value
545
  u_value = 1.0 / r_value if r_value > 0 else 0.0
546
-
547
  return {
548
  "u_value": u_value,
549
  "r_value": r_value,
@@ -552,27 +551,15 @@ def calculate_construction_properties(layers: List[Dict[str, Any]], materials: D
552
  "cost": cost
553
  }
554
 
555
- def check_construction_in_use(construction_name: str) -> bool:
556
  """
557
  Check if a construction is in use in any building components.
558
-
559
- Args:
560
- construction_name: Name of the construction to check
561
-
562
- Returns:
563
- True if the construction is in use, False otherwise
564
  """
565
- # Check if components exist in session state
566
- if "components" not in st.session_state.project_data:
567
- return False
568
-
569
- # Check walls, roofs, and floors
570
  for comp_type in ["walls", "roofs", "floors"]:
571
- if comp_type in st.session_state.project_data["components"]:
572
- for component in st.session_state.project_data["components"][comp_type]:
573
  if component.get("construction") == construction_name:
574
  return True
575
-
576
  return False
577
 
578
  def display_construction_help():
@@ -606,3 +593,13 @@ def display_construction_help():
606
  * Use the Calculate Properties button to evaluate your construction before saving.
607
  * Library constructions cannot be modified, but you can add them to your project and then edit them.
608
  """)
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ BuildSustain - Construction Module (Updated)
3
 
4
  This module handles the construction assembly functionality of the BuildSustain application,
5
  allowing users to create and manage multi-layer constructions for walls, roofs, and floors.
6
  It integrates with the material library to select materials for each layer and calculates
7
+ overall thermal properties. The structure is adapted from old.txt's Constructions tab.
8
 
9
  Developed by: Dr Majed Abuseif, Deakin University
10
  漏 2025
 
18
  import uuid
19
  from typing import Dict, List, Any, Optional, Tuple, Union
20
 
21
+ # Import the centralized data module and materials library
22
  from app.m_c_data import get_default_constructions
23
+ from app.materials_library import get_available_materials, Material
24
 
25
  # Configure logging
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
29
  # Define constants
30
  CONSTRUCTION_TYPES = ["Wall", "Roof", "Floor"]
31
 
32
+ # Surface resistances (EN ISO 6946 or ASHRAE)
33
+ R_SI = 0.12 # Internal surface resistance (m虏路K/W)
34
+ R_SE = 0.04 # External surface resistance (m虏路K/W)
35
+
36
+ class ConstructionLibrary:
37
+ def __init__(self):
38
+ self.library_constructions = {}
39
+
40
+ def to_dataframe(self, project_constructions: Dict, only_project: bool = True) -> pd.DataFrame:
41
+ """Convert project constructions to a DataFrame."""
42
+ data = [
43
+ {
44
+ "Name": name,
45
+ "Component Type": props["type"],
46
+ "U-Value (W/m虏路K)": props.get("u_value", 0.0),
47
+ "R-Value (m虏路K/W)": props.get("r_value", 0.0),
48
+ "Thermal Mass (kJ/m虏路K)": props.get("thermal_mass", 0.0),
49
+ "Embodied Carbon (kgCO鈧俥/m虏)": props.get("embodied_carbon", 0.0),
50
+ "Cost (USD/m虏)": props.get("cost", 0.0),
51
+ "Layers": len(props.get("layers", []))
52
+ }
53
+ for name, props in project_constructions.items()
54
+ ]
55
+ return pd.DataFrame(data)
56
+
57
+ def add_project_construction(self, construction: Dict, project_constructions: Dict) -> Tuple[bool, str]:
58
+ """Add a new construction to the project."""
59
+ try:
60
+ if construction["name"] in project_constructions or construction["name"] in self.library_constructions:
61
+ return False, f"Construction '{construction['name']}' already exists."
62
+ project_constructions[construction["name"]] = construction
63
+ return True, f"Construction '{construction['name']}' added successfully!"
64
+ except Exception as e:
65
+ return False, f"Error adding construction: {str(e)}"
66
+
67
+ def edit_project_construction(self, original_name: str, new_construction: Dict, project_constructions: Dict, components: Dict) -> Tuple[bool, str]:
68
+ """Edit an existing project construction."""
69
+ try:
70
+ if original_name not in project_constructions:
71
+ return False, f"Construction '{original_name}' not found."
72
+ if new_construction["name"] != original_name and (new_construction["name"] in project_constructions or new_construction["name"] in self.library_constructions):
73
+ return False, f"Construction '{new_construction['name']}' already exists."
74
+ project_constructions[new_construction["name"]] = new_construction
75
+ if new_construction["name"] != original_name:
76
+ del project_constructions[original_name]
77
+ return True, f"Construction '{new_construction['name']}' updated successfully!"
78
+ except Exception as e:
79
+ return False, f"Error editing construction: {str(e)}"
80
+
81
+ def delete_project_construction(self, name: str, project_constructions: Dict, components: Dict) -> Tuple[bool, str]:
82
+ """Delete a project construction."""
83
+ try:
84
+ if name not in project_constructions:
85
+ return False, f"Construction '{name}' not found."
86
+ if check_construction_in_use(name, components):
87
+ return False, f"Construction '{name}' is used in components and cannot be deleted."
88
+ del project_constructions[name]
89
+ return True, f"Construction '{name}' deleted successfully!"
90
+ except Exception as e:
91
+ return False, f"Error deleting construction: {str(e)}"
92
+
93
  def display_construction_page():
94
  """
95
+ Display the construction page, adapted from old.txt's Constructions tab.
 
96
  """
97
  st.title("Construction Library")
98
  st.write("Define multi-layer constructions for walls, roofs, and floors based on ASHRAE 2005 Handbook of Fundamentals.")
99
+
100
+ # Display help information
101
  with st.expander("Help & Information"):
102
  display_construction_help()
103
+
104
+ # CSS for box heights, scrolling, and visual appeal
105
+ st.markdown("""
106
+ <style>
107
+ .box-container {
108
+ border: 1px solid #e0e0e0;
109
+ border-radius: 8px;
110
+ padding: 10px;
111
+ background-color: #f9f9f9;
112
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
113
+ overflow-y: auto;
114
+ scrollbar-width: thin;
115
+ }
116
+ .library-box {
117
+ max-height: 250px;
118
+ }
119
+ .project-box {
120
+ max-height: 250px;
121
+ }
122
+ .editor-box {
123
+ min-height: 540px;
124
+ }
125
+ .stButton>button {
126
+ width: 100%;
127
+ font-size: 12px;
128
+ padding: 5px;
129
+ }
130
+ </style>
131
+ """, unsafe_allow_html=True)
132
+
133
+ # Initialize constructions in session state
134
+ initialize_construction()
135
+
136
+ # Check for rerun trigger
137
+ if st.session_state.get("construction_rerun_pending", False):
138
  st.session_state.construction_rerun_pending = False
139
  st.rerun()
140
+
141
+ # Initialize session state
142
+ if 'construction_action' not in st.session_state:
143
+ st.session_state.construction_action = {"action": None, "id": None}
144
+ if 'construction_rerun_pending' not in st.session_state:
145
+ st.session_state.construction_rerun_pending = False
146
+ if 'construction_form_state' not in st.session_state:
147
+ st.session_state.construction_form_state = {}
148
+ if 'construction_editor' not in st.session_state:
149
+ st.session_state.construction_editor = {}
150
+
151
+ # Initialize construction library
152
+ construction_library = ConstructionLibrary()
153
+ construction_library.library_constructions = st.session_state.project_data["constructions"]["library"]
154
+
155
+ # Display constructions content
156
  col1, col2 = st.columns([3, 2])
 
157
  with col1:
158
+ display_constructions_tables(construction_library)
 
 
159
  with col2:
160
+ display_construction_editor(construction_library)
161
+
162
+ # Display project constructions DataFrame
163
+ st.subheader("Project Constructions")
164
+ try:
165
+ construction_filter = st.session_state.get("construction_filter", "All")
166
+ construction_df = construction_library.to_dataframe(st.session_state.project_data["constructions"]["project"])
167
+ if construction_filter != "All":
168
+ construction_df = construction_df[construction_df["Component Type"] == construction_filter]
169
+ if not construction_df.empty:
170
+ st.dataframe(construction_df, use_container_width=True)
171
+ else:
172
+ st.write("No project constructions to display.")
173
+ except Exception as e:
174
+ st.error(f"Error displaying project constructions: {str(e)}")
175
+ st.write("No project constructions to display.")
176
+
177
  # Navigation buttons
178
  col1, col2 = st.columns(2)
 
179
  with col1:
180
  if st.button("Back to Material Library", key="back_to_materials"):
181
  st.session_state.current_page = "Material Library"
182
  st.rerun()
 
183
  with col2:
184
  if st.button("Continue to Building Components", key="continue_to_components"):
185
  st.session_state.current_page = "Building Components"
186
  st.rerun()
187
 
188
+ def initialize_construction():
189
  """Initialize constructions in session state if not present."""
190
+ if "project_data" not in st.session_state:
191
+ st.session_state.project_data = {}
192
  if "constructions" not in st.session_state.project_data:
193
  st.session_state.project_data["constructions"] = {
194
+ "library": get_default_constructions(),
195
  "project": {}
196
  }
197
+ if "components" not in st.session_state.project_data:
198
+ st.session_state.project_data["components"] = {}
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
+ def display_constructions_tables(construction_library: ConstructionLibrary):
201
+ """Display library and project constructions tables."""
202
+ # Component type filter
203
+ filter_options = ["All"] + CONSTRUCTION_TYPES
204
+ construction_filter = st.selectbox("Filter by Component Type", filter_options, key="construction_filter")
205
+
206
+ st.subheader("Library Constructions")
207
+ with st.container():
208
+ library_constructions = list(construction_library.library_constructions.values())
209
+ if construction_filter != "All":
210
+ library_constructions = [c for c in library_constructions if c["type"] == construction_filter]
211
+ cols = st.columns([2, 1, 1, 1, 1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  cols[0].write("**Name**")
213
+ cols[1].write("**Thermal Mass**")
214
+ cols[2].write("**U-Value (W/m虏路K)**")
215
+ cols[3].write("**Preview**")
216
+ cols[4].write("**Copy**")
217
+ for construction in library_constructions:
218
+ cols = st.columns([2, 1, 1, 1, 1])
219
+ cols[0].write(construction["name"])
220
+ cols[1].write(f"{construction.get('thermal_mass', 0.0):.1f}")
221
+ cols[2].write(f"{construction.get('u_value', 0.0):.3f}")
222
+ if cols[3].button("Preview", key=f"preview_lib_cons_{construction['name']}"):
223
+ if st.session_state.get("rerun_trigger") != f"preview_cons_{construction['name']}":
224
+ st.session_state.rerun_trigger = f"preview_cons_{construction['name']}"
225
+ st.session_state.construction_editor = {
226
+ "name": construction["name"],
227
+ "component_type": construction["type"],
228
+ "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]],
229
+ "is_edit": False
230
+ }
231
+ st.session_state.construction_form_state = {
232
+ "name": construction["name"],
233
+ "component_type": construction["type"],
234
+ "num_layers": len(construction["layers"]),
235
+ "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]]
236
+ }
237
+ st.session_state.construction_rerun_pending = True
238
+ if cols[4].button("Copy", key=f"copy_lib_cons_{construction['name']}"):
239
+ new_name = f"{construction['name']}_Project"
240
+ counter = 1
241
+ while new_name in st.session_state.project_data["constructions"]["project"] or new_name in construction_library.library_constructions:
242
+ new_name = f"{construction['name']}_Project_{counter}"
243
+ counter += 1
244
+ new_layers = []
245
+ material_library = st.session_state.get("material_library", None)
246
+ if not material_library:
247
+ from app.materials_library import MaterialLibrary
248
+ material_library = MaterialLibrary()
249
+ st.session_state.material_library = material_library
250
+ for layer in construction["layers"]:
251
+ material_name = layer["material"]
252
+ if material_name in st.session_state.project_data["materials"]["library"]:
253
+ # Copy library material to project
254
+ library_material = st.session_state.project_data["materials"]["library"][material_name]
255
+ new_mat_name = f"{material_name}_Project"
256
+ counter = 1
257
+ while new_mat_name in st.session_state.project_data["materials"]["project"] or new_mat_name in st.session_state.project_data["materials"]["library"]:
258
+ new_mat_name = f"{material_name}_Project_{counter}"
259
+ counter += 1
260
+ new_material = Material(
261
+ name=new_mat_name,
262
+ category=MaterialCategory(library_material["category"]),
263
+ conductivity=library_material["thermal_conductivity"],
264
+ density=library_material["density"],
265
+ specific_heat=library_material["specific_heat"],
266
+ default_thickness=library_material["thickness_range"]["default"],
267
+ embodied_carbon=library_material["embodied_carbon"],
268
+ solar_absorption=library_material.get("solar_absorption", 0.6),
269
+ price=library_material["cost"]["material"],
270
+ emissivity=library_material.get("emissivity", 0.9),
271
+ is_library=False
272
+ )
273
+ success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
274
+ if not success:
275
+ st.error(f"Failed to copy material '{material_name}': {message}")
276
+ break
277
+ material_name = new_mat_name
278
+ new_layers.append({"material": material_name, "thickness": layer["thickness"]})
279
+ else:
280
+ properties = calculate_construction_properties(new_layers, get_available_materials())
281
+ new_construction = {
282
+ "name": new_name,
283
+ "type": construction["type"],
284
+ "layers": new_layers,
285
+ "u_value": properties["u_value"],
286
+ "r_value": properties["r_value"],
287
+ "thermal_mass": properties["thermal_mass"],
288
+ "embodied_carbon": properties["embodied_carbon"],
289
+ "cost": properties["cost"],
290
+ "is_library": False
291
+ }
292
+ success, message = construction_library.add_project_construction(new_construction, st.session_state.project_data["constructions"]["project"])
293
+ if success:
294
+ st.success(message)
295
+ st.session_state.construction_rerun_pending = True
296
+ else:
297
+ st.error(message)
298
+
299
+ st.subheader("Project Constructions")
300
+ with st.container():
301
+ project_constructions = list(st.session_state.project_data["constructions"]["project"].values())
302
+ if construction_filter != "All":
303
+ project_constructions = [c for c in project_constructions if c["type"] == construction_filter]
304
+ if project_constructions:
305
+ cols = st.columns([2, 1, 1, 1, 1])
306
+ cols[0].write("**Name**")
307
+ cols[1].write("**Thermal Mass**")
308
+ cols[2].write("**U-Value (W/m虏路K)**")
309
+ cols[3].write("**Edit**")
310
+ cols[4].write("**Delete**")
311
+ for construction in project_constructions:
312
+ cols = st.columns([2, 1, 1, 1, 1])
313
+ cols[0].write(construction["name"])
314
+ cols[1].write(f"{construction.get('thermal_mass', 0.0):.1f}")
315
+ cols[2].write(f"{construction.get('u_value', 0.0):.3f}")
316
+ if cols[3].button("Edit", key=f"edit_proj_cons_{construction['name']}"):
317
+ if st.session_state.get("rerun_trigger") != f"edit_cons_{construction['name']}":
318
+ st.session_state.rerun_trigger = f"edit_cons_{construction['name']}"
319
  st.session_state.construction_editor = {
320
+ "name": construction["name"],
321
+ "component_type": construction["type"],
322
+ "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]],
323
+ "is_edit": True,
324
+ "original_name": construction["name"]
325
+ }
326
+ st.session_state.construction_form_state = {
327
+ "name": construction["name"],
328
+ "component_type": construction["type"],
329
+ "num_layers": len(construction["layers"]),
330
+ "layers": [{"material_name": layer["material"], "thickness": layer["thickness"]} for layer in construction["layers"]]
331
  }
 
332
  st.session_state.construction_rerun_pending = True
333
+ if cols[4].button("Delete", key=f"delete_proj_cons_{construction['name']}"):
334
+ success, message = construction_library.delete_project_construction(
335
+ construction["name"], st.session_state.project_data["constructions"]["project"], st.session_state.project_data.get("components", {})
336
+ )
337
+ if success:
338
+ st.success(message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  st.session_state.construction_rerun_pending = True
340
+ else:
341
+ st.error(message)
342
+ else:
343
+ st.write("No project constructions added.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ def display_construction_editor(construction_library: ConstructionLibrary):
346
  """Display the construction editor form."""
347
+ is_preview = st.session_state.get("rerun_trigger", "").startswith("preview_cons_")
348
+ materials = get_available_materials()
349
+ material_objects = {name: m for name, m in materials.items() if isinstance(m, Material)}
350
+ if is_preview:
351
+ material_objects.update(st.session_state.project_data["materials"]["library"])
352
+ material_names = list(material_objects.keys())
353
+
354
+ if not material_names and not is_preview:
355
+ st.error("No project materials available. Please create materials in the Materials tab first.")
356
+ if st.button("Go to Materials", key="go_to_materials"):
357
+ st.session_state.current_page = "Material Library"
358
+ st.rerun()
359
+ return
360
+
361
+ with st.container():
362
+ with st.form("construction_editor_form", clear_on_submit=False):
363
+ editor_state = st.session_state.get("construction_editor", {})
364
+ form_state = st.session_state.get("construction_form_state", {
365
+ "name": "",
366
+ "component_type": "Wall",
367
+ "num_layers": 1,
368
+ "layers": [{"material_name": material_names[0] if material_names else "", "thickness": 0.1}]
369
+ })
370
+ is_edit = editor_state.get("is_edit", False)
371
+ original_name = editor_state.get("original_name", "")
372
+ name = st.text_input(
373
+ "Construction Name",
374
+ value=form_state.get("name", editor_state.get("name", "")),
375
+ help="Unique construction identifier",
376
+ key="construction_name_input"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  )
378
+ component_type = st.selectbox(
379
+ "Component Type",
380
+ CONSTRUCTION_TYPES,
381
+ index=CONSTRUCTION_TYPES.index(form_state.get("component_type", editor_state.get("component_type", "Wall"))),
382
+ help="Building component type",
383
+ key="construction_component_type_input"
 
 
 
 
 
 
 
 
 
384
  )
385
+ num_layers = st.number_input(
386
+ "Number of Layers",
387
+ min_value=1,
388
+ max_value=10,
389
+ value=form_state.get("num_layers", 1),
390
+ help="Material layer count",
391
+ key="construction_num_layers_input"
392
+ )
393
+ default_material = material_names[0] if material_names else ""
394
+ layers = form_state.get("layers", [{"material_name": default_material, "thickness": 0.1}])
395
+ if len(layers) < num_layers:
396
+ layers.extend([{"material_name": default_material, "thickness": 0.1} for _ in range(num_layers - len(layers))])
397
+ elif len(layers) > num_layers:
398
+ layers = layers[:num_layers]
399
+ form_state["layers"] = layers
400
+ form_state["num_layers"] = num_layers
401
+ for i in range(num_layers):
402
+ st.write(f"Layer {i + 1} (Exterior to Interior)")
403
+ layer = layers[i]
404
+ material_name = st.selectbox(
405
+ f"Material {i+1}",
406
+ material_names,
407
+ index=material_names.index(layer.get("material_name", default_material)) if layer.get("material_name", default_material) in material_names else 0,
408
+ key=f"cons_mat_{i}_{num_layers}",
409
+ help="Layer material"
410
+ )
411
+ thickness = st.number_input(
412
+ f"Thickness {i+1} (m)",
413
+ min_value=0.001,
414
+ value=layer.get("thickness", 0.1),
415
+ key=f"cons_thick_{i}_{num_layers}",
416
+ help="Layer thickness"
417
+ )
418
+ layers[i] = {"material_name": material_name, "thickness": thickness}
419
+ form_state["layers"] = layers
420
+ st.session_state.construction_form_state = form_state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
+ col1, col2, col3 = st.columns(3)
423
+ with col1:
424
+ if st.form_submit_button("Update Layers"):
425
+ action_id = str(uuid.uuid4())
426
+ if st.session_state.construction_action.get("id") != action_id:
427
+ st.session_state.construction_action = {"action": "update_layers", "id": action_id}
428
+ new_layers = []
429
+ for i in range(num_layers):
430
+ if i < len(layers) and layers[i].get("material_name") in material_names:
431
+ new_layers.append(layers[i])
432
+ else:
433
+ new_layers.append({"material_name": default_material, "thickness": 0.1})
434
+ form_state["layers"] = new_layers
435
+ form_state["num_layers"] = num_layers
436
+ st.session_state.construction_form_state = form_state
437
+ st.session_state.construction_action = {"action": None, "id": None}
438
+ st.session_state.construction_rerun_pending = True
439
+ with col2:
440
+ if st.form_submit_button("Preview U-Value"):
441
+ action_id = str(uuid.uuid4())
442
+ if st.session_state.construction_action.get("id") != action_id:
443
+ st.session_state.construction_action = {"action": "preview_uvalue", "id": action_id}
444
+ if layers:
445
+ valid_layers = []
446
+ for layer in layers:
447
+ material_name = layer.get("material_name")
448
+ thickness = layer.get("thickness", 0.1)
449
+ if material_name in material_objects:
450
+ material = material_objects[material_name]
451
+ valid_layers.append({"material": material, "thickness": thickness})
452
+ if valid_layers:
453
+ r_total = R_SI + sum(layer["thickness"] / layer["material"].conductivity for layer in valid_layers) + R_SE
454
+ u_value = 1.0 / r_total if r_total > 0 else 0.1
455
+ st.write(f"Calculated U-Value: {u_value:.3f} W/m虏路K")
456
+ else:
457
+ st.warning("No valid materials selected for U-Value calculation.")
458
+ else:
459
+ st.warning("No layers defined to calculate U-Value.")
460
+ st.session_state.construction_action = {"action": None, "id": None}
461
+ with col3:
462
+ if st.form_submit_button("Save Construction"):
463
+ action_id = str(uuid.uuid4())
464
+ if st.session_state.construction_action.get("id") != action_id:
465
+ st.session_state.construction_action = {"action": "save", "id": action_id}
466
+ if not name or not name.strip():
467
+ st.error("Construction name cannot be empty.")
468
+ elif (name in st.session_state.project_data["constructions"]["project"] or name in construction_library.library_constructions) and (not is_edit or name != original_name):
469
+ st.error(f"Construction '{name}' already exists.")
470
+ elif not layers or any(not layer.get("material_name") for layer in layers):
471
+ st.error("All layers must have a valid material.")
472
+ else:
473
+ try:
474
+ valid_layers = []
475
+ for layer in layers:
476
+ material_name = layer.get("material_name")
477
+ thickness = layer.get("thickness", 0.1)
478
+ if material_name in material_names:
479
+ valid_layers.append({"material": material_name, "thickness": thickness})
480
+ else:
481
+ st.error(f"Material '{material_name}' not found.")
482
+ break
483
+ else:
484
+ properties = calculate_construction_properties(valid_layers, material_objects)
485
+ new_construction = {
486
+ "name": name,
487
+ "type": component_type,
488
+ "layers": valid_layers,
489
+ "u_value": properties["u_value"],
490
+ "r_value": properties["r_value"],
491
+ "thermal_mass": properties["thermal_mass"],
492
+ "embodied_carbon": properties["embodied_carbon"],
493
+ "cost": properties["cost"],
494
+ "is_library": False
495
+ }
496
+ if is_edit and name == original_name:
497
+ success, message = construction_library.edit_project_construction(
498
+ original_name, new_construction, st.session_state.project_data["constructions"]["project"], st.session_state.project_data.get("components", {})
499
+ )
500
+ else:
501
+ success, message = construction_library.add_project_construction(new_construction, st.session_state.project_data["constructions"]["project"])
502
+ if success:
503
+ st.success(message)
504
+ st.session_state.construction_editor = {}
505
+ st.session_state.construction_form_state = {
506
+ "name": "",
507
+ "component_type": "Wall",
508
+ "num_layers": 1,
509
+ "layers": [{"material_name": default_material, "thickness": 0.1}]
510
+ }
511
+ st.session_state.construction_action = {"action": None, "id": None}
512
+ st.session_state.rerun_trigger = None
513
+ st.session_state.construction_rerun_pending = True
514
+ else:
515
+ st.error(f"Failed to save construction: {message}")
516
+ except Exception as e:
517
+ st.error(f"Error saving construction: {str(e)}")
518
 
519
  def calculate_construction_properties(layers: List[Dict[str, Any]], materials: Dict[str, Any]) -> Dict[str, float]:
520
  """
521
  Calculate the thermal properties of a construction based on its layers.
 
 
 
 
 
 
 
522
  """
523
+ r_value = R_SI # Internal surface resistance
 
524
  thermal_mass = 0.0 # kJ/m虏路K
525
  embodied_carbon = 0.0 # kg CO鈧俥/m虏
526
  cost = 0.0 # $/m虏
527
+
 
 
 
 
528
  for layer in layers:
529
  material_name = layer.get("material", "")
530
  thickness = layer.get("thickness", 0.0)
 
531
  if material_name in materials:
532
  material = materials[material_name]
 
533
  # Thermal resistance
534
+ if material.conductivity > 0:
535
+ r_value += thickness / material.conductivity
 
536
  # Thermal mass
537
+ thermal_mass += material.density * material.specific_heat * thickness / 1000.0
 
538
  # Embodied carbon
539
+ embodied_carbon += material.embodied_carbon * material.density * thickness
 
540
  # Cost
541
+ cost += material.price * thickness
542
+
543
+ r_value += R_SE # External surface resistance
 
 
 
 
544
  u_value = 1.0 / r_value if r_value > 0 else 0.0
545
+
546
  return {
547
  "u_value": u_value,
548
  "r_value": r_value,
 
551
  "cost": cost
552
  }
553
 
554
+ def check_construction_in_use(construction_name: str, components: Dict) -> bool:
555
  """
556
  Check if a construction is in use in any building components.
 
 
 
 
 
 
557
  """
 
 
 
 
 
558
  for comp_type in ["walls", "roofs", "floors"]:
559
+ if comp_type in components:
560
+ for component in components[comp_type]:
561
  if component.get("construction") == construction_name:
562
  return True
 
563
  return False
564
 
565
  def display_construction_help():
 
593
  * Use the Calculate Properties button to evaluate your construction before saving.
594
  * Library constructions cannot be modified, but you can add them to your project and then edit them.
595
  """)
596
+
597
+ def get_available_constructions() -> Dict[str, Any]:
598
+ """
599
+ Get all available constructions (library + project) for use in other modules.
600
+ """
601
+ constructions = {}
602
+ if "constructions" in st.session_state.project_data:
603
+ constructions.update(st.session_state.project_data["constructions"]["library"])
604
+ constructions.update(st.session_state.project_data["constructions"]["project"])
605
+ return constructions