mabuseif commited on
Commit
eb4fe1c
·
verified ·
1 Parent(s): 2d9ca63

Update app/materials_library.py

Browse files
Files changed (1) hide show
  1. app/materials_library.py +865 -645
app/materials_library.py CHANGED
@@ -18,7 +18,7 @@ import uuid
18
  from typing import Dict, List, Any, Optional, Tuple, Union
19
 
20
  # Configure logging
21
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
22
  logger = logging.getLogger(__name__)
23
 
24
  # Define constants
@@ -37,6 +37,17 @@ FENESTRATION_TYPES = [
37
  "Custom"
38
  ]
39
 
 
 
 
 
 
 
 
 
 
 
 
40
  # Default library materials
41
  DEFAULT_MATERIALS = {
42
  "Brick": {
@@ -44,150 +55,180 @@ DEFAULT_MATERIALS = {
44
  "thermal_conductivity": 0.72,
45
  "density": 1920.0,
46
  "specific_heat": 840.0,
47
- "thickness_range": [0.05, 0.3],
48
  "default_thickness": 0.1,
49
- "embodied_carbon": 240.0, # kg CO2e/m³
50
- "cost": 180.0 # USD/m³
 
 
 
51
  },
52
  "Concrete": {
53
  "category": "Structural Materials",
54
  "thermal_conductivity": 1.4,
55
  "density": 2300.0,
56
  "specific_heat": 880.0,
57
- "thickness_range": [0.05, 0.5],
58
  "default_thickness": 0.15,
59
  "embodied_carbon": 320.0,
60
- "cost": 120.0
 
 
 
61
  },
62
  "Gypsum Board": {
63
  "category": "Finishing Materials",
64
  "thermal_conductivity": 0.25,
65
  "density": 900.0,
66
  "specific_heat": 1000.0,
67
- "thickness_range": [0.01, 0.05],
68
  "default_thickness": 0.0125,
69
  "embodied_carbon": 120.0,
70
- "cost": 90.0
 
 
 
71
  },
72
  "Mineral Wool": {
73
  "category": "Insulation",
74
  "thermal_conductivity": 0.04,
75
  "density": 30.0,
76
  "specific_heat": 840.0,
77
- "thickness_range": [0.025, 0.3],
78
  "default_thickness": 0.1,
79
  "embodied_carbon": 45.0,
80
- "cost": 70.0
 
 
 
81
  },
82
  "EPS Insulation": {
83
  "category": "Insulation",
84
  "thermal_conductivity": 0.035,
85
  "density": 25.0,
86
  "specific_heat": 1400.0,
87
- "thickness_range": [0.025, 0.3],
88
  "default_thickness": 0.1,
89
  "embodied_carbon": 88.0,
90
- "cost": 65.0
 
 
 
91
  },
92
  "Wood (Pine)": {
93
  "category": "Structural Materials",
94
  "thermal_conductivity": 0.14,
95
  "density": 550.0,
96
  "specific_heat": 1600.0,
97
- "thickness_range": [0.01, 0.3],
98
  "default_thickness": 0.05,
99
  "embodied_carbon": 110.0,
100
- "cost": 250.0
 
 
 
101
  },
102
  "Steel": {
103
  "category": "Structural Materials",
104
  "thermal_conductivity": 50.0,
105
  "density": 7800.0,
106
  "specific_heat": 450.0,
107
- "thickness_range": [0.001, 0.05],
108
  "default_thickness": 0.005,
109
  "embodied_carbon": 1750.0,
110
- "cost": 800.0
 
 
 
111
  },
112
  "Aluminum": {
113
  "category": "Structural Materials",
114
  "thermal_conductivity": 230.0,
115
  "density": 2700.0,
116
  "specific_heat": 880.0,
117
- "thickness_range": [0.001, 0.05],
118
  "default_thickness": 0.003,
119
  "embodied_carbon": 8500.0,
120
- "cost": 1200.0
 
 
 
121
  },
122
  "Glass Fiber Insulation": {
123
  "category": "Insulation",
124
  "thermal_conductivity": 0.035,
125
  "density": 12.0,
126
  "specific_heat": 840.0,
127
- "thickness_range": [0.025, 0.3],
128
  "default_thickness": 0.1,
129
  "embodied_carbon": 28.0,
130
- "cost": 45.0
 
 
 
131
  },
132
  "Ceramic Tile": {
133
  "category": "Finishing Materials",
134
  "thermal_conductivity": 1.3,
135
  "density": 2300.0,
136
  "specific_heat": 840.0,
137
- "thickness_range": [0.005, 0.025],
138
  "default_thickness": 0.01,
139
  "embodied_carbon": 650.0,
140
- "cost": 280.0
 
 
 
141
  },
142
  "Carpet": {
143
  "category": "Finishing Materials",
144
  "thermal_conductivity": 0.06,
145
  "density": 200.0,
146
  "specific_heat": 1300.0,
147
- "thickness_range": [0.005, 0.02],
148
  "default_thickness": 0.01,
149
  "embodied_carbon": 40.0,
150
- "cost": 150.0
 
 
 
151
  },
152
  "Plywood": {
153
  "category": "Sub-Structural Materials",
154
  "thermal_conductivity": 0.13,
155
  "density": 560.0,
156
  "specific_heat": 1400.0,
157
- "thickness_range": [0.006, 0.025],
158
  "default_thickness": 0.012,
159
  "embodied_carbon": 350.0,
160
- "cost": 180.0
 
 
 
161
  },
162
  "Concrete Block": {
163
  "category": "Structural Materials",
164
  "thermal_conductivity": 0.51,
165
  "density": 1400.0,
166
  "specific_heat": 1000.0,
167
- "thickness_range": [0.1, 0.3],
168
  "default_thickness": 0.2,
169
  "embodied_carbon": 260.0,
170
- "cost": 140.0
 
 
 
171
  },
172
  "Stone (Granite)": {
173
  "category": "Finishing Materials",
174
  "thermal_conductivity": 2.8,
175
  "density": 2600.0,
176
  "specific_heat": 790.0,
177
- "thickness_range": [0.01, 0.1],
178
  "default_thickness": 0.03,
179
  "embodied_carbon": 170.0,
180
- "cost": 450.0
 
 
 
181
  },
182
  "Polyurethane Insulation": {
183
  "category": "Insulation",
184
  "thermal_conductivity": 0.025,
185
  "density": 30.0,
186
  "specific_heat": 1400.0,
187
- "thickness_range": [0.025, 0.2],
188
  "default_thickness": 0.075,
189
  "embodied_carbon": 102.0,
190
- "cost": 95.0
 
 
 
191
  }
192
  }
193
 
@@ -199,8 +240,8 @@ DEFAULT_FENESTRATIONS = {
199
  "shgc": 0.86,
200
  "visible_transmittance": 0.9,
201
  "thickness": 0.006,
202
- "embodied_carbon": 800.0, # kg CO2e/m²
203
- "cost": 120.0 # USD/m²
204
  },
205
  "Double Glazing (Air)": {
206
  "type": "Window",
@@ -285,29 +326,90 @@ DEFAULT_FENESTRATIONS = {
285
  }
286
  }
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  def display_materials_page():
289
  """
290
  Display the material library page.
291
  This is the main function called by main.py when the Material Library page is selected.
292
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  st.title("Material Library")
294
 
295
- # Display help information in an expandable section
296
  with st.expander("Help & Information"):
297
  display_materials_help()
298
 
299
- # Create tabs for materials and fenestrations
300
  tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
301
 
302
- # Materials tab
303
  with tab1:
 
304
  display_materials_tab()
305
 
306
- # Fenestrations tab
307
  with tab2:
 
308
  display_fenestrations_tab()
309
 
310
- # Navigation buttons
311
  col1, col2 = st.columns(2)
312
 
313
  with col1:
@@ -320,860 +422,974 @@ def display_materials_page():
320
  st.session_state.current_page = "Construction"
321
  st.rerun()
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  def display_materials_tab():
324
  """Display the materials tab content."""
325
- # Initialize materials in session state if not present
326
- initialize_materials()
327
 
328
- # Create columns for library and project materials
329
- col1, col2 = st.columns(2)
330
 
331
- # Library Materials
332
  with col1:
 
 
 
333
  st.subheader("Library Materials")
334
- display_library_materials()
335
-
336
- # Project Materials
337
- with col2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  st.subheader("Project Materials")
339
- display_project_materials()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
- # Material Editor
342
- st.markdown("---")
343
- st.subheader("Material Editor")
344
- display_material_editor()
345
 
346
  def display_fenestrations_tab():
347
  """Display the fenestrations tab content."""
348
- # Initialize fenestrations in session state if not present
349
- initialize_fenestrations()
350
 
351
- # Create columns for library and project fenestrations
352
- col1, col2 = st.columns(2)
353
 
354
- # Library Fenestrations
355
  with col1:
 
 
 
356
  st.subheader("Library Fenestrations")
357
- display_library_fenestrations()
358
-
359
- # Project Fenestrations
360
- with col2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  st.subheader("Project Fenestrations")
362
- display_project_fenestrations()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
- # Fenestration Editor
365
- st.markdown("---")
366
- st.subheader("Fenestration Editor")
367
- display_fenestration_editor()
368
 
369
  def initialize_materials():
370
  """Initialize materials in session state if not present."""
371
  if "materials" not in st.session_state.project_data:
372
  st.session_state.project_data["materials"] = {
373
- "library": {},
374
- "project": {}
375
- }
376
-
377
- # Initialize library materials if empty
378
- if not st.session_state.project_data["materials"]["library"]:
379
- st.session_state.project_data["materials"]["library"] = DEFAULT_MATERIALS.copy()
380
-
381
- # Initialize material editor state
382
- if "material_editor" not in st.session_state:
383
- st.session_state.material_editor = {
384
- "name": "",
385
- "category": MATERIAL_CATEGORIES[0],
386
- "thermal_conductivity": 0.5,
387
- "density": 1000.0,
388
- "specific_heat": 1000.0,
389
- "thickness_range": [0.01, 0.2],
390
- "default_thickness": 0.05,
391
- "embodied_carbon": 100.0,
392
- "cost": 100.0,
393
- "edit_mode": False,
394
- "original_name": ""
395
  }
 
396
 
397
  def initialize_fenestrations():
398
  """Initialize fenestrations in session state if not present."""
399
  if "fenestrations" not in st.session_state.project_data:
400
  st.session_state.project_data["fenestrations"] = {
401
- "library": {},
402
- "project": {}
403
- }
404
-
405
- # Initialize library fenestrations if empty
406
- if not st.session_state.project_data["fenestrations"]["library"]:
407
- st.session_state.project_data["fenestrations"]["library"] = DEFAULT_FENESTRATIONS.copy()
408
-
409
- # Initialize fenestration editor state
410
- if "fenestration_editor" not in st.session_state:
411
- st.session_state.fenestration_editor = {
412
- "name": "",
413
- "type": FENESTRATION_TYPES[0],
414
- "u_value": 2.8,
415
- "shgc": 0.7,
416
- "visible_transmittance": 0.8,
417
- "thickness": 0.024,
418
- "embodied_carbon": 900.0,
419
- "cost": 200.0,
420
- "edit_mode": False,
421
- "original_name": ""
422
- }
423
-
424
- def display_library_materials():
425
- """Display the library materials section."""
426
- # Filter options
427
- category_filter = st.selectbox(
428
- "Filter by Category",
429
- ["All"] + MATERIAL_CATEGORIES,
430
- key="library_material_category_filter"
431
- )
432
-
433
- # Get library materials
434
- library_materials = st.session_state.project_data["materials"]["library"]
435
-
436
- # Apply filter
437
- if category_filter != "All":
438
- filtered_materials = {
439
- name: props for name, props in library_materials.items()
440
- if props["category"] == category_filter
441
  }
442
- else:
443
- filtered_materials = library_materials
444
-
445
- # Display materials in a table
446
- if filtered_materials:
447
- # Create a DataFrame for display
448
- data = []
449
- for name, props in filtered_materials.items():
450
- data.append({
451
- "Name": name,
452
- "Category": props["category"],
453
- "Thermal Conductivity (W/m·K)": props["thermal_conductivity"],
454
- "Density (kg/m³)": props["density"],
455
- "Specific Heat (J/kg·K)": props["specific_heat"]
456
- })
457
-
458
- df = pd.DataFrame(data)
459
- st.dataframe(df, use_container_width=True, hide_index=True)
460
-
461
- # Add to project button
462
- selected_material = st.selectbox(
463
- "Select Material to Add to Project",
464
- list(filtered_materials.keys()),
465
- key="library_material_selector"
466
- )
467
-
468
- if st.button("Add to Project", key="add_library_material_to_project"):
469
- # Check if material already exists in project
470
- if selected_material in st.session_state.project_data["materials"]["project"]:
471
- st.warning(f"Material '{selected_material}' already exists in your project.")
472
- else:
473
- # Add to project materials
474
- st.session_state.project_data["materials"]["project"][selected_material] = \
475
- st.session_state.project_data["materials"]["library"][selected_material].copy()
476
- st.success(f"Material '{selected_material}' added to your project.")
477
- logger.info(f"Added library material '{selected_material}' to project")
478
- else:
479
- st.info("No materials found in the selected category.")
480
-
481
- def display_project_materials():
482
- """Display the project materials section."""
483
- # Get project materials
484
- project_materials = st.session_state.project_data["materials"]["project"]
485
-
486
- if project_materials:
487
- # Create a DataFrame for display
488
- data = []
489
- for name, props in project_materials.items():
490
- data.append({
491
- "Name": name,
492
- "Category": props["category"],
493
- "Thermal Conductivity (W/m·K)": props["thermal_conductivity"],
494
- "Density (kg/m³)": props["density"],
495
- "Specific Heat (J/kg·K)": props["specific_heat"]
496
- })
497
-
498
- df = pd.DataFrame(data)
499
- st.dataframe(df, use_container_width=True, hide_index=True)
500
-
501
- # Edit and delete options
502
- col1, col2 = st.columns(2)
503
-
504
- with col1:
505
- selected_material = st.selectbox(
506
- "Select Material to Edit",
507
- list(project_materials.keys()),
508
- key="project_material_edit_selector"
509
- )
510
-
511
- if st.button("Edit Material", key="edit_project_material"):
512
- # Load material data into editor
513
- material_data = project_materials[selected_material]
514
- st.session_state.material_editor = {
515
- "name": selected_material,
516
- "category": material_data["category"],
517
- "thermal_conductivity": material_data["thermal_conductivity"],
518
- "density": material_data["density"],
519
- "specific_heat": material_data["specific_heat"],
520
- "thickness_range": material_data["thickness_range"],
521
- "default_thickness": material_data["default_thickness"],
522
- "embodied_carbon": material_data["embodied_carbon"],
523
- "cost": material_data["cost"],
524
- "edit_mode": True,
525
- "original_name": selected_material
526
- }
527
- st.success(f"Material '{selected_material}' loaded for editing.")
528
-
529
- with col2:
530
- selected_material_delete = st.selectbox(
531
- "Select Material to Delete",
532
- list(project_materials.keys()),
533
- key="project_material_delete_selector"
534
- )
535
-
536
- if st.button("Delete Material", key="delete_project_material"):
537
- # Check if material is in use
538
- is_in_use = check_material_in_use(selected_material_delete)
539
-
540
- if is_in_use:
541
- st.error(f"Cannot delete material '{selected_material_delete}' because it is in use in constructions.")
542
- else:
543
- # Delete material
544
- del st.session_state.project_data["materials"]["project"][selected_material_delete]
545
- st.success(f"Material '{selected_material_delete}' deleted from your project.")
546
- logger.info(f"Deleted material '{selected_material_delete}' from project")
547
- else:
548
- st.info("No materials in your project. Add materials from the library or create custom materials.")
549
 
550
  def display_material_editor():
551
  """Display the material editor form."""
552
- with st.form("material_editor_form"):
553
- # Material name
 
 
 
554
  name = st.text_input(
555
  "Material Name",
556
- value=st.session_state.material_editor["name"],
557
- help="Enter a unique name for the material."
 
558
  )
559
 
560
- # Create two columns for layout
561
  col1, col2 = st.columns(2)
562
 
563
  with col1:
564
- # Category
565
  category = st.selectbox(
566
  "Category",
567
  MATERIAL_CATEGORIES,
568
- index=MATERIAL_CATEGORIES.index(st.session_state.material_editor["category"]) if st.session_state.material_editor["category"] in MATERIAL_CATEGORIES else 0,
569
- help="Select the material category."
 
570
  )
571
 
572
- # Thermal conductivity
573
  thermal_conductivity = st.number_input(
574
  "Thermal Conductivity (W/m·K)",
575
  min_value=0.001,
576
  max_value=1000.0,
577
- value=float(st.session_state.material_editor["thermal_conductivity"]),
578
  format="%.3f",
579
- help="Thermal conductivity in W/m·K. Lower values indicate better insulation."
 
580
  )
581
 
582
- # Density
583
  density = st.number_input(
584
  "Density (kg/m³)",
585
  min_value=1.0,
586
  max_value=20000.0,
587
- value=float(st.session_state.material_editor["density"]),
588
  format="%.1f",
589
- help="Material density in kg/m³."
 
590
  )
591
 
592
  with col2:
593
- # Specific heat
594
  specific_heat = st.number_input(
595
  "Specific Heat (J/kg·K)",
596
  min_value=100.0,
597
  max_value=10000.0,
598
- value=float(st.session_state.material_editor["specific_heat"]),
599
  format="%.1f",
600
- help="Specific heat capacity in J/kg·K. Higher values indicate better thermal mass."
 
601
  )
602
 
603
- # Thickness range
604
- min_thickness, max_thickness = st.session_state.material_editor["thickness_range"]
605
- thickness_range = st.slider(
606
- "Thickness Range (m)",
607
  min_value=0.001,
608
  max_value=0.5,
609
- value=(float(min_thickness), float(max_thickness)),
610
  format="%.3f",
611
- help="Minimum and maximum thickness range for this material in meters."
 
612
  )
613
 
614
- # Default thickness
615
- default_thickness = st.number_input(
616
- "Default Thickness (m)",
617
- min_value=float(thickness_range[0]),
618
- max_value=float(thickness_range[1]),
619
- value=min(max(float(st.session_state.material_editor["default_thickness"]), float(thickness_range[0])), float(thickness_range[1])),
620
- format="%.3f",
621
- help="Default thickness for this material in meters."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  )
623
 
624
- # Additional properties for embodied carbon and cost
625
  st.subheader("Additional Properties")
626
  col1, col2 = st.columns(2)
627
 
628
  with col1:
629
- # Embodied carbon
630
  embodied_carbon = st.number_input(
631
  "Embodied Carbon (kg CO₂e/m³)",
632
  min_value=0.0,
633
  max_value=10000.0,
634
- value=float(st.session_state.material_editor["embodied_carbon"]),
635
  format="%.1f",
636
- help="Embodied carbon in kg CO₂e per cubic meter."
 
637
  )
638
 
639
  with col2:
640
- # Cost
641
  cost = st.number_input(
642
  "Cost (USD/m³)",
643
  min_value=0.0,
644
  max_value=10000.0,
645
- value=float(st.session_state.material_editor["cost"]),
646
  format="%.1f",
647
- help="Material cost in USD per cubic meter."
 
648
  )
649
 
650
- # Form submission buttons
651
  col1, col2 = st.columns(2)
652
 
653
  with col1:
654
- submit_button = st.form_submit_button("Save Material")
655
 
656
  with col2:
657
  clear_button = st.form_submit_button("Clear Form")
658
-
659
- # Handle form submission
660
- if submit_button:
661
- # Validate inputs
662
- validation_errors = validate_material(
663
- name, category, thermal_conductivity, density, specific_heat,
664
- thickness_range, default_thickness, embodied_carbon, cost,
665
- st.session_state.material_editor["edit_mode"], st.session_state.material_editor["original_name"]
666
- )
667
 
668
- if validation_errors:
669
- # Display validation errors
670
- for error in validation_errors:
671
- st.error(error)
672
- else:
673
- # Create material data
674
- material_data = {
675
  "category": category,
676
  "thermal_conductivity": thermal_conductivity,
677
  "density": density,
678
  "specific_heat": specific_heat,
679
- "thickness_range": list(thickness_range),
680
  "default_thickness": default_thickness,
681
  "embodied_carbon": embodied_carbon,
682
- "cost": cost
 
 
 
683
  }
684
-
685
- # Handle edit mode
686
- if st.session_state.material_editor["edit_mode"]:
687
- original_name = st.session_state.material_editor["original_name"]
688
-
689
- # If name changed, delete old entry and create new one
690
- if original_name != name:
691
- del st.session_state.project_data["materials"]["project"][original_name]
692
-
693
- # Update material
694
- st.session_state.project_data["materials"]["project"][name] = material_data
695
- st.success(f"Material '{name}' updated successfully.")
696
- logger.info(f"Updated material '{name}' in project")
697
- else:
698
- # Add new material
699
- st.session_state.project_data["materials"]["project"][name] = material_data
700
- st.success(f"Material '{name}' added to your project.")
701
- logger.info(f"Added new material '{name}' to project")
702
-
703
- # Reset editor
704
- reset_material_editor()
705
-
706
- # Handle clear button
707
- if clear_button:
708
- reset_material_editor()
709
- st.rerun()
710
-
711
- def display_library_fenestrations():
712
- """Display the library fenestrations section."""
713
- # Filter options
714
- type_filter = st.selectbox(
715
- "Filter by Type",
716
- ["All"] + FENESTRATION_TYPES,
717
- key="library_fenestration_type_filter"
718
- )
719
-
720
- # Get library fenestrations
721
- library_fenestrations = st.session_state.project_data["fenestrations"]["library"]
722
-
723
- # Apply filter
724
- if type_filter != "All":
725
- filtered_fenestrations = {
726
- name: props for name, props in library_fenestrations.items()
727
- if props["type"] == type_filter
728
- }
729
- else:
730
- filtered_fenestrations = library_fenestrations
731
-
732
- # Display fenestrations in a table
733
- if filtered_fenestrations:
734
- # Create a DataFrame for display
735
- data = []
736
- for name, props in filtered_fenestrations.items():
737
- data.append({
738
- "Name": name,
739
- "Type": props["type"],
740
- "U-Value (W/m²·K)": props["u_value"],
741
- "SHGC": props["shgc"],
742
- "Visible Transmittance": props["visible_transmittance"]
743
- })
744
-
745
- df = pd.DataFrame(data)
746
- st.dataframe(df, use_container_width=True, hide_index=True)
747
-
748
- # Add to project button
749
- selected_fenestration = st.selectbox(
750
- "Select Fenestration to Add to Project",
751
- list(filtered_fenestrations.keys()),
752
- key="library_fenestration_selector"
753
- )
754
-
755
- if st.button("Add to Project", key="add_library_fenestration_to_project"):
756
- # Check if fenestration already exists in project
757
- if selected_fenestration in st.session_state.project_data["fenestrations"]["project"]:
758
- st.warning(f"Fenestration '{selected_fenestration}' already exists in your project.")
759
- else:
760
- # Add to project fenestrations
761
- st.session_state.project_data["fenestrations"]["project"][selected_fenestration] = \
762
- st.session_state.project_data["fenestrations"]["library"][selected_fenestration].copy()
763
- st.success(f"Fenestration '{selected_fenestration}' added to your project.")
764
- logger.info(f"Added library fenestration '{selected_fenestration}' to project")
765
- else:
766
- st.info("No fenestrations found in the selected type.")
767
-
768
- def display_project_fenestrations():
769
- """Display the project fenestrations section."""
770
- # Get project fenestrations
771
- project_fenestrations = st.session_state.project_data["fenestrations"]["project"]
772
-
773
- if project_fenestrations:
774
- # Create a DataFrame for display
775
- data = []
776
- for name, props in project_fenestrations.items():
777
- data.append({
778
- "Name": name,
779
- "Type": props["type"],
780
- "U-Value (W/m²·K)": props["u_value"],
781
- "SHGC": props["shgc"],
782
- "Visible Transmittance": props["visible_transmittance"]
783
- })
784
-
785
- df = pd.DataFrame(data)
786
- st.dataframe(df, use_container_width=True, hide_index=True)
787
-
788
- # Edit and delete options
789
- col1, col2 = st.columns(2)
790
-
791
- with col1:
792
- selected_fenestration = st.selectbox(
793
- "Select Fenestration to Edit",
794
- list(project_fenestrations.keys()),
795
- key="project_fenestration_edit_selector"
796
  )
797
-
798
- if st.button("Edit Fenestration", key="edit_project_fenestration"):
799
- # Load fenestration data into editor
800
- fenestration_data = project_fenestrations[selected_fenestration]
801
- st.session_state.fenestration_editor = {
802
- "name": selected_fenestration,
803
- "type": fenestration_data["type"],
804
- "u_value": fenestration_data["u_value"],
805
- "shgc": fenestration_data["shgc"],
806
- "visible_transmittance": fenestration_data["visible_transmittance"],
807
- "thickness": fenestration_data["thickness"],
808
- "embodied_carbon": fenestration_data["embodied_carbon"],
809
- "cost": fenestration_data["cost"],
810
- "edit_mode": True,
811
- "original_name": selected_fenestration
812
  }
813
- st.success(f"Fenestration '{selected_fenestration}' loaded for editing.")
814
-
815
- with col2:
816
- selected_fenestration_delete = st.selectbox(
817
- "Select Fenestration to Delete",
818
- list(project_fenestrations.keys()),
819
- key="project_fenestration_delete_selector"
820
- )
821
-
822
- if st.button("Delete Fenestration", key="delete_project_fenestration"):
823
- # Check if fenestration is in use
824
- is_in_use = check_fenestration_in_use(selected_fenestration_delete)
825
-
826
- if is_in_use:
827
- st.error(f"Cannot delete fenestration '{selected_fenestration_delete}' because it is in use in components.")
828
  else:
829
- # Delete fenestration
830
- del st.session_state.project_data["fenestrations"]["project"][selected_fenestration_delete]
831
- st.success(f"Fenestration '{selected_fenestration_delete}' deleted from your project.")
832
- logger.info(f"Deleted fenestration '{selected_fenestration_delete}' from project")
833
- else:
834
- st.info("No fenestrations in your project. Add fenestrations from the library or create custom fenestrations.")
 
 
 
835
 
836
  def display_fenestration_editor():
837
  """Display the fenestration editor form."""
838
- with st.form("fenestration_editor_form"):
839
- # Fenestration name
 
 
 
840
  name = st.text_input(
841
  "Fenestration Name",
842
- value=st.session_state.fenestration_editor["name"],
843
- help="Enter a unique name for the fenestration."
 
844
  )
845
 
846
- # Create two columns for layout
847
  col1, col2 = st.columns(2)
848
 
849
  with col1:
850
- # Type
851
  fenestration_type = st.selectbox(
852
  "Type",
853
  FENESTRATION_TYPES,
854
- index=FENESTRATION_TYPES.index(st.session_state.fenestration_editor["type"]) if st.session_state.fenestration_editor["type"] in FENESTRATION_TYPES else 0,
855
- help="Select the fenestration type."
 
856
  )
857
 
858
- # U-value
859
  u_value = st.number_input(
860
  "U-Value (W/m²·K)",
861
  min_value=0.1,
862
  max_value=10.0,
863
- value=float(st.session_state.fenestration_editor["u_value"]),
864
  format="%.2f",
865
- help="U-value in W/m²·K. Lower values indicate better insulation."
 
866
  )
867
 
868
- # SHGC
869
  shgc = st.number_input(
870
  "Solar Heat Gain Coefficient (SHGC)",
871
  min_value=0.0,
872
  max_value=1.0,
873
- value=float(st.session_state.fenestration_editor["shgc"]),
874
  format="%.2f",
875
- help="Solar Heat Gain Coefficient (0-1). Lower values indicate less solar heat transmission."
 
876
  )
877
-
878
- with col2:
879
- # Visible transmittance
880
- visible_transmittance = st.number_input(
881
  "Visible Transmittance",
882
  min_value=0.0,
883
  max_value=1.0,
884
- value=float(st.session_state.fenestration_editor["visible_transmittance"]),
885
  format="%.2f",
886
- help="Visible Transmittance (0-1). Higher values indicate more visible light transmission."
 
887
  )
888
 
889
- # Thickness
890
  thickness = st.number_input(
891
  "Thickness (m)",
892
  min_value=0.001,
893
  max_value=0.1,
894
- value=float(st.session_state.fenestration_editor["thickness"]),
895
  format="%.3f",
896
- help="Thickness in meters."
 
897
  )
898
 
899
- # Additional properties for embodied carbon and cost
900
  st.subheader("Additional Properties")
 
901
  col1, col2 = st.columns(2)
902
 
903
  with col1:
904
- # Embodied carbon
905
  embodied_carbon = st.number_input(
906
  "Embodied Carbon (kg CO₂e/m²)",
907
  min_value=0.0,
908
  max_value=5000.0,
909
- value=float(st.session_state.fenestration_editor["embodied_carbon"]),
910
  format="%.1f",
911
- help="Embodied carbon in kg CO₂e per square meter."
 
912
  )
913
 
914
  with col2:
915
- # Cost
916
  cost = st.number_input(
917
  "Cost (USD/m²)",
918
  min_value=0.0,
919
  max_value=5000.0,
920
- value=float(st.session_state.fenestration_editor["cost"]),
921
  format="%.1f",
922
- help="Fenestration cost in USD per square meter."
 
923
  )
924
 
925
- # Form submission buttons
926
  col1, col2 = st.columns(2)
927
 
928
  with col1:
929
- submit_button = st.form_submit_button("Save Fenestration")
930
 
931
  with col2:
932
  clear_button = st.form_submit_button("Clear Form")
933
-
934
- # Handle form submission
935
- if submit_button:
936
- # Validate inputs
937
- validation_errors = validate_fenestration(
938
- name, fenestration_type, u_value, shgc, visible_transmittance, thickness,
939
- embodied_carbon, cost, st.session_state.fenestration_editor["edit_mode"],
940
- st.session_state.fenestration_editor["original_name"]
941
- )
942
 
943
- if validation_errors:
944
- # Display validation errors
945
- for error in validation_errors:
946
- st.error(error)
947
- else:
948
- # Create fenestration data
949
- fenestration_data = {
950
  "type": fenestration_type,
951
  "u_value": u_value,
952
  "shgc": shgc,
953
- "visible_transmittance": visible_transmittance,
954
  "thickness": thickness,
955
  "embodied_carbon": embodied_carbon,
956
  "cost": cost
957
  }
958
-
959
- # Handle edit mode
960
- if st.session_state.fenestration_editor["edit_mode"]:
961
- original_name = st.session_state.fenestration_editor["original_name"]
962
-
963
- # If name changed, delete old entry and create new one
964
- if original_name != name:
965
- del st.session_state.project_data["fenestrations"]["project"][original_name]
966
-
967
- # Update fenestration
968
- st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
969
- st.success(f"Fenestration '{name}' updated successfully.")
970
- logger.info(f"Updated fenestration '{name}' in project")
971
  else:
972
- # Add new fenestration
973
- st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
974
- st.success(f"Fenestration '{name}' added to your project.")
975
- logger.info(f"Added new fenestration '{name}' to project")
976
-
977
- # Reset editor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  reset_fenestration_editor()
979
-
980
- # Handle clear button
981
- if clear_button:
982
- reset_fenestration_editor()
983
- st.rerun()
984
 
985
  def validate_material(
986
  name: str, category: str, thermal_conductivity: float, density: float,
987
- specific_heat: float, thickness_range: Tuple[float, float], default_thickness: float,
988
- embodied_carbon: float, cost: float, edit_mode: bool, original_name: str
989
- ) -> List[str]:
 
990
  """
991
  Validate material inputs.
992
-
993
- Args:
994
- name: Material name
995
- category: Material category
996
- thermal_conductivity: Thermal conductivity in W/m·K
997
- density: Density in kg/m³
998
- specific_heat: Specific heat in J/kg·K
999
- thickness_range: Tuple of (min_thickness, max_thickness) in meters
1000
- default_thickness: Default thickness in meters
1001
- embodied_carbon: Embodied carbon in kg CO₂e/m³
1002
- cost: Cost in USD/m³
1003
- edit_mode: Whether in edit mode
1004
- original_name: Original name if in edit mode
1005
-
1006
- Returns:
1007
- List of validation error messages, empty if all inputs are valid
1008
  """
1009
- errors = []
1010
-
1011
- # Validate name
1012
  if not name or name.strip() == "":
1013
- errors.append("Material name is required.")
1014
 
1015
- # Check for name uniqueness if not in edit mode or if name changed
1016
- if not edit_mode or (edit_mode and name != original_name):
1017
- if name in st.session_state.project_data["materials"]["project"]:
1018
- errors.append(f"Material name '{name}' already exists in your project.")
1019
 
1020
- # Validate category
1021
  if category not in MATERIAL_CATEGORIES:
1022
- errors.append("Please select a valid material category.")
1023
 
1024
- # Validate thermal conductivity
1025
  if thermal_conductivity <= 0:
1026
- errors.append("Thermal conductivity must be greater than zero.")
1027
 
1028
- # Validate density
1029
  if density <= 0:
1030
- errors.append("Density must be greater than zero.")
1031
 
1032
- # Validate specific heat
1033
  if specific_heat <= 0:
1034
- errors.append("Specific heat must be greater than zero.")
1035
 
1036
- # Validate thickness range
1037
- if thickness_range[0] <= 0:
1038
- errors.append("Minimum thickness must be greater than zero.")
1039
- if thickness_range[0] >= thickness_range[1]:
1040
- errors.append("Maximum thickness must be greater than minimum thickness.")
1041
 
1042
- # Validate default thickness
1043
- if default_thickness < thickness_range[0] or default_thickness > thickness_range[1]:
1044
- errors.append("Default thickness must be within the thickness range.")
1045
-
1046
- # Validate embodied carbon
1047
  if embodied_carbon < 0:
1048
- errors.append("Embodied carbon cannot be negative.")
1049
 
1050
- # Validate cost
1051
  if cost < 0:
1052
- errors.append("Cost cannot be negative.")
 
 
 
 
 
 
 
 
 
1053
 
1054
- return errors
 
 
 
 
 
 
 
 
 
 
1055
 
1056
  def validate_fenestration(
1057
  name: str, fenestration_type: str, u_value: float, shgc: float,
1058
- visible_transmittance: float, thickness: float, embodied_carbon: float,
1059
  cost: float, edit_mode: bool, original_name: str
1060
- ) -> List[str]:
1061
  """
1062
  Validate fenestration inputs.
1063
-
1064
- Args:
1065
- name: Fenestration name
1066
- fenestration_type: Fenestration type
1067
- u_value: U-value in W/m²·K
1068
- shgc: Solar Heat Gain Coefficient (0-1)
1069
- visible_transmittance: Visible Transmittance (0-1)
1070
- thickness: Thickness in meters
1071
- embodied_carbon: Embodied carbon in kg CO₂e/m²
1072
- cost: Cost in USD/m²
1073
- edit_mode: Whether in edit mode
1074
- original_name: Original name if in edit mode
1075
-
1076
- Returns:
1077
- List of validation error messages, empty if all inputs are valid
1078
  """
1079
- errors = []
1080
-
1081
- # Validate name
1082
  if not name or name.strip() == "":
1083
- errors.append("Fenestration name is required.")
1084
 
1085
- # Check for name uniqueness if not in edit mode or if name changed
1086
- if not edit_mode or (edit_mode and name != original_name):
1087
- if name in st.session_state.project_data["fenestrations"]["project"]:
1088
- errors.append(f"Fenestration name '{name}' already exists in your project.")
1089
 
1090
- # Validate type
1091
  if fenestration_type not in FENESTRATION_TYPES:
1092
- errors.append("Please select a valid fenestration type.")
1093
 
1094
- # Validate u_value
1095
  if u_value <= 0:
1096
- errors.append("U-value must be greater than zero.")
1097
 
1098
- # Validate shgc
1099
  if shgc < 0 or shgc > 1:
1100
- errors.append("SHGC must be between 0 and 1.")
1101
 
1102
- # Validate visible transmittance
1103
- if visible_transmittance < 0 or visible_transmittance > 1:
1104
- errors.append("Visible transmittance must be between 0 and 1.")
1105
 
1106
- # Validate thickness
1107
  if thickness <= 0:
1108
- errors.append("Thickness must be greater than zero.")
1109
-
1110
- # Validate embodied carbon
1111
  if embodied_carbon < 0:
1112
- errors.append("Embodied carbon cannot be negative.")
1113
 
1114
- # Validate cost
1115
  if cost < 0:
1116
- errors.append("Cost cannot be negative.")
1117
 
1118
- return errors
1119
 
1120
  def reset_material_editor():
1121
- """Reset the material editor to default values."""
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
  st.session_state.material_editor = {
1123
  "name": "",
1124
  "category": MATERIAL_CATEGORIES[0],
1125
  "thermal_conductivity": 0.5,
1126
  "density": 1000.0,
1127
  "specific_heat": 1000.0,
1128
- "thickness_range": [0.01, 0.2],
1129
  "default_thickness": 0.05,
1130
  "embodied_carbon": 100.0,
1131
  "cost": 100.0,
 
 
 
1132
  "edit_mode": False,
1133
- "original_name": ""
 
1134
  }
 
 
1135
 
1136
  def reset_fenestration_editor():
1137
- """Reset the fenestration editor to default values."""
 
 
 
 
 
 
 
 
 
 
1138
  st.session_state.fenestration_editor = {
1139
  "name": "",
1140
  "type": FENESTRATION_TYPES[0],
1141
  "u_value": 2.8,
1142
  "shgc": 0.7,
1143
- "visible_transmittance": 0.8,
1144
  "thickness": 0.024,
1145
  "embodied_carbon": 900.0,
1146
  "cost": 200.0,
1147
  "edit_mode": False,
1148
- "original_name": ""
 
1149
  }
 
 
1150
 
1151
  def check_material_in_use(material_name: str) -> bool:
1152
  """
1153
  Check if a material is in use in any constructions.
1154
-
1155
- Args:
1156
- material_name: Name of the material to check
1157
-
1158
- Returns:
1159
- True if the material is in use, False otherwise
1160
  """
1161
- # This is a placeholder function that will be implemented when constructions are added
1162
- # For now, we'll assume materials are not in use
1163
  return False
1164
 
1165
- def check_fenestration_in_use(fenestration_name: str) -> bool:
1166
  """
1167
  Check if a fenestration is in use in any components.
1168
-
1169
- Args:
1170
- fenestration_name: Name of the fenestration to check
1171
-
1172
- Returns:
1173
- True if the fenestration is in use, False otherwise
1174
  """
1175
- # This is a placeholder function that will be implemented when components are added
1176
- # For now, we'll assume fenestrations are not in use
1177
  return False
1178
 
1179
  def display_materials_help():
@@ -1185,31 +1401,35 @@ def display_materials_help():
1185
 
1186
  **Materials Tab:**
1187
 
1188
- * **Library Materials**: Pre-defined materials with standard thermal properties.
1189
- * **Project Materials**: Materials you've added to your project from the library or created custom.
1190
- * **Material Editor**: Create new materials or edit existing ones in your project.
1191
 
1192
  **Fenestrations Tab:**
1193
 
1194
- * **Library Fenestrations**: Pre-defined windows, doors, and skylights with standard thermal properties.
1195
- * **Project Fenestrations**: Fenestrations you've added to your project from the library or created custom.
1196
- * **Fenestration Editor**: Create new fenestrations or edit existing ones in your project.
1197
 
1198
  **Key Properties:**
1199
 
1200
- * **Thermal Conductivity (W/m·K)**: Rate of heat transfer through a material. Lower values indicate better insulation.
1201
- * **Density (kg/m³)**: Mass per unit volume.
1202
- * **Specific Heat (J/kg·K)**: Energy required to raise the temperature of 1 kg by 1 K. Higher values indicate better thermal mass.
1203
- * **U-Value (W/m²·K)**: Overall heat transfer coefficient for fenestrations. Lower values indicate better insulation.
1204
- * **SHGC**: Solar Heat Gain Coefficient (0-1). Fraction of incident solar radiation that enters through a fenestration.
1205
- * **Visible Transmittance**: Fraction of visible light that passes through a fenestration.
1206
- * **Embodied Carbon**: Carbon emissions associated with material production, measured in kg CO₂e per unit volume or area.
1207
- * **Cost**: Material cost in USD per unit volume or area.
 
 
 
 
1208
 
1209
  **Workflow:**
1210
 
1211
  1. Browse the library materials and fenestrations
1212
- 2. Add items to your project or create custom ones
1213
  3. Edit properties as needed for your specific project
1214
  4. Continue to the Construction page to create assemblies using these materials
1215
- """)
 
