mabuseif commited on
Commit
a9ec9ed
verified
1 Parent(s): 0f8dd22

Update app/materials_library.py

Browse files
Files changed (1) hide show
  1. app/materials_library.py +66 -195
app/materials_library.py CHANGED
@@ -2,9 +2,8 @@
2
  BuildSustain - Material Library Module
3
 
4
  This module handles the material library functionality of the BuildSustain application,
5
- allowing users to manage building materials and doors (previously part of fenestrations)
6
- and fenestrations (windows, skylights). It provides both predefined library materials
7
- and the ability to create project-specific materials.
8
 
9
  Developed by: Dr Majed Abuseif, Deakin University
10
  漏 2025
@@ -20,7 +19,7 @@ from typing import Dict, List, Any, Optional, Tuple, Union
20
  from enum import Enum
21
 
22
  # Import default data and constants from centralized module
23
- from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS, DEFAULT_MATERIAL_PROPERTIES, DEFAULT_WINDOW_PROPERTIES, DEFAULT_DOOR_PROPERTIES
24
 
25
  # Configure logging
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -31,12 +30,7 @@ MATERIAL_CATEGORIES = [
31
  "Insulation",
32
  "Structural",
33
  "Finishing",
34
- "Sub-Structural",
35
- "Door"
36
- ]
37
-
38
- FENESTRATION_TYPES = [
39
- "Window/Skylight"
40
  ]
41
 
42
  # Surface resistances (EN ISO 6946 or ASHRAE standards)
@@ -48,13 +42,11 @@ class MaterialCategory(Enum):
48
  STRUCTURAL = "Structural"
49
  FINISHING = "Finishing"
50
  SUB_STRUCTURAL = "Sub-Structural"
51
- DOOR = "Door"
52
 
53
  class Material:
54
  def __init__(self, name: str, category: MaterialCategory, conductivity: float, density: float,
55
  specific_heat: float, default_thickness: float, embodied_carbon: float,
56
- absorptivity: float, price: float, emissivity: float, is_library: bool = True,
57
- u_value: Optional[float] = None):
58
  self.name = name
59
  self.category = category
60
  self.conductivity = conductivity
@@ -66,12 +58,9 @@ class Material:
66
  self.price = price
67
  self.emissivity = emissivity
68
  self.is_library = is_library
69
- self.u_value = u_value # For doors, u_value may be provided directly
70
 
71
  def get_thermal_mass(self) -> str:
72
  # Calculate areal heat capacity: 蟻 * cp * d (J/m虏路K)
73
- if self.category == MaterialCategory.DOOR:
74
- return "N/A" # Doors typically don't use thermal mass
75
  thermal_mass = self.density * self.specific_heat * self.default_thickness
76
  # Categorize based on thresholds
77
  if thermal_mass < 30000:
@@ -82,9 +71,6 @@ class Material:
82
  return "High"
83
 
84
  def get_u_value(self) -> float:
85
- # For doors, return provided u_value if available
86
- if self.category == MaterialCategory.DOOR and self.u_value is not None:
87
- return self.u_value
88
  # Calculate U-value: U = 1 / (R_si + (d / 位) + R_se) (W/m虏路K)
89
  if self.default_thickness > 0 and self.conductivity > 0:
90
  r_value = R_SI + (self.default_thickness / self.conductivity) + R_SE
@@ -92,12 +78,11 @@ class Material:
92
  return 0.0
93
 
94
  class GlazingMaterial:
95
- def __init__(self, name: str, shgc: float, u_value: float, h_o: float, category: str, is_library: bool = True):
96
  self.name = name
97
  self.shgc = shgc
98
  self.u_value = u_value
99
  self.h_o = h_o
100
- self.category = category
101
  self.is_library = is_library
102
 
103
  class MaterialLibrary:
@@ -130,7 +115,6 @@ class MaterialLibrary:
130
  data = [
131
  {
132
  "Name": g.name,
133
- "Category": g.category,
134
  "SHGC": g.shgc,
135
  "U-Value (W/m虏路K)": g.u_value,
136
  "Exterior Conductance (W/m虏路K)": g.h_o
@@ -166,9 +150,6 @@ class MaterialLibrary:
166
  try:
167
  if name not in project_materials:
168
  return False, f"Material '{name}' not found."
169
- if any(comp.get("material") and comp["material"].name == name
170
- for comp_list in components.values() for comp in comp_list):
171
- return False, f"Material '{name}' is used in components and cannot be deleted."
172
  del project_materials[name]
173
  return True, f"Material '{name}' deleted successfully!"
174
  except Exception as e:
@@ -180,7 +161,7 @@ def display_materials_page():
180
  This is the main function called by main.py when the Material Library page is selected.
181
  """
182
  st.title("Material Library")
183
- st.write("Manage building materials (including doors) and fenestrations for thermal analysis.")
184
 
185
  # CSS for box heights, scrolling, and visual appeal
186
  st.markdown("""
@@ -255,16 +236,15 @@ def display_materials_page():
255
  material_library.library_materials[k] = Material(
256
  name=k,
257
  category=MaterialCategory[category_str],
258
- conductivity=m["thermal_properties"].get("conductivity", 0.0),
259
- density=m["thermal_properties"].get("density", 1000.0),
260
- specific_heat=m["thermal_properties"].get("specific_heat", 1000.0),
261
- default_thickness=m["thickness_range"].get("default", 0.05) if "thickness_range" in m else m.get("default_thickness", 0.05),
262
- embodied_carbon=m.get("embodied_carbon", 0.5),
263
  absorptivity=m.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]),
264
- price=m["cost"].get("material", 50.0),
265
  emissivity=m.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"]),
266
- is_library=True,
267
- u_value=m.get("performance", {}).get("u_value") if m.get("category") == "Door" else None
268
  )
