mabuseif commited on
Commit
4740b99
·
verified ·
1 Parent(s): 5615cd1

Update app/materials_library.py

Browse files
Files changed (1) hide show
  1. app/materials_library.py +390 -319
app/materials_library.py CHANGED
@@ -347,10 +347,8 @@ def categorize_thermal_mass(thermal_mass: float) -> str:
347
 
348
  def get_stable_button_key(prefix: str, name: str, action: str) -> str:
349
  """Generate a stable button key based on prefix, name, and action."""
350
- # Create a stable hash-based key that won't change across reruns
351
  import hashlib
352
  key_string = f"{prefix}_{name}_{action}"
353
- # Use first 8 characters of hash for shorter keys
354
  hash_key = hashlib.md5(key_string.encode()).hexdigest()[:8]
355
  return f"{prefix}_{action}_{hash_key}"
356
 
@@ -359,52 +357,72 @@ def display_materials_page():
359
  Display the material library page.
360
  This is the main function called by main.py when the Material Library page is selected.
361
  """
362
- # Initialize session state flags
363
- if "material_saved" not in st.session_state:
364
- st.session_state.material_saved = False
365
- if "fenestration_saved" not in st.session_state:
366
- st.session_state.fenestration_saved = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
  st.title("Material Library")
369
 
370
- # Display help information in an expandable section
371
  with st.expander("Help & Information"):
372
  display_materials_help()
373
 
374
- # Create tabs for materials and fenestrations
375
  tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
376
 
377
- # Materials tab
378
  with tab1:
 
 
379
  display_materials_tab()
380
 
381
- # Fenestrations tab
382
  with tab2:
 
 
383
  display_fenestrations_tab()
384
 
385
- # Navigation buttons
386
  col1, col2 = st.columns(2)
387
 
388
  with col1:
389
  if st.button("Back to Climate Data", key="back_to_climate"):
390
  st.session_state.current_page = "Climate Data"
391
- st.rerun()
392
 
393
  with col2:
394
  if st.button("Continue to Construction", key="continue_to_construction"):
395
  st.session_state.current_page = "Construction"
396
- st.rerun()
397
 
398
  def display_materials_tab():
399
  """Display the materials tab content."""
400
- # Initialize materials in session state if not present
401
  initialize_materials()
402
 
403
  st.subheader("Materials")
404
  col1, col2 = st.columns([3, 2])
405
 
406
  with col1:
407
- # Library Materials
408
  filter_options = ["All"] + MATERIAL_CATEGORIES
409
  category = st.selectbox("Filter by Category", filter_options, key="material_filter")
410
 
@@ -435,29 +453,43 @@ def display_materials_tab():
435
  cols[1].write(thermal_mass_category)
436
  cols[2].write(f"{u_value:.3f}")
437
 
438
- # Use stable keys for buttons
439
  preview_key = get_stable_button_key("lib_mat", name, "preview")
440
  copy_key = get_stable_button_key("lib_mat", name, "copy")
441
 
442
  if cols[3].button("Preview", key=preview_key):
443
- st.session_state.material_editor = {
444
- "name": name,
445
- "category": material["category"],
446
- "thermal_conductivity": material["thermal_conductivity"],
447
- "density": material["density"],
448
- "specific_heat": material["specific_heat"],
449
- "default_thickness": material["default_thickness"],
450
- "embodied_carbon": material["embodied_carbon"],
451
- "cost": material["cost"],
452
- "absorptivity": material["absorptivity"],
453
- "emissivity": material["emissivity"],
454
- "colour": material["colour"],
455
- "edit_mode": False,
456
- "original_name": name,
457
- "is_library": True
458
- }
459
- st.session_state.active_tab = "Materials"
460
- st.success(f"Previewing material '{name}'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
  if cols[4].button("Copy", key=copy_key):
463
  new_name = f"{name}_Project"
@@ -468,11 +500,11 @@ def display_materials_tab():
468
  st.session_state.project_data["materials"]["project"][new_name] = material.copy()
469
  st.success(f"Material '{new_name}' copied to project.")
470
  logger.info(f"Copied library material '{name}' as '{new_name}' to project")
471
- st.rerun()
 
472
  else:
473
  st.info("No materials found in the selected category.")
474
 
475
- # Project Materials
476
  project_category = st.selectbox("Filter Project by Category", filter_options, key="project_material_filter")
477
  st.subheader("Project Materials")
478
  with st.container():
@@ -501,29 +533,43 @@ def display_materials_tab():
501
  cols[1].write(thermal_mass_category)
502
  cols[2].write(f"{u_value:.3f}")
503
 
504
- # Use stable keys for buttons
505
  edit_key = get_stable_button_key("proj_mat", name, "edit")
506
  delete_key = get_stable_button_key("proj_mat", name, "delete")
507
 
508
  if cols[3].button("Edit", key=edit_key):
509
- st.session_state.material_editor = {
510
- "name": name,
511
- "category": material["category"],
512
- "thermal_conductivity": material["thermal_conductivity"],
513
- "density": material["density"],
514
- "specific_heat": material["specific_heat"],
515
- "default_thickness": material["default_thickness"],
516
- "embodied_carbon": material["embodied_carbon"],
517
- "cost": material["cost"],
518
- "absorptivity": material["absorptivity"],
519
- "emissivity": material["emissivity"],
520
- "colour": material["colour"],
521
- "edit_mode": True,
522
- "original_name": name,
523
- "is_library": False
524
- }
525
- st.session_state.active_tab = "Materials"
526
- st.success(f"Editing material '{name}'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
  if cols[4].button("Delete", key=delete_key):
529
  is_in_use = check_material_in_use(name)
@@ -533,11 +579,11 @@ def display_materials_tab():
533
  del st.session_state.project_data["materials"]["project"][name]
534
  st.success(f"Material '{name}' deleted from project.")
535
  logger.info(f"Deleted material '{name}' from project")
536
- st.rerun()
 
537
  else:
538
  st.info("No project materials in the selected category.")
539
 
540
- # Comprehensive Materials Table
541
  st.subheader("All Project Materials")
542
  project_materials = st.session_state.project_data["materials"]["project"]
543
  if project_materials:
@@ -573,18 +619,15 @@ def display_materials_tab():
573
 
574
  def display_fenestrations_tab():
575
  """Display the fenestrations tab content."""
576
- # Initialize fenestrations in session state if not present
577
  initialize_fenestrations()
578
 
579
  st.subheader("Fenestrations")
580
  col1, col2 = st.columns([3, 2])
581
 
582
  with col1:
583
- # Type filter
584
  filter_options = ["All"] + FENESTRATION_TYPES
585
  fen_type = st.selectbox("Filter by Type", filter_options, key="fenestration_filter")
586
 
587
- # Library Fenestrations
588
  st.subheader("Library Fenestrations")
589
  with st.container():
590
  library_fenestrations = st.session_state.project_data["fenestrations"]["library"]
@@ -609,26 +652,37 @@ def display_fenestrations_tab():
609
  cols[1].write(fenestration["type"])
610
  cols[2].write(f"{fenestration['u_value']:.2f}")
611
 
612
- # Use stable keys for buttons
613
  preview_key = get_stable_button_key("lib_fen", name, "preview")
614
  copy_key = get_stable_button_key("lib_fen", name, "copy")
615
 
616
  if cols[3].button("Preview", key=preview_key):
617
- st.session_state.fenestration_editor = {
618
- "name": name,
619
- "type": fenestration["type"],
620
- "u_value": fenestration["u_value"],
621
- "shgc": fenestration["shgc"],
622
- "visible_transmittance": fenestration["visible_transmittance"],
623
- "thickness": fenestration["thickness"],
624
- "embodied_carbon": fenestration["embodied_carbon"],
625
- "cost": fenestration["cost"],
626
- "edit_mode": False,
627
- "original_name": name,
628
- "is_library": True
629
- }
630
- st.session_state.active_tab = "Fenestrations"
631
- st.success(f"Previewing fenestration '{name}'")
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
  if cols[4].button("Copy", key=copy_key):
634
  new_name = f"{name}_Project"
@@ -639,11 +693,11 @@ def display_fenestrations_tab():
639
  st.session_state.project_data["fenestrations"]["project"][new_name] = fenestration.copy()
640
  st.success(f"Fenestration '{new_name}' copied to project.")
641
  logger.info(f"Copied library fenestration '{name}' as '{new_name}' to project")
642
- st.rerun()
 
643
  else:
644
  st.info("No fenestrations found in the selected type.")
645
 
646
- # Project Fenestrations
647
  project_fen_type = st.selectbox("Filter Project by Type", filter_options, key="project_fenestration_filter")
648
  st.subheader("Project Fenestrations")
649
  with st.container():
@@ -669,53 +723,64 @@ def display_fenestrations_tab():
669
  cols[1].write(fenestration["type"])
670
  cols[2].write(f"{fenestration['u_value']:.2f}")
671
 
672
- # Use stable keys for buttons
673
  edit_key = get_stable_button_key("proj_fen", name, "edit")
674
  delete_key = get_stable_button_key("proj_fen", name, "delete")
675
 
676
  if cols[3].button("Edit", key=edit_key):
677
- st.session_state.fenestration_editor = {
678
- "name": name,
679
- "type": fenestration["type"],
680
- "u_value": fenestration["u_value"],
681
- "shgc": fenestration["shgc"],
682
- "visible_transmittance": fenestration["visible_transmittance"],
683
- "thickness": fenestration["thickness"],
684
- "embodied_carbon": fenestration["embodied_carbon"],
685
- "cost": fenestration["cost"],
686
- "edit_mode": True,
687
- "original_name": name,
688
- "is_library": False
689
- }
690
- st.session_state.active_tab = "Fenestrations"
691
- st.success(f"Editing fenestration '{name}'")
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
  if cols[4].button("Delete", key=delete_key):
694
  is_in_use = check_fenestration_in_use(name)
695
  if is_in_use:
696
- st.error(f"Cannot delete fenestration '{name}' because it is in use in components.")
697
  else:
698
  del st.session_state.project_data["fenestrations"]["project"][name]
699
  st.success(f"Fenestration '{name}' deleted from project.")
700
- logger.info(f"Deleted fenestration '{name}' from project")
701
- st.rerun()
 
702
  else:
703
  st.info("No project fenestrations in the selected type.")
704
 
705
- # Comprehensive Fenestrations Table
706
  st.subheader("All Project Fenestrations")
707
  project_fenestrations = st.session_state.project_data["fenestrations"]["project"]
708
  if project_fenestrations:
709
  data = []
710
- for name, props in project_fenestrations.items():
711
  data.append({
712
  "Name": name,
713
  "Type": props["type"],
714
  "U-Value (W/m²·K)": props["u_value"],
715
  "SHGC": props["shgc"],
716
- "Visible Transmittance": props["visible_transmittance"],
717
  "Thickness (m)": props["thickness"],
718
- "Embodied Carbon (kg CO₂e/m²)": props["embodied_carbon"],
719
  "Cost (USD/m²)": props["cost"]
720
  })
721
  df = pd.DataFrame(data)
@@ -737,11 +802,9 @@ def initialize_materials():
737
  "table": None
738
  }
739
 
740
- # Initialize library materials if empty
741
  if not st.session_state.project_data["materials"]["library"]:
742
  st.session_state.project_data["materials"]["library"] = DEFAULT_MATERIALS.copy()
743
 
744
- # Initialize material editor state
745
  if "material_editor" not in st.session_state:
746
  st.session_state.material_editor = {
747
  "name": "",
@@ -751,7 +814,7 @@ def initialize_materials():
751
  "specific_heat": 1000.0,
752
  "default_thickness": 0.05,
753
  "embodied_carbon": 100.0,
754
- "cost": 100.0,
755
  "absorptivity": 0.60,
756
  "emissivity": 0.90,
757
  "colour": "Medium",
@@ -759,6 +822,21 @@ def initialize_materials():
759
  "original_name": "",
760
  "is_library": False
761
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
 
763
  def initialize_fenestrations():
764
  """Initialize fenestrations in session state if not present."""
@@ -769,18 +847,16 @@ def initialize_fenestrations():
769
  "table": None
770
  }
771
 
772
- # Initialize library fenestrations if empty
773
  if not st.session_state.project_data["fenestrations"]["library"]:
774
  st.session_state.project_data["fenestrations"]["library"] = DEFAULT_FENESTRATIONS.copy()
775
 
776
- # Initialize fenestration editor state
777
- if "fenestration_editor" not in st.session_state:
778
  st.session_state.fenestration_editor = {
779
  "name": "",
780
  "type": FENESTRATION_TYPES[0],
781
  "u_value": 2.8,
782
  "shgc": 0.7,
783
- "visible_transmittance": 0.8,
784
  "thickness": 0.024,
785
  "embodied_carbon": 900.0,
786
  "cost": 200.0,
@@ -788,144 +864,144 @@ def initialize_fenestrations():
788
  "original_name": "",
789
  "is_library": False
790
  }
 
 
 
 
 
 
 
 
 
 
 
 
791
 
792
  def display_material_editor():
793
  """Display the material editor form."""
794
  with st.form("material_editor_form"):
795
  editor_state = st.session_state.material_editor
 
796
  is_library = editor_state.get("is_library", False)
797
 
798
- # Material name
799
  name = st.text_input(
800
  "Material Name",
801
- value=editor_state["name"],
802
  help="Enter a unique name for the material.",
803
  disabled=is_library
804
  )
805
 
806
- # Create two columns for layout
807
  col1, col2 = st.columns(2)
808
 
809
  with col1:
810
- # Category
811
  category = st.selectbox(
812
  "Category",
813
  MATERIAL_CATEGORIES,
814
- index=MATERIAL_CATEGORIES.index(editor_state["category"]) if editor_state["category"] in MATERIAL_CATEGORIES else 0,
815
  help="Select the material category.",
816
  disabled=is_library
817
  )
818
 
819
- # Thermal conductivity
820
  thermal_conductivity = st.number_input(
821
  "Thermal Conductivity (W/m·K)",
822
  min_value=0.001,
823
  max_value=1000.0,
824
- value=float(editor_state["thermal_conductivity"]),
825
  format="%.3f",
826
  help="Thermal conductivity in W/m·K. Lower values indicate better insulation.",
827
  disabled=is_library
828
  )
829
 
830
- # Density
831
  density = st.number_input(
832
  "Density (kg/m³)",
833
  min_value=1.0,
834
  max_value=20000.0,
835
- value=float(editor_state["density"]),
836
  format="%.1f",
837
  help="Material density in kg/m³.",
838
  disabled=is_library
839
  )
840
 
841
  with col2:
842
- # Specific heat
843
  specific_heat = st.number_input(
844
  "Specific Heat (J/kg·K)",
845
  min_value=100.0,
846
  max_value=10000.0,
847
- value=float(editor_state["specific_heat"]),
848
  format="%.1f",
849
  help="Specific heat capacity in J/kg·K. Higher values indicate better thermal mass.",
850
  disabled=is_library
851
  )
852
 
853
- # Default thickness
854
  default_thickness = st.number_input(
855
  "Thickness (m)",
856
  min_value=0.001,
857
  max_value=0.5,
858
- value=float(editor_state["default_thickness"]),
859
  format="%.3f",
860
  help="Thickness for this material in meters.",
861
  disabled=is_library
862
  )
863
 
864
- # Absorptivity
865
  absorptivity = st.number_input(
866
  "Absorptivity",
867
  min_value=0.0,
868
  max_value=1.0,
869
- value=float(editor_state["absorptivity"]),
870
  format="%.2f",
871
  help="Solar radiation absorbed (0-1).",
872
  disabled=is_library
873
  )
874
 
875
- # Other properties
876
  col1, col2 = st.columns(2)
877
 
878
  with col1:
879
- # Emissivity
880
  emissivity = st.number_input(
881
  "Emissivity",
882
  min_value=0.0,
883
  max_value=1.0,
884
- value=float(editor_state["emissivity"]),
885
  format="%.2f",
886
  help="Ratio of radiation emitted (0-1).",
887
  disabled=is_library
888
  )
889
 
890
  with col2:
891
- # Colour
892
  colour = st.selectbox(
893
  "Colour Category",
894
  COLOUR_CATEGORIES,
895
- index=COLOUR_CATEGORIES.index(editor_state["colour"]) if editor_state["colour"] in COLOUR_CATEGORIES else 0,
896
  help="Colour category affecting absorptivity.",
897
  disabled=is_library
898
  )
899
 
900
- # Additional properties
901
  st.subheader("Additional Properties")
902
  col1, col2 = st.columns(2)
903
 
904
  with col1:
905
- # Embodied carbon
906
  embodied_carbon = st.number_input(
907
  "Embodied Carbon (kg CO₂e/m³)",
908
  min_value=0.0,
909
  max_value=10000.0,
910
- value=float(editor_state["embodied_carbon"]),
911
  format="%.1f",
912
  help="Embodied carbon in kg CO₂e per cubic meter.",
913
  disabled=is_library
914
  )
915
 
916
  with col2:
917
- # Cost
918
  cost = st.number_input(
919
  "Cost (USD/m³)",
920
  min_value=0.0,
921
  max_value=10000.0,
922
- value=float(editor_state["cost"]),
923
  format="%.1f",
924
  help="Material cost in USD per cubic meter.",
925
  disabled=is_library
926
  )
927
 
928
- # Form submission buttons
929
  col1, col2 = st.columns(2)
930
 
931
  with col1:
@@ -934,22 +1010,12 @@ def display_material_editor():
934
  with col2:
935
  clear_button = st.form_submit_button("Clear Form")
936
 
937
- # Handle form submission
938
  if submit_button and not is_library:
939
- # Validate inputs
940
- validation_errors = validate_material(
941
- name, category, thermal_conductivity, density, specific_heat,
942
- default_thickness, embodied_carbon, cost,
943
- editor_state["edit_mode"], editor_state["original_name"],
944
- absorptivity, emissivity, colour
945
- )
946
-
947
- if validation_errors:
948
- for error in validation_errors:
949
- st.error(error)
950
- else:
951
- # Create material data
952
- material_data = {
953
  "category": category,
954
  "thermal_conductivity": thermal_conductivity,
955
  "density": density,
@@ -961,132 +1027,138 @@ def display_material_editor():
961
  "emissivity": emissivity,
962
  "colour": colour
963
  }
964
-
965
- # Handle edit mode
966
- if editor_state["edit_mode"]:
967
- original_name = editor_state["original_name"]
968
- if original_name != name:
969
- del st.session_state.project_data["materials"]["project"][original_name]
970
- st.session_state.project_data["materials"]["project"][name] = material_data
971
- st.success(f"Material '{name}' updated successfully.")
972
- logger.info(f"Updated material '{name}' in project")
973
  else:
974
- st.session_state.project_data["materials"]["project"][name] = material_data
975
- st.success(f"Material '{name}' added to your project.")
976
- logger.info(f"Added new material '{name}' to project")
977
-
978
- # Reset editor and trigger rerun
979
- reset_material_editor()
980
- logger.info("Triggering rerun after saving material editor")
981
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
982
 
983
- # Handle clear button
984
  if clear_button:
985
  reset_material_editor()
986
- logger.info("Triggering rerun after clearing material editor")
987
- st.rerun()
988
 
989
  def display_fenestration_editor():
990
  """Display the fenestration editor form."""
991
  with st.form("fenestration_editor_form"):
992
  editor_state = st.session_state.fenestration_editor
 
993
  is_library = editor_state.get("is_library", False)
994
 
995
- # Fenestration name
996
  name = st.text_input(
997
  "Fenestration Name",
998
- value=editor_state["name"],
999
  help="Enter a unique name for the fenestration.",
1000
  disabled=is_library
1001
  )
1002
 
1003
- # Create two columns for layout
1004
  col1, col2 = st.columns(2)
1005
 
1006
  with col1:
1007
- # Type
1008
  fenestration_type = st.selectbox(
1009
  "Type",
1010
  FENESTRATION_TYPES,
1011
- index=FENESTRATION_TYPES.index(editor_state["type"]) if editor_state["type"] in FENESTRATION_TYPES else 0,
1012
  help="Select the fenestration type.",
1013
  disabled=is_library
1014
  )
1015
 
1016
- # U-value
1017
  u_value = st.number_input(
1018
  "U-Value (W/m²·K)",
1019
  min_value=0.1,
1020
  max_value=10.0,
1021
- value=float(editor_state["u_value"]),
1022
  format="%.2f",
1023
- help="U-value in W/m²·K. Lower values indicate better insulation.",
1024
  disabled=is_library
1025
  )
1026
 
1027
- # SHGC
1028
  shgc = st.number_input(
1029
  "Solar Heat Gain Coefficient (SHGC)",
1030
  min_value=0.0,
1031
  max_value=1.0,
1032
- value=float(editor_state["shgc"]),
1033
  format="%.2f",
1034
- help="Solar Heat Gain Coefficient (0-1). Lower values indicate less solar heat transmission.",
1035
  disabled=is_library
1036
  )
1037
-
1038
- with col2:
1039
- # Visible transmittance
1040
- visible_transmittance = st.number_input(
1041
  "Visible Transmittance",
1042
  min_value=0.0,
1043
  max_value=1.0,
1044
- value=float(editor_state["visible_transmittance"]),
1045
  format="%.2f",
1046
- help="Visible Transmittance (0-1). Higher values indicate more visible light transmission.",
1047
  disabled=is_library
1048
  )
1049
 
1050
- # Thickness
1051
  thickness = st.number_input(
1052
  "Thickness (m)",
1053
  min_value=0.001,
1054
- max_value=0.1,
1055
- value=float(editor_state["thickness"]),
1056
  format="%.3f",
1057
  help="Thickness in meters.",
1058
  disabled=is_library
1059
  )
1060
 
1061
- # Additional properties
1062
  st.subheader("Additional Properties")
 
1063
  col1, col2 = st.columns(2)
1064
 
1065
  with col1:
1066
- # Embodied carbon
1067
  embodied_carbon = st.number_input(
1068
  "Embodied Carbon (kg CO₂e/m²)",
1069
  min_value=0.0,
1070
  max_value=5000.0,
1071
- value=float(editor_state["embodied_carbon"]),
1072
  format="%.1f",
1073
- help="Embodied carbon in kg CO₂e per square meter.",
1074
  disabled=is_library
1075
  )
1076
 
1077
  with col2:
1078
- # Cost
1079
  cost = st.number_input(
1080
  "Cost (USD/m²)",
1081
  min_value=0.0,
1082
  max_value=5000.0,
1083
- value=float(editor_state["cost"]),
1084
  format="%.1f",
1085
- help="Fenestration cost in USD per square meter.",
1086
  disabled=is_library
1087
  )
1088
 
1089
- # Form submission buttons
1090
  col1, col2 = st.columns(2)
1091
 
1092
  with col1:
@@ -1095,114 +1167,100 @@ def display_fenestration_editor():
1095
  with col2:
1096
  clear_button = st.form_submit_button("Clear Form")
1097
 
1098
- # Handle form submission
1099
  if submit_button and not is_library:
1100
- # Validate inputs
1101
- validation_errors = validate_fenestration(
1102
- name, fenestration_type, u_value, shgc, visible_transmittance, thickness,
1103
- embodied_carbon, cost, editor_state["edit_mode"], editor_state["original_name"]
1104
- )
1105
-
1106
- if validation_errors:
1107
- for error in validation_errors:
1108
- st.error(error)
1109
- else:
1110
- # Create fenestration data
1111
- fenestration_data = {
1112
  "type": fenestration_type,
1113
  "u_value": u_value,
1114
  "shgc": shgc,
1115
- "visible_transmittance": visible_transmittance,
1116
  "thickness": thickness,
1117
  "embodied_carbon": embodied_carbon,
1118
  "cost": cost
1119
  }
1120
-
1121
- # Handle edit mode
1122
- if editor_state["edit_mode"]:
1123
- original_name = editor_state["original_name"]
1124
- if original_name != name:
1125
- del st.session_state.project_data["fenestrations"]["project"][original_name]
1126
- st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
1127
- st.success(f"Fenestration '{name}' updated successfully.")
1128
- logger.info(f"Updated fenestration '{name}' in project")
1129
  else:
1130
- st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
1131
- st.success(f"Fenestration '{name}' added to your project.")
1132
- logger.info(f"Added new fenestration '{name}' to project")
1133
-
1134
- # Reset editor and trigger rerun
1135
- reset_fenestration_editor()
1136
- logger.info("Triggering rerun after saving fenestration editor")
1137
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1138
 
1139
- # Handle clear button
1140
  if clear_button:
1141
  reset_fenestration_editor()
1142
- logger.info("Triggering rerun after clearing fenestration editor")
1143
- st.rerun()
1144
 
1145
  def validate_material(
1146
  name: str, category: str, thermal_conductivity: float, density: float,
1147
  specific_heat: float, default_thickness: float, embodied_carbon: float,
1148
  cost: float, edit_mode: bool, original_name: str,
1149
  absorptivity: float, emissivity: float, colour: str
1150
- ) -> List[str]:
1151
  """