18
  from typing import Dict, List, Any, Optional, Tuple, Union
19
 
20
  # Configure logging
21
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
22
  logger = logging.getLogger(__name__)
23
 
24
  # Define constants
 
37
  "Custom"
38
  ]
39
 
40
+ COLOUR_CATEGORIES = [
41
+ "Light", # Absorptivity: 0.30–0.50 (e.g., white paints, aluminium coatings)
42
+ "Medium", # Absorptivity: 0.60–0.70 (e.g., beige, tan, concrete)
43
+ "Dark", # Absorptivity: 0.85–0.95 (e.g., black surfaces, dark roofs)
44
+ "Reflective" # Absorptivity: 0.10–0.20 (e.g., polished metals)
45
+ ]
46
+
47
+ # Surface resistances
48
+ R_SI = 0.12 # Internal surface resistance (m²·K/W)
49
+ R_SE = 0.04 # External surface resistance (m²·K/W)
50
+
51
  # Default library materials
52
  DEFAULT_MATERIALS = {
53
  "Brick": {
 
55
  "thermal_conductivity": 0.72,
56
  "density": 1920.0,
57
  "specific_heat": 840.0,
 
58
  "default_thickness": 0.1,
59
+ "embodied_carbon": 240.0,
60
+ "cost": 180.0,
61
+ "absorptivity": 0.65,
62
+ "emissivity": 0.90,
63
+ "colour": "Medium"
64
  },
65
  "Concrete": {
66
  "category": "Structural Materials",
67
  "thermal_conductivity": 1.4,
68
  "density": 2300.0,
69
  "specific_heat": 880.0,
 
70
  "default_thickness": 0.15,
71
  "embodied_carbon": 320.0,
72
+ "cost": 120.0,
73
+ "absorptivity": 0.60,
74
+ "emissivity": 0.88,
75
+ "colour": "Medium"
76
  },
77
  "Gypsum Board": {
78
  "category": "Finishing Materials",
79
  "thermal_conductivity": 0.25,
80
  "density": 900.0,
81
  "specific_heat": 1000.0,
 
82
  "default_thickness": 0.0125,
83
  "embodied_carbon": 120.0,
84
+ "cost": 90.0,
85
+ "absorptivity": 0.40,
86
+ "emissivity": 0.90,
87
+ "colour": "Light"
88
  },
89
  "Mineral Wool": {
90
  "category": "Insulation",
91
  "thermal_conductivity": 0.04,
92
  "density": 30.0,
93
  "specific_heat": 840.0,
 
94
  "default_thickness": 0.1,
95
  "embodied_carbon": 45.0,
96
+ "cost": 70.0,
97
+ "absorptivity": 0.50,
98
+ "emissivity": 0.95,
99
+ "colour": "Light"
100
  },
101
  "EPS Insulation": {
102
  "category": "Insulation",
103
  "thermal_conductivity": 0.035,
104
  "density": 25.0,
105
  "specific_heat": 1400.0,
 
106
  "default_thickness": 0.1,
107
  "embodied_carbon": 88.0,
108
+ "cost": 65.0,
109
+ "absorptivity": 0.40,
110
+ "emissivity": 0.92,
111
+ "colour": "Light"
112
  },
113
  "Wood (Pine)": {
114
  "category": "Structural Materials",
115
  "thermal_conductivity": 0.14,
116
  "density": 550.0,
117
  "specific_heat": 1600.0,
 
118
  "default_thickness": 0.05,
119
  "embodied_carbon": 110.0,
120
+ "cost": 250.0,
121
+ "absorptivity": 0.65,
122
+ "emissivity": 0.90,
123
+ "colour": "Medium"
124
  },
125
  "Steel": {
126
  "category": "Structural Materials",
127
  "thermal_conductivity": 50.0,
128
  "density": 7800.0,
129
  "specific_heat": 450.0,
 
130
  "default_thickness": 0.005,
131
  "embodied_carbon": 1750.0,
132
+ "cost": 800.0,
133
+ "absorptivity": 0.15,
134
+ "emissivity": 0.20,
135
+ "colour": "Reflective"
136
  },
137
  "Aluminum": {
138
  "category": "Structural Materials",
139
  "thermal_conductivity": 230.0,
140
  "density": 2700.0,
141
  "specific_heat": 880.0,
 
142
  "default_thickness": 0.003,
143
  "embodied_carbon": 8500.0,
144
+ "cost": 1200.0,
145
+ "absorptivity": 0.10,
146
+ "emissivity": 0.05,
147
+ "colour": "Reflective"
148
  },
149
  "Glass Fiber Insulation": {
150
  "category": "Insulation",
151
  "thermal_conductivity": 0.035,
152
  "density": 12.0,
153
  "specific_heat": 840.0,
 
154
  "default_thickness": 0.1,
155
  "embodied_carbon": 28.0,
156
+ "cost": 45.0,
157
+ "absorptivity": 0.50,
158
+ "emissivity": 0.95,
159
+ "colour": "Light"
160
  },
161
  "Ceramic Tile": {
162
  "category": "Finishing Materials",
163
  "thermal_conductivity": 1.3,
164
  "density": 2300.0,
165
  "specific_heat": 840.0,
 
166
  "default_thickness": 0.01,
167
  "embodied_carbon": 650.0,
168
+ "cost": 280.0,
169
+ "absorptivity": 0.60,
170
+ "emissivity": 0.90,
171
+ "colour": "Medium"
172
  },
173
  "Carpet": {
174
  "category": "Finishing Materials",
175
  "thermal_conductivity": 0.06,
176
  "density": 200.0,
177
  "specific_heat": 1300.0,
 
178
  "default_thickness": 0.01,
179
  "embodied_carbon": 40.0,
180
+ "cost": 150.0,
181
+ "absorptivity": 0.70,
182
+ "emissivity": 0.95,
183
+ "colour": "Medium"
184
  },
185
  "Plywood": {
186
  "category": "Sub-Structural Materials",
187
  "thermal_conductivity": 0.13,
188
  "density": 560.0,
189
  "specific_heat": 1400.0,
 
190
  "default_thickness": 0.012,
191
  "embodied_carbon": 350.0,
192
+ "cost": 180.0,
193
+ "absorptivity": 0.65,
194
+ "emissivity": 0.90,
195
+ "colour": "Medium"
196
  },
197
  "Concrete Block": {
198
  "category": "Structural Materials",
199
  "thermal_conductivity": 0.51,
200
  "density": 1400.0,
201
  "specific_heat": 1000.0,
 
202
  "default_thickness": 0.2,
203
  "embodied_carbon": 260.0,
204
+ "cost": 140.0,
205
+ "absorptivity": 0.60,
206
+ "emissivity": 0.88,
207
+ "colour": "Medium"
208
  },
209
  "Stone (Granite)": {
210
  "category": "Finishing Materials",
211
  "thermal_conductivity": 2.8,
212
  "density": 2600.0,
213
  "specific_heat": 790.0,
 
214
  "default_thickness": 0.03,
215
  "embodied_carbon": 170.0,
216
+ "cost": 450.0,
217
+ "absorptivity": 0.85,
218
+ "emissivity": 0.90,
219
+ "colour": "Dark"
220
  },
221
  "Polyurethane Insulation": {
222
  "category": "Insulation",
223
  "thermal_conductivity": 0.025,
224
  "density": 30.0,
225
  "specific_heat": 1400.0,
 
226
  "default_thickness": 0.075,
227
  "embodied_carbon": 102.0,
228
+ "cost": 95.0,
229
+ "absorptivity": 0.40,
230
+ "emissivity": 0.92,
231
+ "colour": "Light"
232
  }
233
  }
