mabuseif commited on
Commit
294d3af
verified
1 Parent(s): 2e64c46

Update app/materials_library.py

Browse files
Files changed (1) hide show
  1. app/materials_library.py +127 -54
app/materials_library.py CHANGED
@@ -2,8 +2,9 @@
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, doors).
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
@@ -30,12 +31,12 @@ MATERIAL_CATEGORIES = [
30
  "Insulation",
31
  "Structural",
32
  "Finishing",
33
- "Sub-Structural"
 
34
  ]
35
 
36
  FENESTRATION_TYPES = [
37
- "Window/Skylight",
38
- "Door"
39
  ]
40
 
41
  # Surface resistances (EN ISO 6946 or ASHRAE standards)
@@ -47,11 +48,13 @@ class MaterialCategory(Enum):
47
  STRUCTURAL = "Structural"
48
  FINISHING = "Finishing"
49
  SUB_STRUCTURAL = "Sub-Structural"
 
50
 
51
  class Material:
52
  def __init__(self, name: str, category: MaterialCategory, conductivity: float, density: float,
53
  specific_heat: float, default_thickness: float, embodied_carbon: float,
54
- absorptivity: float, price: float, emissivity: float, is_library: bool = True):
 
55
  self.name = name
56
  self.category = category
57
  self.conductivity = conductivity
@@ -63,9 +66,12 @@ class Material:
63
  self.price = price
64
  self.emissivity = emissivity
65
  self.is_library = is_library
 
66
 
67
  def get_thermal_mass(self) -> str:
68
  # Calculate areal heat capacity: 蟻 * cp * d (J/m虏路K)
 
 
69
  thermal_mass = self.density * self.specific_heat * self.default_thickness
70
  # Categorize based on thresholds
71
  if thermal_mass < 30000:
@@ -76,6 +82,9 @@ class Material:
76
  return "High"
77
 
78
  def get_u_value(self) -> float:
 
 
 
79
  # Calculate U-value: U = 1 / (R_si + (d / 位) + R_se) (W/m虏路K)
80
  if self.default_thickness > 0 and self.conductivity > 0:
81
  r_value = R_SI + (self.default_thickness / self.conductivity) + R_SE
@@ -157,6 +166,9 @@ class MaterialLibrary:
157
  try:
158
  if name not in project_materials:
159
  return False, f"Material '{name}' not found."
 
 
 
160
  del project_materials[name]
161
  return True, f"Material '{name}' deleted successfully!"
162
  except Exception as e:
@@ -168,7 +180,7 @@ def display_materials_page():
168
  This is the main function called by main.py when the Material Library page is selected.
169
  """
170
  st.title("Material Library")
171
- st.write("Manage building materials and fenestrations for thermal analysis.")
172
 
173
  # CSS for box heights, scrolling, and visual appeal
174
  st.markdown("""
@@ -251,7 +263,8 @@ def display_materials_page():
251
  absorptivity=m.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]),
252
  price=m["cost"]["material"],
253
  emissivity=m.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"]),
254
- is_library=True
 
255
  )
256
  logger.debug(f"Loaded material: {k}, Category: {m['category']}")
257
  except (KeyError, ValueError) as e:
@@ -270,16 +283,7 @@ def display_materials_page():
270
  category="Window/Skylight",
271
  is_library=True
272
  )
273
- elif f["type"] == "door":
274
- material_library.library_glazing_materials[k] = GlazingMaterial(
275
- name=k,
276
- shgc=0.0,
277
- u_value=f["performance"]["u_value"],
278
- h_o=f.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"]),
279
- category="Door",
280
- is_library=True
281
- )
282
- logger.debug(f"Loaded fenestration: {k}, Type: {f['type']}")
283
  except KeyError as e:
284
  logger.error(f"Error processing fenestration {k}: Missing key {e}")
285
  continue
@@ -311,30 +315,51 @@ def initialize_materials_and_fenestrations():
311
  }
312
  logger.info(f"Initialized materials in session state with {len(SAMPLE_MATERIALS)} materials")