1152
  Validate material inputs.
 
1153
  """
1154
- errors = []
1155
-
1156
- # Validate name
1157
  if not name or name.strip() == "":
1158
- errors.append("Material name is required.")
1159
 
1160
- # Check for name uniqueness
1161
- if not edit_mode or (edit_mode and name != original_name):
1162
- if name in st.session_state.project_data["materials"]["project"]:
1163
- errors.append(f"Material name '{name}' already exists in your project.")
1164
 
1165
- # Validate category
1166
- if category not in MATERIAL_CATEGORIES:
1167
- errors.append("Please select a valid material category.")
1168
 
1169
- # Validate thermal conductivity
1170
  if thermal_conductivity <= 0:
1171
- errors.append("Thermal conductivity must be greater than zero.")
1172
 
1173
- # Validate density
1174
  if density <= 0:
1175
- errors.append("Density must be greater than zero.")
1176
 
1177
- # Validate specific heat
1178
  if specific_heat <= 0:
1179
- errors.append("Specific heat must be greater than zero.")
1180
 
1181
- # Validate default thickness
1182
  if default_thickness <= 0:
1183
- errors.append("Thickness must be greater than zero.")
1184
 
1185
- # Validate embodied carbon
1186
  if embodied_carbon < 0:
1187
- errors.append("Embodied carbon cannot be negative.")
1188
 
1189
- # Validate cost
1190
  if cost < 0:
1191
- errors.append("Cost cannot be negative.")
1192
 
1193
- # Validate absorptivity
1194
  if absorptivity < 0 or absorptivity > 1:
1195
- errors.append("Absorptivity must be between 0 and 1.")
1196
 
1197
- # Validate emissivity
1198
  if emissivity < 0 or emissivity > 1:
1199
- errors.append("Emissivity must be between 0 and 1.")
1200
 
1201
- # Validate colour
1202
  if colour not in COLOUR_CATEGORIES:
1203
- errors.append("Please select a valid colour category.")
1204
 
1205
- # Validate absorptivity based on colour
1206
  colour_ranges = {
1207
  "Light": (0.30, 0.50),
1208
  "Medium": (0.60, 0.70),
@@ -1211,93 +1269,105 @@ def validate_material(
1211
  }
1212
  min_abs, max_abs = colour_ranges[colour]
1213
  if not (min_abs <= absorptivity <= max_abs):
1214
- errors.append(f"Absorptivity for {colour} colour must be between {min_abs} and {max_abs}.")
1215
 
1216
- return errors
1217
 
1218
  def validate_fenestration(
1219
  name: str, fenestration_type: str, u_value: float, shgc: float,
1220
- visible_transmittance: float, thickness: float, embodied_carbon: float,
1221
  cost: float, edit_mode: bool, original_name: str
1222
- ) -> List[str]:
1223
  """