234
 
 
240
  "shgc": 0.86,
241
  "visible_transmittance": 0.9,
242
  "thickness": 0.006,
243
+ "embodied_carbon": 800.0,
244
+ "cost": 120.0
245
  },
246
  "Double Glazing (Air)": {
247
  "type": "Window",
 
326
  }
327
  }
328
 
329
+ def calculate_u_value(thermal_conductivity: float, thickness: float) -> float:
330
+ """Calculate U-value for a material."""
331
+ r_material = thickness / thermal_conductivity if thermal_conductivity > 0 else float('inf')
332
+ r_total = R_SI + r_material + R_SE
333
+ return 1 / r_total if r_total > 0 else 0.0
334
+
335
+ def calculate_thermal_mass(density: float, specific_heat: float, thickness: float) -> float:
336
+ """Calculate areal thermal mass for a material."""
337
+ return density * specific_heat * thickness
338
+
339
+ def categorize_thermal_mass(thermal_mass: float) -> str:
340
+ """Categorize thermal mass based on given ranges."""
341
+ if thermal_mass < 30000:
342
+ return "Low"
343
+ elif 30000 <= thermal_mass <= 90000:
344
+ return "Medium"
345
+ else:
346
+ return "High"
347
+
348
+ def get_stable_button_key(prefix: str, name: str, action: str, unique_id: str = None) -> str:
349
+ """Generate a stable button key based on prefix, name, action, and optional unique identifier."""
350
+ import hashlib
351
+ unique_id = unique_id or str(uuid.uuid4())[:8]
352
+ key_string = f"{prefix}_{name}_{action}_{unique_id}"
353
+ hash_key = hashlib.md5(key_string.encode()).hexdigest()[:8]
354
+ return f"{prefix}_{action}_{name}_{hash_key}"
355
+
356
+ def initialize_session_state():
357
+ """Initialize all required session state variables."""
358
+ if "project_data" not in st.session_state:
359
+ st.session_state.project_data = {
360
+ "materials": {"library": DEFAULT_MATERIALS.copy(), "project": {}, "table": None},
361
+ "fenestrations": {"library": DEFAULT_FENESTRATIONS.copy(), "project": {}, "table": None}
362
+ }
363
+ if "material_editor" not in st.session_state:
364
+ reset_material_editor()
365
+ if "fenestration_editor" not in st.session_state:
366
+ reset_fenestration_editor()
367
+ if "active_tab" not in st.session_state:
368
+ st.session_state.active_tab = "Materials"
369
+ if "material_action" not in st.session_state:
370
+ st.session_state.material_action = {"action": None, "id": None}
371
+ if "fenestration_action" not in st.session_state:
372
+ st.session_state.fenestration_action = {"action": None, "id": None}
373
+ if "current_page" not in st.session_state:
374
+ st.session_state.current_page = "Material Library"
375
+ logger.debug("Session state initialized")
376
+ logger.debug(f"Project materials: {list(st.session_state.project_data['materials']['project'].keys())}")
377
+ logger.debug(f"Project fenestrations: {list(st.session_state.project_data['fenestrations']['project'].keys())}")
378
+
379
  def display_materials_page():