269
  logger.debug(f"Loaded material: {k}, Category: {m['category']}")
270
  except (KeyError, ValueError) as e:
@@ -274,16 +254,14 @@ def display_materials_page():
274
  material_library.library_glazing_materials = {}
275
  for k, f in st.session_state.project_data["fenestrations"]["library"].items():
276
  try:
277
- if f["type"] == "window":
278
- material_library.library_glazing_materials[k] = GlazingMaterial(
279
- name=k,
280
- shgc=f["performance"]["shgc"],
281
- u_value=f["performance"]["u_value"],
282
- h_o=f.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"]),
283
- category="Window/Skylight",
284
- is_library=True
285
- )
286
- logger.debug(f"Loaded fenestration: {k}, Type: {f['type']}")
287
  except KeyError as e:
288
  logger.error(f"Error processing fenestration {k}: Missing key {e}")
289
  continue
@@ -315,63 +293,30 @@ def initialize_materials_and_fenestrations():
315
  }
316
  logger.info(f"Initialized materials in session state with {len(SAMPLE_MATERIALS)} materials")
317
  if "fenestrations" not in st.session_state.project_data:
318
- # Move doors from fenestrations to materials
319
- fenestrations = dict(SAMPLE_FENESTRATIONS)
320
- doors = {k: v for k, v in fenestrations.items() if v["type"] == "door"}
321
- windows = {k: v for k, v in fenestrations.items() if v["type"] == "window"}
322
  st.session_state.project_data["fenestrations"] = {
323
- "library": windows,
324
  "project": {}
325
  }
326
- # Add doors to materials
327
- for k, v in doors.items():
328
- try:
329
- st.session_state.project_data["materials"]["library"][k] = {
330
- "category": "Door",
331
- "thermal_properties": {
332
- "conductivity": v["thermal_properties"].get("conductivity", 0.0),
333
- "density": v["thermal_properties"].get("density", 1000.0),
334
- "specific_heat": v["thermal_properties"].get("specific_heat", 1000.0)
335
- },
336
- "default_thickness": v.get("default_thickness", 0.05),
337
- "embodied_carbon": v.get("embodied_carbon", 0.5),
338
- "cost": {
339
- "material": v["cost"].get("material", 100.0),
340
- "labor": v["cost"].get("labor", 30.0),
341
- "replacement_years": v["cost"].get("replacement_years", 25)
342
- },
343
- "absorptivity": v.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]),
344
- "emissivity": v.get("emissivity", DEFAULT_DOOR_PROPERTIES["emissivity"]),
345
- "performance": {
346
- "u_value": v["performance"].get("u_value", 5.0)
347
- },
348
- "description": v.get("description", f"{k} for door applications")
349
- }
350
- logger.debug(f"Moved door {k} to materials library")
351
- except KeyError as e:
352
- logger.error(f"Error moving door {k} to materials: Missing key {e}")
353
- continue
354
- logger.info(f"Initialized fenestrations in session state with {len(windows)} fenestrations")
355
- logger.info(f"Moved {len(doors)} doors to materials library")
356
 
357
  def display_materials_tab(material_library: MaterialLibrary):
358
- """Display the materials tab content with two-column layout, including doors."""
359
  col1, col2 = st.columns([3, 2])
360
  with col1:
361
- st.subheader("Materials and Doors")
362
  # Category filter
363
  filter_options = ["All", "None"] + MATERIAL_CATEGORIES
364
  category = st.selectbox("Filter by Category", filter_options, key="material_filter")
365
 
366
- st.subheader("Library Materials and Doors")
367
  with st.container():
368
  library_materials = list(material_library.library_materials.values())
369
  if not library_materials:
370
- st.warning("No library materials or doors loaded. Check data initialization.")
371
  # Fallback: Display raw material names from session state
372
  raw_materials = st.session_state.project_data["materials"]["library"]
373
  if raw_materials:
374
- st.write("Raw material and door names available in session state:")
375
  st.write(list(raw_materials.keys()))
376
  if category == "None":
377
  library_materials = []
@@ -383,7 +328,7 @@ def display_materials_tab(material_library: MaterialLibrary):
383
  cols[2].write("**U-Value (W/m虏路K)**")
384
  cols[3].write("**Preview**")
385
  cols[4].write("**Clone**")
386
- for material in library_materials:
387
  cols = st.columns([2, 1, 1, 1, 1])
388
  cols[0].write(material.name)
389
  cols[1].write(material.get_thermal_mass())
@@ -402,7 +347,6 @@ def display_materials_tab(material_library: MaterialLibrary):
402
  "absorptivity": material.absorptivity,
403
  "price": material.price,
404
  "emissivity": material.emissivity,
405
- "u_value": material.u_value,
406
  "is_edit": False,