313
  if "fenestrations" not in st.session_state.project_data:
 
 
 
 
314
  st.session_state.project_data["fenestrations"] = {
315
- "library": dict(SAMPLE_FENESTRATIONS),
316
  "project": {}
317
  }
318
- logger.info(f"Initialized fenestrations in session state with {len(SAMPLE_FENESTRATIONS)} fenestrations")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
  def display_materials_tab(material_library: MaterialLibrary):
321
- """Display the materials tab content with two-column layout."""
322
  col1, col2 = st.columns([3, 2])
323
  with col1:
324
- st.subheader("Materials")
325
  # Category filter
326
  filter_options = ["All", "None"] + MATERIAL_CATEGORIES
327
  category = st.selectbox("Filter by Category", filter_options, key="material_filter")
328
 
329
- st.subheader("Library Materials")
330
  with st.container():
331
  library_materials = list(material_library.library_materials.values())
332
  if not library_materials:
333
- st.warning("No library materials loaded. Check data initialization.")
334
  # Fallback: Display raw material names from session state
335
  raw_materials = st.session_state.project_data["materials"]["library"]
336
  if raw_materials:
337
- st.write("Raw material names available in session state:")
338
  st.write(list(raw_materials.keys()))
339
  if category == "None":
340
  library_materials = []
@@ -365,6 +390,7 @@ def display_materials_tab(material_library: MaterialLibrary):
365
  "absorptivity": material.absorptivity,
366
  "price": material.price,
367
  "emissivity": material.emissivity,
 
368
  "is_edit": False,
369
  "edit_source": "library"
370
  }
@@ -378,7 +404,8 @@ def display_materials_tab(material_library: MaterialLibrary):
378
  "embodied_carbon": material.embodied_carbon,
379
  "absorptivity": material.absorptivity,
380
  "price": material.price,
381
- "emissivity": material.emissivity
 
382
  }
383
  st.session_state.active_tab = "Materials"
384
  if cols[4].button("Clone", key=f"copy_lib_mat_{material.name}"):
@@ -398,7 +425,8 @@ def display_materials_tab(material_library: MaterialLibrary):
398
  absorptivity=material.absorptivity,
399
  price=material.price,
400
  emissivity=material.emissivity,
401
- is_library=False
 
402
  )
403
  success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
404
  if success:
@@ -407,7 +435,7 @@ def display_materials_tab(material_library: MaterialLibrary):
407
  else:
408
  st.error(message)
409
 
410
- st.subheader("Project Materials")
411
  with st.container():
412
  if st.session_state.project_data["materials"]["project"]:
413
  cols = st.columns([2, 1, 1, 1, 1])
@@ -435,6 +463,7 @@ def display_materials_tab(material_library: MaterialLibrary):
435
  "absorptivity": material.absorptivity,
436
  "price": material.price,
437
  "emissivity": material.emissivity,
 
438
  "is_edit": True,
439
  "edit_source": "project",
440
  "original_name": material.name
@@ -449,7 +478,8 @@ def display_materials_tab(material_library: MaterialLibrary):
449
  "embodied_carbon": material.embodied_carbon,
450
  "absorptivity": material.absorptivity,
451
  "price": material.price,
452
- "emissivity": material.emissivity
 
453
  }
454
  st.session_state.active_tab = "Materials"
455
  if cols[4].button("Delete", key=f"delete_mat_{material.name}"):
@@ -462,21 +492,21 @@ def display_materials_tab(material_library: MaterialLibrary):
462
  else:
463
  st.error(message)
464
  else:
465
- st.write("No project materials added.")
466
 
467
- st.subheader("Project Materials")
468
  try:
469
  material_df = material_library.to_dataframe("materials", project_materials=st.session_state.project_data["materials"]["project"])
470
  if not material_df.empty:
471
  st.dataframe(material_df, use_container_width=True)
472
  else:
473
- st.write("No project materials to display.")
474
  except Exception as e:
475
- st.error(f"Error displaying project materials: {str(e)}")
476
- st.write("No project materials to display.")
477
 