380
  """
381
  Display the material library page.
382
  This is the main function called by main.py when the Material Library page is selected.
383
  """
384
+ initialize_session_state()
385
+
386
+ # Apply custom CSS for button styling
387
+ st.markdown("""
388
+ <style>
389
+ .stButton>button {
390
+ width: 100%;
391
+ font-size: 12px;
392
+ padding: 5px;
393
+ margin: 0;
394
+ }
395
+ </style>
396
+ """, unsafe_allow_html=True)
397
+
398
  st.title("Material Library")
399
 
 
400
  with st.expander("Help & Information"):
401
  display_materials_help()
402
 
 
403
  tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
404
 
 
405
  with tab1:
406
+ st.session_state.active_tab = "Materials"
407
  display_materials_tab()
408
 
 
409
  with tab2:
410
+ st.session_state.active_tab = "Fenestrations"
411
  display_fenestrations_tab()
412
 
 
413
  col1, col2 = st.columns(2)
414
 
415
  with col1:
 
422
  st.session_state.current_page = "Construction"
423
  st.rerun()
424
 
425
+ def handle_material_action(action: Dict[str, Any]):
426
+ """Handle material-related actions."""
427
+ if action["action"] == "preview":
428
+ material = action["material"]
429
+ name = action["name"]
430
+ st.session_state.material_editor = {
431
+ "name": name,
432
+ "category": material["category"],
433
+ "thermal_conductivity": material["thermal_conductivity"],
434
+ "density": material["density"],
435
+ "specific_heat": material["specific_heat"],
436
+ "default_thickness": material["default_thickness"],
437
+ "embodied_carbon": material["embodied_carbon"],
438
+ "cost": material["cost"],
439
+ "absorptivity": material["absorptivity"],
440
+ "emissivity": material["emissivity"],
441
+ "colour": material["colour"],
442
+ "edit_mode": False,
443
+ "original_name": name,
444
+ "is_library": action["is_library"]
445
+ }
446
+ st.session_state.material_form_state = {
447
+ "name": name,
448
+ "category": material["category"],
449
+ "thermal_conductivity": float(material["thermal_conductivity"]),
450
+ "density": float(material["density"]),
451
+ "specific_heat": float(material["specific_heat"]),
452
+ "default_thickness": float(material["default_thickness"]),
453
+ "embodied_carbon": float(material["embodied_carbon"]),
454
+ "cost": float(material["cost"]),
455
+ "absorptivity": float(material["absorptivity"]),
456
+ "emissivity": float(material["emissivity"]),
457
+ "colour": material["colour"]
458
+ }
459
+ logger.info(f"Previewed material '{name}'")
460
+ st.rerun()
461
+ elif action["action"] == "copy":
462
+ name = action["name"]
463
+ material = action["material"]
464
+ new_name = f"{name}_Project"
465
+ counter = 1
466
+ library_materials = st.session_state.project_data["materials"]["library"]
467
+ while new_name in st.session_state.project_data["materials"]["project"] or new_name in library_materials:
468
+ new_name = f"{name}_Project_{counter}"
469
+ counter += 1
470
+ st.session_state.project_data["materials"]["project"][new_name] = material.copy()
471
+ st.success(f"Material '{new_name}' copied to project.")
472
+ logger.info(f"Copied library material '{name}' as '{new_name}' to project")
473
+ # Update project materials table
474
+ update_project_materials_table()
475
+ st.rerun()
476
+ elif action["action"] == "edit":
477
+ material = action["material"]
478
+ name = action["name"]
479
+ st.session_state.material_editor = {
480
+ "name": name,
481
+ "category": material["category"],
482
+ "thermal_conductivity": material["thermal_conductivity"],
483
+ "density": material["density"],
484
+ "specific_heat": material["specific_heat"],
485
+ "default_thickness": material["default_thickness"],
486
+ "embodied_carbon": material["embodied_carbon"],
487
+ "cost": material["cost"],
488
+ "absorptivity": material["absorptivity"],
489
+ "emissivity": material["emissivity"],
490
+ "colour": material["colour"],
491
+ "edit_mode": True,
492
+ "original_name": name,
493
+ "is_library": False
494
+ }
495
+ st.session_state.material_form_state = {
496
+ "name": name,
497
+ "category": material["category"],
498
+ "thermal_conductivity": float(material["thermal_conductivity"]),
499
+ "density": float(material["density"]),
500
+ "specific_heat": float(material["specific_heat"]),
501
+ "default_thickness": float(material["default_thickness"]),
502
+ "embodied_carbon": float(material["embodied_carbon"]),
503
+ "cost": float(material["cost"]),
504
+ "absorptivity": float(material["absorptivity"]),
505
+ "emissivity": float(material["emissivity"]),
506
+ "colour": material["colour"]
507
+ }
508
+ logger.info(f"Editing material '{name}'")
509
+ st.rerun()
510
+ elif action["action"] == "delete":
511
+ name = action["name"]
512
+ is_in_use = check_material_in_use(name)
513
+ if is_in_use:
514
+ st.error(f"Cannot delete material '{name}' because it is in use in constructions.")
515
+ else:
516
+ del st.session_state.project_data["materials"]["project"][name]
517
+ st.success(f"Material '{name}' deleted from project.")
518
+ logger.info(f"Deleted material '{name}' from project")
519
+ # Update project materials table
520
+ update_project_materials_table()
521
+ st.rerun()
522
+
523
+ def handle_fenestration_action(action: Dict[str, Any]):
524
+ """Handle fenestration-related actions."""
525
+ if action["action"] == "preview":
526
+ fenestration = action["fenestration"]
527
+ name = action["name"]
528
+ st.session_state.fenestration_editor = {
529
+ "name": name,
530
+ "type": fenestration["type"],
531
+ "u_value": fenestration["u_value"],
532
+ "shgc": fenestration["shgc"],
533
+ "visible_trans": fenestration["visible_transmittance"],
534
+ "thickness": fenestration["thickness"],
535
+ "embodied_carbon": fenestration["embodied_carbon"],
536
+ "cost": fenestration["cost"],
537
+ "edit_mode": False,
538
+ "original_name": name,
539
+ "is_library": action["is_library"]
540
+ }
541
+ st.session_state.fenestration_form_state = {
542
+ "name": name,
543
+ "type": fenestration["type"],
544
+ "u_value": float(fenestration["u_value"]),
545
+ "shgc": float(fenestration["shgc"]),
546
+ "visible_trans": float(fenestration["visible_transmittance"]),
547
+ "thickness": float(fenestration["thickness"]),
548
+ "embodied_carbon": float(fenestration["embodied_carbon"]),
549
+ "cost": float(fenestration["cost"])
550
+ }
551
+ logger.info(f"Previewed fenestration '{name}'")
552
+ st.rerun()
553
+ elif action["action"] == "copy":
554
+ name = action["name"]
555
+ fenestration = action["fenestration"]
556
+ new_name = f"{name}_Project"
557
+ counter = 1
558
+ library_fenestrations = st.session_state.project_data["fenestrations"]["library"]
559
+ while new_name in st.session_state.project_data["fenestrations"]["project"] or new_name in library_fenestrations:
560
+ new_name = f"{name}_Project_{counter}"
561
+ counter += 1
562
+ st.session_state.project_data["fenestrations"]["project"][new_name] = fenestration.copy()
563
+ st.success(f"Fenestration '{new_name}' copied to project.")
564
+ logger.info(f"Copied library fenestration '{name}' as '{new_name}' to project")
565
+ # Update project fenestrations table
566
+ update_project_fenestrations_table()
567
+ st.rerun()
568
+ elif action["action"] == "edit":
569
+ fenestration = action["fenestration"]
570
+ name = action["name"]
571
+ st.session_state.fenestration_editor = {
572
+ "name": name,
573
+ "type": fenestration["type"],
574
+ "u_value": fenestration["u_value"],
575
+ "shgc": fenestration["shgc"],
576
+ "visible_trans": fenestration["visible_transmittance"],
577
+ "thickness": fenestration["thickness"],
578
+ "embodied_carbon": fenestration["embodied_carbon"],
579
+ "cost": fenestration["cost"],
580
+ "edit_mode": True,
581
+ "original_name": name,
582
+ "is_library": False
583
+ }
584
+ st.session_state.fenestration_form_state = {
585
+ "name": name,
586
+ "type": fenestration["type"],
587
+ "u_value": float(fenestration["u_value"]),
588
+ "shgc": float(fenestration["shgc"]),
589
+ "visible_trans": float(fenestration["visible_transmittance"]),
590
+ "thickness": float(fenestration["thickness"]),
591
+ "embodied_carbon": float(fenestration["embodied_carbon"]),
592
+ "cost": float(fenestration["cost"])
593
+ }
594
+ logger.info(f"Editing fenestration '{name}'")
595
+ st.rerun()
596
+ elif action["action"] == "delete":
597
+ name = action["name"]
598
+ is_in_use = check_fenestration_in_use(name)
599
+ if is_in_use:
600
+ st.error(f"Cannot delete fenestration '{name}' because it is in use in components.")
601
+ else:
602
+ del st.session_state.project_data["fenestrations"]["project"][name]
603
+ st.success(f"Fenestration '{name}' deleted from project.")
604
+ logger.info(f"Deleted fenestration '{name}' from project")
605
+ # Update project fenestrations table
606
+ update_project_fenestrations_table()
607
+ st.rerun()
608
+
609
+ def update_project_materials_table():
610
+ """Update the project materials table in session state."""
611
+ project_materials = st.session_state.project_data["materials"]["project"]
612
+ data = []
613
+ for name, props in project_materials.items():
614
+ thermal_mass = calculate_thermal_mass(props["density"], props["specific_heat"], props["default_thickness"])
615
+ thermal_mass_category = categorize_thermal_mass(thermal_mass)
616
+ u_value = calculate_u_value(props["thermal_conductivity"], props["default_thickness"])
617
+ data.append({
618
+ "Name": name,
619
+ "Category": props["category"],
620
+ "Thermal Conductivity (W/m·K)": props["thermal_conductivity"],
621
+ "Density (kg/m³)": props["density"],
622
+ "Specific Heat (J/kg·K)": props["specific_heat"],
623
+ "Thickness (m)": props["default_thickness"],
624
+ "Embodied Carbon (kg CO₂e/m³)": props["embodied_carbon"],
625
+ "Cost (USD/m³)": props["cost"],
626
+ "Absorptivity": props["absorptivity"],
627
+ "Emissivity": props["emissivity"],
628
+ "Colour": props["colour"],
629
+ "Thermal Mass": thermal_mass_category,
630
+ "U-Value (W/m²·K)": u_value
631
+ })
632
+ st.session_state.project_data["materials"]["table"] = pd.DataFrame(data)
633
+ logger.debug(f"Updated project materials table with {len(data)} entries")
634
+
635
+ def update_project_fenestrations_table():
636
+ """Update the project fenestrations table in session state."""
637
+ project_fenestrations = st.session_state.project_data["fenestrations"]["project"]
638
+ data = []
639
+ for name, props in sorted(project_fenestrations.items()):
640
+ data.append({
641
+ "Name": name,
642
+ "Type": props["type"],
643
+ "U-Value (W/m²·K)": props["u_value"],
644
+ "SHGC": props["shgc"],
645
+ "Visible Transmittance": props["visible_transmittance"],
646
+ "Thickness (m)": props["thickness"],
647
+ "Embodied Carbon (kg/m²)": props["embodied_carbon"],
648
+ "Cost (USD/m²)": props["cost"]
649
+ })
650
+ st.session_state.project_data["fenestrations"]["table"] = pd.DataFrame(data)
651
+ logger.debug(f"Updated project fenestrations table with {len(data)} entries")
652
+
653
  def display_materials_tab():