1224
  Validate fenestration inputs.
1225
- """
1226
- errors = []
1227
-
1228
- # Validate name
1229
  if not name or name.strip() == "":
1230
- errors.append("Fenestration name is required.")
1231
 
1232
- # Check for name uniqueness
1233
- if not edit_mode or (edit_mode and name != original_name):
1234
- if name in st.session_state.project_data["fenestrations"]["project"]:
1235
- errors.append(f"Fenestration name '{name}' already exists in your project.")
1236
 
1237
- # Validate type
1238
  if fenestration_type not in FENESTRATION_TYPES:
1239
- errors.append("Please select a valid fenestration type.")
1240
 
1241
- # Validate u_value
1242
  if u_value <= 0:
1243
- errors.append("U-value must be greater than zero.")
1244
 
1245
- # Validate shgc
1246
  if shgc < 0 or shgc > 1:
1247
- errors.append("SHGC must be between 0 and 1.")
1248
 
1249
- # Validate visible transmittance
1250
- if visible_transmittance < 0 or visible_transmittance > 1:
1251
- errors.append("Visible transmittance must be between 0 and 1.")
1252
 
1253
- # Validate thickness
1254
  if thickness <= 0:
1255
- errors.append("Thickness must be greater than zero.")
1256
-
1257
- # Validate embodied carbon
1258
  if embodied_carbon < 0:
1259
- errors.append("Embodied carbon cannot be negative.")
1260
 
1261
- # Validate cost
1262
  if cost < 0:
1263
- errors.append("Cost cannot be negative.")
1264
 
1265
- return errors
1266
 
1267
  def reset_material_editor():
1268
- """Reset the material editor to default values."""
1269
- st.session_state.material_editor = {
1270
  "name": "",
1271
  "category": MATERIAL_CATEGORIES[0],
1272
  "thermal_conductivity": 0.5,
1273
  "density": 1000.0,
1274
  "specific_heat": 1000.0,
1275
  "default_thickness": 0.05,
1276
- "embodied_carbon": 100.0,
1277
- "cost": 100.0,
1278
  "absorptivity": 0.60,
1279
- "emissivity": 0.90,
1280
- "colour": "Medium",
 
 
 
 
 
 
 
 
 
 
 
 
 
1281
  "edit_mode": False,
1282
  "original_name": "",
1283
  "is_library": False
1284
  }
 
 
1285
 
1286
  def reset_fenestration_editor():
1287
- """Reset the fenestration editor to default values."""
1288
- st.session_state.fenestration_editor = {
1289
  "name": "",
1290
  "type": FENESTRATION_TYPES[0],
1291
- "u_value": 2.8,
1292
- "shgc": 0.7,
1293
- "visible_transmittance": 0.8,
1294
- "thickness": 0.024,
1295
- "embodied_carbon": 900.0,
1296
- "cost": 200.0,
1297
- "edit_mode": False,
1298
- "original_name": "",
1299
- "is_library": False
1300
  }
 
 
 
 
 
 
 
 
 
 
 
 
1301
 
1302
  def check_material_in_use(material_name: str) -> bool:
1303
  """