478
  with col2:
479
- st.subheader("Material Editor/Creator")
480
  with st.container():
481
  with st.form("material_editor_form", clear_on_submit=False):
482
  editor_state = st.session_state.get("material_editor", {})
@@ -490,14 +520,15 @@ def display_materials_tab(material_library: MaterialLibrary):
490
  "embodied_carbon": 0.5,
491
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
492
  "price": 50.0,
493
- "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"]
 
494
  })
495
  is_edit = editor_state.get("is_edit", False)
496
  original_name = editor_state.get("original_name", "")
497
  name = st.text_input(
498
- "Material Name",
499
  value=form_state.get("name", editor_state.get("name", "")),
500
- help="Unique material identifier",
501
  key="material_name_input"
502
  )
503
  filter_category = st.session_state.get("material_filter", "All")
@@ -509,9 +540,26 @@ def display_materials_tab(material_library: MaterialLibrary):
509
  "Category",
510
  MATERIAL_CATEGORIES,
511
  index=category_index,
512
- help="Material type classification",
513
  key="material_category_input"
514
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  density = st.number_input(
516
  "Density (kg/m鲁)",
517
  min_value=1.0,
@@ -564,34 +612,34 @@ def display_materials_tab(material_library: MaterialLibrary):
564
  key="material_emissivity_input"
565
  )
566
 
567
- if st.form_submit_button("Save Material"):
568
  action_id = str(uuid.uuid4())
569
  if st.session_state.material_action.get("id") != action_id:
570
  st.session_state.material_action = {"action": "save", "id": action_id}
571
  st.session_state.material_form_state = {
572
  "name": name,
573
  "category": category,
 
574
  "density": density,
575
  "specific_heat": specific_heat,
576
  "default_thickness": default_thickness,
577
  "embodied_carbon": embodied_carbon,
578
  "absorptivity": absorptivity,
579
  "price": price,
580
- "emissivity": emissivity
 
581
  }
582
  if not name or not name.strip():
583
- st.error("Material name cannot be empty.")
584
  elif (name in st.session_state.project_data["materials"]["project"] or
585
  name in st.session_state.project_data["materials"]["library"]) and (not is_edit or name != original_name):
586
- st.error(f"Material '{name}' already exists.")
587
  else:
588
  try:
589
- # Use default conductivity from library or a fallback value
590
- conductivity = form_state.get("conductivity", editor_state.get("conductivity", 0.1))
591
  new_material = Material(
592
  name=name,
593
  category=MaterialCategory[category.upper().replace("-", "_")],
594
- conductivity=conductivity,
595
  density=density,
596
  specific_heat=specific_heat,
597
  default_thickness=default_thickness,
@@ -599,7 +647,8 @@ def display_materials_tab(material_library: MaterialLibrary):
599
  absorptivity=absorptivity,
600
  price=price,
601
  emissivity=emissivity,
602
- is_library=False
 
603
  )
604
  if is_edit and editor_state.get("edit_source") == "project":
605
  success, message = material_library.edit_project_material(
@@ -613,27 +662,29 @@ def display_materials_tab(material_library: MaterialLibrary):
613
  st.session_state.material_form_state = {
614
  "name": "",
615
  "category": "Insulation",
 
616
  "density": 1000.0,
617
  "specific_heat": 1000.0,
618
  "default_thickness": 0.1,
619
  "embodied_carbon": 0.5,
620
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
621
  "price": 50.0,
622
- "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"]
 
623
  }
624
  st.session_state.material_action = {"action": None, "id": None}
625
  st.session_state.rerun_trigger = None
626
  st.session_state.materials_rerun_pending = True
627
  else:
628
- st.error(f"Failed to save material: {message}")
629
  except Exception as e:
630
- st.error(f"Error saving material: {str(e)}")
631
 
632
  def display_fenestrations_tab(material_library: MaterialLibrary):
633
- """Display the fenestrations tab content with two-column layout."""
634
  col1, col2 = st.columns([3, 2])