654
  """Display the materials tab content."""
655
+ logger.debug("Rendering materials tab")
656
+ logger.debug(f"Project materials: {list(st.session_state.project_data['materials']['project'].keys())}")
657
 
658
+ st.subheader("Materials")
659
+ col1, col2 = st.columns([3, 2])
660
 
 
661
  with col1:
662
+ filter_options = ["All"] + MATERIAL_CATEGORIES
663
+ category = st.selectbox("Filter by Category", filter_options, key="material_filter")
664
+
665
  st.subheader("Library Materials")
666
+ with st.container():
667
+ library_materials = st.session_state.project_data["materials"]["library"]
668
+ filtered_materials = []
669
+ if category == "All":
670
+ filtered_materials = list(library_materials.values())
671
+ else:
672
+ filtered_materials = [m for m in library_materials.values() if m["category"] == category]
673
+
674
+ if filtered_materials:
675
+ cols = st.columns([2, 1, 1, 1, 1])
676
+ cols[0].write("**Name**")
677
+ cols[1].write("**Thermal Mass**")
678
+ cols[2].write("**U-Value (W/m²·K)**")
679
+ cols[3].write("**Preview**")
680
+ cols[4].write("**Copy**")
681
+
682
+ for material in filtered_materials:
683
+ cols = st.columns([2, 1, 1, 1, 1])
684
+ name = [k for k, v in library_materials.items() if v == material][0]
685
+ thermal_mass = calculate_thermal_mass(material["density"], material["specific_heat"], material["default_thickness"])
686
+ thermal_mass_category = categorize_thermal_mass(thermal_mass)
687
+ u_value = calculate_u_value(material["thermal_conductivity"], material["default_thickness"])
688
+ cols[0].write(name)
689
+ cols[1].write(thermal_mass_category)
690
+ cols[2].write(f"{u_value:.3f}")
691
+
692
+ preview_key = get_stable_button_key("lib_mat", name, "preview")
693
+ copy_key = get_stable_button_key("lib_mat", name, "copy")
694
+
695
+ cols[3].button("Preview", key=preview_key, on_click=handle_material_action, args=([{
696
+ "action": "preview",
697
+ "name": name,
698
+ "material": material,
699
+ "is_library": True
700
+ }],))
701
+
702
+ cols[4].button("Copy", key=copy_key, on_click=handle_material_action, args=([{
703
+ "action": "copy",
704
+ "name": name,
705
+ "material": material
706
+ }],))
707
+
708
+ else:
709
+ st.info("No materials found in the selected category.")
710
+
711
+ project_category = st.selectbox("Filter Project by Category", filter_options, key="project_material_filter")
712
  st.subheader("Project Materials")