@@ -1305,15 +1375,16 @@ def check_material_in_use(material_name: str) -> bool:
1305
  """
1306
  return False
1307
 
1308
- def check_fenestration_in_use(fenestration_name: str) -> bool:
1309
  """
1310
- Check if a fenestration is in use in any components.
1311
  """
1312
  return False
1313
 
1314
  def display_materials_help():
1315
  """Display help information for the material library page."""
1316
  st.markdown("""
 
1317
  ### Material Library Help
1318
 
1319
  This section allows you to manage building materials and fenestrations for your project.
@@ -1322,32 +1393,32 @@ def display_materials_help():
1322
 
1323
  * **Library Materials**: Pre-defined materials with standard thermal properties. Use 'Preview' to view details or 'Copy' to add to your project.
1324
  * **Project Materials**: Materials you've added to your project. Use 'Edit' to modify or 'Delete' to remove.
1325
- * **Material Editor/Creator**: Create new materials or edit existing project materials. Library materials can only be previewed.
1326
 
1327
  **Fenestrations Tab:**
1328
 
1329
  * **Library Fenestrations**: Pre-defined windows, doors, and skylights. Use 'Preview' to view details or 'Copy' to add to your project.
1330
  * **Project Fenestrations**: Fenestrations you've added to your project. Use 'Edit' to modify or 'Delete' to remove.
1331
- * **Fenestration Editor/Creator**: Create new fenestrations or edit existing project fenestrations. Library fenestrations can only be previewed.
1332
 
1333
  **Key Properties:**
1334
 
1335
  * **Thermal Conductivity (W/m·K)**: Rate of heat transfer through a material. Lower values indicate better insulation.
1336
  * **Density (kg/m³)**: Mass per unit volume.
1337
- * **Specific Heat (J/kg·K)**: Energy required to raise the temperature of 1 kg by 1 K. Higher values indicate better thermal mass.
1338
  * **Thermal Mass**: Areal heat capacity, categorized as Low (<30,000 J/m²·K), Medium (30,000–90,000 J/m²·K), or High (>90,000 J/m²·K).
1339
  * **U-Value (W/m²·K)**: Overall heat transfer coefficient, accounting for material and surface resistances. Lower values indicate better insulation.
1340
- * **SHGC**: Solar Heat Gain Coefficient (0-1). Fraction of incident solar radiation that enters through a fenestration.
1341
  * **Visible Transmittance**: Fraction of visible light that passes through a fenestration.
1342
  * **Embodied Carbon**: Carbon emissions associated with material production, measured in kg CO₂e per unit volume or area.
1343
- * **Cost**: Material cost in USD per unit volume or area.
1344
  * **Absorptivity**: Fraction of solar radiation absorbed (0-1). Depends on colour category.
1345
  * **Emissivity**: Ratio of radiation emitted by the material (0-1).
1346
  * **Colour**: Category affecting absorptivity (Light, Medium, Dark, Reflective).
1347
 
1348
- **Workflow:**
1349
 
1350
- 1. Browse the library materials and fenestrations
1351
  2. Copy items to your project or create custom ones
1352
  3. Edit properties as needed for your specific project
1353
  4. Continue to the Construction page to create assemblies using these materials
 
347
 
348
  def get_stable_button_key(prefix: str, name: str, action: str) -> str:
349
  """Generate a stable button key based on prefix, name, and action."""
 
350
  import hashlib
351
  key_string = f"{prefix}_{name}_{action}"
 
352
  hash_key = hashlib.md5(key_string.encode()).hexdigest()[:8]
353
  return f"{prefix}_{action}_{hash_key}"
354
 
 
357
  Display the material library page.
358
  This is the main function called by main.py when the Material Library page is selected.