635
  with col1:
636
- st.subheader("Fenestrations")
637
  filter_options = ["All", "None"] + FENESTRATION_TYPES
638
  fenestration_filter = st.selectbox("Filter Fenestrations", filter_options, key="fenestration_filter")
639
 
@@ -802,7 +853,7 @@ def display_fenestrations_tab(material_library: MaterialLibrary):
802
  "Category",
803
  FENESTRATION_TYPES,
804
  index=category_index,
805
- help="Fenestration type (Window/Skylight or Door)",
806
  key="fenestration_category_input"
807
  )
808
  shgc = st.number_input(
@@ -883,9 +934,31 @@ def get_available_materials():
883
  return materials
884
 
885
  def get_available_fenestrations():
886
- """Get all available fenestrations (library + project) for use in other modules."""
887
  fenestrations = {}
888
  if "fenestrations" in st.session_state.project_data:
889
  fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
890
  fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  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 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
 
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
  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
  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
  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
 
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
  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("""
 
263
  absorptivity=m.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]),
264
  price=m["cost"]["material"],
265
  emissivity=m.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"]),
266
+ is_library=True,
267
+ u_value=m["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:
 
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
  }
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
+ st.session_state.project_data["materials"]["library"][k] = {
329
+ "category": "Door",
330
+ "thermal_properties": {
331
+ "conductivity": 0.0, # Not used for doors
332
+ "density": v.get("density", 1000.0),
333
+ "specific_heat": v.get("specific_heat", 1000.0)
334
+ },
335
+ "thickness_range": {"default": v.get("thickness", 0.05)},
336
+ "embodied_carbon": v.get("embodied_carbon", 0.5),
337
+ "cost": {"material": v.get("cost", 100.0)},
338
+ "absorptivity": v.get("absorptivity", DEFAULT_MATERIAL_PROPERTIES["absorptivity"]),
339
+ "emissivity": v.get("emissivity", DEFAULT_MATERIAL_PROPERTIES["emissivity"]),
340
+ "performance": {"u_value": v["performance"]["u_value"]}
341
+ }
342
+ logger.info(f"Initialized fenestrations in session state with {len(windows)} fenestrations")
343
+ logger.info(f"Moved {len(doors)} doors to materials library")
344
 
345
  def display_materials_tab(material_library: MaterialLibrary):
346
+ """Display the materials tab content with two-column layout, including doors."""
347
  col1, col2 = st.columns([3, 2])
348
  with col1:
349
+ st.subheader("Materials and Doors")
350
  # Category filter
351
  filter_options = ["All", "None"] + MATERIAL_CATEGORIES
352
  category = st.selectbox("Filter by Category", filter_options, key="material_filter")
353
 
354
+ st.subheader("Library Materials and Doors")
355
  with st.container():
356
  library_materials = list(material_library.library_materials.values())
357
  if not library_materials:
358
+ st.warning("No library materials or doors loaded. Check data initialization.")
359
  # Fallback: Display raw material names from session state
360
  raw_materials = st.session_state.project_data["materials"]["library"]
361
  if raw_materials:
362
+ st.write("Raw material and door names available in session state:")
363
  st.write(list(raw_materials.keys()))
364
  if category == "None":
365
  library_materials = []
 
390
  "absorptivity": material.absorptivity,
391
  "price": material.price,
392
  "emissivity": material.emissivity,
393
+ "u_value": material.u_value,
394
  "is_edit": False,
395
  "edit_source": "library"
396
  }
 
404
  "embodied_carbon": material.embodied_carbon,
405
  "absorptivity": material.absorptivity,
406
  "price": material.price,
407
+ "emissivity": material.emissivity,
408
+ "u_value": material.u_value
409
  }
410
  st.session_state.active_tab = "Materials"
411
  if cols[4].button("Clone", key=f"copy_lib_mat_{material.name}"):
 
425
  absorptivity=material.absorptivity,
426
  price=material.price,
427
  emissivity=material.emissivity,
428
+ is_library=False,
429
+ u_value=material.u_value
430
  )
431
  success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
432
  if success:
 
435
  else:
436
  st.error(message)
437
 
438
+ st.subheader("Project Materials and Doors")
439
  with st.container():
440
  if st.session_state.project_data["materials"]["project"]:
441
  cols = st.columns([2, 1, 1, 1, 1])
 
463
  "absorptivity": material.absorptivity,
464
  "price": material.price,
465
  "emissivity": material.emissivity,
466
+ "u_value": material.u_value,
467
  "is_edit": True,
468
  "edit_source": "project",
469
  "original_name": material.name
 
478
  "embodied_carbon": material.embodied_carbon,
479
  "absorptivity": material.absorptivity,
480
  "price": material.price,
481
+ "emissivity": material.emissivity,
482
+ "u_value": material.u_value
483
  }
484
  st.session_state.active_tab = "Materials"
485
  if cols[4].button("Delete", key=f"delete_mat_{material.name}"):
 
492
  else:
493
  st.error(message)
494
  else:
495
+ st.write("No project materials or doors added.")
496
 
497
+ st.subheader("Project Materials and Doors")
498
  try:
499
  material_df = material_library.to_dataframe("materials", project_materials=st.session_state.project_data["materials"]["project"])
500
  if not material_df.empty:
501
  st.dataframe(material_df, use_container_width=True)
502
  else:
503
+ st.write("No project materials or doors to display.")
504
  except Exception as e:
505
+ st.error(f"Error displaying project materials and doors: {str(e)}")
506
+ st.write("No project materials or doors to display.")
507
 
508
  with col2:
509
+ st.subheader("Material and Door Editor/Creator")
510
  with st.container():
511
  with st.form("material_editor_form", clear_on_submit=False):
512
  editor_state = st.session_state.get("material_editor", {})
 
520
  "embodied_carbon": 0.5,
521
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
522
  "price": 50.0,
523
+ "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"],
524
+ "u_value": 5.0
525
  })
526
  is_edit = editor_state.get("is_edit", False)
527
  original_name = editor_state.get("original_name", "")
528
  name = st.text_input(
529
+ "Material/Door Name",
530
  value=form_state.get("name", editor_state.get("name", "")),
531
+ help="Unique material or door identifier",
532
  key="material_name_input"
533
  )
534
  filter_category = st.session_state.get("material_filter", "All")
 
540
  "Category",
541
  MATERIAL_CATEGORIES,
542
  index=category_index,
543
+ help="Material or door type classification",
544
  key="material_category_input"
545
  )
546
+ u_value = None
547
+ if category == "Door":
548
+ u_value = st.number_input(
549
+ "U-Value (W/m虏路K)",
550
+ min_value=0.1,
551
+ value=form_state.get("u_value", editor_state.get("u_value", 5.0)),
552
+ help="Thermal transmittance for doors",
553
+ key="material_u_value_input"
554
+ )
555
+ else:
556
+ conductivity = st.number_input(
557
+ "Conductivity (W/m路K)",
558
+ min_value=0.01,
559
+ value=form_state.get("conductivity", editor_state.get("conductivity", 0.1)),
560
+ help="Thermal conductivity",
561
+ key="material_conductivity_input"
562
+ )
563
  density = st.number_input(
564
  "Density (kg/m鲁)",
565
  min_value=1.0,
 
612
  key="material_emissivity_input"
613
  )
614
 
615
+ if st.form_submit_button("Save Material/Door"):
616
  action_id = str(uuid.uuid4())
617
  if st.session_state.material_action.get("id") != action_id:
618
  st.session_state.material_action = {"action": "save", "id": action_id}
619
  st.session_state.material_form_state = {
620
  "name": name,
621
  "category": category,
622
+ "conductivity": conductivity if category != "Door" else 0.0,
623
  "density": density,
624
  "specific_heat": specific_heat,
625
  "default_thickness": default_thickness,
626
  "embodied_carbon": embodied_carbon,
627
  "absorptivity": absorptivity,
628
  "price": price,
629
+ "emissivity": emissivity,
630
+ "u_value": u_value
631
  }
632
  if not name or not name.strip():
633
+ st.error("Material/Door name cannot be empty.")
634
  elif (name in st.session_state.project_data["materials"]["project"] or
635
  name in st.session_state.project_data["materials"]["library"]) and (not is_edit or name != original_name):
636
+ st.error(f"Material/Door '{name}' already exists.")
637
  else:
638
  try:
 
 
639
  new_material = Material(
640
  name=name,
641
  category=MaterialCategory[category.upper().replace("-", "_")],
642
+ conductivity=0.0 if category == "Door" else conductivity,
643
  density=density,
644
  specific_heat=specific_heat,
645
  default_thickness=default_thickness,
 
647
  absorptivity=absorptivity,
648
  price=price,
649
  emissivity=emissivity,
650
+ is_library=False,
651
+ u_value=u_value if category == "Door" else None
652
  )
653
  if is_edit and editor_state.get("edit_source") == "project":
654
  success, message = material_library.edit_project_material(
 
662
  st.session_state.material_form_state = {
663
  "name": "",
664
  "category": "Insulation",
665
+ "conductivity": 0.1,
666
  "density": 1000.0,
667
  "specific_heat": 1000.0,
668
  "default_thickness": 0.1,
669
  "embodied_carbon": 0.5,
670
  "absorptivity": DEFAULT_MATERIAL_PROPERTIES["absorptivity"],
671
  "price": 50.0,
672
+ "emissivity": DEFAULT_MATERIAL_PROPERTIES["emissivity"],
673
+ "u_value": 5.0
674
  }
675
  st.session_state.material_action = {"action": None, "id": None}
676
  st.session_state.rerun_trigger = None
677
  st.session_state.materials_rerun_pending = True
678
  else:
679
+ st.error(f"Failed to save material/door: {message}")
680
  except Exception as e:
681
+ st.error(f"Error saving material/door: {str(e)}")
682
 
683
  def display_fenestrations_tab(material_library: MaterialLibrary):
684
+ """Display the fenestrations tab content with two-column layout, excluding doors."""
685
  col1, col2 = st.columns([3, 2])
686
  with col1:
687
+ st.subheader("Fenestrations (Windows/Skylights)")
688
  filter_options = ["All", "None"] + FENESTRATION_TYPES
689
  fenestration_filter = st.selectbox("Filter Fenestrations", filter_options, key="fenestration_filter")
690
 
 
853
  "Category",
854
  FENESTRATION_TYPES,
855
  index=category_index,
856
+ help="Fenestration type (Window/Skylight)",
857
  key="fenestration_category_input"
858
  )
859
  shgc = st.number_input(
 
934
  return materials
935
 
936
  def get_available_fenestrations():
937
+ """Get all available fenestrations (library + project windows + doors from materials) for use in other modules."""
938
  fenestrations = {}
939
  if "fenestrations" in st.session_state.project_data:
940
  fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
941
  fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
942
+ # Include doors from materials as fenestrations for compatibility
943
+ if "materials" in st.session_state.project_data:
944
+ for k, m in st.session_state.project_data["materials"]["library"].items():
945
+ if m.get("category") == "Door":
946
+ fenestrations[k] = GlazingMaterial(
947
+ name=k,
948
+ shgc=0.0,
949
+ u_value=m["performance"]["u_value"],
950
+ h_o=m.get("h_o", DEFAULT_WINDOW_PROPERTIES["h_o"]),
951
+ category="Door",
952
+ is_library=True
953
+ )
954
+ for k, m in st.session_state.project_data["materials"]["project"].items():
955
+ if m.category == MaterialCategory.DOOR:
956
+ fenestrations[k] = GlazingMaterial(
957
+ name=k,
958
+ shgc=0.0,
959
+ u_value=m.u_value,
960
+ h_o=DEFAULT_WINDOW_PROPERTIES["h_o"],
961
+ category="Door",
962
+ is_library=False
963
+ )
964
  return fenestrations