713
+ with st.container():
714
+ project_materials = st.session_state.project_data["materials"]["project"]
715
+ filtered_project_materials = []
716
+ if project_category == "All":
717
+ filtered_project_materials = list(project_materials.values())
718
+ else:
719
+ filtered_project_materials = [m for m in project_materials.values() if m["category"] == project_category]
720
+
721
+ if filtered_project_materials:
722
+ cols = st.columns([2, 1, 1, 1, 1])
723
+ cols[0].write("**Name**")
724
+ cols[1].write("**Thermal Mass**")
725
+ cols[2].write("**U-Value (W/m²·K)**")
726
+ cols[3].write("**Edit**")
727
+ cols[4].write("**Delete**")
728
+
729
+ for material in filtered_project_materials:
730
+ cols = st.columns([2, 1, 1, 1, 1])
731
+ name = [k for k, v in project_materials.items() if v == material][0]
732
+ thermal_mass = calculate_thermal_mass(material["density"], material["specific_heat"], material["default_thickness"])
733
+ thermal_mass_category = categorize_thermal_mass(thermal_mass)
734
+ u_value = calculate_u_value(material["thermal_conductivity"], material["default_thickness"])
735
+ cols[0].write(name)
736
+ cols[1].write(thermal_mass_category)
737
+ cols[2].write(f"{u_value:.3f}")
738
+
739
+ edit_key = get_stable_button_key("proj_mat", name, "edit")
740
+ delete_key = get_stable_button_key("proj_mat", name, "delete")
741
+
742
+ cols[3].button("Edit", key=edit_key, on_click=handle_material_action, args=([{
743
+ "action": "edit",
744
+ "name": name,
745
+ "material": material
746
+ }],))
747
+
748
+ cols[4].button("Delete", key=delete_key, on_click=handle_material_action, args=([{
749
+ "action": "delete",
750
+ "name": name
751
+ }],))
752
+
753
+ else:
754
+ st.info("No project materials in the selected category.")
755
+
756
+ st.subheader("All Project Materials")
757
+ if st.session_state.project_data["materials"]["table"] is not None:
758
+ st.dataframe(st.session_state.project_data["materials"]["table"], use_container_width=True, hide_index=True)
759
+ else:
760
+ st.info("No project materials to display.")
761
 
762
+ with col2:
763
+ st.subheader("Material Editor/Creator")
764
+ display_material_editor()
 
765
 
766
  def display_fenestrations_tab():
767
  """Display the fenestrations tab content."""
768
+ logger.debug("Rendering fenestrations tab")
769
+ logger.debug(f"Project fenestrations: {list(st.session_state.project_data['fenestrations']['project'].keys())}")
770
 
771
+ st.subheader("Fenestrations")
772
+ col1, col2 = st.columns([3, 2])
773
 
 
774
  with col1:
775
+ filter_options = ["All"] + FENESTRATION_TYPES
776
+ fen_type = st.selectbox("Filter by Type", filter_options, key="fenestration_filter")
777
+
778
  st.subheader("Library Fenestrations")
779
+ with st.container():
780
+ library_fenestrations = st.session_state.project_data["fenestrations"]["library"]
781
+ filtered_fenestrations = []
782
+ if fen_type == "All":
783
+ filtered_fenestrations = list(library_fenestrations.values())
784
+ else:
785
+ filtered_fenestrations = [f for f in library_fenestrations.values() if f["type"] == fen_type]
786
+
787
+ if filtered_fenestrations:
788
+ cols = st.columns([2, 1, 1, 1, 1])
789
+ cols[0].write("**Name**")
790
+ cols[1].write("**Type**")
791
+ cols[2].write("**U-Value (W/m²·K)**")
792
+ cols[3].write("**Preview**")
793
+ cols[4].write("**Copy**")
794
+
795
+ for fenestration in filtered_fenestrations:
796
+ cols = st.columns([2, 1, 1, 1, 1])
797
+ name = [k for k, v in library_fenestrations.items() if v == fenestration][0]
798
+ cols[0].write(name)
799
+ cols[1].write(fenestration["type"])
800
+ cols[2].write(f"{fenestration['u_value']:.2f}")
801
+
802
+ preview_key = get_stable_button_key("lib_fen", name, "preview")
803
+ copy_key = get_stable_button_key("lib_fen", name, "copy")
804
+
805
+ cols[3].button("Preview", key=preview_key, on_click=handle_fenestration_action, args=([{
806
+ "action": "preview",
807
+ "name": name,
808
+ "fenestration": fenestration,
809
+ "is_library": True
810
+ }],))
811
+
812
+ cols[4].button("Copy", key=copy_key, on_click=handle_fenestration_action, args=([{
813
+ "action": "copy",
814
+ "name": name,
815
+ "fenestration": fenestration
816
+ }],))
817
+
818
+ else:
819
+ st.info("No fenestrations found in the selected type.")
820
+
821
+ project_fen_type = st.selectbox("Filter Project by Type", filter_options, key="project_fenestration_filter")
822
  st.subheader("Project Fenestrations")
823
+ with st.container():
824
+ project_fenestrations = st.session_state.project_data["fenestrations"]["project"]
825
+ filtered_project_fenestrations = []
826
+ if project_fen_type == "All":
827
+ filtered_project_fenestrations = list(project_fenestrations.values())
828
+ else:
829
+ filtered_project_fenestrations = [f for f in project_fenestrations.values() if f["type"] == project_fen_type]
830
+
831
+ if filtered_project_fenestrations:
832
+ cols = st.columns([2, 1, 1, 1, 1])
833
+ cols[0].write("**Name**")
834
+ cols[1].write("**Type**")
835
+ cols[2].write("**U-Value (W/m²·K)**")
836
+ cols[3].write("**Edit**")
837
+ cols[4].write("**Delete**")
838
+
839
+ for fenestration in filtered_project_fenestrations:
840
+ cols = st.columns([2, 1, 1, 1, 1])
841
+ name = [k for k, v in project_fenestrations.items() if v == fenestration][0]
842
+ cols[0].write(name)
843
+ cols[1].write(fenestration["type"])
844
+ cols[2].write(f"{fenestration['u_value']:.2f}")
845
+
846
+ edit_key = get_stable_button_key("proj_fen", name, "edit")
847
+ delete_key = get_stable_button_key("proj_fen", name, "delete")
848
+
849
+ cols[3].button("Edit", key=edit_key, on_click=handle_fenestration_action, args=([{
850
+ "action": "edit",
851
+ "name": name,
852
+ "fenestration": fenestration
853
+ }],))
854
+
855
+ cols[4].button("Delete", key=delete_key, on_click=handle_fenestration_action, args=([{
856
+ "action": "delete",
857
+ "name": name
858
+ }],))
859
+
860
+ else:
861
+ st.info("No project fenestrations in the selected type.")
862
+
863
+ st.subheader("All Project Fenestrations")
864
+ if st.session_state.project_data["fenestrations"]["table"] is not None:
865
+ st.dataframe(st.session_state.project_data["fenestrations"]["table"], use_container_width=True, hide_index=True)
866
+ else:
867
+ st.info("No project fenestrations to display.")
868
 