359
  """
360
+ # Initialize session state
361
+ if "rerun_pending" not in st.session_state:
362
+ st.session_state.rerun_pending = False
363
+ if "active_tab" not in st.session_state:
364
+ st.session_state.active_tab = "Materials"
365
+ if "material_action" not in st.session_state:
366
+ st.session_state.material_action = {"action": None, "id": None}
367
+ if "fenestration_action" not in st.session_state:
368
+ st.session_state.fenestration_action = {"action": None, "id": None}
369
+ if "rerun_trigger" not in st.session_state:
370
+ st.session_state.rerun_trigger = None
371
+
372
+ # Check for rerun trigger
373
+ if st.session_state.rerun_pending:
374
+ st.session_state.rerun_pending = False
375
+ st.rerun()
376
+
377
+ # Apply custom CSS for button styling
378
+ st.markdown("""
379
+ <style>
380
+ .stButton>button {
381
+ width: 100%;
382
+ font-size: 12px;
383
+ padding: 5px;
384
+ margin: 0;
385
+ }
386
+ </style>
387
+ """, unsafe_allow_html=True)
388
 
389
  st.title("Material Library")
390
 
 
391
  with st.expander("Help & Information"):
392
  display_materials_help()
393
 
 
394
  tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
395
 
 
396
  with tab1:
397
+ if st.session_state.active_tab == "Materials":
398
+ st.session_state.active_tab = "Materials"
399
  display_materials_tab()
400
 
 
401
  with tab2:
402
+ if st.session_state.active_tab == "Fenestrations":
403
+ st.session_state.active_tab = "Fenestrations"
404
  display_fenestrations_tab()
405
 
 
406
  col1, col2 = st.columns(2)
407
 
408
  with col1:
409
  if st.button("Back to Climate Data", key="back_to_climate"):
410
  st.session_state.current_page = "Climate Data"
411
+ st.session_state.rerun_pending = True
412
 
413
  with col2:
414
  if st.button("Continue to Construction", key="continue_to_construction"):
415
  st.session_state.current_page = "Construction"
416
+ st.session_state.rerun_pending = True
417
 
418
  def display_materials_tab():
419
  """Display the materials tab content."""
 
420
  initialize_materials()
421
 
422
  st.subheader("Materials")
423
  col1, col2 = st.columns([3, 2])
424
 
425
  with col1:
 
426
  filter_options = ["All"] + MATERIAL_CATEGORIES
427
  category = st.selectbox("Filter by Category", filter_options, key="material_filter")
428
 
 
453
  cols[1].write(thermal_mass_category)
454
  cols[2].write(f"{u_value:.3f}")
455
 
 
456
  preview_key = get_stable_button_key("lib_mat", name, "preview")
457
  copy_key = get_stable_button_key("lib_mat", name, "copy")
458
 
459
  if cols[3].button("Preview", key=preview_key):
460
+ if st.session_state.rerun_trigger != f"preview_mat_{name}":
461
+ st.session_state.rerun_trigger = f"preview_mat_{name}"
462
+ st.session_state.material_editor = {
463
+ "name": name,
464
+ "category": material["category"],
465
+ "thermal_conductivity": material["thermal_conductivity"],
466
+ "density": material["density"],
467
+ "specific_heat": material["specific_heat"],
468
+ "default_thickness": material["default_thickness"],
469
+ "embodied_carbon": material["embodied_carbon"],
470
+ "cost": material["cost"],
471
+ "absorptivity": material["absorptivity"],
472
+ "emissivity": material["emissivity"],
473
+ "colour": material["colour"],
474
+ "edit_mode": False,
475
+ "original_name": name,
476
+ "is_library": True
477
+ }
478
+ st.session_state.material_form_state = {
479
+ "name": name,
480
+ "category": material["category"],
481
+ "thermal_conductivity": material["thermal_conductivity"],
482
+ "density": material["density"],
483
+ "specific_heat": material["specific_heat"],
484
+ "default_thickness": material["default_thickness"],
485
+ "embodied_carbon": material["embodied_carbon"],
486
+ "cost": material["cost"],
487
+ "absorptivity": material["absorptivity"],
488
+ "emissivity": material["emissivity"],
489
+ "colour": material["colour"]
490
+ }
491
+ st.session_state.active_tab = "Materials"
492
+ st.session_state.rerun_pending = True
493
 
494
  if cols[4].button("Copy", key=copy_key):
495
  new_name = f"{name}_Project"
 
500
  st.session_state.project_data["materials"]["project"][new_name] = material.copy()
501
  st.success(f"Material '{new_name}' copied to project.")
502
  logger.info(f"Copied library material '{name}' as '{new_name}' to project")
503
+ st.session_state.rerun_pending = True
504
+
505
  else:
506
  st.info("No materials found in the selected category.")
507
 
 
508
  project_category = st.selectbox("Filter Project by Category", filter_options, key="project_material_filter")
509
  st.subheader("Project Materials")
510
  with st.container():
 
533
  cols[1].write(thermal_mass_category)
534
  cols[2].write(f"{u_value:.3f}")
535
 
 
536
  edit_key = get_stable_button_key("proj_mat", name, "edit")
537
  delete_key = get_stable_button_key("proj_mat", name, "delete")
538
 
539
  if cols[3].button("Edit", key=edit_key):
540
+ if st.session_state.rerun_trigger != f"edit_mat_{name}":
541
+ st.session_state.rerun_trigger = f"edit_mat_{name}"
542
+ st.session_state.material_editor = {
543
+ "name": name,
544
+ "category": material["category"],
545
+ "thermal_conductivity": material["thermal_conductivity"],
546
+ "density": material["density"],
547
+ "specific_heat": material["specific_heat"],
548
+ "default_thickness": material["default_thickness"],
549
+ "embodied_carbon": material["embodied_carbon"],
550
+ "cost": material["cost"],
551
+ "absorptivity": material["absorptivity"],
552
+ "emissivity": material["emissivity"],
553
+ "colour": material["colour"],
554
+ "edit_mode": True,
555
+ "original_name": name,
556
+ "is_library": False
557
+ }
558
+ st.session_state.material_form_state = {
559
+ "name": name,
560
+ "category": material["category"],
561
+ "thermal_conductivity": material["thermal_conductivity"],
562
+ "density": material["density"],
563
+ "specific_heat": material["specific_heat"],
564
+ "default_thickness": material["default_thickness"],
565
+ "embodied_carbon": material["embodied_carbon"],
566
+ "cost": material["cost"],
567
+ "absorptivity": material["absorptivity"],
568
+ "emissivity": material["emissivity"],
569
+ "colour": material["colour"]
570
+ }
571
+ st.session_state.active_tab = "Materials"
572
+ st.session_state.rerun_pending = True
573
 
574
  if cols[4].button("Delete", key=delete_key):
575
  is_in_use = check_material_in_use(name)
 
579
  del st.session_state.project_data["materials"]["project"][name]
580
  st.success(f"Material '{name}' deleted from project.")
581
  logger.info(f"Deleted material '{name}' from project")
582
+ st.session_state.rerun_pending = True
583
+
584
  else:
585
  st.info("No project materials in the selected category.")
586
 
 
587
  st.subheader("All Project Materials")
588
  project_materials = st.session_state.project_data["materials"]["project"]
589
  if project_materials:
 
619
 
620
  def display_fenestrations_tab():
621
  """Display the fenestrations tab content."""
 
622
  initialize_fenestrations()
623
 
624
  st.subheader("Fenestrations")
625
  col1, col2 = st.columns([3, 2])
626
 
627
  with col1:
 
628
  filter_options = ["All"] + FENESTRATION_TYPES
629
  fen_type = st.selectbox("Filter by Type", filter_options, key="fenestration_filter")
630
 
 
631
  st.subheader("Library Fenestrations")
632
  with st.container():
633
  library_fenestrations = st.session_state.project_data["fenestrations"]["library"]
 
652
  cols[1].write(fenestration["type"])
653
  cols[2].write(f"{fenestration['u_value']:.2f}")
654
 
 
655
  preview_key = get_stable_button_key("lib_fen", name, "preview")
656
  copy_key = get_stable_button_key("lib_fen", name, "copy")
657
 
658
  if cols[3].button("Preview", key=preview_key):
659
+ if st.session_state.rerun_trigger != f"preview_fen_{name}":
660
+ st.session_state.rerun_trigger = f"preview_fen_{name}"
661
+ st.session_state.fenestration_editor = {
662
+ "name": name,
663
+ "type": fenestration["type"],
664
+ "u_value": fenestration["u_value"],
665
+ "shgc": fenestration["shgc"],
666
+ "visible_trans": fenestration["visible_transmittance"],
667
+ "thickness": fenestration["thickness"],
668
+ "embodied_carbon": fenestration["embodied_carbon"],
669
+ "cost": fenestration["cost"],
670
+ "edit_mode": False,
671
+ "original_name": name,
672
+ "is_library": True
673
+ }
674
+ st.session_state.fenestration_form_state = {
675
+ "name": name,
676
+ "type": fenestration["type"],
677
+ "u_value": fenestration["u_value"],
678
+ "shgc": fenestration["shgc"],
679
+ "visible_trans": fenestration["visible_transmittance"],
680
+ "thickness": fenestration["thickness"],
681
+ "embodied_carbon": fenestration["embodied_carbon"],
682
+ "cost": fenestration["cost"]
683
+ }
684
+ st.session_state.active_tab = "Fenestrations"
685
+ st.session_state.rerun_pending = True
686
 
687
  if cols[4].button("Copy", key=copy_key):
688
  new_name = f"{name}_Project"
 
693
  st.session_state.project_data["fenestrations"]["project"][new_name] = fenestration.copy()
694
  st.success(f"Fenestration '{new_name}' copied to project.")
695
  logger.info(f"Copied library fenestration '{name}' as '{new_name}' to project")
696
+ st.session_state.rerun_pending = True
697
+
698
  else:
699
  st.info("No fenestrations found in the selected type.")
700
 
 
701
  project_fen_type = st.selectbox("Filter Project by Type", filter_options, key="project_fenestration_filter")
702
  st.subheader("Project Fenestrations")
703
  with st.container():
 
723
  cols[1].write(fenestration["type"])
724
  cols[2].write(f"{fenestration['u_value']:.2f}")
725
 
 
726
  edit_key = get_stable_button_key("proj_fen", name, "edit")
727
  delete_key = get_stable_button_key("proj_fen", name, "delete")
728
 
729
  if cols[3].button("Edit", key=edit_key):
730
+ if st.session_state.rerun_trigger != f"edit_fen_{name}":
731
+ st.session_state.rerun_trigger = f"edit_fen_{name}"
732
+ st.session_state.fenestration_editor = {
733
+ "name": name,
734
+ "type": fenestration["type"],
735
+ "u_value": fenestration["u_value"],
736
+ "shgc": fenestration["shgc"],
737
+ "visible_trans": fenestration["visible_transmittance"],
738
+ "thickness": fenestration["thickness"],
739
+ "embodied_carbon": fenestration["embodied_carbon"],
740
+ "cost": fenestration["cost"],
741
+ "edit_mode": True,
742
+ "original_name": name,
743
+ "is_library": False
744
+ }
745
+ st.session_state.fenestration_form_state = {
746
+ "name": name,
747
+ "type": fenestration["type"],
748
+ "u_value": fenestration["u_value"],
749
+ "shgc": fenestration["shgc"],
750
+ "visible_trans": fenestration["visible_transmittance"],
751
+ "thickness": fenestration["thickness"],
752
+ "embodied_carbon": fenestration["embodied_carbon"],
753
+ "cost": fenestration["cost"]
754
+ }
755
+ st.session_state.active_tab = "Fenestrations"
756
+ st.session_state.rerun_pending = True
757
 
758
  if cols[4].button("Delete", key=delete_key):
759
  is_in_use = check_fenestration_in_use(name)
760
  if is_in_use:
761
+ st.error(f"Cannot delete fenestration_fenestration '{name}' because it is in use in components.")
762
  else:
763
  del st.session_state.project_data["fenestrations"]["project"][name]
764
  st.success(f"Fenestration '{name}' deleted from project.")
765
+ st.logger.info(f"Deleted fenestration '{name}' from project")
766
+ st.session_state.rerun_pending = True
767
+
768
  else:
769
  st.info("No project fenestrations in the selected type.")
770
 
 
771
  st.subheader("All Project Fenestrations")
772
  project_fenestrations = st.session_state.project_data["fenestrations"]["project"]
773
  if project_fenestrations:
774
  data = []
775
+ for name, props in sorted(project_fenestrations.items()):
776
  data.append({
777
  "Name": name,
778
  "Type": props["type"],
779
  "U-Value (W/m²·K)": props["u_value"],
780
  "SHGC": props["shgc"],
781
+ "VisibleLight Transmittance": props["visible_transmittance"],
782
  "Thickness (m)": props["thickness"],
783
+ "Embodied Carbon (kg/m²)": props["embodied_carbon"],
784
  "Cost (USD/m²)": props["cost"]
785
  })
786
  df = pd.DataFrame(data)
 
802
  "table": None
803
  }
804
 
 
805
  if not st.session_state.project_data["materials"]["library"]:
806
  st.session_state.project_data["materials"]["library"] = DEFAULT_MATERIALS.copy()
807
 
 
808
  if "material_editor" not in st.session_state:
809
  st.session_state.material_editor = {
810
  "name": "",
 
814
  "specific_heat": 1000.0,
815
  "default_thickness": 0.05,
816
  "embodied_carbon": 100.0,
817
+ "cost": 100.100,
818
  "absorptivity": 0.60,
819
  "emissivity": 0.90,
820
  "colour": "Medium",
 
822
  "original_name": "",
823
  "is_library": False
824
  }
825
+
826
+ if "material_form_state" not in st.session_state:
827
+ st.session_state.material_form_state = {
828
+ "name": "",
829
+ "category": MATERIAL_CATEGORIES[0],
830
+ "thermal_conductivity": 0.5,
831
+ "density": 1000.0,
832
+ "specific_heat": 1000.0,
833
+ "default_thickness": 0.05,
834
+ "embodied_carbon": "",
835
+ "cost": "",
836
+ "absorptivity": 0.6,
837
+ "emissivity": "",
838
+ "colour": "Medium"
839
+ }
840
 
841
  def initialize_fenestrations():
842
  """Initialize fenestrations in session state if not present."""
 
847
  "table": None
848
  }
849
 
 
850
  if not st.session_state.project_data["fenestrations"]["library"]:
851
  st.session_state.project_data["fenestrations"]["library"] = DEFAULT_FENESTRATIONS.copy()
852
 
853
+ if "fenestration_editor_form" not in st.session_state:
 
854
  st.session_state.fenestration_editor = {
855
  "name": "",
856
  "type": FENESTRATION_TYPES[0],
857
  "u_value": 2.8,
858
  "shgc": 0.7,
859
+ "visible_trans": 0.8,
860
  "thickness": 0.024,
861
  "embodied_carbon": 900.0,
862
  "cost": 200.0,
 
864
  "original_name": "",
865
  "is_library": False
866
  }
867
+
868
+ if "fenestration_form_state" not in st.session_state:
869
+ st.session_state.fenestration_form_state = {
870
+ "name": "",
871
+ "type": FENESTRATION_TYPES[0],
872
+ "u_value": 2.8,
873
+ "shgc": 0.7,
874
+ "visible_trans": 0.8,
875
+ "thickness": 0.024,
876
+ "embodied_carbon": "",
877
+ "cost": ""
878
+ }
879
 
880
  def display_material_editor():
881
  """Display the material editor form."""
882
  with st.form("material_editor_form"):
883
  editor_state = st.session_state.material_editor
884
+ form_state = st.session_state.material_form_state
885
  is_library = editor_state.get("is_library", False)
886
 
 
887
  name = st.text_input(
888
  "Material Name",
889
+ value=form_state.get("name", editor_state["name"]),
890
  help="Enter a unique name for the material.",
891
  disabled=is_library
892
  )
893
 
 
894
  col1, col2 = st.columns(2)
895
 
896
  with col1:
897
+ category_index = MATERIAL_CATEGORIES.index(form_state.get("category", editor_state["category", MATERIAL_CATEGORIES[0]))
898
  category = st.selectbox(
899
  "Category",
900
  MATERIAL_CATEGORIES,
901
+ index=category_index,
902
  help="Select the material category.",
903
  disabled=is_library
904
  )
905
 
 
906
  thermal_conductivity = st.number_input(
907
  "Thermal Conductivity (W/m·K)",
908
  min_value=0.001,
909
  max_value=1000.0,
910
+ value=float(form_state.get("thermal_conductivity", editor_state["thermal_conductivity"])),
911
  format="%.3f",
912
  help="Thermal conductivity in W/m·K. Lower values indicate better insulation.",
913
  disabled=is_library
914
  )
915
 
 
916
  density = st.number_input(
917
  "Density (kg/m³)",
918
  min_value=1.0,
919
  max_value=20000.0,
920
+ value=float(form_state.get("density", editor_state["density"])),
921
  format="%.1f",
922
  help="Material density in kg/m³.",
923
  disabled=is_library
924
  )
925
 
926
  with col2:
 
927
  specific_heat = st.number_input(
928
  "Specific Heat (J/kg·K)",
929
  min_value=100.0,
930
  max_value=10000.0,
931
+ value=float(form_state.get("specific_heat", editor_state["specific_heat"])),
932
  format="%.1f",
933
  help="Specific heat capacity in J/kg·K. Higher values indicate better thermal mass.",
934
  disabled=is_library
935
  )
936
 
 
937
  default_thickness = st.number_input(
938
  "Thickness (m)",
939
  min_value=0.001,
940
  max_value=0.5,
941
+ value=float(form_state.get("default_thickness", editor_state["default_thickness"])),
942
  format="%.3f",
943
  help="Thickness for this material in meters.",
944
  disabled=is_library
945
  )
946
 
 
947
  absorptivity = st.number_input(
948
  "Absorptivity",
949
  min_value=0.0,
950
  max_value=1.0,
951
+ value=float(form_state.get("absorptivity", editor_state["absorptivity"])),
952
  format="%.2f",
953
  help="Solar radiation absorbed (0-1).",
954
  disabled=is_library
955
  )
956
 
 
957
  col1, col2 = st.columns(2)
958
 
959
  with col1:
 
960
  emissivity = st.number_input(
961
  "Emissivity",
962
  min_value=0.0,
963
  max_value=1.0,
964
+ value=float(form_state.get("emissivity", editor_state["emissivity"])),
965
  format="%.2f",
966
  help="Ratio of radiation emitted (0-1).",
967
  disabled=is_library
968
  )
969
 
970
  with col2:
971
+ colour_index = COLOUR_CATEGORIES.index(form_state.get("colour", editor_state["colour"])) if form_state.get("colour", editor_state["colour"]) in COLOUR_CATEGORIES else 0
972
  colour = st.selectbox(
973
  "Colour Category",
974
  COLOUR_CATEGORIES,
975
+ index=colour_index,
976
  help="Colour category affecting absorptivity.",
977
  disabled=is_library
978
  )
979
 
 
980
  st.subheader("Additional Properties")
981
  col1, col2 = st.columns(2)
982
 
983
  with col1:
 
984
  embodied_carbon = st.number_input(
985
  "Embodied Carbon (kg CO₂e/m³)",
986
  min_value=0.0,
987
  max_value=10000.0,
988
+ value=float(form_state.get("embodied_carbon", editor_state.get("embodied_carbon", ""))),
989
  format="%.1f",
990
  help="Embodied carbon in kg CO₂e per cubic meter.",
991
  disabled=is_library
992
  )
993
 
994
  with col2:
 
995
  cost = st.number_input(
996
  "Cost (USD/m³)",
997
  min_value=0.0,
998
  max_value=10000.0,
999
+ value=float(form_state.get("cost", editor_state.get("cost", ""))),
1000
  format="%.1f",
1001
  help="Material cost in USD per cubic meter.",
1002
  disabled=is_library
1003
  )
1004
 
 
1005
  col1, col2 = st.columns(2)
1006
 
1007
  with col1:
 
1010
  with col2:
1011
  clear_button = st.form_submit_button("Clear Form")
1012
 
 
1013
  if submit_button and not is_library:
1014
+ action_id = str(uuid.uuid4())
1015
+ if st.session_state.material_action.get("id") != action_id:
1016
+ st.session_state.material_action = {"action": "save", "id": action_id}
1017
+ st.session_state.material_form_state = {
1018
+ "name": name,
 
 
 
 
 
 
 
 
 
1019
  "category": category,
1020
  "thermal_conductivity": thermal_conductivity,
1021
  "density": density,
 
1027
  "emissivity": emissivity,
1028
  "colour": colour
1029
  }
1030
+ success, message = validate_material(
1031
+ name, category, thermal_conductivity, density, specific_heat,
1032
+ default_thickness, embodied_carbon, cost,
1033
+ editor_state["edit_mode"], editor_state["original_name"],
1034
+ absorptivity, emissivity, colour
1035
+ )
1036
+ if not success:
1037
+ st.error(message)
 
1038
  else:
1039
+ material_data = {
1040
+ "category": category,
1041
+ "thermal_conductivity": thermal_conductivity,
1042
+ "density": density,
1043
+ "specific_heat": specific_heat,
1044
+ "default_thickness": default_thickness,
1045
+ "embodied_carbon": embodied_carbon,
1046
+ "cost": cost,
1047
+ "absorptivity": absorptivity,
1048
+ "emissivity": emissivity,
1049
+ "colour": colour
1050
+ }
1051
+ if editor_state["edit_mode"]:
1052
+ original_name = editor_state["original_name"]
1053
+ if original_name != name:
1054
+ del st.session_state.project_data["materials"]["project"][original_name]
1055
+ st.session_state.project_data["materials"]["project"][name] = material_data
1056
+ st.success(f"Material '{name}' updated successfully.")
1057
+ logger.info(f"Updated material '{name}' in project")
1058
+ else:
1059
+ st.session_state.project_data["materials"]["project"][name] = material_data
1060
+ st.success(f"Material '{name}' added to your project.")
1061
+ logger.info(f"Added new material '{name}' to project")
1062
+ reset_material_editor()
1063
+ st.session_state.rerun_pending = True
1064
 
 
1065
  if clear_button:
1066
  reset_material_editor()
1067
+ st.session_state.rerun_pending = True
 
1068
 
1069
  def display_fenestration_editor():
1070
  """Display the fenestration editor form."""
1071
  with st.form("fenestration_editor_form"):
1072
  editor_state = st.session_state.fenestration_editor
1073
+ form_state = st.session_state.fenestration_form_state
1074
  is_library = editor_state.get("is_library", False)
1075
 
 
1076
  name = st.text_input(
1077
  "Fenestration Name",
1078
+ value=form_state.get("name", editor_state["name"]),
1079
  help="Enter a unique name for the fenestration.",
1080
  disabled=is_library
1081
  )
1082
 
 
1083
  col1, col2 = st.columns(2)
1084
 
1085
  with col1:
1086
+ 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
1087
  fenestration_type = st.selectbox(
1088
  "Type",
1089
  FENESTRATION_TYPES,
1090
+ index=type_index,
1091
  help="Select the fenestration type.",
1092
  disabled=is_library
1093
  )
1094
 
 
1095
  u_value = st.number_input(
1096
  "U-Value (W/m²·K)",
1097
  min_value=0.1,
1098
  max_value=10.0,
1099
+ value=float(form_state.get("u_value", editor_state.get("u_value", ""))),
1100
  format="%.2f",
1101
+ help="U-value in W/m²·K. value",
1102
  disabled=is_library
1103
  )
1104
 
1105
+ with col2:
1106
  shgc = st.number_input(
1107
  "Solar Heat Gain Coefficient (SHGC)",
1108
  min_value=0.0,
1109
  max_value=1.0,
1110
+ value=float(form_state.get("shgc", editor_state.get("shgc", ""))),
1111
  format="%.2f",
1112
+ help="Solar Heat Gain Coefficient (0-1).",
1113
  disabled=is_library
1114
  )
1115
+
1116
+ visible_trans = st.number_input(
 
 
1117
  "Visible Transmittance",
1118
  min_value=0.0,
1119
  max_value=1.0,
1120
+ value=float(form_state.get("visible_trans", editor_state.get("visible_trans", ""))),
1121
  format="%.2f",
1122
+ help="Visible light transmission (0-1).",
1123
  disabled=is_library
1124
  )
1125
 
 
1126
  thickness = st.number_input(
1127
  "Thickness (m)",
1128
  min_value=0.001,
1129
+ max_value=0.0.1,
1130
+ value=float(form_state.get("thickness", editor_state.get("thickness", ""))),
1131
  format="%.3f",
1132
  help="Thickness in meters.",
1133
  disabled=is_library
1134
  )
1135
 
 
1136
  st.subheader("Additional Properties")
1137
+
1138
  col1, col2 = st.columns(2)
1139
 
1140
  with col1:
 
1141
  embodied_carbon = st.number_input(
1142
  "Embodied Carbon (kg CO₂e/m²)",
1143
  min_value=0.0,
1144
  max_value=5000.0,
1145
+ value=float(form_state.get("embodied_carbon", editor_state.get("embodied_carbon", ""))),
1146
  format="%.1f",
1147
+ help="Embodied carbon in kg CO₂e/m².",
1148
  disabled=is_library
1149
  )
1150
 
1151
  with col2:
 
1152
  cost = st.number_input(
1153
  "Cost (USD/m²)",
1154
  min_value=0.0,
1155
  max_value=5000.0,
1156
+ value=float(form_state.get("cost", editor_state.get("cost", ""))),
1157
  format="%.1f",
1158
+ help="Fenestration cost in USD/m².",
1159
  disabled=is_library
1160
  )
1161
 
 
1162
  col1, col2 = st.columns(2)
1163
 
1164
  with col1:
 
1167
  with col2:
1168
  clear_button = st.form_submit_button("Clear Form")
1169
 
 
1170
  if submit_button and not is_library:
1171
+ action_id = str(uuid.uuid4())
1172
+ if st.session_state.fenestration_action.get("id") != action_id:
1173
+ st.session_state.fenestration_action = {"action": "save", "id": action_id}
1174
+ st.session_state.fenestration_form_state = {
1175
+ "name": name,
 
 
 
 
 
 
 
1176
  "type": fenestration_type,
1177
  "u_value": u_value,
1178
  "shgc": shgc,
1179
+ "visible_trans": visible_trans,
1180
  "thickness": thickness,
1181
  "embodied_carbon": embodied_carbon,
1182
  "cost": cost
1183
  }
1184
+ success, message = validate_fenestration(
1185
+ name, fenestration_type, u_value, shgc, visible_trans, thickness,
1186
+ embodied_carbon, cost, editor_state["edit_mode"], editor_state["original_name"]
1187
+ )
1188
+ if not success:
1189
+ st.error(message)
 
 
 
1190
  else:
1191
+ fenestration_data = {
1192
+ "type": fenestration_type,
1193
+ "u_value": u_value,
1194
+ "shgc": shgc,
1195
+ "visible_transmittance": visible_trans,
1196
+ "thickness": thickness,
1197
+ "embodied_carbon": embodied_carbon,
1198
+ "cost": cost
1199
+ }
1200
+ if editor_state["edit_mode"]:
1201
+ original_name = editor_state["original_name"]
1202
+ if original_name != st.name:
1203
+ del st.session_state.project_data["fenestrations"]["project"][original_name]
1204
+ st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
1205
+ st.success(f"Fenestration '{name}' updated successfully.")
1206
+ logger.info(f"Updated fenestration '{name}' in project")
1207
+ else:
1208
+ st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
1209
+ st.success(f"Fenestration '{name}' added to your project.")
1210
+ logger.info(f"Added new fenestration '{name}' to project")
1211
+ reset_fenestration_editor()
1212
+ st.session_state.rerun_pending = True
1213
 
 
1214
  if clear_button:
1215
  reset_fenestration_editor()
1216
+ st.session_state.rerun_pending = True
 
1217
 
1218
  def validate_material(
1219
  name: str, category: str, thermal_conductivity: float, density: float,
1220
  specific_heat: float, default_thickness: float, embodied_carbon: float,
1221
  cost: float, edit_mode: bool, original_name: str,
1222
  absorptivity: float, emissivity: float, colour: str
1223
+ ) -> Tuple[bool, str]:
1224
  """