407
  "edit_source": "library"
408
  }
@@ -416,8 +360,7 @@ def display_materials_tab(material_library: MaterialLibrary):
416
  "embodied_carbon": material.embodied_carbon,
417
  "absorptivity": material.absorptivity,
418
  "price": material.price,
419
- "emissivity": material.emissivity,
420
- "u_value": material.u_value
421
  }
422
  st.session_state.active_tab = "Materials"
423
  if cols[4].button("Clone", key=f"copy_lib_mat_{material.name}"):
@@ -437,8 +380,7 @@ def display_materials_tab(material_library: MaterialLibrary):
437
  absorptivity=material.absorptivity,
438
  price=material.price,
439
  emissivity=material.emissivity,
440
- is_library=False,
441
- u_value=material.u_value
442
  )
443
  success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
444
  if success:
@@ -447,7 +389,7 @@ def display_materials_tab(material_library: MaterialLibrary):
447
  else:
448
  st.error(message)
449
 
450
- st.subheader("Project Materials and Doors")
451
  with st.container():
452
  if st.session_state.project_data["materials"]["project"]:
453
  cols = st.columns([2, 1, 1, 1, 1])
@@ -475,7 +417,6 @@ def display_materials_tab(material_library: MaterialLibrary):
475
  "absorptivity": material.absorptivity,
476
  "price": material.price,
477
  "emissivity": material.emissivity,
478
- "u_value": material.u_value,
479
  "is_edit": True,
480
  "edit_source": "project",
481
  "original_name": material.name
@@ -490,8 +431,7 @@ def display_materials_tab(material_library: MaterialLibrary):
490
  "embodied_carbon": material.embodied_carbon,
491
  "absorptivity": material.absorptivity,
492
  "price": material.price,
493
- "emissivity": material.emissivity,
494
- "u_value": material.u_value
495
  }
496
  st.session_state.active_tab = "Materials"
497
  if cols[4].button("Delete", key=f"delete_mat_{material.name}"):
@@ -504,21 +444,21 @@ def display_materials_tab(material_library: MaterialLibrary):
504
  else:
505
  st.error(message)
506
  else:
507
- st.write("No project materials or doors added.")
508
 
509
- st.subheader("Project Materials and Doors")
510
  try:
511
  material_df = material_library.to_dataframe("materials", project_materials=st.session_state.project_data["materials"]["project"])
512
  if not material_df.empty:
513
  st.dataframe(material_df, use_container_width=True)
514
  else:
515
- st.write("No project materials or doors to display.")
516
  except Exception as e:
517
- st.error(f"Error displaying project materials and doors: {str(e)}")
518
- st.write("No project materials or doors to display.")
519
 
520
  with col2:
521
- st.subheader("Material and Door Editor/Creator")
522
  with st.container():
523
  with st.form("material_editor_form", clear_on_submit=False):
524
  editor_state = st.session_state.get("material_editor", {})
@@ -532,15 +472,14 @@ def display_materials_tab(material_library: MaterialLibrary):
532
  "embodied_carbon": 0.5,
533
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
534
  "price": 50.0,
535
- "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"],
536
- "u_value": 5.0
537
  })
538
  is_edit = editor_state.get("is_edit", False)
539
  original_name = editor_state.get("original_name", "")
540
  name = st.text_input(
541
- "Material/Door Name",
542
  value=form_state.get("name", editor_state.get("name", "")),
543
- help="Unique material or door identifier",
544
  key="material_name_input"
545
  )
546
  filter_category = st.session_state.get("material_filter", "All")
@@ -552,26 +491,9 @@ def display_materials_tab(material_library: MaterialLibrary):
552
  "Category",
553
  MATERIAL_CATEGORIES,
554
  index=category_index,
555
- help="Material or door type classification",
556
  key="material_category_input"
557
  )
558
- u_value = None
559
- if category == "Door":
560
- u_value = st.number_input(
561
- "U-Value (W/m虏路K)",
562
- min_value=0.1,
563
- value=form_state.get("u_value", editor_state.get("u_value", 5.0)),
564
- help="Thermal transmittance for doors",
565
- key="material_u_value_input"
566
- )
567
- else:
568
- conductivity = st.number_input(
569
- "Conductivity (W/m路K)",
570
- min_value=0.01,
571
- value=form_state.get("conductivity", editor_state.get("conductivity", 0.1)),
572
- help="Thermal conductivity",
573
- key="material_conductivity_input"
574
- )
575
  density = st.number_input(
576
  "Density (kg/m鲁)",
577
  min_value=1.0,
@@ -624,34 +546,34 @@ def display_materials_tab(material_library: MaterialLibrary):
624
  key="material_emissivity_input"
625
  )
626
 
627
- if st.form_submit_button("Save Material/Door"):
628
  action_id = str(uuid.uuid4())
629
  if st.session_state.material_action.get("id") != action_id:
630
  st.session_state.material_action = {"action": "save", "id": action_id}
631
  st.session_state.material_form_state = {
632
  "name": name,
633
  "category": category,
634
- "conductivity": conductivity if category != "Door" else 0.0,
635
  "density": density,
636
  "specific_heat": specific_heat,
637
  "default_thickness": default_thickness,
638
  "embodied_carbon": embodied_carbon,
639
  "absorptivity": absorptivity,
640
  "price": price,
641
- "emissivity": emissivity,
642
- "u_value": u_value
643
  }