869
+ with col2:
870
+ st.subheader("Fenestration Editor/Creator")
871
+ display_fenestration_editor()
 
872
 
873
  def initialize_materials():
874
  """Initialize materials in session state if not present."""
875
  if "materials" not in st.session_state.project_data:
876
  st.session_state.project_data["materials"] = {
877
+ "library": DEFAULT_MATERIALS.copy(),
878
+ "project": {},
879
+ "table": None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880
  }
881
+ logger.debug("Materials initialized")
882
 
883
  def initialize_fenestrations():
884
  """Initialize fenestrations in session state if not present."""
885
  if "fenestrations" not in st.session_state.project_data:
886
  st.session_state.project_data["fenestrations"] = {
887
+ "library": DEFAULT_FENESTRATIONS.copy(),
888
+ "project": {},
889
+ "table": None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  }
891
+ logger.debug("Fenestrations initialized")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
892
 
893
  def display_material_editor():
894
  """Display the material editor form."""
895
+ with st.form("material_editor_form", clear_on_submit=True):
896
+ editor_state = st.session_state.material_editor
897
+ form_state = st.session_state.material_form_state
898
+ is_library = editor_state.get("is_library", False)
899
+
900
  name = st.text_input(
901
  "Material Name",
902
+ value=form_state.get("name", editor_state["name"]),
903
+ help="Enter a unique name for the material.",
904
+ disabled=is_library
905
  )
906
 
 
907
  col1, col2 = st.columns(2)
908
 
909
  with col1:
910
+ category_index = MATERIAL_CATEGORIES.index(form_state.get("category", editor_state.get("category", MATERIAL_CATEGORIES[0])))
911
  category = st.selectbox(
912
  "Category",
913
  MATERIAL_CATEGORIES,
914
+ index=category_index,
915
+ help="Select the material category.",
916
+ disabled=is_library
917
  )
918
 
 
919
  thermal_conductivity = st.number_input(
920
  "Thermal Conductivity (W/m·K)",
921
  min_value=0.001,
922
  max_value=1000.0,
923
+ value=float(form_state.get("thermal_conductivity", editor_state.get("thermal_conductivity", 0.5))),
924
  format="%.3f",
925
+ help="Thermal conductivity in W/m·K. Lower values indicate better insulation.",
926
+ disabled=is_library
927
  )
928
 
 
929
  density = st.number_input(
930
  "Density (kg/m³)",
931
  min_value=1.0,
932
  max_value=20000.0,
933
+ value=float(form_state.get("density", editor_state.get("density", 1000.0))),
934
  format="%.1f",
935
+ help="Material density in kg/m³.",
936
+ disabled=is_library
937
  )
938
 
939
  with col2:
 
940
  specific_heat = st.number_input(
941
  "Specific Heat (J/kg·K)",
942
  min_value=100.0,
943
  max_value=10000.0,
944
+ value=float(form_state.get("specific_heat", editor_state.get("specific_heat", 1000.0))),
945
  format="%.1f",
946
+ help="Specific heat capacity in J/kg·K. Higher values indicate better thermal mass.",
947
+ disabled=is_library
948
  )
949
 
950
+ default_thickness = st.number_input(
951
+ "Thickness (m)",
 
 
952
  min_value=0.001,
953
  max_value=0.5,
954
+ value=float(form_state.get("default_thickness", editor_state.get("default_thickness", 0.05))),
955
  format="%.3f",
956
+ help="Thickness for this material in meters.",
957
+ disabled=is_library
958
  )
959
 
960
+ absorptivity = st.number_input(
961
+ "Absorptivity",
962
+ min_value=0.0,
963
+ max_value=1.0,
964
+ value=float(form_state.get("absorptivity", editor_state.get("absorptivity", 0.60))),
965
+ format="%.2f",
966
+ help="Solar radiation absorbed (0-1).",
967
+ disabled=is_library
968
+ )
969
+
970
+ col1, col2 = st.columns(2)
971
+
972
+ with col1:
973
+ emissivity = st.number_input(
974
+ "Emissivity",
975
+ min_value=0.0,
976
+ max_value=1.0,
977
+ value=float(form_state.get("emissivity", editor_state.get("emissivity", 0.90))),
978
+ format="%.2f",
979
+ help="Ratio of radiation emitted (0-1).",
980
+ disabled=is_library
981
+ )
982
+
983
+ with col2:
984
+ colour_index = COLOUR_CATEGORIES.index(form_state.get("colour", editor_state.get("colour", "Medium"))) if form_state.get("colour", editor_state.get("colour", "Medium")) in COLOUR_CATEGORIES else 0
985
+ colour = st.selectbox(
986
+ "Colour Category",
987
+ COLOUR_CATEGORIES,
988
+ index=colour_index,
989
+ help="Colour category affecting absorptivity.",
990
+ disabled=is_library
991
  )
992
 
 
993
  st.subheader("Additional Properties")
994
  col1, col2 = st.columns(2)
995
 
996
  with col1:
 
997
  embodied_carbon = st.number_input(
998
  "Embodied Carbon (kg CO₂e/m³)",
999
  min_value=0.0,
1000
  max_value=10000.0,
1001
+ value=float(form_state.get("embodied_carbon", editor_state.get("embodied_carbon", 100.0))),
1002
  format="%.1f",
1003
+ help="Embodied carbon in kg CO₂e per cubic meter.",
1004
+ disabled=is_library
1005
  )
1006
 
1007
  with col2:
 
1008
  cost = st.number_input(
1009
  "Cost (USD/m³)",
1010
  min_value=0.0,
1011
  max_value=10000.0,
1012
+ value=float(form_state.get("cost", editor_state.get("cost", 100.0))),
1013
  format="%.1f",
1014
+ help="Material cost in USD per cubic meter.",
1015
+ disabled=is_library
1016
  )
1017
 
 
1018
  col1, col2 = st.columns(2)
1019
 
1020
  with col1:
1021
+ submit_button = st.form_submit_button("Save Material", disabled=is_library)
1022
 
1023
  with col2:
1024
  clear_button = st.form_submit_button("Clear Form")
 
 
 
 
 
 
 
 
 
1025
 
1026
+ if submit_button and not is_library:
1027
+ st.session_state.material_form_state = {
1028
+ "name": name,
 
 
 
 
1029
  "category": category,
1030
  "thermal_conductivity": thermal_conductivity,
1031
  "density": density,
1032
  "specific_heat": specific_heat,
 
1033
  "default_thickness": default_thickness,
1034
  "embodied_carbon": embodied_carbon,
1035
+ "cost": cost,
1036
+ "absorptivity": absorptivity,
1037
+ "emissivity": emissivity,
1038
+ "colour": colour
1039
  }
1040
+ success, message = validate_material(
1041
+ name, category, thermal_conductivity, density, specific_heat,
1042
+ default_thickness, embodied_carbon, cost,
1043
+ editor_state["edit_mode"], editor_state["original_name"],
1044
+ absorptivity, emissivity, colour
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1045
  )
1046
+ if not success:
1047
+ st.error(message)
1048
+ else:
1049
+ material_data = {
1050
+ "category": category,
1051
+ "thermal_conductivity": thermal_conductivity,
1052
+ "density": density,
1053
+ "specific_heat": specific_heat,
1054
+ "default_thickness": default_thickness,
1055
+ "embodied_carbon": embodied_carbon,
1056
+ "cost": cost,
1057
+ "absorptivity": absorptivity,
1058
+ "emissivity": emissivity,
1059
+ "colour": colour
 
1060
  }
1061
+ if editor_state["edit_mode"]:
1062
+ original_name = editor_state["original_name"]
1063
+ if original_name != name:
1064
+ del st.session_state.project_data["materials"]["project"][original_name]
1065
+ st.session_state.project_data["materials"]["project"][name] = material_data
1066
+ st.success(f"Material '{name}' updated successfully.")
1067
+ logger.info(f"Updated material '{name}' in project")
 
 
 
 
 
 
 
 
1068
  else:
1069
+ st.session_state.project_data["materials"]["project"][name] = material_data
1070
+ st.success(f"Material '{name}' added to your project.")
1071
+ logger.info(f"Added new material '{name}' to project")
1072
+ update_project_materials_table()
1073
+ reset_material_editor()
1074
+ st.rerun()
1075
+
1076
+ if clear_button:
1077
+ reset_material_editor()
1078
 
1079
  def display_fenestration_editor():
1080
  """Display the fenestration editor form."""
1081
+ with st.form("fenestration_editor_form", clear_on_submit=True):
1082
+ editor_state = st.session_state.fenestration_editor
1083
+ form_state = st.session_state.fenestration_form_state
1084
+ is_library = editor_state.get("is_library", False)
1085
+
1086
  name = st.text_input(
1087
  "Fenestration Name",
1088
+ value=form_state.get("name", editor_state["name"]),
1089
+ help="Enter a unique name for the fenestration.",
1090
+ disabled=is_library
1091
  )
1092
 
 
1093
  col1, col2 = st.columns(2)
1094
 
1095
  with col1:
1096
+ type_index = FENESTRATION_TYPES.index(form_state.get("type", editor_state["type"])) if form_state.get("type", editor_state["type"]) in FENESTRATION_TYPES else 0
1097
  fenestration_type = st.selectbox(
1098
  "Type",
1099
  FENESTRATION_TYPES,
1100
+ index=type_index,
1101
+ help="Select the fenestration type.",
1102
+ disabled=is_library
1103
  )
1104
 
 
1105
  u_value = st.number_input(
1106
  "U-Value (W/m²·K)",
1107
  min_value=0.1,
1108
  max_value=10.0,
1109
+ value=float(form_state.get("u_value", editor_state.get("u_value", 2.8))),
1110
  format="%.2f",
1111
+ help="U-value in W/m²·K.",
1112
+ disabled=is_library
1113
  )
1114
 
1115
+ with col2:
1116
  shgc = st.number_input(
1117
  "Solar Heat Gain Coefficient (SHGC)",
1118
  min_value=0.0,
1119
  max_value=1.0,
1120
+ value=float(form_state.get("shgc", editor_state.get("shgc", 0.7))),
1121
  format="%.2f",
1122
+ help="Solar Heat Gain Coefficient (0-1).",
1123
+ disabled=is_library
1124
  )
1125
+
1126
+ visible_trans = st.number_input(
 
 
1127
  "Visible Transmittance",
1128
  min_value=0.0,
1129
  max_value=1.0,
1130
+ value=float(form_state.get("visible_trans", editor_state.get("visible_trans", 0.8))),
1131
  format="%.2f",
1132
+ help="Visible light transmission (0-1).",
1133
+ disabled=is_library
1134
  )
1135
 
 
1136
  thickness = st.number_input(
1137
  "Thickness (m)",
1138
  min_value=0.001,
1139
  max_value=0.1,
1140
+ value=float(form_state.get("thickness", editor_state.get("thickness", 0.024))),
1141
  format="%.3f",
1142
+ help="Thickness in meters.",
1143
+ disabled=is_library
1144
  )
1145
 
 
1146
  st.subheader("Additional Properties")
1147
+
1148
  col1, col2 = st.columns(2)
1149
 
1150
  with col1:
 
1151
  embodied_carbon = st.number_input(
1152
  "Embodied Carbon (kg CO₂e/m²)",
1153
  min_value=0.0,
1154
  max_value=5000.0,
1155
+ value=float(form_state.get("embodied_carbon", editor_state.get("embodied_carbon", 900.0))),
1156
  format="%.1f",
1157
+ help="Embodied carbon in kg CO₂e/m².",
1158
+ disabled=is_library
1159
  )
1160
 
1161
  with col2:
 