1225
  Validate material inputs.
1226
+ Returns (success, message) tuple.
1227
  """
 
 
 
1228
  if not name or name.strip() == "":
1229
+ return False, "Material name is required."
1230
 
1231
+ if (not edit_mode or name != original_name) and name in st.session_state.project_data["materials"]["project"]:
1232
+ return False, f"Material name '{name}' already exists in project materials."
 
 
1233
 
1234
+ if category is not in MATERIAL_CATEGORIES:
1235
+ return False, "Invalid material category."
 
1236
 
 
1237
  if thermal_conductivity <= 0:
1238
+ return False, "Thermal conductivity must be greater than zero."
1239
 
 
1240
  if density <= 0:
1241
+ return False, "Density must be greater than zero."
1242
 
 
1243
  if specific_heat <= 0:
1244
+ return False, "Specific heat must be greater than zero."
1245
 
 
1246
  if default_thickness <= 0:
1247
+ return False, "Thickness must be greater than zero."
1248
 
 
1249
  if embodied_carbon < 0:
1250
+ return False, "Embodied carbon cannot be negative."
1251
 
 
1252
  if cost < 0:
1253
+ return False, "Cost cannot be negative."
1254
 
 
1255
  if absorptivity < 0 or absorptivity > 1:
1256
+ return False, "Absorptivity must be between 0 and 1."
1257
 
 
1258
  if emissivity < 0 or emissivity > 1:
1259
+ return False, "Emissivity must be between 0 and 1."
1260
 
 
1261
  if colour not in COLOUR_CATEGORIES:
1262
+ return False, "Invalid colour category."
1263
 
 
1264
  colour_ranges = {
1265
  "Light": (0.30, 0.50),
1266
  "Medium": (0.60, 0.70),
 
1269
  }
1270
  min_abs, max_abs = colour_ranges[colour]
1271
  if not (min_abs <= absorptivity <= max_abs):
1272
+ return False, f"Absorptivity for {colour} colour must be between {min_abs} and {max_abs}."
1273
 
1274
+ return True, ""
1275
 
1276
  def validate_fenestration(
1277
  name: str, fenestration_type: str, u_value: float, shgc: float,
1278
+ visible_trans: float, thickness: float, embodied_carbon: float,
1279
  cost: float, edit_mode: bool, original_name: str
1280
+ ) -> Tuple[bool, str]:
1281
  """