644
  if not name or not name.strip():
645
- st.error("Material/Door name cannot be empty.")
646
  elif (name in st.session_state.project_data["materials"]["project"] or
647
  name in st.session_state.project_data["materials"]["library"]) and (not is_edit or name != original_name):
648
- st.error(f"Material/Door '{name}' already exists.")
649
  else:
650
  try:
 
 
651
  new_material = Material(
652
  name=name,
653
  category=MaterialCategory[category.upper().replace("-", "_")],
654
- conductivity=0.0 if category == "Door" else conductivity,
655
  density=density,
656
  specific_heat=specific_heat,
657
  default_thickness=default_thickness,
@@ -659,8 +581,7 @@ def display_materials_tab(material_library: MaterialLibrary):
659
  absorptivity=absorptivity,
660
  price=price,
661
  emissivity=emissivity,
662
- is_library=False,
663
- u_value=u_value if category == "Door" else None
664
  )
665
  if is_edit and editor_state.get("edit_source") == "project":
666
  success, message = material_library.edit_project_material(
@@ -674,32 +595,27 @@ def display_materials_tab(material_library: MaterialLibrary):
674
  st.session_state.material_form_state = {
675
  "name": "",
676
  "category": "Insulation",
677
- "conductivity": 0.1,
678
  "density": 1000.0,
679
  "specific_heat": 1000.0,
680
  "default_thickness": 0.1,
681
  "embodied_carbon": 0.5,
682
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
683
  "price": 50.0,
684
- "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"],
685
- "u_value": 5.0
686
  }
687
  st.session_state.material_action = {"action": None, "id": None}
688
  st.session_state.rerun_trigger = None
689
  st.session_state.materials_rerun_pending = True
690
  else:
691
- st.error(f"Failed to save material/door: {message}")
692
  except Exception as e:
693
- st.error(f"Error saving material/door: {str(e)}")
694
 
695
  def display_fenestrations_tab(material_library: MaterialLibrary):
696
- """Display the fenestrations tab content with two-column layout, excluding doors."""
697
  col1, col2 = st.columns([3, 2])
698
  with col1:
699
- st.subheader("Fenestrations (Windows/Skylights)")
700
- filter_options = ["All", "None"] + FENESTRATION_TYPES
701
- fenestration_filter = st.selectbox("Filter Fenestrations", filter_options, key="fenestration_filter")
702
-
703
  st.subheader("Library Fenestrations")
704
  with st.container():
705
  library_fenestrations = list(material_library.library_glazing_materials.values())
@@ -709,10 +625,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
709
  if raw_fenestrations:
710
  st.write("Raw fenestration names available in session state:")
711
  st.write(list(raw_fenestrations.keys()))
712
- if fenestration_filter == "None":
713
- library_fenestrations = []
714
- elif fenestration_filter != "All":
715
- library_fenestrations = [f for f in library_fenestrations if f.category == fenestration_filter]
716
  cols = st.columns([2, 1, 1, 1, 1])
717
  cols[0].write("**Name**")
718
  cols[1].write("**SHGC**")
@@ -729,7 +641,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
729
  st.session_state.rerun_trigger = f"preview_fen_{fenestration.name}"
730
  st.session_state.fenestration_editor = {
731
  "name": fenestration.name,
732
- "category": fenestration.category,
733
  "shgc": fenestration.shgc,
734
  "u_value": fenestration.u_value,
735
  "h_o": fenestration.h_o,
@@ -738,7 +649,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
738
  }
739
  st.session_state.fenestration_form_state = {
740
  "name": fenestration.name,
741
- "category": fenestration.category,
742
  "shgc": fenestration.shgc,
743
  "u_value": fenestration.u_value,
744
  "h_o": fenestration.h_o
@@ -755,7 +665,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
755
  shgc=fenestration.shgc,
756
  u_value=fenestration.u_value,
757
  h_o=fenestration.h_o,
758
- category=fenestration.category,
759
  is_library=False
760
  )
761
  st.session_state.project_data["fenestrations"]["project"][new_name] = new_fenestration
@@ -781,7 +690,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
781
  st.session_state.rerun_trigger = f"edit_fen_{fenestration.name}"
782
  st.session_state.fenestration_editor = {
783
  "name": fenestration.name,
784
- "category": fenestration.category,
785
  "shgc": fenestration.shgc,
786
  "u_value": fenestration.u_value,
787
  "h_o": fenestration.h_o,
@@ -791,9 +699,8 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
791
  }
792
  st.session_state.fenestration_form_state = {
793
  "name": fenestration.name,
794
- "category": fenestration.category,
795
  "shgc": fenestration.shgc,
796
- "u_value": fenestration.u_value,
797
  "h_o": fenestration.h_o
798
  }
799
  st.session_state.active_tab = "Fenestrations"
