mabuseif commited on
Commit
a7603e1
·
verified ·
1 Parent(s): aa4e0fb

Update app/materials_library.py

Browse files
Files changed (1) hide show
  1. app/materials_library.py +717 -650
app/materials_library.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
- BuildSustain - Material Library Module (Restructured)
3
 
4
  This module handles the material library functionality of the BuildSustain application,
5
- allowing users to manage building materials and fenestrations (windows, doors, skylights).
6
  It provides both predefined library materials and the ability to create custom materials.
7
 
8
  Developed by: Dr Majed Abuseif, Deakin University
@@ -16,6 +16,7 @@ import json
16
  import logging
17
  import uuid
18
  from typing import Dict, List, Any, Optional, Tuple, Union
 
19
 
20
  # Import default data from centralized module
21
  from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS
@@ -36,11 +37,119 @@ MATERIAL_CATEGORIES = [
36
 
37
  FENESTRATION_TYPES = [
38
  "Window",
39
- "Door",
40
  "Skylight",
41
  "Custom"
42
  ]
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def display_materials_page():
45
  """
46
  Display the material library page.
@@ -48,721 +157,679 @@ def display_materials_page():
48
  """
49
  st.title("Material Library")
50
  st.write("Manage building materials and fenestrations for thermal analysis.")
51
-
52
- # Display help information in an expandable section
53
- with st.expander("Help & Information"):
54
- display_materials_help()
55
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  # Initialize materials and fenestrations in session state if not present
57
  initialize_materials_and_fenestrations()
58
-
59
- # Check if rerun is pending
60
- if 'materials_rerun_pending' not in st.session_state:
61
- st.session_state.materials_rerun_pending = False
62
-
63
- if st.session_state.materials_rerun_pending:
64
  st.session_state.materials_rerun_pending = False
65
  st.rerun()
66
-
67
- # Create tabs for materials and fenestrations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
69
-
70
- # Materials tab
71
  with tab1:
72
- display_materials_tab()
73
-
74
- # Fenestrations tab
75
  with tab2:
76
- display_fenestrations_tab()
77
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  # Navigation buttons
79
  col1, col2 = st.columns(2)
80
-
81
  with col1:
82
- if st.button("Back to Climate Data", key="back_to_climate"):
83
  st.session_state.current_page = "Climate Data"
84
  st.rerun()
85
-
86
  with col2:
87
- if st.button("Continue to Construction", key="continue_to_construction"):
88
  st.session_state.current_page = "Construction"
89
  st.rerun()
90
 
91
  def initialize_materials_and_fenestrations():
92
  """Initialize materials and fenestrations in session state if not present."""
93
- # Initialize materials
 
94
  if "materials" not in st.session_state.project_data:
95
  st.session_state.project_data["materials"] = {
96
  "library": dict(SAMPLE_MATERIALS),
97
  "project": {}
98
  }
99
-
100
- # Initialize fenestrations
101
  if "fenestrations" not in st.session_state.project_data:
102
  st.session_state.project_data["fenestrations"] = {
103
  "library": dict(SAMPLE_FENESTRATIONS),
104
  "project": {}
105
  }
106
 
107
- def display_materials_tab():
108
  """Display the materials tab content with two-column layout."""
109
- # Split the display into two columns
110
  col1, col2 = st.columns([3, 2])
111
-
112
  with col1:
113
- st.subheader("Saved Materials")
114
-
115
- # Get materials from session state
116
- materials_library = st.session_state.project_data["materials"]["library"]
117
- materials_project = st.session_state.project_data["materials"]["project"]
118
-
119
- # Display library materials
120
- if materials_library:
121
- st.write("**Library Materials:**")
122
- display_materials_table(materials_library, "library")
123
-
124
- # Display project materials
125
- if materials_project:
126
- st.write("**Project Materials:**")
127
- display_materials_table(materials_project, "project")
128
-
129
- if not materials_library and not materials_project:
130
- st.write("No materials available.")
131
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  with col2:
133
  st.subheader("Material Editor/Creator")
134
-
135
- # Check if we have an editor state for materials
136
- if "material_editor" not in st.session_state:
137
- st.session_state.material_editor = {}
138
-
139
- # Check if we have an action state for materials
140
- if "material_action" not in st.session_state:
141
- st.session_state.material_action = {"action": None, "id": None}
142
-
143
- # Display the material editor form
144
- with st.form("material_editor_form", clear_on_submit=True):
145
- editor_state = st.session_state.get("material_editor", {})
146
- is_edit = editor_state.get("is_edit", False)
147
-
148
- # Material name
149
- name = st.text_input(
150
- "Material Name",
151
- value=editor_state.get("name", ""),
152
- help="Enter a unique name for this material."
153
- )
154
-
155
- # Material category
156
- category = st.selectbox(
157
- "Category",
158
- MATERIAL_CATEGORIES,
159
- index=MATERIAL_CATEGORIES.index(editor_state.get("category", MATERIAL_CATEGORIES[0])) if editor_state.get("category") in MATERIAL_CATEGORIES else 0,
160
- help="Select the material category."
161
- )
162
-
163
- # Thermal properties
164
- st.write("**Thermal Properties:**")
165
- thermal_conductivity = st.number_input(
166
- "Thermal Conductivity (W/m·K)",
167
- min_value=0.001,
168
- max_value=500.0,
169
- value=float(editor_state.get("thermal_conductivity", 0.1)),
170
- format="%.3f",
171
- help="Thermal conductivity of the material."
172
- )
173
-
174
- density = st.number_input(
175
- "Density (kg/m³)",
176
- min_value=1.0,
177
- max_value=10000.0,
178
- value=float(editor_state.get("density", 1000.0)),
179
- format="%.1f",
180
- help="Density of the material."
181
- )
182
-
183
- specific_heat = st.number_input(
184
- "Specific Heat (J/kg·K)",
185
- min_value=100.0,
186
- max_value=5000.0,
187
- value=float(editor_state.get("specific_heat", 1000.0)),
188
- format="%.1f",
189
- help="Specific heat capacity of the material."
190
- )
191
-
192
- # Thickness range
193
- st.write("**Thickness Range:**")
194
- col_min, col_max, col_default = st.columns(3)
195
-
196
- with col_min:
197
- min_thickness = st.number_input(
198
- "Min (m)",
199
- min_value=0.001,
200
- max_value=1.0,
201
- value=float(editor_state.get("min_thickness", 0.01)),
202
- format="%.3f",
203
- help="Minimum thickness for this material."
204
  )
205
-
206
- with col_max:
207
- max_thickness = st.number_input(
208
- "Max (m)",
209
- min_value=0.001,
210
- max_value=1.0,
211
- value=float(editor_state.get("max_thickness", 0.3)),
212
- format="%.3f",
213
- help="Maximum thickness for this material."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  )
215
-
216
- with col_default:
217
  default_thickness = st.number_input(
218
- "Default (m)",
219
  min_value=0.001,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  max_value=1.0,
221
- value=float(editor_state.get("default_thickness", 0.1)),
222
- format="%.3f",
223
- help="Default thickness for this material."
224
  )
225
-
226
- # Environmental and cost properties
227
- st.write("**Environmental & Cost Properties:**")
228
- embodied_carbon = st.number_input(
229
- "Embodied Carbon (kgCO2e/kg)",
230
- min_value=0.0,
231
- max_value=50.0,
232
- value=float(editor_state.get("embodied_carbon", 0.5)),
233
- format="%.3f",
234
- help="Embodied carbon per unit mass of material."
235
- )
236
-
237
- col_mat_cost, col_lab_cost = st.columns(2)
238
-
239
- with col_mat_cost:
240
- material_cost = st.number_input(
241
- "Material Cost ($/m³)",
242
  min_value=0.0,
243
- max_value=10000.0,
244
- value=float(editor_state.get("material_cost", 100.0)),
245
- format="%.2f",
246
- help="Material cost per cubic meter."
247
  )
248
-
249
- with col_lab_cost:
250
- labor_cost = st.number_input(
251
- "Labor Cost ($/m²)",
252
  min_value=0.0,
253
- max_value=1000.0,
254
- value=float(editor_state.get("labor_cost", 50.0)),
255
- format="%.2f",
256
- help="Labor cost per square meter."
257
  )
258
-
259
- replacement_years = st.number_input(
260
- "Replacement Years",
261
- min_value=1,
262
- max_value=100,
263
- value=int(editor_state.get("replacement_years", 50)),
264
- help="Expected lifespan before replacement."
265
- )
266
-
267
- # Submit buttons
268
- col1, col2 = st.columns(2)
269
- with col1:
270
- submit_label = "Update Material" if is_edit else "Add Material"
271
- submit = st.form_submit_button(submit_label)
272
-
273
- with col2:
274
- cancel = st.form_submit_button("Cancel")
275
-
276
- # Handle form submission
277
- if submit:
278
- # Validate inputs
279
- if not name.strip():
280
- st.error("Material name is required.")
281
- elif min_thickness >= max_thickness:
282
- st.error("Minimum thickness must be less than maximum thickness.")
283
- elif default_thickness < min_thickness or default_thickness > max_thickness:
284
- st.error("Default thickness must be between minimum and maximum thickness.")
285
- else:
286
- # Create material data
287
- material_data = {
288
- "name": name.strip(),
289
- "type": "opaque",
290
- "category": category,
291
- "thermal_conductivity": thermal_conductivity,
292
- "density": density,
293
- "specific_heat": specific_heat,
294
- "thickness_range": {
295
- "min": min_thickness,
296
- "max": max_thickness,
297
- "default": default_thickness
298
- },
299
- "embodied_carbon": embodied_carbon,
300
- "cost": {
301
- "material": material_cost,
302
- "labor": labor_cost,
303
- "replacement_years": replacement_years
304
- },
305
- "description": f"Custom {category.lower()} material"
306
- }
307
-
308
- # Check if editing or adding new
309
- if is_edit and st.session_state.material_editor.get("edit_id"):
310
- # Update existing material
311
- edit_id = st.session_state.material_editor["edit_id"]
312
- edit_source = st.session_state.material_editor["edit_source"]
313
-
314
- if edit_source == "project":
315
- st.session_state.project_data["materials"]["project"][edit_id] = material_data
316
- st.success(f"Material '{name}' updated successfully!")
317
- else:
318
- st.error("Cannot edit library materials.")
319
- else:
320
- # Add new material to project
321
- material_id = str(uuid.uuid4())
322
- st.session_state.project_data["materials"]["project"][material_id] = material_data
323
- st.success(f"Material '{name}' added successfully!")
324
-
325
- # Clear editor state
326
- st.session_state.material_editor = {}
327
- st.session_state.materials_rerun_pending = True
328
- st.rerun()
329
-
330
- if cancel:
331
- # Clear editor state
332
- st.session_state.material_editor = {}
333
- st.session_state.materials_rerun_pending = True
334
- st.rerun()
335
 
336
- def display_fenestrations_tab():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  """Display the fenestrations tab content with two-column layout."""
338
- # Split the display into two columns
339
  col1, col2 = st.columns([3, 2])
340
-
341
  with col1:
342
- st.subheader("Saved Fenestrations")
343
-
344
- # Get fenestrations from session state
345
- fenestrations_library = st.session_state.project_data["fenestrations"]["library"]
346
- fenestrations_project = st.session_state.project_data["fenestrations"]["project"]
347
-
348
- # Display library fenestrations
349
- if fenestrations_library:
350
- st.write("**Library Fenestrations:**")
351
- display_fenestrations_table(fenestrations_library, "library")
352
-
353
- # Display project fenestrations
354
- if fenestrations_project:
355
- st.write("**Project Fenestrations:**")
356
- display_fenestrations_table(fenestrations_project, "project")
357
-
358
- if not fenestrations_library and not fenestrations_project:
359
- st.write("No fenestrations available.")
360
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  with col2:
362
  st.subheader("Fenestration Editor/Creator")
363
-
364
- # Check if we have an editor state for fenestrations
365
- if "fenestration_editor" not in st.session_state:
366
- st.session_state.fenestration_editor = {}
367
-
368
- # Check if we have an action state for fenestrations
369
- if "fenestration_action" not in st.session_state:
370
- st.session_state.fenestration_action = {"action": None, "id": None}
371
-
372
- # Display the fenestration editor form
373
- with st.form("fenestration_editor_form", clear_on_submit=True):
374
- editor_state = st.session_state.get("fenestration_editor", {})
375
- is_edit = editor_state.get("is_edit", False)
376
-
377
- # Fenestration name
378
- name = st.text_input(
379
- "Fenestration Name",
380
- value=editor_state.get("name", ""),
381
- help="Enter a unique name for this fenestration."
382
- )
383
-
384
- # Fenestration type
385
- fenestration_type = st.selectbox(
386
- "Type",
387
- FENESTRATION_TYPES,
388
- index=FENESTRATION_TYPES.index(editor_state.get("type", FENESTRATION_TYPES[0])) if editor_state.get("type") in FENESTRATION_TYPES else 0,
389
- help="Select the fenestration type."
390
- )
391
-
392
- # Thermal properties
393
- st.write("**Thermal Properties:**")
394
- u_value = st.number_input(
395
- "U-Value (W/m²·K)",
396
- min_value=0.1,
397
- max_value=10.0,
398
- value=float(editor_state.get("u_value", 2.5)),
399
- format="%.2f",
400
- help="Overall heat transfer coefficient."
401
- )
402
-
403
- # Solar properties (only for windows and skylights)
404
- if fenestration_type in ["Window", "Skylight"]:
405
- st.write("**Solar Properties:**")
406
- shgc = st.number_input(
407
- "SHGC",
408
- min_value=0.0,
409
- max_value=1.0,
410
- value=float(editor_state.get("shgc", 0.7)),
411
- format="%.2f",
412
- help="Solar Heat Gain Coefficient."
413
  )
414
-
415
- visible_transmittance = st.number_input(
416
- "Visible Transmittance",
417
- min_value=0.0,
418
- max_value=1.0,
419
- value=float(editor_state.get("visible_transmittance", 0.8)),
420
- format="%.2f",
421
- help="Visible light transmittance."
422
- )
423
- else:
424
- # For doors, use solar absorptivity instead
425
- st.write("**Solar Properties:**")
426
- solar_absorptivity = st.number_input(
427
- "Solar Absorptivity",
428
  min_value=0.0,
429
  max_value=1.0,
430
- value=float(editor_state.get("solar_absorptivity", 0.7)),
431
- format="%.2f",
432
- help="Solar absorptivity of the door surface."
433
  )
434
-
435
- # Physical properties
436
- st.write("**Physical Properties:**")
437
- thickness = st.number_input(
438
- "Thickness (m)",
439
- min_value=0.003,
440
- max_value=0.1,
441
- value=float(editor_state.get("thickness", 0.024)),
442
- format="%.3f",
443
- help="Overall thickness of the fenestration."
444
- )
445
-
446
- # Environmental and cost properties
447
- st.write("**Environmental & Cost Properties:**")
448
- embodied_carbon = st.number_input(
449
- "Embodied Carbon (kgCO2e/m²)",
450
- min_value=0.0,
451
- max_value=2000.0,
452
- value=float(editor_state.get("embodied_carbon", 500.0)),
453
- format="%.1f",
454
- help="Embodied carbon per unit area."
455
- )
456
-
457
- col_mat_cost, col_lab_cost = st.columns(2)
458
-
459
- with col_mat_cost:
460
- material_cost = st.number_input(
461
- "Material Cost ($/m²)",
462
- min_value=0.0,
463
- max_value=2000.0,
464
- value=float(editor_state.get("material_cost", 200.0)),
465
- format="%.2f",
466
- help="Material cost per square meter."
467
  )
468
-
469
- with col_lab_cost:
470
- labor_cost = st.number_input(
471
- "Labor Cost ($/m²)",
472
  min_value=0.0,
473
- max_value=500.0,
474
- value=float(editor_state.get("labor_cost", 80.0)),
475
- format="%.2f",
476
- help="Labor cost per square meter."
477
  )
478
-
479
- replacement_years = st.number_input(
480
- "Replacement Years",
481
- min_value=1,
482
- max_value=100,
483
- value=int(editor_state.get("replacement_years", 25)),
484
- help="Expected lifespan before replacement."
485
- )
486
-
487
- # Submit buttons
488
- col1, col2 = st.columns(2)
489
- with col1:
490
- submit_label = "Update Fenestration" if is_edit else "Add Fenestration"
491
- submit = st.form_submit_button(submit_label)
492
-
493
- with col2:
494
- cancel = st.form_submit_button("Cancel")
495
-
496
- # Handle form submission
497
- if submit:
498
- # Validate inputs
499
- if not name.strip():
500
- st.error("Fenestration name is required.")
501
- else:
502
- # Create fenestration data
503
- fenestration_data = {
504
- "name": name.strip(),
505
- "type": fenestration_type,
506
- "u_value": u_value,
507
- "thickness": thickness,
508
- "embodied_carbon": embodied_carbon,
509
- "cost": {
510
- "material": material_cost,
511
- "labor": labor_cost,
512
- "replacement_years": replacement_years
513
- },
514
- "description": f"Custom {fenestration_type.lower()}"
515
- }
516
-
517
- # Add type-specific properties
518
- if fenestration_type in ["Window", "Skylight"]:
519
- fenestration_data["shgc"] = shgc
520
- fenestration_data["visible_transmittance"] = visible_transmittance
521
- else:
522
- fenestration_data["solar_absorptivity"] = solar_absorptivity
523
-
524
- # Check if editing or adding new
525
- if is_edit and st.session_state.fenestration_editor.get("edit_id"):
526
- # Update existing fenestration
527
- edit_id = st.session_state.fenestration_editor["edit_id"]
528
- edit_source = st.session_state.fenestration_editor["edit_source"]
529
-
530
- if edit_source == "project":
531
- st.session_state.project_data["fenestrations"]["project"][edit_id] = fenestration_data
532
- st.success(f"Fenestration '{name}' updated successfully!")
533
- else:
534
- st.error("Cannot edit library fenestrations.")
535
- else:
536
- # Add new fenestration to project
537
- fenestration_id = str(uuid.uuid4())
538
- st.session_state.project_data["fenestrations"]["project"][fenestration_id] = fenestration_data
539
- st.success(f"Fenestration '{name}' added successfully!")
540
-
541
- # Clear editor state
542
- st.session_state.fenestration_editor = {}
543
- st.session_state.materials_rerun_pending = True
544
- st.rerun()
545
-
546
- if cancel:
547
- # Clear editor state
548
- st.session_state.fenestration_editor = {}
549
- st.session_state.materials_rerun_pending = True
550
- st.rerun()
551
 
552
- def display_materials_table(materials: Dict[str, Any], source: str):
553
- """Display materials in a table format with edit/delete buttons."""
554
- if not materials:
555
- return
556
-
557
- # Create table data
558
- table_data = []
559
- for material_id, material in materials.items():
560
- table_data.append({
561
- "Name": material.get("name", "Unknown"),
562
- "Category": material.get("category", "Unknown"),
563
- "k (W/m·K)": f"{material.get('thermal_conductivity', 0):.3f}",
564
- "ρ (kg/m³)": f"{material.get('density', 0):.0f}",
565
- "Cp (J/kg·K)": f"{material.get('specific_heat', 0):.0f}",
566
- "Thickness (m)": f"{material.get('thickness_range', {}).get('default', 0):.3f}",
567
- "Actions": material_id
568
- })
569
-
570
- if table_data:
571
- df = pd.DataFrame(table_data)
572
-
573
- # Display table
574
- st.dataframe(df.drop('Actions', axis=1), use_container_width=True)
575
-
576
- # Display action buttons
577
- st.write("**Actions:**")
578
- for i, row in enumerate(table_data):
579
- material_id = row["Actions"]
580
- material_name = row["Name"]
581
-
582
- col1, col2, col3 = st.columns([2, 1, 1])
583
-
584
- with col1:
585
- st.write(f"{i+1}. {material_name}")
586
-
587
- with col2:
588
- if source == "library":
589
- if st.button("Add to Project", key=f"add_material_{material_id}_{i}"):
590
- # Add library material to project
591
- project_id = str(uuid.uuid4())
592
- st.session_state.project_data["materials"]["project"][project_id] = materials[material_id].copy()
593
- st.success(f"Material '{material_name}' added to project!")
594
- st.session_state.materials_rerun_pending = True
595
- st.rerun()
596
- else:
597
- if st.button("Edit", key=f"edit_material_{material_id}_{i}"):
598
- # Set up editor for editing
599
- material = materials[material_id]
600
- st.session_state.material_editor = {
601
- "is_edit": True,
602
- "edit_id": material_id,
603
- "edit_source": source,
604
- "name": material.get("name", ""),
605
- "category": material.get("category", MATERIAL_CATEGORIES[0]),
606
- "thermal_conductivity": material.get("thermal_conductivity", 0.1),
607
- "density": material.get("density", 1000.0),
608
- "specific_heat": material.get("specific_heat", 1000.0),
609
- "min_thickness": material.get("thickness_range", {}).get("min", 0.01),
610
- "max_thickness": material.get("thickness_range", {}).get("max", 0.3),
611
- "default_thickness": material.get("thickness_range", {}).get("default", 0.1),
612
- "embodied_carbon": material.get("embodied_carbon", 0.5),
613
- "material_cost": material.get("cost", {}).get("material", 100.0),
614
- "labor_cost": material.get("cost", {}).get("labor", 50.0),
615
- "replacement_years": material.get("cost", {}).get("replacement_years", 50)
616
- }
617
- st.session_state.materials_rerun_pending = True
618
- st.rerun()
619
-
620
- with col3:
621
- if source == "project":
622
- if st.button("Delete", key=f"delete_material_{material_id}_{i}"):
623
- # Delete project material
624
- del st.session_state.project_data["materials"]["project"][material_id]
625
- st.success(f"Material '{material_name}' deleted!")
626
- st.session_state.materials_rerun_pending = True
627
- st.rerun()
628
-
629
- def display_fenestrations_table(fenestrations: Dict[str, Any], source: str):
630
- """Display fenestrations in a table format with edit/delete buttons."""
631
- if not fenestrations:
632
- return
633
-
634
- # Create table data
635
- table_data = []
636
- for fenestration_id, fenestration in fenestrations.items():
637
- table_data.append({
638
- "Name": fenestration.get("name", "Unknown"),
639
- "Type": fenestration.get("type", "Unknown"),
640
- "U-Value (W/m²·K)": f"{fenestration.get('u_value', 0):.2f}",
641
- "SHGC": f"{fenestration.get('shgc', 0):.2f}" if fenestration.get('shgc') is not None else "N/A",
642
- "VT": f"{fenestration.get('visible_transmittance', 0):.2f}" if fenestration.get('visible_transmittance') is not None else "N/A",
643
- "Thickness (m)": f"{fenestration.get('thickness', 0):.3f}",
644
- "Actions": fenestration_id
645
- })
646
-
647
- if table_data:
648
- df = pd.DataFrame(table_data)
649
-
650
- # Display table
651
- st.dataframe(df.drop('Actions', axis=1), use_container_width=True)
652
-
653
- # Display action buttons
654
- st.write("**Actions:**")
655
- for i, row in enumerate(table_data):
656
- fenestration_id = row["Actions"]
657
- fenestration_name = row["Name"]
658
-
659
- col1, col2, col3 = st.columns([2, 1, 1])
660
-
661
- with col1:
662
- st.write(f"{i+1}. {fenestration_name}")
663
-
664
- with col2:
665
- if source == "library":
666
- if st.button("Add to Project", key=f"add_fenestration_{fenestration_id}_{i}"):
667
- # Add library fenestration to project
668
- project_id = str(uuid.uuid4())
669
- st.session_state.project_data["fenestrations"]["project"][project_id] = fenestrations[fenestration_id].copy()
670
- st.success(f"Fenestration '{fenestration_name}' added to project!")
671
- st.session_state.materials_rerun_pending = True
672
- st.rerun()
673
- else:
674
- if st.button("Edit", key=f"edit_fenestration_{fenestration_id}_{i}"):
675
- # Set up editor for editing
676
- fenestration = fenestrations[fenestration_id]
677
- editor_data = {
678
- "is_edit": True,
679
- "edit_id": fenestration_id,
680
- "edit_source": source,
681
- "name": fenestration.get("name", ""),
682
- "type": fenestration.get("type", FENESTRATION_TYPES[0]),
683
- "u_value": fenestration.get("u_value", 2.5),
684
- "thickness": fenestration.get("thickness", 0.024),
685
- "embodied_carbon": fenestration.get("embodied_carbon", 500.0),
686
- "material_cost": fenestration.get("cost", {}).get("material", 200.0),
687
- "labor_cost": fenestration.get("cost", {}).get("labor", 80.0),
688
- "replacement_years": fenestration.get("cost", {}).get("replacement_years", 25)
689
  }
690
-
691
- # Add type-specific properties
692
- if fenestration.get("type") in ["Window", "Skylight"]:
693
- editor_data["shgc"] = fenestration.get("shgc", 0.7)
694
- editor_data["visible_transmittance"] = fenestration.get("visible_transmittance", 0.8)
695
  else:
696
- editor_data["solar_absorptivity"] = fenestration.get("solar_absorptivity", 0.7)
697
-
698
- st.session_state.fenestration_editor = editor_data
699
- st.session_state.materials_rerun_pending = True
700
- st.rerun()
701
-
702
- with col3:
703
- if source == "project":
704
- if st.button("Delete", key=f"delete_fenestration_{fenestration_id}_{i}"):
705
- # Delete project fenestration
706
- del st.session_state.project_data["fenestrations"]["project"][fenestration_id]
707
- st.success(f"Fenestration '{fenestration_name}' deleted!")
708
- st.session_state.materials_rerun_pending = True
709
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
- def display_materials_help():
712
- """Display help information for the materials page."""
713
- st.markdown("""
714
- ### Material Library Help
715
-
716
- This page allows you to manage building materials and fenestrations for thermal analysis.
717
-
718
- #### Materials
719
- - **Library Materials**: Pre-defined materials with standard thermal properties
720
- - **Project Materials**: Custom materials created for your specific project
721
- - **Thermal Properties**:
722
- - **Thermal Conductivity (k)**: Rate of heat transfer through the material (W/m·K)
723
- - **Density (ρ)**: Mass per unit volume (kg/m³)
724
- - **Specific Heat (Cp)**: Heat capacity per unit mass (J/kg·K)
725
-
726
- #### Fenestrations
727
- - **Windows/Skylights**: Include solar heat gain coefficient (SHGC) and visible transmittance
728
- - **Doors**: Include solar absorptivity for opaque surfaces
729
- - **U-Value**: Overall heat transfer coefficient (W/m²·K)
730
-
731
- #### Usage
732
- 1. Browse library materials/fenestrations and add them to your project
733
- 2. Create custom materials/fenestrations using the editor
734
- 3. Edit or delete project materials/fenestrations as needed
735
- 4. All materials will be available for use in construction assemblies
736
- """)
737
-
738
- # Helper functions for backward compatibility
739
  def get_available_materials():
740
  """Get all available materials (library + project) for use in other modules."""
741
  materials = {}
742
-
743
  if "materials" in st.session_state.project_data:
744
- # Add library materials
745
- if "library" in st.session_state.project_data["materials"]:
746
- materials.update(st.session_state.project_data["materials"]["library"])
747
-
748
- # Add project materials
749
- if "project" in st.session_state.project_data["materials"]:
750
- materials.update(st.session_state.project_data["materials"]["project"])
751
-
752
  return materials
753
 
754
  def get_available_fenestrations():
755
  """Get all available fenestrations (library + project) for use in other modules."""
756
  fenestrations = {}
757
-
758
  if "fenestrations" in st.session_state.project_data:
759
- # Add library fenestrations
760
- if "library" in st.session_state.project_data["fenestrations"]:
761
- fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
762
-
763
- # Add project fenestrations
764
- if "project" in st.session_state.project_data["fenestrations"]:
765
- fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
766
-
767
- return fenestrations
768
-
 
1
  """
2
+ BuildSustain - Material Library Module
3
 
4
  This module handles the material library functionality of the BuildSustain application,
5
+ allowing users to manage building materials and fenestrations (windows, skylights).
6
  It provides both predefined library materials and the ability to create custom materials.
7
 
8
  Developed by: Dr Majed Abuseif, Deakin University
 
16
  import logging
17
  import uuid
18
  from typing import Dict, List, Any, Optional, Tuple, Union
19
+ from enum import Enum
20
 
21
  # Import default data from centralized module
22
  from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS
 
37
 
38
  FENESTRATION_TYPES = [
39
  "Window",
 
40
  "Skylight",
41
  "Custom"
42
  ]
43
 
44
+ class MaterialCategory(Enum):
45
+ MASONRY = "Masonry"
46
+ INSULATION = "Insulation"
47
+ METAL = "Metal"
48
+ WOOD = "Wood"
49
+ FINISHING = "Finishing"
50
+ CUSTOM = "Custom"
51
+
52
+ class Material:
53
+ def __init__(self, name: str, category: MaterialCategory, conductivity: float, density: float,
54
+ specific_heat: float, default_thickness: float, embodied_carbon: float,
55
+ absorptivity: float, price: float, emissivity: float, is_library: bool = True):
56
+ self.name = name
57
+ self.category = category
58
+ self.conductivity = conductivity
59
+ self.density = density
60
+ self.specific_heat = specific_heat
61
+ self.default_thickness = default_thickness
62
+ self.embodied_carbon = embodied_carbon
63
+ self.absorptivity = absorptivity
64
+ self.price = price
65
+ self.emissivity = emissivity
66
+ self.is_library = is_library
67
+
68
+ def get_thermal_mass(self):
69
+ class ThermalMass:
70
+ def __init__(self, value): self.value = value
71
+ return ThermalMass(self.density * self.specific_heat * self.default_thickness)
72
+
73
+ def get_u_value(self):
74
+ return self.conductivity / self.default_thickness if self.default_thickness > 0 else 0.0
75
+
76
+ class GlazingMaterial:
77
+ def __init__(self, name: str, shgc: float, u_value: float, h_o: float, is_library: bool = True):
78
+ self.name = name
79
+ self.shgc = shgc
80
+ self.u_value = u_value
81
+ self.h_o = h_o
82
+ self.is_library = is_library
83
+
84
+ class MaterialLibrary:
85
+ def __init__(self):
86
+ self.library_materials = {}
87
+ self.library_glazing_materials = {}
88
+
89
+ def to_dataframe(self, material_type: str, **kwargs):
90
+ if material_type == "materials":
91
+ project_materials = kwargs.get("project_materials", {})
92
+ data = [
93
+ {
94
+ "Name": m.name,
95
+ "Category": m.category.value,
96
+ "Thermal Conductivity (W/m·K)": m.conductivity,
97
+ "Density (kg/m³)": m.density,
98
+ "Specific Heat (J/kg·K)": m.specific_heat,
99
+ "Default Thickness (m)": m.default_thickness,
100
+ "Embodied Carbon (kgCO₂e/kg)": m.embodied_carbon,
101
+ "absorptivity": m.absorptivity,
102
+ "Price (USD/m²)": m.price,
103
+ "Emissivity": m.emissivity
104
+ }
105
+ for m in project_materials.values()
106
+ ]
107
+ return pd.DataFrame(data)
108
+ elif material_type == "glazing":
109
+ project_glazing_materials = kwargs.get("project_glazing_materials", {})
110
+ data = [
111
+ {
112
+ "Name": g.name,
113
+ "SHGC": g.shgc,
114
+ "U-Value (W/m²·K)": g.u_value,
115
+ "Exterior Conductance (W/m²·K)": g.h_o
116
+ }
117
+ for g in project_glazing_materials.values()
118
+ ]
119
+ return pd.DataFrame(data)
120
+ return pd.DataFrame()
121
+
122
+ def add_project_material(self, material: Material, project_materials: Dict):
123
+ try:
124
+ if material.name in project_materials or material.name in self.library_materials:
125
+ return False, f"Material '{material.name}' already exists."
126
+ project_materials[material.name] = material
127
+ return True, f"Material '{material.name}' added successfully!"
128
+ except Exception as e:
129
+ return False, f"Error adding material: {str(e)}"
130
+
131
+ def edit_project_material(self, original_name: str, new_material: Material, project_materials: Dict, components: Dict):
132
+ try:
133
+ if original_name not in project_materials:
134
+ return False, f"Material '{original_name}' not found."
135
+ if new_material.name != original_name and (new_material.name in project_materials or new_material.name in self.library_materials):
136
+ return False, f"Material '{new_material.name}' already exists."
137
+ project_materials[new_material.name] = new_material
138
+ if new_material.name != original_name:
139
+ del project_materials[original_name]
140
+ return True, f"Material '{new_material.name}' updated successfully!"
141
+ except Exception as e:
142
+ return False, f"Error editing material: {str(e)}"
143
+
144
+ def delete_project_material(self, name: str, project_materials: Dict, components: Dict):
145
+ try:
146
+ if name not in project_materials:
147
+ return False, f"Material '{name}' not found."
148
+ del project_materials[name]
149
+ return True, f"Material '{name}' deleted successfully!"
150
+ except Exception as e:
151
+ return False, f"Error deleting material: {str(e)}"
152
+
153
  def display_materials_page():
154
  """
155
  Display the material library page.
 
157
  """
158
  st.title("Material Library")
159
  st.write("Manage building materials and fenestrations for thermal analysis.")
160
+
161
+ # CSS for box heights, scrolling, and visual appeal
162
+ st.markdown("""
163
+ <style>
164
+ .box-container {
165
+ border: 1px solid #e0e0e0;
166
+ border-radius: 8px;
167
+ padding: 10px;
168
+ background-color: #f9f9f9;
169
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
170
+ overflow-y: auto;
171
+ scrollbar-width: thin;
172
+ }
173
+ .library-box {
174
+ max-height: 250px;
175
+ }
176
+ .project-box {
177
+ max-height: 250px;
178
+ }
179
+ .editor-box {
180
+ min-height: 540px;
181
+ }
182
+ .stButton>button {
183
+ width: 100%;
184
+ font-size: 12px;
185
+ padding: 5px;
186
+ }
187
+ </style>
188
+ """, unsafe_allow_html=True)
189
+
190
  # Initialize materials and fenestrations in session state if not present
191
  initialize_materials_and_fenestrations()
192
+
193
+ # Check for rerun trigger
194
+ if st.session_state.get("materials_rerun_pending", False):
 
 
 
195
  st.session_state.materials_rerun_pending = False
196
  st.rerun()
197
+
198
+ # Initialize session state
199
+ if 'active_tab' not in st.session_state:
200
+ st.session_state.active_tab = "Materials"
201
+ if 'material_action' not in st.session_state:
202
+ st.session_state.material_action = {"action": None, "id": None}
203
+ if 'fenestration_action' not in st.session_state:
204
+ st.session_state.fenestration_action = {"action": None, "id": None}
205
+ if 'materials_rerun_pending' not in st.session_state:
206
+ st.session_state.materials_rerun_pending = False
207
+ if 'material_form_state' not in st.session_state:
208
+ st.session_state.material_form_state = {}
209
+ if 'fenestration_form_state' not in st.session_state:
210
+ st.session_state.fenestration_form_state = {}
211
+
212
+ # Create tabs and set active tab
213
  tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
214
+ active_tab = st.session_state.active_tab
 
215
  with tab1:
216
+ if active_tab == "Materials":
217
+ st.session_state.active_tab = "Materials"
 
218
  with tab2:
219
+ if active_tab == "Fenestrations":
220
+ st.session_state.active_tab = "Fenestrations"
221
+
222
+ # Initialize material library
223
+ material_library = MaterialLibrary()
224
+ material_library.library_materials = {
225
+ k: Material(
226
+ name=m["name"],
227
+ category=MaterialCategory(m["category"]),
228
+ conductivity=m["thermal_conductivity"],
229
+ density=m["density"],
230
+ specific_heat=m["specific_heat"],
231
+ default_thickness=m["thickness_range"]["default"],
232
+ embodied_carbon=m["embodied_carbon"],
233
+ absorptivity=m.get("absorptivity", 0.6),
234
+ price=m["cost"]["material"],
235
+ emissivity=m.get("emissivity", 0.9),
236
+ is_library=True
237
+ ) for k, m in st.session_state.project_data["materials"]["library"].items()
238
+ }
239
+ material_library.library_glazing_materials = {
240
+ k: GlazingMaterial(
241
+ name=f["name"],
242
+ shgc=f["shgc"] if "shgc" in f else 0.7,
243
+ u_value=f["u_value"],
244
+ h_o=f.get("h_o", 23.0),
245
+ is_library=True
246
+ ) for k, f in st.session_state.project_data["fenestrations"]["library"].items()
247
+ if f["type"] in ["Window", "Skylight"]
248
+ }
249
+
250
+ with tab1:
251
+ display_materials_tab(material_library)
252
+ with tab2:
253
+ display_fenestrations_tab(material_library)
254
+
255
  # Navigation buttons
256
  col1, col2 = st.columns(2)
 
257
  with col1:
258
+ if st.button("Back to Climate Data", key="material_back_to_climate"):
259
  st.session_state.current_page = "Climate Data"
260
  st.rerun()
 
261
  with col2:
262
+ if st.button("Continue to Construction", key="material_to_components"):
263
  st.session_state.current_page = "Construction"
264
  st.rerun()
265
 
266
  def initialize_materials_and_fenestrations():
267
  """Initialize materials and fenestrations in session state if not present."""
268
+ if "project_data" not in st.session_state:
269
+ st.session_state.project_data = {}
270
  if "materials" not in st.session_state.project_data:
271
  st.session_state.project_data["materials"] = {
272
  "library": dict(SAMPLE_MATERIALS),
273
  "project": {}
274
  }
 
 
275
  if "fenestrations" not in st.session_state.project_data:
276
  st.session_state.project_data["fenestrations"] = {
277
  "library": dict(SAMPLE_FENESTRATIONS),
278
  "project": {}
279
  }
280
 
281
+ def display_materials_tab(material_library: MaterialLibrary):
282
  """Display the materials tab content with two-column layout."""
 
283
  col1, col2 = st.columns([3, 2])
 
284
  with col1:
285
+ st.subheader("Materials")
286
+ # Category filter
287
+ filter_options = ["All", "None"] + MATERIAL_CATEGORIES
288
+ category = st.selectbox("Filter by Category", filter_options, key="material_filter")
289
+
290
+ st.subheader("Library Materials")
291
+ with st.container():
292
+ library_materials = list(material_library.library_materials.values())
293
+ if category == "None":
294
+ library_materials = []
295
+ elif category != "All":
296
+ library_materials = [m for m in library_materials if m.category.value == category]
297
+ cols = st.columns([2, 1, 1, 1, 1])
298
+ cols[0].write("**Name**")
299
+ cols[1].write("**Thermal Mass**")
300
+ cols[2].write("**U-Value (W/m²·K)**")
301
+ cols[3].write("**Preview**")
302
+ cols[4].write("**Copy**")
303
+ for material in library_materials:
304
+ cols = st.columns([2, 1, 1, 1, 1])
305
+ cols[0].write(material.name)
306
+ cols[1].write(f"{material.get_thermal_mass().value:.0f}")
307
+ cols[2].write(f"{material.get_u_value():.3f}")
308
+ if cols[3].button("Preview", key=f"preview_lib_mat_{material.name}"):
309
+ if st.session_state.get("rerun_trigger") != f"preview_mat_{material.name}":
310
+ st.session_state.rerun_trigger = f"preview_mat_{material.name}"
311
+ st.session_state.material_editor = {
312
+ "name": material.name,
313
+ "category": material.category.value,
314
+ "conductivity": material.conductivity,
315
+ "density": material.density,
316
+ "specific_heat": material.specific_heat,
317
+ "default_thickness": material.default_thickness,
318
+ "embodied_carbon": material.embodied_carbon,
319
+ "absorptivity": material.absorptivity,
320
+ "price": material.price,
321
+ "emissivity": material.emissivity,
322
+ "is_edit": False,
323
+ "edit_source": "library"
324
+ }
325
+ st.session_state.material_form_state = {
326
+ "name": material.name,
327
+ "category": material.category.value,
328
+ "conductivity": material.conductivity,
329
+ "density": material.density,
330
+ "specific_heat": material.specific_heat,
331
+ "default_thickness": material.default_thickness,
332
+ "embodied_carbon": material.embodied_carbon,
333
+ "absorptivity": material.absorptivity,
334
+ "price": material.price,
335
+ "emissivity": material.emissivity
336
+ }
337
+ st.session_state.active_tab = "Materials"
338
+ if cols[4].button("Copy", key=f"copy_lib_mat_{material.name}"):
339
+ new_name = f"{material.name}_Project"
340
+ counter = 1
341
+ while new_name in st.session_state.project_data["materials"]["project"] or new_name in st.session_state.project_data["materials"]["library"]:
342
+ new_name = f"{material.name}_Project_{counter}"
343
+ counter += 1
344
+ new_material = Material(
345
+ name=new_name,
346
+ category=material.category,
347
+ conductivity=material.conductivity,
348
+ density=material.density,
349
+ specific_heat=material.specific_heat,
350
+ default_thickness=material.default_thickness,
351
+ embodied_carbon=material.embodied_carbon,
352
+ absorptivity=material.absorptivity,
353
+ price=material.price,
354
+ emissivity=material.emissivity,
355
+ is_library=False
356
+ )
357
+ success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
358
+ if success:
359
+ st.success(message)
360
+ st.session_state.materials_rerun_pending = True
361
+ else:
362
+ st.error(message)
363
+
364
+ st.subheader("Project Materials")
365
+ with st.container():
366
+ if st.session_state.project_data["materials"]["project"]:
367
+ cols = st.columns([2, 1, 1, 1, 1])
368
+ cols[0].write("**Name**")
369
+ cols[1].write("**Thermal Mass**")
370
+ cols[2].write("**U-Value (W/m²·K)**")
371
+ cols[3].write("**Edit**")
372
+ cols[4].write("**Delete**")
373
+ for material in st.session_state.project_data["materials"]["project"].values():
374
+ cols = st.columns([2, 1, 1, 1, 1])
375
+ cols[0].write(material.name)
376
+ cols[1].write(f"{material.get_thermal_mass().value:.0f}")
377
+ cols[2].write(f"{material.get_u_value():.3f}")
378
+ if cols[3].button("Edit", key=f"edit_proj_mat_{material.name}"):
379
+ if st.session_state.get("rerun_trigger") != f"edit_mat_{material.name}"):
380
+ st.session_state.rerun_trigger = f"edit_mat_{material.name}"
381
+ st.session_state.material_editor = {
382
+ "name": material.name,
383
+ "category": material.category.value,
384
+ "conductivity": material.conductivity,
385
+ "density": material.density,
386
+ "specific_heat": material.specific_heat,
387
+ "default_thickness": material.default_thickness,
388
+ "embodied_carbon": material.embodied_carbon,
389
+ "absorptivity": material.absorptivity,
390
+ "price": material.price,
391
+ "emissivity": material.emissivity,
392
+ "is_edit": True,
393
+ "edit_source": "project",
394
+ "original_name": material.name
395
+ }
396
+ st.session_state.material_form_state = {
397
+ "name": material.name,
398
+ "category": material.category.value,
399
+ "conductivity": material.conductivity,
400
+ "density": material.density,
401
+ "specific_heat": material.specific_heat,
402
+ "default_thickness": material.default_thickness,
403
+ "embodied_carbon": material.embodied_carbon,
404
+ "absorptivity": material.absorptivity,
405
+ "price": material.price,
406
+ "emissivity": material.emissivity
407
+ }
408
+ st.session_state.active_tab = "Materials"
409
+ if cols[4].button("Delete", key=f"delete_proj_mat_{material.name}"):
410
+ success, message = material_library.delete_project_material(
411
+ material.name, st.session_state.project_data["materials"]["project"], st.session_state.get("components", {})
412
+ )
413
+ if success:
414
+ st.success(message)
415
+ st.session_state.materials_rerun_pending = True
416
+ else:
417
+ st.error(message)
418
+ else:
419
+ st.write("No project materials added.")
420
+
421
+ st.subheader("Project Materials")
422
+ try:
423
+ material_df = material_library.to_dataframe("materials", project_materials=st.session_state.project_data["materials"]["project"])
424
+ if not material_df.empty:
425
+ st.dataframe(material_df, use_container_width=True)
426
+ else:
427
+ st.write("No project materials to display.")
428
+ except Exception as e:
429
+ st.error(f"Error displaying project materials: {str(e)}")
430
+ st.write("No project materials to display.")
431
+
432
  with col2:
433
  st.subheader("Material Editor/Creator")
434
+ with st.container():
435
+ with st.form("material_editor_form", clear_on_submit=False):
436
+ editor_state = st.session_state.get("material_editor", {})
437
+ form_state = st.session_state.get("material_form_state", {
438
+ "name": "",
439
+ "category": "Insulation",
440
+ "conductivity": 0.1,
441
+ "density": 1000.0,
442
+ "specific_heat": 1000.0,
443
+ "default_thickness": 0.1,
444
+ "embodied_carbon": 0.5,
445
+ "absorptivity": 0.6,
446
+ "price": 50.0,
447
+ "emissivity": 0.925
448
+ })
449
+ is_edit = editor_state.get("is_edit", False)
450
+ original_name = editor_state.get("original_name", "")
451
+ name = st.text_input(
452
+ "Material Name",
453
+ value=form_state.get("name", editor_state.get("name", "")),
454
+ help="Unique material identifier",
455
+ key="material_name_input"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  )
457
+ filter_category = st.session_state.get("material_filter", "All")
458
+ default_category = (filter_category if filter_category in MATERIAL_CATEGORIES
459
+ else editor_state.get("category", "Insulation"))
460
+ category_index = (MATERIAL_CATEGORIES.index(default_category)
461
+ if default_category in MATERIAL_CATEGORIES else 0)
462
+ category = st.selectbox(
463
+ "Category",
464
+ MATERIAL_CATEGORIES,
465
+ index=category_index,
466
+ help="Material type classification",
467
+ key="material_category_input"
468
+ )
469
+ conductivity = st.number_input(
470
+ "Thermal Conductivity (W/m·K)",
471
+ min_value=0.01,
472
+ value=form_state.get("conductivity", editor_state.get("conductivity", 0.1)),
473
+ help="Heat flow ease",
474
+ key="material_conductivity_input"
475
+ )
476
+ density = st.number_input(
477
+ "Density (kg/m³)",
478
+ min_value=1.0,
479
+ value=form_state.get("density", editor_state.get("density", 1000.0)),
480
+ help="Mass per volume",
481
+ key="material_density_input"
482
+ )
483
+ specific_heat = st.number_input(
484
+ "Specific Heat (J/kg·K)",
485
+ min_value=100.0,
486
+ value=form_state.get("specific_heat", editor_state.get("specific_heat", 1000.0)),
487
+ help="Heat storage capacity",
488
+ key="material_specific_heat_input"
489
  )
 
 
490
  default_thickness = st.number_input(
491
+ "Default Thickness (m)",
492
  min_value=0.001,
493
+ value=form_state.get("default_thickness", editor_state.get("default_thickness", 0.1)),
494
+ help="Typical layer thickness",
495
+ key="material_default_thickness_input"
496
+ )
497
+ embodied_carbon = st.number_input(
498
+ "Embodied Carbon (kgCO₂e/kg)",
499
+ min_value=0.0,
500
+ value=form_state.get("embodied_carbon", editor_state.get("embodied_carbon", 0.5)),
501
+ help="Production carbon emissions",
502
+ key="material_embodied_carbon_input"
503
+ )
504
+ absorptivity = st.number_input(
505
+ "absorptivity",
506
+ min_value=0.0,
507
  max_value=1.0,
508
+ value=form_state.get("absorptivity", editor_state.get("absorptivity", 0.6)),
509
+ help="Solar radiation absorbed",
510
+ key="material_absorptivity_input"
511
  )
512
+ price = st.number_input(
513
+ "Price (USD/m²)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  min_value=0.0,
515
+ value=form_state.get("price", editor_state.get("price", 50.0)),
516
+ help="Cost per area",
517
+ key="material_price_input"
 
518
  )
519
+ emissivity = st.number_input(
520
+ "Emissivity",
 
 
521
  min_value=0.0,
522
+ max_value=1.0,
523
+ value=form_state.get("emissivity", editor_state.get("emissivity", 0.925)),
524
+ help="Ratio of radiation emitted by the material (0.0 to 1.0)",
525
+ key="material_emissivity_input"
526
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
+ if st.form_submit_button("Save Material"):
529
+ action_id = str(uuid.uuid4())
530
+ if st.session_state.material_action.get("id") != action_id:
531
+ st.session_state.material_action = {"action": "save", "id": action_id}
532
+ st.session_state.material_form_state = {
533
+ "name": name,
534
+ "category": category,
535
+ "conductivity": conductivity,
536
+ "density": density,
537
+ "specific_heat": specific_heat,
538
+ "default_thickness": default_thickness,
539
+ "embodied_carbon": embodied_carbon,
540
+ "absorptivity": absorptivity,
541
+ "price": price,
542
+ "emissivity": emissivity
543
+ }
544
+ if not name or not name.strip():
545
+ st.error("Material name cannot be empty.")
546
+ elif (name in st.session_state.project_data["materials"]["project"] or
547
+ name in st.session_state.project_data["materials"]["library"]) and (not is_edit or name != original_name):
548
+ st.error(f"Material '{name}' already exists.")
549
+ else:
550
+ try:
551
+ new_material = Material(
552
+ name=name,
553
+ category=MaterialCategory(category),
554
+ conductivity=conductivity,
555
+ density=density,
556
+ specific_heat=specific_heat,
557
+ default_thickness=default_thickness,
558
+ embodied_carbon=embodied_carbon,
559
+ absorptivity=absorptivity,
560
+ price=price,
561
+ emissivity=emissivity,
562
+ is_library=False
563
+ )
564
+ if is_edit and editor_state.get("edit_source") == "project" and name == original_name:
565
+ success, message = material_library.edit_project_material(
566
+ original_name, new_material, st.session_state.project_data["materials"]["project"], st.session_state.get("components", {})
567
+ )
568
+ else:
569
+ success, message = material_library.add_project_material(new_material, st.session_state.project_data["materials"]["project"])
570
+ if success:
571
+ st.success(message)
572
+ st.session_state.material_editor = {}
573
+ st.session_state.material_form_state = {
574
+ "name": "",
575
+ "category": "Insulation",
576
+ "conductivity": 0.1,
577
+ "density": 1000.0,
578
+ "specific_heat": 1000.0,
579
+ "default_thickness": 0.1,
580
+ "embodied_carbon": 0.5,
581
+ "absorptivity": 0.6,
582
+ "price": 50.0,
583
+ "emissivity": 0.925
584
+ }
585
+ st.session_state.material_action = {"action": None, "id": None}
586
+ st.session_state.rerun_trigger = None
587
+ st.session_state.materials_rerun_pending = True
588
+ else:
589
+ st.error(f"Failed to save material: {message}")
590
+ except Exception as e:
591
+ st.error(f"Error saving material: {str(e)}")
592
+
593
+ def display_fenestrations_tab(material_library: MaterialLibrary):
594
  """Display the fenestrations tab content with two-column layout."""
 
595
  col1, col2 = st.columns([3, 2])
 
596
  with col1:
597
+ st.subheader("Fenestrations")
598
+ filter_options = ["All", "None"] + FENESTRATION_TYPES
599
+ fenestration_filter = st.selectbox("Filter Fenestrations", filter_options, key="fenestration_filter")
600
+
601
+ st.subheader("Library Fenestrations")
602
+ with st.container():
603
+ library_fenestrations = list(material_library.library_glazing_materials.values())
604
+ if fenestration_filter == "None":
605
+ library_fenestrations = []
606
+ elif fenestration_filter != "All":
607
+ library_fenestrations = [f for f in library_fenestrations if f.__class__.__name__ == fenestration_filter]
608
+ cols = st.columns([2, 1, 1, 1, 1])
609
+ cols[0].write("**Name**")
610
+ cols[1].write("**SHGC**")
611
+ cols[2].write("**U-Value (W/m²·K)**")
612
+ cols[3].write("**Preview**")
613
+ cols[4].write("**Copy**")
614
+ for fenestration in library_fenestrations:
615
+ cols = st.columns([2, 1, 1, 1, 1])
616
+ cols[0].write(fenestration.name)
617
+ cols[1].write(f"{fenestration.shgc:.2f}")
618
+ cols[2].write(f"{fenestration.u_value:.3f}")
619
+ if cols[3].button("Preview", key=f"preview_lib_fen_{fenestration.name}"):
620
+ if st.session_state.get("rerun_trigger") != f"preview_fen_{fenestration.name}"):
621
+ st.session_state.rerun_trigger = f"preview_fen_{fenestration.name}"
622
+ st.session_state.fenestration_editor = {
623
+ "name": fenestration.name,
624
+ "shgc": fenestration.shgc,
625
+ "u_value": fenestration.u_value,
626
+ "h_o": fenestration.h_o,
627
+ "is_edit": False,
628
+ "edit_source": "library"
629
+ }
630
+ st.session_state.fenestration_form_state = {
631
+ "name": fenestration.name,
632
+ "shgc": fenestration.shgc,
633
+ "u_value": fenestration.u_value,
634
+ "h_o": fenestration.h_o
635
+ }
636
+ st.session_state.active_tab = "Fenestrations"
637
+ if cols[4].button("Copy", key=f"copy_lib_fen_{fenestration.name}"):
638
+ new_name = f"{fenestration.name}_Project"
639
+ counter = 1
640
+ while new_name in st.session_state.project_data["fenestrations"]["project"] or new_name in st.session_state.project_data["fenestrations"]["library"]:
641
+ new_name = f"{fenestration.name}_Project_{counter}"
642
+ counter += 1
643
+ new_fenestration = GlazingMaterial(
644
+ name=new_name,
645
+ shgc=fenestration.shgc,
646
+ u_value=fenestration.u_value,
647
+ h_o=fenestration.h_o,
648
+ is_library=False
649
+ )
650
+ st.session_state.project_data["fenestrations"]["project"][new_name] = new_fenestration
651
+ st.success(f"Fenestration '{new_name}' copied!")
652
+ st.session_state.materials_rerun_pending = True
653
+
654
+ st.subheader("Project Fenestrations")
655
+ with st.container():
656
+ if st.session_state.project_data["fenestrations"]["project"]:
657
+ cols = st.columns([2, 1, 1, 1, 1])
658
+ cols[0].write("**Name**")
659
+ cols[1].write("**SHGC**")
660
+ cols[2].write("**U-Value (W/m²·K)**")
661
+ cols[3].write("**Edit**")
662
+ cols[4].write("**Delete**")
663
+ for fenestration in st.session_state.project_data["fenestrations"]["project"].values():
664
+ cols = st.columns([2, 1, 1, 1, 1])
665
+ cols[0].write(fenestration.name)
666
+ cols[1].write(f"{fenestration.shgc:.2f}")
667
+ cols[2].write(f"{fenestration.u_value:.3f}")
668
+ if cols[3].button("Edit", key=f"edit_proj_fen_{fenestration.name}"):
669
+ if st.session_state.get("rerun_trigger") != f"edit_fen_{fenestration.name}"):
670
+ st.session_state.rerun_trigger = f"edit_fen_{fenestration.name}"
671
+ st.session_state.fenestration_editor = {
672
+ "name": fenestration.name,
673
+ "shgc": fenestration.shgc,
674
+ "u_value": fenestration.u_value,
675
+ "h_o": fenestration.h_o,
676
+ "is_edit": True,
677
+ "edit_source": "project",
678
+ "original_name": fenestration.name
679
+ }
680
+ st.session_state.fenestration_form_state = {
681
+ "name": fenestration.name,
682
+ "shgc": fenestration.shgc,
683
+ "u_value": fenestration.u_value,
684
+ "h_o": fenestration.h_o
685
+ }
686
+ st.session_state.active_tab = "Fenestrations"
687
+ if cols[4].button("Delete", key=f"delete_proj_fen_{fenestration.name}"):
688
+ if any(comp.get("fenestration_material") and comp["fenestration_material"].name == fenestration.name
689
+ for comp_list in st.session_state.get("components", {}).values() for comp in comp_list):
690
+ st.error(f"Fenestration '{fenestration.name}' is used in components and cannot be deleted.")
691
+ else:
692
+ del st.session_state.project_data["fenestrations"]["project"][fenestration.name]
693
+ st.success(f"Fenestration '{fenestration.name}' deleted!")
694
+ st.session_state.materials_rerun_pending = True
695
+ else:
696
+ st.write("No project fenestrations added.")
697
+
698
+ st.subheader("Project Fenestrations")
699
+ if st.session_state.project_data["fenestrations"]["project"]:
700
+ try:
701
+ fenestration_df = material_library.to_dataframe("glazing", project_glazing_materials=st.session_state.project_data["fenestrations"]["project"])
702
+ if fenestration_df.empty:
703
+ fenestration_data = [
704
+ {
705
+ "Name": f.name,
706
+ "SHGC": f.shgc,
707
+ "U-Value (W/m²·K)": f.u_value,
708
+ "Exterior Conductance (W/m²·K)": f.h_o
709
+ }
710
+ for f in st.session_state.project_data["fenestrations"]["project"].values()
711
+ ]
712
+ fenestration_df = pd.DataFrame(fenestration_data)
713
+ if not fenestration_df.empty:
714
+ st.dataframe(fenestration_df, use_container_width=True)
715
+ else:
716
+ st.write("No project fenestrations to display.")
717
+ except Exception as e:
718
+ fenestration_data = [
719
+ {
720
+ "Name": f.name,
721
+ "SHGC": f.shgc,
722
+ "U-Value (W/m²·K)": f.u_value,
723
+ "Exterior Conductance (W/m²·K)": f.h_o
724
+ }
725
+ for f in st.session_state.project_data["fenestrations"]["project"].values()
726
+ ]
727
+ fenestration_df = pd.DataFrame(fenestration_data)
728
+ if not fenestration_df.empty:
729
+ st.dataframe(fenestration_df, use_container_width=True)
730
+ else:
731
+ st.error(f"Error displaying project fenestrations: {str(e)}")
732
+ st.write("No project fenestrations to display.")
733
+ else:
734
+ st.write("No project fenestrations to display.")
735
+
736
  with col2:
737
  st.subheader("Fenestration Editor/Creator")
738
+ with st.container():
739
+ with st.form("fenestration_editor_form", clear_on_submit=False):
740
+ editor_state = st.session_state.get("fenestration_editor", {})
741
+ form_state = st.session_state.get("fenestration_form_state", {
742
+ "name": "",
743
+ "shgc": 0.7,
744
+ "u_value": 5.0,
745
+ "h_o": 23.0
746
+ })
747
+ is_edit = editor_state.get("is_edit", False)
748
+ original_name = editor_state.get("original_name", "")
749
+ name = st.text_input(
750
+ "Fenestration Name",
751
+ value=form_state.get("name", editor_state.get("name", "")),
752
+ help="Unique fenestration identifier",
753
+ key="fenestration_name_input"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  )
755
+ shgc = st.number_input(
756
+ "Solar Heat Gain Coefficient (SHGC)",
 
 
 
 
 
 
 
 
 
 
 
 
757
  min_value=0.0,
758
  max_value=1.0,
759
+ value=form_state.get("shgc", editor_state.get("shgc", 0.7)),
760
+ help="Fraction of solar radiation admitted",
761
+ key="fenestration_shgc_input"
762
  )
763
+ u_value = st.number_input(
764
+ "U-Value (W/m²·K)",
765
+ min_value=0.1,
766
+ value=form_state.get("u_value", editor_state.get("u_value", 5.0)),
767
+ help="Thermal transmittance",
768
+ key="fenestration_u_value_input"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  )
770
+ h_o = st.number_input(
771
+ "Exterior Surface Conductance (W/m²·K)",
 
 
772
  min_value=0.0,
773
+ value=form_state.get("h_o", editor_state.get("h_o", 23.0)),
774
+ help="Exterior surface heat transfer coefficient",
775
+ key="fenestration_h_o_input"
 
776
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
 
778
+ if st.form_submit_button("Save Fenestration"):
779
+ action_id = str(uuid.uuid4())
780
+ if st.session_state.fenestration_action.get("id") != action_id:
781
+ st.session_state.fenestration_action = {"action": "save", "id": action_id}
782
+ st.session_state.fenestration_form_state = {
783
+ "name": name,
784
+ "shgc": shgc,
785
+ "u_value": u_value,
786
+ "h_o": h_o
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
  }
788
+ if not name or not name.strip():
789
+ st.error("Fenestration name cannot be empty.")
790
+ elif (name in st.session_state.project_data["fenestrations"]["project"] or
791
+ name in st.session_state.project_data["fenestrations"]["library"]) and (not is_edit or name != original_name):
792
+ st.error(f"Fenestration '{name}' already exists.")
793
  else:
794
+ try:
795
+ new_fenestration = GlazingMaterial(
796
+ name=name,
797
+ shgc=shgc,
798
+ u_value=u_value,
799
+ h_o=h_o,
800
+ is_library=False
801
+ )
802
+ if is_edit and editor_state.get("edit_source") == "project" and name == original_name:
803
+ st.session_state.project_data["fenestrations"]["project"][original_name] = new_fenestration
804
+ st.success(f"Fenestration '{name}' updated!")
805
+ else:
806
+ st.session_state.project_data["fenestrations"]["project"][name] = new_fenestration
807
+ st.success(f"Fenestration '{name}' added!")
808
+ st.session_state.fenestration_editor = {}
809
+ st.session_state.fenestration_form_state = {
810
+ "name": "",
811
+ "shgc": 0.7,
812
+ "u_value": 5.0,
813
+ "h_o": 23.0
814
+ }
815
+ st.session_state.fenestration_action = {"action": None, "id": None}
816
+ st.session_state.rerun_trigger = None
817
+ st.session_state.materials_rerun_pending = True
818
+ except Exception as e:
819
+ st.error(f"Error saving fenestration: {str(e)}")
820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  def get_available_materials():
822
  """Get all available materials (library + project) for use in other modules."""
823
  materials = {}
 
824
  if "materials" in st.session_state.project_data:
825
+ materials.update(st.session_state.project_data["materials"]["library"])
826
+ materials.update(st.session_state.project_data["materials"]["project"])
 
 
 
 
 
 
827
  return materials
828
 
829
  def get_available_fenestrations():
830
  """Get all available fenestrations (library + project) for use in other modules."""
831
  fenestrations = {}
 
832
  if "fenestrations" in st.session_state.project_data:
833
+ fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
834
+ fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
835
+ return fenestrations