1282
  Validate fenestration inputs.
1283
+ Returns (success, message) tuple."""
 
 
 
1284
  if not name or name.strip() == "":
1285
+ return False, "Fenestration name is required."
1286
 
1287
+ if (not edit_mode or name != original_name) and name in st.session_state.project_data["fenestrations"]["project"]:
1288
+ return False, f"Fenestration name '{name}' already exists in project."
 
 
1289
 
 
1290
  if fenestration_type not in FENESTRATION_TYPES:
1291
+ return False, "Invalid fenestration type."
1292
 
 
1293
  if u_value <= 0:
1294
+ return False, "U-value must be greater than zero."
1295
 
 
1296
  if shgc < 0 or shgc > 1:
1297
+ return False, "SHGC must be between 0 and 1."
1298
 
1299
+ if visible_trans < 0 or visible_trans > 1:
1300
+ return False, "Visible transmittance must be between 0 and 1."
 
1301
 
 
1302
  if thickness <= 0:
1303
+ return False, "Thickness must be greater than zero."
1304
+
 
1305
  if embodied_carbon < 0:
1306
+ return False, "Embodied carbon cannot be negative."
1307
 
 
1308
  if cost < 0:
1309
+ return False, "Cost cannot be negative."
1310
 
1311
+ return True, ""
1312
 
1313
  def reset_material_editor():
1314
+ """Reset the material editor and form state."""
1315
+ st.session_state.material_form_state = {
1316
  "name": "",
1317
  "category": MATERIAL_CATEGORIES[0],
1318
  "thermal_conductivity": 0.5,
1319
  "density": 1000.0,
1320
  "specific_heat": 1000.0,
1321
  "default_thickness": 0.05,
1322
+ "embodied_carbon": "",
1323
+ "cost": "",
1324
  "absorptivity": 0.60,
1325
+ "emissivity": "",
1326
+ "colour": ""
1327
+ }
1328
+ st.session_state.material_editor = {
1329
+ "name": "",
1330
+ "category": MATERIAL_CATEGORIES[0],
1331
+ "thermal_conductivity": "",
1332
+ "density": 0.0,
1333
+ "specific_heat": "",
1334
+ "default_thickness": "",
1335
+ "embodied_carbon": "",
1336
+ "cost": "",
1337
+ "absorptivity": "",
1338
+ "emissivity": "",
1339
+ "colour": "",
1340
  "edit_mode": False,
1341
  "original_name": "",
1342
  "is_library": False
1343
  }
1344
+ st.session_state.material_action = {"action": None, "id": None}
1345
+ st.session_state.rerun_trigger = None
1346
 
1347
  def reset_fenestration_editor():
1348
+ """Reset the fenestration editor and form state."""
1349
+ st.session_state.fenestration_form_state = {
1350
  "name": "",
1351
  "type": FENESTRATION_TYPES[0],
1352
+ "u_value": "",
1353
+ "shgc": "",
1354
+ "visible_trans": "",
1355
+ "thickness": "",
1356
+ "embodied_carbon": "",
1357
+ "cost": ""
 
 
 
1358
  }
1359
+ st.session_state.fenestration_form_state = {
1360
+ "name": "",
1361
+ "type": FENESTRATION_TYPES[0],
1362
+ "u_value": "",
1363
+ "shgc": "",
1364
+ "visible_trans": "",
1365
+ "thickness": "",
1366
+ "embodied_carbon": "",
1367
+ "cost": ""
1368
+ }
1369
+ st.session_state.fenestration_action = {"action": None, "id": None}
1370
+ st.session_state.rerun_trigger = None
1371
 
1372
  def check_material_in_use(material_name: str) -> bool:
1373
  """
 