@@ -818,7 +725,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
818
  fenestration_data = [
819
  {
820
  "Name": f.name,
821
- "Category": f.category,
822
  "SHGC": f.shgc,
823
  "U-Value (W/m虏路K)": f.u_value,
824
  "Exterior Conductance (W/m虏路K)": f.h_o
@@ -843,33 +749,23 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
843
  editor_state = st.session_state.get("fenestration_editor", {})
844
  form_state = st.session_state.get("fenestration_form_state", {
845
  "name": "",
846
- "category": "Window/Skylight",
847
  "shgc": 0.7,
848
  "u_value": 5.0,
849
  "h_o": DEFAULT_WINDOW_PROPERTIES["h_o"]
850
  })
851
  is_edit = editor_state.get("is_edit", False)
852
- default_category = editor_state.get("category", "Window/Skylight")
853
- category_index = (FENESTRATION_TYPES.index(default_category)
854
- if default_category in FENESTRATION_TYPES else 0)
855
  name = st.text_input(
856
  "Fenestration Name",
857
  value=form_state.get("name", editor_state.get("name", "")),
858
  help="Unique fenestration identifier",
859
  key="fenestration_name_input"
860
  )
861
- category = st.selectbox(
862
- "Category",
863
- FENESTRATION_TYPES,
864
- index=category_index,
865
- help="Fenestration type (Window/Skylight)",
866
- key="fenestration_category_input"
867
- )
868
  shgc = st.number_input(
869
  "Solar Heat Gain Coefficient (SHGC)",
870
  min_value=0.0,
871
  max_value=1.0,
872
- value=form_state.get("shgc", editor_state.get("shgc", 0.0)),
873
  help="Fraction of solar radiation admitted",
874
  key="fenestration_shgc_input"
875
  )
@@ -894,7 +790,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
894
  st.session_state.fenestration_action = {"action": "save", "id": action_id}
895
  st.session_state.fenestration_form_state = {
896
  "name": name,
897
- "category": category,
898
  "shgc": shgc,
899
  "u_value": u_value,
900
  "h_o": h_o
@@ -911,7 +806,6 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
911
  shgc=shgc,
912
  u_value=u_value,
913
  h_o=h_o,
914
- category=category,
915
  is_library=False
916
  )
917
  if is_edit and editor_state.get("edit_source") == "project":
@@ -920,17 +814,16 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
920
  else:
921
  st.session_state.project_data["fenestrations"]["project"][name] = new_fenestration
922
  st.success(f"Fenestration '{name}' added successfully!")
923
- st.session_state.fenestrations_editor = {}
924
- st.session_state.fenestrations_form_state = {
925
  "name": "",
926
- "category": "Window/Skylight",
927
- "shgc""": 0.7,
928
  "u_value": 5.0,
929
  "h_o": DEFAULT_WINDOW_PROPERTIES["h_o"]
930
  }
931
- st.session_state.fenestrations_action = {"action": None, "id": None}
932
  st.session_state.rerun_trigger = None
933
- st.session_state.materials_rerun_triggered = True
934
  except Exception as e:
935
  st.error(f"Error saving fenestration: {str(e)}")
936
 
@@ -943,31 +836,9 @@ def get_available_materials():
943
  return materials
944
 
945
  def get_available_fenestrations():
946
- """Get all available fenestrations (library + project windows + doors from materials) for use in other modules."""
947
  fenestrations = {}
948
  if "fenestrations" in st.session_state.project_data:
949
  fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
950
  fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
951
- # Include doors from materials as fenestrations for compatibility
952
- if "materials" in st.session_state.project_data["materials"]:
953
- for k, m in st.session_state.project_data["materials"]["library"].items():
954
- if m.get("category") == "Door":
955
- fenestrations[k] = GlazingMaterial(
956
- name=k,
957
- shgc=0.0,
958
- u_value=m.get("performance", {}).get("u_value", 5.0),
959
- h_o=m.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"]),
960
- category="Door",
961
- is_library=True
962
- )
963
- for k, m in st.session_state.project_data["materials"]["project"].items():
964
- if isinstance(m, Material) and m.category == MaterialCategory.DOOR:
965
- fenestrations[k] = GlazingMaterial(
966
- name=k,
967
- shgc=0.0,
968
- u_value=m.u_value if m.u_value is not None else 5.0,
969
- h_o=DEFAULT_WINDOW_PROPERTIES["h_o"],
970
- category="Door",
971
- is_library=False
972
- )
973
  return fenestrations
 
2
  BuildSustain - Material Library Module
3
 
4
  This module handles the material library functionality of the BuildSustain application,
5
+ allowing users to manage building materials and fenestrations (windows, skylights).
6
+ It provides both predefined library materials and the ability to create project-specific materials.
 
7
 
8
  Developed by: Dr Majed Abuseif, Deakin University
9
  漏 2025
 
19
  from enum import Enum
20
 
21
  # Import default data and constants from centralized module
22
+ from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS, DEFAULT_MATERIAL_PROPERTIES, DEFAULT_WINDOW_PROPERTIES
23
 
24
  # Configure logging
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
30
  "Insulation",
31
  "Structural",
32
  "Finishing",
33
+ "Sub-Structural"
 
 
 
 
 
34
  ]
35
 
36
  # Surface resistances (EN ISO 6946 or ASHRAE standards)
 
42
  STRUCTURAL = "Structural"
43
  FINISHING = "Finishing"
44
  SUB_STRUCTURAL = "Sub-Structural"
 
45
 
46
  class Material:
47
  def __init__(self, name: str, category: MaterialCategory, conductivity: float, density: float,
48
  specific_heat: float, default_thickness: float, embodied_carbon: float,
49
+ absorptivity: float, price: float, emissivity: float, is_library: bool = True):
 
50
  self.name = name
51
  self.category = category
52
  self.conductivity = conductivity
 
58
  self.price = price
59
  self.emissivity = emissivity
60
  self.is_library = is_library
 
61
 
62
  def get_thermal_mass(self) -> str:
63
  # Calculate areal heat capacity: 蟻 * cp * d (J/m虏路K)
 
 
64
  thermal_mass = self.density * self.specific_heat * self.default_thickness
65
  # Categorize based on thresholds
66
  if thermal_mass < 30000:
 
71
  return "High"
72
 
73
  def get_u_value(self) -> float:
 
 
 
74
  # Calculate U-value: U = 1 / (R_si + (d / 位) + R_se) (W/m虏路K)
75
  if self.default_thickness > 0 and self.conductivity > 0:
76
  r_value = R_SI + (self.default_thickness / self.conductivity) + R_SE
 
78
  return 0.0
79
 
80
  class GlazingMaterial:
81
+ def __init__(self, name: str, shgc: float, u_value: float, h_o: float, is_library: bool = True):
82
  self.name = name
83
  self.shgc = shgc
84
  self.u_value = u_value
85
  self.h_o = h_o
 
86
  self.is_library = is_library
87
 
88
  class MaterialLibrary:
 
115
  data = [
116
  {
117
  "Name": g.name,
 
118
  "SHGC": g.shgc,
119
  "U-Value (W/m虏路K)": g.u_value,
120
  "Exterior Conductance (W/m虏路K)": g.h_o
 
150
  try:
151
  if name not in project_materials:
152
  return False, f"Material '{name}' not found."
 
 
 
153
  del project_materials[name]
154
  return True, f"Material '{name}' deleted successfully!"
155
  except Exception as e:
 
161
  This is the main function called by main.py when the Material Library page is selected.
162
  """
163
  st.title("Material Library")
164
+ st.write("Manage building materials and fenestrations for thermal analysis.")
165
 
166
  # CSS for box heights, scrolling, and visual appeal
167
  st.markdown("""
 
236
  material_library.library_materials[k] = Material(
237
  name=k,
238
  category=MaterialCategory[category_str],
239
+ conductivity=m["thermal_properties"]["conductivity"],
240
+ density=m["thermal_properties"]["density"],
241
+ specific_heat=m["thermal_properties"]["specific_heat"],
242
+ default_thickness=m["thickness_range"]["default"],
243
+ embodied_carbon=m["embodied_carbon"],
244
  absorptivity=m.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]),
245
+ price=m["cost"]["material"],
246
  emissivity=m.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"]),
247
+ is_library=True
 
248
  )
249
  logger.debug(f"Loaded material: {k}, Category: {m['category']}")
250
  except (KeyError, ValueError) as e:
 
254
  material_library.library_glazing_materials = {}
255
  for k, f in st.session_state.project_data["fenestrations"]["library"].items():
256
  try:
257
+ material_library.library_glazing_materials[k] = GlazingMaterial(
258
+ name=k,
259
+ shgc=f["performance"]["shgc"],
260
+ u_value=f["performance"]["u_value"],
261
+ h_o=f.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"]),
262
+ is_library=True
263
+ )
264
+ logger.debug(f"Loaded fenestration: {k}, Type: {f['type']}")
 
 
265
  except KeyError as e:
266
  logger.error(f"Error processing fenestration {k}: Missing key {e}")
267
  continue
 
293
  }