1162
  cost = st.number_input(
1163
  "Cost (USD/m²)",
1164
  min_value=0.0,
1165
  max_value=5000.0,
1166
+ value=float(form_state.get("cost", editor_state.get("cost", 200.0))),
1167
  format="%.1f",
1168
+ help="Fenestration cost in USD/m².",
1169
+ disabled=is_library
1170
  )
1171
 
 
1172
  col1, col2 = st.columns(2)
1173
 
1174
  with col1:
1175
+ submit_button = st.form_submit_button("Save Fenestration", disabled=is_library)
1176
 
1177
  with col2:
1178
  clear_button = st.form_submit_button("Clear Form")
 
 
 
 
 
 
 
 
 
1179
 
1180
+ if submit_button and not is_library:
1181
+ st.session_state.fenestration_form_state = {
1182
+ "name": name,
 
 
 
 
1183
  "type": fenestration_type,
1184
  "u_value": u_value,
1185
  "shgc": shgc,
1186
+ "visible_trans": visible_trans,
1187
  "thickness": thickness,
1188
  "embodied_carbon": embodied_carbon,
1189
  "cost": cost
1190
  }
1191
+ success, message = validate_fenestration(
1192
+ name, fenestration_type, u_value, shgc, visible_trans, thickness,
1193
+ embodied_carbon, cost, editor_state["edit_mode"], editor_state["original_name"]
1194
+ )
1195
+ if not success:
1196
+ st.error(message)
 
 
 
 
 
 
 
1197
  else:
1198
+ fenestration_data = {
1199
+ "type": fenestration_type,
1200
+ "u_value": u_value,
1201
+ "shgc": shgc,
1202
+ "visible_transmittance": visible_trans,
1203
+ "thickness": thickness,
1204
+ "embodied_carbon": embodied_carbon,
1205
+ "cost": cost
1206
+ }
1207
+ if editor_state["edit_mode"]:
1208
+ original_name = editor_state["original_name"]
1209
+ if original_name != name:
1210
+ del st.session_state.project_data["fenestrations"]["project"][original_name]
1211
+ st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
1212
+ st.success(f"Fenestration '{name}' updated successfully.")
1213
+ logger.info(f"Updated fenestration '{name}' in project")
1214
+ else:
1215
+ st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
1216
+ st.success(f"Fenestration '{name}' added to your project.")
1217
+ logger.info(f"Added new fenestration '{name}' to project")
1218
+ update_project_fenestrations_table()
1219
+ reset_fenestration_editor()
1220
+ st.rerun()
1221
+
1222
+ if clear_button:
1223
  reset_fenestration_editor()
 
 
 
 
 
1224
 
1225
  def validate_material(
1226
  name: str, category: str, thermal_conductivity: float, density: float,
1227
+ specific_heat: float, default_thickness: float, embodied_carbon: float,
1228
+ cost: float, edit_mode: bool, original_name: str,
1229
+ absorptivity: float, emissivity: float, colour: str
1230
+ ) -> Tuple[bool, str]:
1231
  """
1232
  Validate material inputs.
1233
+ Returns (success, message) tuple.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1234
  """
 
 
 
1235
  if not name or name.strip() == "":
1236
+ return False, "Material name is required."
1237
 
1238
+ if (not edit_mode or name != original_name) and name in st.session_state.project_data["materials"]["project"]:
1239
+ return False, f"Material name '{name}' already exists in project materials."
 
 
1240
 
 
1241
  if category not in MATERIAL_CATEGORIES:
1242
+ return False, "Invalid material category."
1243
 
 
1244
  if thermal_conductivity <= 0:
1245
+ return False, "Thermal conductivity must be greater than zero."
1246
 
 
1247
  if density <= 0:
1248
+ return False, "Density must be greater than zero."
1249
 
 
1250
  if specific_heat <= 0:
1251
+ return False, "Specific heat must be greater than zero."
1252
 
1253
+ if default_thickness <= 0:
1254
+ return False, "Thickness must be greater than zero."
 
 
 
1255
 
 
 
 
 
 
1256
  if embodied_carbon < 0:
1257
+ return False, "Embodied carbon cannot be negative."
1258
 
 
1259
  if cost < 0:
1260
+ return False, "Cost cannot be negative."
1261
+
1262
+ if absorptivity < 0 or absorptivity > 1:
1263
+ return False, "Absorptivity must be between 0 and 1."
1264
+
1265
+ if emissivity < 0 or emissivity > 1:
1266
+ return False, "Emissivity must be between 0 and 1."
1267
+
1268
+ if colour not in COLOUR_CATEGORIES:
1269
+ return False, "Invalid colour category."
1270
 
1271
+ colour_ranges = {
1272
+ "Light": (0.30, 0.50),
1273
+ "Medium": (0.60, 0.70),
1274
+ "Dark": (0.85, 0.95),
1275
+ "Reflective": (0.10, 0.20)
1276
+ }
1277
+ min_abs, max_abs = colour_ranges[colour]
1278
+ if not (min_abs <= absorptivity <= max_abs):
1279
+ return False, f"Absorptivity for {colour} colour must be between {min_abs} and {max_abs}."
1280
+
1281
+ return True, ""
1282
 
1283
  def validate_fenestration(
1284
  name: str, fenestration_type: str, u_value: float, shgc: float,
1285
+ visible_trans: float, thickness: float, embodied_carbon: float,
1286
  cost: float, edit_mode: bool, original_name: str
1287
+ ) -> Tuple[bool, str]:
1288
  """
1289
  Validate fenestration inputs.
1290
+ Returns (success, message) tuple.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1291
  """
 
 
 
1292
  if not name or name.strip() == "":
1293
+ return False, "Fenestration name is required."
1294
 
1295
+ if (not edit_mode or name != original_name) and name in st.session_state.project_data["fenestrations"]["project"]:
1296
+ return False, f"Fenestration name '{name}' already exists in project."
 
 
1297
 
 
1298
  if fenestration_type not in FENESTRATION_TYPES:
1299
+ return False, "Invalid fenestration type."
1300
 
 
1301
  if u_value <= 0:
1302
+ return False, "U-value must be greater than zero."
1303
 
 
1304
  if shgc < 0 or shgc > 1:
1305
+ return False, "SHGC must be between 0 and 1."
1306
 
1307
+ if visible_trans < 0 or visible_trans > 1:
1308
+ return False, "Visible transmittance must be between 0 and 1."
 
1309
 
 
1310
  if thickness <= 0:
1311
+ return False, "Thickness must be greater than zero."
1312
+
 
1313
  if embodied_carbon < 0:
1314
+ return False, "Embodied carbon cannot be negative."
1315
 
 
1316
  if cost < 0:
1317
+ return False, "Cost cannot be negative."
1318
 
1319
+ return True, ""
1320
 
1321
  def reset_material_editor():
1322
+ """Reset the material editor and form state."""
1323
+ st.session_state.material_form_state = {
1324
+ "name": "",
1325
+ "category": MATERIAL_CATEGORIES[0],
1326
+ "thermal_conductivity": 0.5,
1327
+ "density": 1000.0,
1328
+ "specific_heat": 1000.0,
1329
+ "default_thickness": 0.05,
1330
+ "embodied_carbon": 100.0,
1331
+ "cost": 100.0,
1332
+ "absorptivity": 0.60,
1333
+ "emissivity": 0.90,
1334
+ "colour": "Medium"
1335
+ }
1336
  st.session_state.material_editor = {
1337
  "name": "",
1338
  "category": MATERIAL_CATEGORIES[0],
1339
  "thermal_conductivity": 0.5,
1340
  "density": 1000.0,
1341
  "specific_heat": 1000.0,
 
1342
  "default_thickness": 0.05,
1343
  "embodied_carbon": 100.0,
1344
  "cost": 100.0,
1345
+ "absorptivity": 0.60,
1346
+ "emissivity": 0.90,
1347
+ "colour": "Medium",
1348
  "edit_mode": False,
1349
+ "original_name": "",
1350
+ "is_library": False
1351
  }
1352
+ st.session_state.material_action = {"action": None, "id": None}
1353
+ logger.debug("Material editor reset")
1354
 
1355
  def reset_fenestration_editor():
1356
+ """Reset the fenestration editor and form state."""
1357
+ st.session_state.fenestration_form_state = {
1358
+ "name": "",
1359
+ "type": FENESTRATION_TYPES[0],
1360
+ "u_value": 2.8,
1361
+ "shgc": 0.7,
1362
+ "visible_trans": 0.8,
1363
+ "thickness": 0.024,
1364
+ "embodied_carbon": 900.0,
1365
+ "cost": 200.0
1366
+ }
1367
  st.session_state.fenestration_editor = {
1368
  "name": "",
1369
  "type": FENESTRATION_TYPES[0],
1370
  "u_value": 2.8,
1371
  "shgc": 0.7,
1372
+ "visible_trans": 0.8,
1373
  "thickness": 0.024,
1374
  "embodied_carbon": 900.0,
1375
  "cost": 200.0,
1376
  "edit_mode": False,
1377
+ "original_name": "",
1378
+ "is_library": False
1379
  }
1380
+ st.session_state.fenestration_action = {"action": None, "id": None}
1381
+ logger.debug("Fenestration editor reset")
1382
 
1383
  def check_material_in_use(material_name: str) -> bool:
1384
  """
1385
  Check if a material is in use in any constructions.
 
 
 
 
 
 
1386
  """
 
 
1387
  return False
1388
 
1389
+ def check_fenestration_in_use(name: str) -> bool:
1390
  """
1391
  Check if a fenestration is in use in any components.
 
 
 
 
 
 
1392
  """
 
 
1393
  return False
1394
 
1395
  def display_materials_help():
 
1401
 
1402
  **Materials Tab:**
1403
 
1404
+ - **Library Materials**: Pre-defined materials with standard thermal properties. Use 'Preview' to view details or 'Copy' to add to your project.
1405
+ - **Project Materials**: Materials you've added to your project. Use 'Edit' to modify or 'Delete' to remove.
1406
+ - **Material Editor/Creator**: Create new materials or edit existing project materials. Library materials can only be previewed.
1407
 
1408
  **Fenestrations Tab:**
1409
 
1410
+ - **Library Fenestrations**: Pre-defined windows, doors, and skylights. Use 'Preview' to view details or 'Copy' to add to your project.
1411
+ - **Project Fenestrations**: Fenestrations you've added to your project. Use 'Edit' to modify or 'Delete' to remove.
1412
+ - **Fenestration Editor/Creator**: Create new fenestrations or edit existing project fenestrations. Library fenestrations can only be previewed.
1413
 
1414
  **Key Properties:**
1415
 
1416
+ - **Thermal Conductivity (W/m·K)**: Rate of heat transfer through a material. Lower values indicate better insulation.
1417
+ - **Density (kg/m³)**: Mass per unit volume.
1418
+ - **Specific Heat (J/kg·K)**: Energy required to raise the temperature of 1 kg by 1 K. Higher values indicate better thermal mass.
1419
+ - **Thermal Mass (J/m²·K)**: Areal heat capacity (density × specific heat × thickness).
1420
+ - **U-Value (W/m²·K)**: Overall heat transfer coefficient, accounting for material and surfaces resistances. Lower values indicate better insulation.
1421
+ - **SHGC**: Solar Heat Gain Coefficient (0-1). Fraction of incident solar radiation that enters through a fenestration.
1422
+ - **Visible Transmittance**: Fraction of visible light that passes through a fenestration.
1423
+ - **Embodied Carbon**: Carbon emissions associated with material production, measured in kg CO₂e per unit volume or area.
1424
+ - **Cost**: Material cost in USD per unit volume or area.
1425
+ - **Absorptivity**: Fraction of solar radiation absorbed (0-1). Depends on colour category.
1426
+ - **Emissivity**: Ratio of radiation emitted by the material (0-1).
1427
+ - **Colour**: Category affecting absorptivity (Light, Medium, Dark, Reflective).
1428
 
1429
  **Workflow:**
1430
 
1431
  1. Browse the library materials and fenestrations
1432
+ 2. Copy items to your project or create custom ones
1433
  3. Edit properties as needed for your specific project
1434
  4. Continue to the Construction page to create assemblies using these materials
1435
+ """)