1375
  """
1376
  return False
1377
 
1378
+ def check_fenestration_in_use(name: str) -> bool:
1379
  """
1380
+ Check if a fenestration is in use in any components."""
1381
  """
1382
  return False
1383
 
1384
  def display_materials_help():
1385
  """Display help information for the material library page."""
1386
  st.markdown("""
1387
+ """
1388
  ### Material Library Help
1389
 
1390
  This section allows you to manage building materials and fenestrations for your project.
 
1393
 
1394
  * **Library Materials**: Pre-defined materials with standard thermal properties. Use 'Preview' to view details or 'Copy' to add to your project.
1395
  * **Project Materials**: Materials you've added to your project. Use 'Edit' to modify or 'Delete' to remove.
1396
+ * **Material Editor/Creator**: Create new materials or edit existing project materials. Library materials can only be copied.
1397
 
1398
  **Fenestrations Tab:**
1399
 
1400
  * **Library Fenestrations**: Pre-defined windows, doors, and skylights. Use 'Preview' to view details or 'Copy' to add to your project.
1401
  * **Project Fenestrations**: Fenestrations you've added to your project. Use 'Edit' to modify or 'Delete' to remove.
1402
+ * **Fenestration Editor/Creator**: Create new fenestrations or edit existing project fenestrations. Library fenestrations can only be copied.
1403
 
1404
  **Key Properties:**
1405
 
1406
  * **Thermal Conductivity (W/m·K)**: Rate of heat transfer through a material. Lower values indicate better insulation.
1407
  * **Density (kg/m³)**: Mass per unit volume.
1408
+ * **Specific Heat (J/kg·K)**: Energy required to raise the temperature of 1 kg by 1 K.
1409
  * **Thermal Mass**: Areal heat capacity, categorized as Low (<30,000 J/m²·K), Medium (30,000–90,000 J/m²·K), or High (>90,000 J/m²·K).
1410
  * **U-Value (W/m²·K)**: Overall heat transfer coefficient, accounting for material and surface resistances. Lower values indicate better insulation.
1411
+ * **SHGC**: Solar absorption coefficient (0-1). Fraction of incident solar radiation that enters through a fenestration.
1412
  * **Visible Transmittance**: Fraction of visible light that passes through a fenestration.
1413
  * **Embodied Carbon**: Carbon emissions associated with material production, measured in kg CO₂e per unit volume or area.
1414
+ * **Cost**: Material’s cost in USD per unit volume or area.
1415
  * **Absorptivity**: Fraction of solar radiation absorbed (0-1). Depends on colour category.
1416
  * **Emissivity**: Ratio of radiation emitted by the material (0-1).
1417
  * **Colour**: Category affecting absorptivity (Light, Medium, Dark, Reflective).
1418
 
1419
+ **Workflow**:
1420
 
1421
+ 1. Browse library materials and fenestrations
1422
  2. Copy items to your project or create custom ones
1423
  3. Edit properties as needed for your specific project
1424
  4. Continue to the Construction page to create assemblies using these materials