294
  logger.info(f"Initialized materials in session state with {len(SAMPLE_MATERIALS)} materials")
295
  if "fenestrations" not in st.session_state.project_data:
 
 
 
 
296
  st.session_state.project_data["fenestrations"] = {
297
+ "library": dict(SAMPLE_FENESTRATIONS),
298
  "project": {}
299
  }
300
+ logger.info(f"Initialized fenestrations in session state with {len(SAMPLE_FENESTRATIONS)} fenestrations")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  def display_materials_tab(material_library: MaterialLibrary):
303
+ """Display the materials tab content with two-column layout."""
304
  col1, col2 = st.columns([3, 2])
305
  with col1:
306
+ st.subheader("Materials")
307
  # Category filter
308
  filter_options = ["All", "None"] + MATERIAL_CATEGORIES
309
  category = st.selectbox("Filter by Category", filter_options, key="material_filter")
310
 
311
+ st.subheader("Library Materials")
312
  with st.container():
313
  library_materials = list(material_library.library_materials.values())
314
  if not library_materials:
315
+ st.warning("No library materials loaded. Check data initialization.")
316
  # Fallback: Display raw material names from session state
317
  raw_materials = st.session_state.project_data["materials"]["library"]
318
  if raw_materials:
319
+ st.write("Raw material names available in session state:")
320
  st.write(list(raw_materials.keys()))
321
  if category == "None":
322
  library_materials = []
 
328
  cols[2].write("**U-Value (W/m虏路K)**")
329
  cols[3].write("**Preview**")
330
  cols[4].write("**Clone**")
331
+ for material in library acque:
332
  cols = st.columns([2, 1, 1, 1, 1])
333
  cols[0].write(material.name)
334
  cols[1].write(material.get_thermal_mass())
 
347
  "absorptivity": material.absorptivity,
348
  "price": material.price,
349
  "emissivity": material.emissivity,
 
350
  "is_edit": False,
351
  "edit_source": "library"
352
  }
 
360
  "embodied_carbon": material.embodied_carbon,
361
  "absorptivity": material.absorptivity,
362
  "price": material.price,
363
+ "emissivity": material.emissivity
 
364
  }
365
  st.session_state.active_tab = "Materials"
366
  if cols[4].button("Clone", key=f"copy_lib_mat_{material.name}"):
 
380
  absorptivity=material.absorptivity,
381
  price=material.price,
382
  emissivity=material.emissivity,
383
+ is_library=False
 
384
  )
385
  success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
386
  if success:
 
389
  else:
390
  st.error(message)
391
 
392
+ st.subheader("Project Materials")
393
  with st.container():
394
  if st.session_state.project_data["materials"]["project"]:
395
  cols = st.columns([2, 1, 1, 1, 1])
 
417
  "absorptivity": material.absorptivity,
418
  "price": material.price,
419
  "emissivity": material.emissivity,
 
420
  "is_edit": True,
421
  "edit_source": "project",
422
  "original_name": material.name
 
431
  "embodied_carbon": material.embodied_carbon,
432
  "absorptivity": material.absorptivity,
433
  "price": material.price,
434
+ "emissivity": material.emissivity
 
435
  }
436
  st.session_state.active_tab = "Materials"
437
  if cols[4].button("Delete", key=f"delete_mat_{material.name}"):
 
444
  else:
445
  st.error(message)
446
  else:
447
+ st.write("No project materials added.")
448
 
449
+ st.subheader("Project Materials")
450
  try:
451
  material_df = material_library.to_dataframe("materials", project_materials=st.session_state.project_data["materials"]["project"])
452
  if not material_df.empty:
453
  st.dataframe(material_df, use_container_width=True)
454
  else:
455
+ st.write("No project materials to display.")
456
  except Exception as e:
457
+ st.error(f"Error displaying project materials: {str(e)}")
458
+ st.write("No project materials to display.")
459
 
460
  with col2:
461
+ st.subheader("Material Editor/Creator")
462
  with st.container():
463
  with st.form("material_editor_form", clear_on_submit=False):
464
  editor_state = st.session_state.get("material_editor", {})
 
472
  "embodied_carbon": 0.5,
473
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
474
  "price": 50.0,
475
+ "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"]
 
476
  })
477
  is_edit = editor_state.get("is_edit", False)
478
  original_name = editor_state.get("original_name", "")
479
  name = st.text_input(
480
+ "Material Name",
481
  value=form_state.get("name", editor_state.get("name", "")),
482
+ help="Unique material identifier",
483
  key="material_name_input"
484
  )
485
  filter_category = st.session_state.get("material_filter", "All")
 
491
  "Category",
492
  MATERIAL_CATEGORIES,
493
  index=category_index,
494
+ help="Material type classification",
495
  key="material_category_input"
496
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  density = st.number_input(
498
  "Density (kg/m鲁)",
499
  min_value=1.0,
 
546
  key="material_emissivity_input"
547
  )
548
 
549
+ if st.form_submit_button("Save Material"):
550
  action_id = str(uuid.uuid4())
551
  if st.session_state.material_action.get("id") != action_id:
552
  st.session_state.material_action = {"action": "save", "id": action_id}
553
  st.session_state.material_form_state = {
554
  "name": name,
555
  "category": category,
 
556
  "density": density,
557
  "specific_heat": specific_heat,
558
  "default_thickness": default_thickness,
559
  "embodied_carbon": embodied_carbon,
560
  "absorptivity": absorptivity,
561
  "price": price,
562
+ "emissivity": emissivity
 
563
  }
564
  if not name or not name.strip():
565
+ st.error("Material name cannot be empty.")
566
  elif (name in st.session_state.project_data["materials"]["project"] or
567
  name in st.session_state.project_data["materials"]["library"]) and (not is_edit or name != original_name):
568
+ st.error(f"Material '{name}' already exists.")
569
  else:
570
  try:
571
+ # Use default conductivity from library or a fallback value
572
+ conductivity = form_state.get("conductivity", editor_state.get("conductivity", 0.1))
573
  new_material = Material(
574
  name=name,
575
  category=MaterialCategory[category.upper().replace("-", "_")],
576
+ conductivity=conductivity,
577
  density=density,
578
  specific_heat=specific_heat,
579
  default_thickness=default_thickness,
 
581
  absorptivity=absorptivity,
582
  price=price,
583
  emissivity=emissivity,
584
+ is_library=False
 
585
  )
586
  if is_edit and editor_state.get("edit_source") == "project":
587
  success, message = material_library.edit_project_material(
 
595
  st.session_state.material_form_state = {
596
  "name": "",
597
  "category": "Insulation",
 
598
  "density": 1000.0,
599
  "specific_heat": 1000.0,
600
  "default_thickness": 0.1,
601
  "embodied_carbon": 0.5,
602
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
603
  "price": 50.0,
604
+ "emissivity": DEFAULT_MATERIAL_PAGE["emissivity"]
 
605
  }
606
  st.session_state.material_action = {"action": None, "id": None}
607
  st.session_state.rerun_trigger = None
608
  st.session_state.materials_rerun_pending = True
609
  else:
610
+ st.error(f"Failed to save material: {message}")
611
  except Exception as e:
612
+ st.error(f"Error saving material: {str(e)}")
613
 
614
  def display_fenestrations_tab(material_library: MaterialLibrary):
615
+ """Display the fenestrations tab content with two-column layout."""
616
  col1, col2 = st.columns([3, 2])
617
  with col1:
618
+ st.subheader("Fenestrations")
 
 
 
619
  st.subheader("Library Fenestrations")
620
  with st.container():
621
  library_fenestrations = list(material_library.library_glazing_materials.values())
 
625
  if raw_fenestrations:
626
  st.write("Raw fenestration names available in session state:")
627
  st.write(list(raw_fenestrations.keys()))
 
 
 
 
628
  cols = st.columns([2, 1, 1, 1, 1])
629
  cols[0].write("**Name**")
630
  cols[1].write("**SHGC**")
 
641
  st.session_state.rerun_trigger = f"preview_fen_{fenestration.name}"
642
  st.session_state.fenestration_editor = {
643
  "name": fenestration.name,
 
644
  "shgc": fenestration.shgc,
645
  "u_value": fenestration.u_value,
646
  "h_o": fenestration.h_o,
 
649
  }
650
  st.session_state.fenestration_form_state = {
651
  "name": fenestration.name,
 
652
  "shgc": fenestration.shgc,
653
  "u_value": fenestration.u_value,
654
  "h_o": fenestration.h_o
 
665
  shgc=fenestration.shgc,
666
  u_value=fenestration.u_value,
667
  h_o=fenestration.h_o,
 
668
  is_library=False
669
  )
670
  st.session_state.project_data["fenestrations"]["project"][new_name] = new_fenestration
 
690
  st.session_state.rerun_trigger = f"edit_fen_{fenestration.name}"
691
  st.session_state.fenestration_editor = {
692
  "name": fenestration.name,
 
693
  "shgc": fenestration.shgc,
694
  "u_value": fenestration.u_value,
695
  "h_o": fenestration.h_o,
 
699
  }
700
  st.session_state.fenestration_form_state = {
701
  "name": fenestration.name,
 
702
  "shgc": fenestration.shgc,
703
+ "d": fenestration.u_value,
704
  "h_o": fenestration.h_o
705
  }
706
  st.session_state.active_tab = "Fenestrations"
 
725
  fenestration_data = [
726
  {
727
  "Name": f.name,
 
728
  "SHGC": f.shgc,
729
  "U-Value (W/m虏路K)": f.u_value,
730
  "Exterior Conductance (W/m虏路K)": f.h_o
 
749
  editor_state = st.session_state.get("fenestration_editor", {})
750
  form_state = st.session_state.get("fenestration_form_state", {
751
  "name": "",
 
752
  "shgc": 0.7,
753
  "u_value": 5.0,
754
  "h_o": DEFAULT_WINDOW_PROPERTIES["h_o"]
755
  })
756
  is_edit = editor_state.get("is_edit", False)
757
+ original_name = editor_state.get("original_name", "")
 
 
758
  name = st.text_input(
759
  "Fenestration Name",
760
  value=form_state.get("name", editor_state.get("name", "")),
761
  help="Unique fenestration identifier",
762
  key="fenestration_name_input"
763
  )
 
 
 
 
 
 
 
764
  shgc = st.number_input(
765
  "Solar Heat Gain Coefficient (SHGC)",
766
  min_value=0.0,
767
  max_value=1.0,
768
+ value=form_state.get("shgc", editor_state.get("shgc", 0.7)),
769
  help="Fraction of solar radiation admitted",
770
  key="fenestration_shgc_input"
771
  )
 
790
  st.session_state.fenestration_action = {"action": "save", "id": action_id}
791
  st.session_state.fenestration_form_state = {
792
  "name": name,
 
793
  "shgc": shgc,
794
  "u_value": u_value,
795
  "h_o": h_o
 
806
  shgc=shgc,
807
  u_value=u_value,
808
  h_o=h_o,
 
809
  is_library=False
810
  )
811
  if is_edit and editor_state.get("edit_source") == "project":
 
814
  else:
815
  st.session_state.project_data["fenestrations"]["project"][name] = new_fenestration
816
  st.success(f"Fenestration '{name}' added successfully!")
817
+ st.session_state.fenestration_editor = {}
818
+ st.session_state.fenestration_form_state = {
819
  "name": "",
820
+ "shgc": 0.7,
 
821
  "u_value": 5.0,
822
  "h_o": DEFAULT_WINDOW_PROPERTIES["h_o"]
823
  }
824
+ st.session_state.fenestration_action = {"action": None, "id": None}
825
  st.session_state.rerun_trigger = None
826
+ st.session_state.materials_rerun_pending = True
827
  except Exception as e:
828
  st.error(f"Error saving fenestration: {str(e)}")
829
 
 
836
  return materials
837
 
838
  def get_available_fenestrations():
839
+ """Get all available fenestrations (library + project) for use in other modules."""
840
  fenestrations = {}
841
  if "fenestrations" in st.session_state.project_data:
842
  fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
843
  fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  return fenestrations