Spaces:
Sleeping
Sleeping
Update app/component_selection.py
Browse files- app/component_selection.py +484 -599
app/component_selection.py
CHANGED
|
@@ -34,12 +34,6 @@ class ComponentType(Enum):
|
|
| 34 |
WINDOW = "Window"
|
| 35 |
DOOR = "Door"
|
| 36 |
|
| 37 |
-
class ShadingType(Enum):
|
| 38 |
-
NONE = "None"
|
| 39 |
-
INTERNAL = "Internal"
|
| 40 |
-
EXTERNAL = "External"
|
| 41 |
-
BETWEEN_GLASS = "Between-glass"
|
| 42 |
-
|
| 43 |
# --- Data Models ---
|
| 44 |
@dataclass
|
| 45 |
class MaterialLayer:
|
|
@@ -55,208 +49,186 @@ class BuildingComponent:
|
|
| 55 |
u_value: float = 0.0 # W/(m²·K)
|
| 56 |
area: float = 0.0 # m²
|
| 57 |
orientation: Orientation = Orientation.NOT_APPLICABLE
|
| 58 |
-
material_layers: List[MaterialLayer] = field(default_factory=list)
|
| 59 |
|
| 60 |
def __post_init__(self):
|
| 61 |
-
if self.area
|
| 62 |
-
raise ValueError("Area
|
| 63 |
-
if self.u_value
|
| 64 |
-
raise ValueError("U-value
|
| 65 |
|
| 66 |
def to_dict(self) -> dict:
|
| 67 |
return {
|
| 68 |
-
"id": self.id,
|
| 69 |
-
"
|
| 70 |
-
"component_type": self.component_type.value,
|
| 71 |
-
"u_value": self.u_value,
|
| 72 |
-
"area": self.area,
|
| 73 |
-
"orientation": self.orientation.value,
|
| 74 |
-
"material_layers": [{"name": l.name, "thickness": l.thickness, "conductivity": l.conductivity} for l in self.material_layers]
|
| 75 |
}
|
| 76 |
|
| 77 |
@dataclass
|
| 78 |
class Wall(BuildingComponent):
|
| 79 |
wall_type: str = "Brick"
|
| 80 |
wall_group: str = "Wall"
|
| 81 |
-
|
|
|
|
|
|
|
| 82 |
|
| 83 |
def __post_init__(self):
|
| 84 |
super().__post_init__()
|
| 85 |
self.component_type = ComponentType.WALL
|
| 86 |
-
if not 0 <= self.
|
| 87 |
-
raise ValueError("
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
def to_dict(self) -> dict:
|
| 90 |
base_dict = super().to_dict()
|
| 91 |
-
base_dict.update({
|
|
|
|
|
|
|
|
|
|
| 92 |
return base_dict
|
| 93 |
|
| 94 |
@dataclass
|
| 95 |
class Roof(BuildingComponent):
|
| 96 |
roof_type: str = "Concrete"
|
| 97 |
roof_group: str = "C"
|
|
|
|
|
|
|
| 98 |
|
| 99 |
def __post_init__(self):
|
| 100 |
super().__post_init__()
|
| 101 |
self.component_type = ComponentType.ROOF
|
| 102 |
-
self.orientation
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
def to_dict(self) -> dict:
|
| 105 |
base_dict = super().to_dict()
|
| 106 |
-
base_dict.update({
|
|
|
|
|
|
|
| 107 |
return base_dict
|
| 108 |
|
| 109 |
@dataclass
|
| 110 |
class Floor(BuildingComponent):
|
| 111 |
floor_type: str = "Concrete"
|
|
|
|
|
|
|
| 112 |
|
| 113 |
def __post_init__(self):
|
| 114 |
super().__post_init__()
|
| 115 |
self.component_type = ComponentType.FLOOR
|
| 116 |
-
self.orientation = Orientation.
|
| 117 |
|
| 118 |
def to_dict(self) -> dict:
|
| 119 |
base_dict = super().to_dict()
|
| 120 |
-
base_dict.update({
|
|
|
|
|
|
|
| 121 |
return base_dict
|
| 122 |
|
| 123 |
@dataclass
|
| 124 |
class Window(BuildingComponent):
|
| 125 |
shgc: float = 0.7
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
shading_device_id: str = "preset_none"
|
| 132 |
|
| 133 |
def __post_init__(self):
|
| 134 |
super().__post_init__()
|
| 135 |
self.component_type = ComponentType.WINDOW
|
| 136 |
if not 0 <= self.shgc <= 1:
|
| 137 |
raise ValueError("SHGC must be between 0 and 1")
|
| 138 |
-
if not 0 <= self.
|
| 139 |
-
raise ValueError("
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
def to_dict(self) -> dict:
|
| 142 |
base_dict = super().to_dict()
|
| 143 |
base_dict.update({
|
| 144 |
-
"shgc": self.shgc, "
|
| 145 |
-
"
|
| 146 |
-
"low_e_coating": self.low_e_coating, "shading_device_id": self.shading_device_id
|
| 147 |
})
|
| 148 |
return base_dict
|
| 149 |
|
| 150 |
@dataclass
|
| 151 |
class Door(BuildingComponent):
|
| 152 |
door_type: str = "Solid Wood"
|
|
|
|
| 153 |
|
| 154 |
def __post_init__(self):
|
| 155 |
super().__post_init__()
|
| 156 |
self.component_type = ComponentType.DOOR
|
|
|
|
|
|
|
| 157 |
|
| 158 |
def to_dict(self) -> dict:
|
| 159 |
base_dict = super().to_dict()
|
| 160 |
-
base_dict.update({"door_type": self.door_type})
|
| 161 |
return base_dict
|
| 162 |
|
| 163 |
-
# --- Shading System ---
|
| 164 |
-
@dataclass
|
| 165 |
-
class ShadingDevice:
|
| 166 |
-
id: str
|
| 167 |
-
name: str
|
| 168 |
-
shading_type: ShadingType
|
| 169 |
-
shading_coefficient: float
|
| 170 |
-
coverage_percentage: float = 100.0
|
| 171 |
-
description: str = ""
|
| 172 |
-
|
| 173 |
-
def __post_init__(self):
|
| 174 |
-
if self.shading_coefficient < 0 or self.shading_coefficient > 1:
|
| 175 |
-
raise ValueError("Shading coefficient must be between 0 and 1")
|
| 176 |
-
if self.coverage_percentage < 0 or self.coverage_percentage > 100:
|
| 177 |
-
raise ValueError("Coverage percentage must be between 0 and 100")
|
| 178 |
-
|
| 179 |
-
@property
|
| 180 |
-
def effective_shading_coefficient(self) -> float:
|
| 181 |
-
coverage_factor = self.coverage_percentage / 100.0
|
| 182 |
-
return self.shading_coefficient * coverage_factor + 1.0 * (1 - coverage_factor)
|
| 183 |
-
|
| 184 |
-
def to_dict(self) -> Dict[str, Any]:
|
| 185 |
-
return {
|
| 186 |
-
"id": self.id, "name": self.name, "shading_type": self.shading_type.value,
|
| 187 |
-
"shading_coefficient": self.shading_coefficient, "coverage_percentage": self.coverage_percentage,
|
| 188 |
-
"description": self.description, "effective_shading_coefficient": self.effective_shading_coefficient
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
class ShadingSystem:
|
| 192 |
-
def __init__(self):
|
| 193 |
-
self.shading_devices = {}
|
| 194 |
-
self.load_preset_devices()
|
| 195 |
-
|
| 196 |
-
def load_preset_devices(self) -> None:
|
| 197 |
-
self.shading_devices["preset_none"] = ShadingDevice(
|
| 198 |
-
id="preset_none", name="None", shading_type=ShadingType.NONE, shading_coefficient=1.0, description="No shading"
|
| 199 |
-
)
|
| 200 |
-
self.shading_devices["preset_venetian_blinds"] = ShadingDevice(
|
| 201 |
-
id="preset_venetian_blinds", name="Venetian Blinds", shading_type=ShadingType.INTERNAL, shading_coefficient=0.6, description="Standard internal venetian blinds"
|
| 202 |
-
)
|
| 203 |
-
self.shading_devices["preset_overhang"] = ShadingDevice(
|
| 204 |
-
id="preset_overhang", name="Overhang", shading_type=ShadingType.EXTERNAL, shading_coefficient=0.4, description="External overhang"
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
def get_device(self, device_id: str) -> Optional[ShadingDevice]:
|
| 208 |
-
return self.shading_devices.get(device_id)
|
| 209 |
-
|
| 210 |
-
def calculate_effective_shgc(self, base_shgc: float, device_id: str) -> float:
|
| 211 |
-
device = self.get_device(device_id)
|
| 212 |
-
if not device:
|
| 213 |
-
return base_shgc
|
| 214 |
-
return base_shgc * device.effective_shading_coefficient
|
| 215 |
-
|
| 216 |
-
shading_system = ShadingSystem()
|
| 217 |
-
|
| 218 |
# --- Reference Data ---
|
| 219 |
class ReferenceData:
|
| 220 |
def __init__(self):
|
| 221 |
self.data = {
|
| 222 |
"materials": {
|
| 223 |
-
"
|
| 224 |
-
"
|
| 225 |
-
"
|
| 226 |
-
"
|
| 227 |
-
"
|
| 228 |
},
|
| 229 |
"wall_types": {
|
| 230 |
-
"
|
| 231 |
-
"
|
| 232 |
-
"
|
| 233 |
-
"
|
| 234 |
-
"
|
| 235 |
-
"
|
| 236 |
-
"
|
| 237 |
-
"
|
| 238 |
-
"
|
| 239 |
-
"
|
| 240 |
},
|
| 241 |
"roof_types": {
|
| 242 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
},
|
| 244 |
"floor_types": {
|
| 245 |
-
"
|
|
|
|
| 246 |
},
|
| 247 |
"window_types": {
|
| 248 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
},
|
| 250 |
"door_types": {
|
| 251 |
-
"
|
|
|
|
| 252 |
}
|
| 253 |
}
|
| 254 |
|
| 255 |
def get_materials(self) -> List[Dict[str, Any]]:
|
| 256 |
-
return
|
| 257 |
-
|
| 258 |
-
def get_component_data(self, component_type: str) -> Dict[str, Any]:
|
| 259 |
-
return self.data.get(f"{component_type.lower()}_types", {})
|
| 260 |
|
| 261 |
reference_data = ReferenceData()
|
| 262 |
|
|
@@ -264,40 +236,12 @@ reference_data = ReferenceData()
|
|
| 264 |
class ComponentLibrary:
|
| 265 |
def __init__(self):
|
| 266 |
self.components = {}
|
| 267 |
-
self._load_preset_components()
|
| 268 |
-
|
| 269 |
-
def _load_preset_components(self):
|
| 270 |
-
for type_key in ["wall_types", "roof_types", "floor_types", "window_types", "door_types"]:
|
| 271 |
-
for component_data in reference_data.data[type_key].values():
|
| 272 |
-
component_data["orientation"] = Orientation(component_data["orientation"])
|
| 273 |
-
if component_data["component_type"] == ComponentType.WALL.value:
|
| 274 |
-
component = Wall(**component_data)
|
| 275 |
-
elif component_data["component_type"] == ComponentType.ROOF.value:
|
| 276 |
-
component = Roof(**component_data)
|
| 277 |
-
elif component_data["component_type"] == ComponentType.FLOOR.value:
|
| 278 |
-
component = Floor(**component_data)
|
| 279 |
-
elif component_data["component_type"] == ComponentType.WINDOW.value:
|
| 280 |
-
component = Window(**component_data)
|
| 281 |
-
elif component_data["component_type"] == ComponentType.DOOR.value:
|
| 282 |
-
component = Door(**component_data)
|
| 283 |
-
self.components[component.id] = component
|
| 284 |
-
|
| 285 |
-
def get_component(self, component_id: str) -> BuildingComponent:
|
| 286 |
-
return self.components.get(component_id)
|
| 287 |
-
|
| 288 |
-
def get_preset_components_by_type(self, component_type: ComponentType) -> List[BuildingComponent]:
|
| 289 |
-
return [comp for comp in self.components.values() if comp.component_type == component_type and comp.id.startswith("preset_")]
|
| 290 |
|
| 291 |
def add_component(self, component: BuildingComponent):
|
| 292 |
self.components[component.id] = component
|
| 293 |
|
| 294 |
-
def update_component(self, component_id: str, component: BuildingComponent):
|
| 295 |
-
self.components[component_id] = component
|
| 296 |
-
|
| 297 |
def remove_component(self, component_id: str):
|
| 298 |
-
if component_id.startswith("preset_"):
|
| 299 |
-
raise ValueError("Cannot remove preset components")
|
| 300 |
-
if component_id in self.components:
|
| 301 |
del self.components[component_id]
|
| 302 |
|
| 303 |
component_library = ComponentLibrary()
|
|
@@ -307,14 +251,9 @@ class UValueCalculator:
|
|
| 307 |
def __init__(self):
|
| 308 |
self.materials = reference_data.get_materials()
|
| 309 |
|
| 310 |
-
def
|
| 311 |
-
return self.materials
|
| 312 |
-
|
| 313 |
-
def calculate_u_value(self, layers: List[Dict[str, float]]) -> float:
|
| 314 |
-
r_outside = 0.04 # m²·K/W
|
| 315 |
-
r_inside = 0.13 # m²·K/W
|
| 316 |
r_layers = sum(layer["thickness"] / 1000 / layer["conductivity"] for layer in layers)
|
| 317 |
-
r_total =
|
| 318 |
return 1 / r_total if r_total > 0 else 0
|
| 319 |
|
| 320 |
u_value_calculator = UValueCalculator()
|
|
@@ -324,7 +263,6 @@ class ComponentSelectionInterface:
|
|
| 324 |
def __init__(self):
|
| 325 |
self.component_library = component_library
|
| 326 |
self.u_value_calculator = u_value_calculator
|
| 327 |
-
self.shading_system = shading_system
|
| 328 |
self.reference_data = reference_data
|
| 329 |
|
| 330 |
def display_component_selection(self, session_state: Any) -> None:
|
|
@@ -332,7 +270,11 @@ class ComponentSelectionInterface:
|
|
| 332 |
|
| 333 |
if 'components' not in session_state:
|
| 334 |
session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
|
| 337 |
|
| 338 |
with tabs[0]:
|
|
@@ -348,489 +290,451 @@ class ComponentSelectionInterface:
|
|
| 348 |
with tabs[5]:
|
| 349 |
self._display_u_value_calculator_tab(session_state)
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
if st.button("Save Components"):
|
| 354 |
-
self._save_components(session_state)
|
| 355 |
-
with col2:
|
| 356 |
-
uploaded_file = st.file_uploader("Load Components", type=["json"])
|
| 357 |
-
if uploaded_file and st.button("Load Uploaded Components"):
|
| 358 |
-
self._load_components(session_state, uploaded_file)
|
| 359 |
|
| 360 |
def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
|
| 361 |
type_name = component_type.value.lower()
|
| 362 |
st.subheader(f"{type_name.capitalize()} Components")
|
| 363 |
|
| 364 |
-
with st.expander(f"Add {type_name.capitalize()}", expanded=
|
| 365 |
if component_type == ComponentType.WALL:
|
| 366 |
self._display_add_wall_form(session_state)
|
| 367 |
-
|
| 368 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
components = session_state.components.get(type_name + 's', [])
|
| 371 |
-
if components:
|
| 372 |
st.subheader(f"Existing {type_name.capitalize()} Components")
|
| 373 |
self._display_components_table(session_state, component_type, components)
|
| 374 |
|
| 375 |
def _display_add_wall_form(self, session_state: Any) -> None:
|
| 376 |
st.write("Add walls manually or upload a file.")
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
with st.form("add_wall_form", clear_on_submit=True):
|
| 384 |
col1, col2 = st.columns(2)
|
| 385 |
with col1:
|
| 386 |
name = st.text_input("Name", "New Wall")
|
| 387 |
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 388 |
-
orientation = st.selectbox("Orientation", [o.value for o in Orientation], index=0)
|
| 389 |
with col2:
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, max_value=9.0, value=float(session_state.u_value), step=0.01, key=f"u_value_{selected_wall}")
|
| 398 |
-
shading_percentage = st.slider("Shading Percentage (%)", min_value=0.0, max_value=100.0, value=0.0, step=5.0)
|
| 399 |
-
|
| 400 |
submitted = st.form_submit_button("Add Wall")
|
| 401 |
if submitted and not session_state.add_wall_submitted:
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
elif area <= 0:
|
| 405 |
-
st.error("Wall area must be greater than zero!")
|
| 406 |
-
elif u_value <= 0 or u_value >= 9:
|
| 407 |
-
st.error("Wall U-value must be between 0 and 9!")
|
| 408 |
-
else:
|
| 409 |
new_wall = Wall(
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
)
|
| 414 |
self.component_library.add_component(new_wall)
|
| 415 |
session_state.components['walls'].append(new_wall)
|
| 416 |
st.success(f"Added {new_wall.name}")
|
| 417 |
session_state.add_wall_submitted = True
|
| 418 |
st.rerun()
|
| 419 |
-
|
|
|
|
|
|
|
| 420 |
if session_state.add_wall_submitted:
|
| 421 |
session_state.add_wall_submitted = False
|
| 422 |
-
|
| 423 |
-
elif
|
| 424 |
-
st.
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
except UnicodeDecodeError:
|
| 445 |
-
continue
|
| 446 |
-
if df is None:
|
| 447 |
-
raise UnicodeDecodeError("Unable to decode CSV file with available encodings.")
|
| 448 |
-
else: # .xlsx
|
| 449 |
-
df = pd.read_excel(uploaded_file)
|
| 450 |
-
|
| 451 |
-
# Normalize column names
|
| 452 |
-
df.columns = [col.strip() for col in df.columns]
|
| 453 |
-
if "Group" in df.columns and "Wall Group" not in df.columns:
|
| 454 |
-
df.rename(columns={"Group": "Wall Group"}, inplace=True)
|
| 455 |
-
if not all(col in df.columns for col in required_cols):
|
| 456 |
-
st.error(f"File must contain all required columns: {', '.join(required_cols)}")
|
| 457 |
-
else:
|
| 458 |
-
# Overwrite existing walls
|
| 459 |
-
session_state.components['walls'] = []
|
| 460 |
-
for comp_id in list(self.component_library.components.keys()):
|
| 461 |
-
if not comp_id.startswith("preset_") and self.component_library.components[comp_id].component_type == ComponentType.WALL:
|
| 462 |
-
self.component_library.remove_component(comp_id)
|
| 463 |
-
for _, row in df.iterrows():
|
| 464 |
-
try:
|
| 465 |
-
new_wall = Wall(
|
| 466 |
-
id=str(uuid.uuid4()), name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]),
|
| 467 |
-
area=float(row["Area (m²)"]), orientation=Orientation(row["Orientation"]),
|
| 468 |
-
wall_type=str(row["Wall Type"]), wall_group=str(row["Wall Group"]),
|
| 469 |
-
shading_percentage=float(row["Shading (%)"])
|
| 470 |
-
)
|
| 471 |
-
self.component_library.add_component(new_wall)
|
| 472 |
-
session_state.components['walls'].append(new_wall)
|
| 473 |
-
except ValueError as e:
|
| 474 |
-
st.error(f"Error in row: {row['Name']} - {str(e)}")
|
| 475 |
-
st.success("Walls uploaded and overwritten successfully!")
|
| 476 |
-
# Clear the file uploader by incrementing the key
|
| 477 |
-
session_state.uploaded_file_key += 1
|
| 478 |
-
session_state.file_uploaded = True
|
| 479 |
-
except Exception as e:
|
| 480 |
-
st.error(f"Error processing file: {str(e)}")
|
| 481 |
-
|
| 482 |
-
# Provide a downloadable template
|
| 483 |
-
template_df = pd.DataFrame(columns=required_cols)
|
| 484 |
-
buffer = io.BytesIO()
|
| 485 |
-
template_df.to_csv(buffer, index=False, encoding='utf-8')
|
| 486 |
-
st.download_button(
|
| 487 |
-
label="Download Wall Template (CSV)",
|
| 488 |
-
data=buffer.getvalue(),
|
| 489 |
-
file_name="wall_template.csv",
|
| 490 |
-
mime="text/csv"
|
| 491 |
-
)
|
| 492 |
-
|
| 493 |
-
def _display_add_component_form(self, session_state: Any, component_type: ComponentType) -> None:
|
| 494 |
-
type_name = component_type.value.lower()
|
| 495 |
-
preset_components = self.component_library.get_preset_components_by_type(component_type)
|
| 496 |
-
|
| 497 |
-
if f"add_{type_name}_submitted" not in session_state:
|
| 498 |
-
session_state[f"add_{type_name}_submitted"] = False
|
| 499 |
-
|
| 500 |
-
with st.form(f"add_{type_name}_form", clear_on_submit=True):
|
| 501 |
-
col1, col2 = st.columns(2)
|
| 502 |
-
with col1:
|
| 503 |
-
name = st.text_input("Name", f"New {type_name.capitalize()}")
|
| 504 |
-
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 505 |
-
if component_type not in [ComponentType.ROOF, ComponentType.FLOOR]:
|
| 506 |
-
orientation = st.selectbox("Orientation", [o.value for o in Orientation], index=0)
|
| 507 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
orientation = Orientation.HORIZONTAL.value
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
else:
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
try:
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
)
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
st.rerun()
|
| 595 |
except ValueError as e:
|
| 596 |
st.error(f"Error: {str(e)}")
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
|
| 601 |
def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
|
| 602 |
type_name = component_type.value.lower()
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
elif component_type == ComponentType.DOOR:
|
| 616 |
-
row.update({"Door Type": comp.door_type})
|
| 617 |
-
data.append(row)
|
| 618 |
-
|
| 619 |
-
df = pd.DataFrame(data)
|
| 620 |
-
display_cols = [col for col in df.columns if col != "ID"]
|
| 621 |
-
|
| 622 |
-
if component_type == ComponentType.WALL:
|
| 623 |
-
# Custom table rendering for walls with delete button in the row
|
| 624 |
-
st.write("### Walls Table")
|
| 625 |
-
# Display headers
|
| 626 |
-
headers = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Shading (%)", "Delete"]
|
| 627 |
-
cols = st.columns([1, 1, 1, 1, 1, 1, 1, 1])
|
| 628 |
for i, header in enumerate(headers):
|
| 629 |
cols[i].write(f"**{header}**")
|
| 630 |
-
|
| 631 |
-
# Display rows
|
| 632 |
-
for i, row in df.iterrows():
|
| 633 |
-
cols = st.columns([1, 1, 1, 1, 1, 1, 1, 1])
|
| 634 |
-
cols[0].write(row["Name"])
|
| 635 |
-
cols[1].write(row["Area (m²)"])
|
| 636 |
-
cols[2].write(row["U-Value (W/m²·K)"])
|
| 637 |
-
cols[3].write(row["Orientation"])
|
| 638 |
-
cols[4].write(row["Wall Type"])
|
| 639 |
-
cols[5].write(row["Wall Group"])
|
| 640 |
-
cols[6].write(row["Shading (%)"])
|
| 641 |
-
if cols[7].button("Delete", key=f"delete_{row['ID']}"):
|
| 642 |
-
if not row["ID"].startswith("preset_"):
|
| 643 |
-
self.component_library.remove_component(row["ID"])
|
| 644 |
-
session_state.components['walls'] = [c for c in components if c.id != row["ID"]]
|
| 645 |
-
st.success(f"Deleted {row['Name']}")
|
| 646 |
-
st.rerun()
|
| 647 |
-
else:
|
| 648 |
-
st.warning("Cannot delete preset components.")
|
| 649 |
-
|
| 650 |
-
# Download walls table (excluding Delete column)
|
| 651 |
-
csv_df = df[["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Shading (%)"]]
|
| 652 |
-
csv_buffer = io.BytesIO()
|
| 653 |
-
csv_df.to_csv(csv_buffer, index=False, encoding='utf-8')
|
| 654 |
-
st.download_button(label="Download Walls Table", data=csv_buffer.getvalue(), file_name="walls_table.csv", mime="text/csv")
|
| 655 |
-
else:
|
| 656 |
-
st.dataframe(df[display_cols], use_container_width=True)
|
| 657 |
-
for i, row in df.iterrows():
|
| 658 |
-
col1, col2 = st.columns(2)
|
| 659 |
-
with col1:
|
| 660 |
-
if st.button("Edit", key=f"edit_{row['ID']}"):
|
| 661 |
-
session_state[f"edit_{type_name}"] = row["ID"]
|
| 662 |
-
st.rerun()
|
| 663 |
-
with col2:
|
| 664 |
-
if st.button("Delete", key=f"delete_{row['ID']}"):
|
| 665 |
-
if not row["ID"].startswith("preset_"):
|
| 666 |
-
self.component_library.remove_component(row["ID"])
|
| 667 |
-
session_state.components[type_name + 's'] = [c for c in components if c.id != row["ID"]]
|
| 668 |
-
st.success(f"Deleted {row['Name']}")
|
| 669 |
-
st.rerun()
|
| 670 |
-
else:
|
| 671 |
-
st.warning("Cannot delete preset components.")
|
| 672 |
-
|
| 673 |
-
if f"edit_{type_name}" in session_state and component_type != ComponentType.WALL:
|
| 674 |
-
self._display_edit_component_form(session_state, component_type, session_state[f"edit_{type_name}"])
|
| 675 |
-
del session_state[f"edit_{type_name}"]
|
| 676 |
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
with col2:
|
| 696 |
-
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=component.u_value, step=0.01)
|
| 697 |
-
if component_type == ComponentType.ROOF:
|
| 698 |
-
roof_type = st.text_input("Roof Type", value=component.roof_type)
|
| 699 |
-
roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"],
|
| 700 |
-
index=["A", "B", "C", "D", "E", "F", "G"].index(component.roof_group))
|
| 701 |
elif component_type == ComponentType.FLOOR:
|
| 702 |
-
|
|
|
|
|
|
|
| 703 |
elif component_type == ComponentType.WINDOW:
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
low_e_coating = st.checkbox("Low-E Coating", value=component.low_e_coating)
|
| 711 |
-
shading_options = {d.name: d.id for d in self.shading_system.shading_devices.values()}
|
| 712 |
-
shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()),
|
| 713 |
-
index=list(shading_options.keys()).index(self.shading_system.get_device(component.shading_device_id).name))
|
| 714 |
elif component_type == ComponentType.DOOR:
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
st.
|
| 721 |
-
elif area <= 0:
|
| 722 |
-
st.error(f"{type_name.capitalize()} area must be greater than zero!")
|
| 723 |
-
elif u_value <= 0:
|
| 724 |
-
st.error(f"{type_name.capitalize()} U-value must be greater than zero!")
|
| 725 |
-
else:
|
| 726 |
-
component.name = name
|
| 727 |
-
component.area = area
|
| 728 |
-
component.u_value = u_value
|
| 729 |
-
component.orientation = Orientation(orientation)
|
| 730 |
-
if component_type == ComponentType.ROOF:
|
| 731 |
-
component.roof_type = roof_type
|
| 732 |
-
component.roof_group = roof_group
|
| 733 |
-
elif component_type == ComponentType.FLOOR:
|
| 734 |
-
component.floor_type = floor_type
|
| 735 |
-
elif component_type == ComponentType.WINDOW:
|
| 736 |
-
component.shgc = shgc
|
| 737 |
-
component.vt = vt
|
| 738 |
-
component.window_type = window_type
|
| 739 |
-
component.glazing_layers = glazing_layers
|
| 740 |
-
component.gas_fill = gas_fill
|
| 741 |
-
component.low_e_coating = low_e_coating
|
| 742 |
-
component.shading_device_id = shading_options[shading_device]
|
| 743 |
-
elif component_type == ComponentType.DOOR:
|
| 744 |
-
component.door_type = door_type
|
| 745 |
-
self.component_library.update_component(component_id, component)
|
| 746 |
-
st.success(f"Updated {name}")
|
| 747 |
st.rerun()
|
| 748 |
|
| 749 |
def _display_u_value_calculator_tab(self, session_state: Any) -> None:
|
| 750 |
-
st.subheader("U-Value Calculator")
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
session_state.u_value_applied = False
|
| 757 |
-
|
| 758 |
-
components = []
|
| 759 |
-
for type_name in ['walls', 'roofs', 'floors', 'windows', 'doors']:
|
| 760 |
-
components.extend(session_state.components[type_name])
|
| 761 |
-
|
| 762 |
-
if not components:
|
| 763 |
-
st.warning("No components available. Add components first.")
|
| 764 |
-
return
|
| 765 |
-
|
| 766 |
-
selected_component = st.selectbox("Select Component", options=[c.name for c in components])
|
| 767 |
-
component = next((c for c in components if c.name == selected_component), None)
|
| 768 |
-
if not component:
|
| 769 |
-
st.error("Component not found.")
|
| 770 |
-
return
|
| 771 |
-
|
| 772 |
-
if session_state.material_layers:
|
| 773 |
-
st.write("Current Material Layers (outside to inside):")
|
| 774 |
layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
|
| 775 |
-
|
| 776 |
-
for i, l in enumerate(session_state.
|
| 777 |
-
st.dataframe(pd.DataFrame(layer_data)
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
st.metric("U-Value", f"{u_value:.3f} W/m²·K")
|
| 786 |
-
with col3:
|
| 787 |
-
if st.button("Use This U-Value") and not session_state.u_value_applied:
|
| 788 |
-
component.u_value = u_value
|
| 789 |
-
self.component_library.update_component(component.id, component)
|
| 790 |
-
session_state.material_layers = []
|
| 791 |
-
st.success(f"U-Value set to {u_value:.3f} W/m²·K for {component.name}")
|
| 792 |
-
session_state.u_value_applied = True
|
| 793 |
-
st.rerun()
|
| 794 |
-
|
| 795 |
-
if session_state.u_value_applied:
|
| 796 |
-
session_state.u_value_applied = False
|
| 797 |
-
|
| 798 |
-
with st.form("material_layer_form"):
|
| 799 |
col1, col2 = st.columns(2)
|
| 800 |
with col1:
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
material_options = {m["name"]: m for m in preset_materials}
|
| 805 |
-
material_name = st.selectbox("Material", options=list(material_options.keys()))
|
| 806 |
-
conductivity = material_options[material_name]["conductivity"]
|
| 807 |
-
st.number_input("Conductivity (W/m·K)", value=conductivity, disabled=True)
|
| 808 |
-
else:
|
| 809 |
-
material_name = st.text_input("Material Name", "Custom Material")
|
| 810 |
-
conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=1.0, step=0.01)
|
| 811 |
-
|
| 812 |
with col2:
|
| 813 |
thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
|
| 814 |
-
if conductivity > 0:
|
| 815 |
-
r_value = thickness / 1000 / conductivity
|
| 816 |
-
st.metric("Layer R-Value", f"{r_value:.3f} m²·K/W")
|
| 817 |
-
|
| 818 |
submitted = st.form_submit_button("Add Layer")
|
| 819 |
if submitted:
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
session_state.
|
| 828 |
-
st.success(f"Added layer: {material_name}")
|
| 829 |
st.rerun()
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
session_state.material_layers.pop()
|
| 834 |
st.rerun()
|
| 835 |
|
| 836 |
def _save_components(self, session_state: Any) -> None:
|
|
@@ -839,7 +743,9 @@ class ComponentSelectionInterface:
|
|
| 839 |
"roofs": [c.to_dict() for c in session_state.components["roofs"]],
|
| 840 |
"floors": [c.to_dict() for c in session_state.components["floors"]],
|
| 841 |
"windows": [c.to_dict() for c in session_state.components["windows"]],
|
| 842 |
-
"doors": [c.to_dict() for c in session_state.components["doors"]]
|
|
|
|
|
|
|
| 843 |
}
|
| 844 |
file_path = "components_export.json"
|
| 845 |
with open(file_path, 'w') as f:
|
|
@@ -848,27 +754,6 @@ class ComponentSelectionInterface:
|
|
| 848 |
st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
|
| 849 |
st.success("Components saved successfully.")
|
| 850 |
|
| 851 |
-
def _load_components(self, session_state: Any, uploaded_file: Any) -> None:
|
| 852 |
-
data = json.load(uploaded_file)
|
| 853 |
-
session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
|
| 854 |
-
for type_name in data:
|
| 855 |
-
for comp_dict in data[type_name]:
|
| 856 |
-
comp_dict["orientation"] = Orientation(comp_dict["orientation"])
|
| 857 |
-
if type_name == "walls":
|
| 858 |
-
comp = Wall(**comp_dict)
|
| 859 |
-
elif type_name == "roofs":
|
| 860 |
-
comp = Roof(**comp_dict)
|
| 861 |
-
elif type_name == "floors":
|
| 862 |
-
comp = Floor(**comp_dict)
|
| 863 |
-
elif type_name == "windows":
|
| 864 |
-
comp = Window(**comp_dict)
|
| 865 |
-
elif type_name == "doors":
|
| 866 |
-
comp = Door(**comp_dict)
|
| 867 |
-
self.component_library.add_component(comp)
|
| 868 |
-
session_state.components[type_name].append(comp)
|
| 869 |
-
st.success("Loaded components successfully.")
|
| 870 |
-
st.rerun()
|
| 871 |
-
|
| 872 |
# --- Main Execution ---
|
| 873 |
if __name__ == "__main__":
|
| 874 |
interface = ComponentSelectionInterface()
|
|
|
|
| 34 |
WINDOW = "Window"
|
| 35 |
DOOR = "Door"
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
# --- Data Models ---
|
| 38 |
@dataclass
|
| 39 |
class MaterialLayer:
|
|
|
|
| 49 |
u_value: float = 0.0 # W/(m²·K)
|
| 50 |
area: float = 0.0 # m²
|
| 51 |
orientation: Orientation = Orientation.NOT_APPLICABLE
|
|
|
|
| 52 |
|
| 53 |
def __post_init__(self):
|
| 54 |
+
if self.area <= 0:
|
| 55 |
+
raise ValueError("Area must be greater than zero")
|
| 56 |
+
if self.u_value <= 0:
|
| 57 |
+
raise ValueError("U-value must be greater than zero")
|
| 58 |
|
| 59 |
def to_dict(self) -> dict:
|
| 60 |
return {
|
| 61 |
+
"id": self.id, "name": self.name, "component_type": self.component_type.value,
|
| 62 |
+
"u_value": self.u_value, "area": self.area, "orientation": self.orientation.value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
@dataclass
|
| 66 |
class Wall(BuildingComponent):
|
| 67 |
wall_type: str = "Brick"
|
| 68 |
wall_group: str = "Wall"
|
| 69 |
+
absorptivity: float = 0.6
|
| 70 |
+
shading_coefficient: float = 1.0
|
| 71 |
+
infiltration_rate_cfm: float = 0.0
|
| 72 |
|
| 73 |
def __post_init__(self):
|
| 74 |
super().__post_init__()
|
| 75 |
self.component_type = ComponentType.WALL
|
| 76 |
+
if not 0 <= self.absorptivity <= 1:
|
| 77 |
+
raise ValueError("Absorptivity must be between 0 and 1")
|
| 78 |
+
if not 0 <= self.shading_coefficient <= 1:
|
| 79 |
+
raise ValueError("Shading coefficient must be between 0 and 1")
|
| 80 |
+
if self.infiltration_rate_cfm < 0:
|
| 81 |
+
raise ValueError("Infiltration rate cannot be negative")
|
| 82 |
|
| 83 |
def to_dict(self) -> dict:
|
| 84 |
base_dict = super().to_dict()
|
| 85 |
+
base_dict.update({
|
| 86 |
+
"wall_type": self.wall_type, "wall_group": self.wall_group, "absorptivity": self.absorptivity,
|
| 87 |
+
"shading_coefficient": self.shading_coefficient, "infiltration_rate_cfm": self.infiltration_rate_cfm
|
| 88 |
+
})
|
| 89 |
return base_dict
|
| 90 |
|
| 91 |
@dataclass
|
| 92 |
class Roof(BuildingComponent):
|
| 93 |
roof_type: str = "Concrete"
|
| 94 |
roof_group: str = "C"
|
| 95 |
+
slope: str = "Flat"
|
| 96 |
+
absorptivity: float = 0.6
|
| 97 |
|
| 98 |
def __post_init__(self):
|
| 99 |
super().__post_init__()
|
| 100 |
self.component_type = ComponentType.ROOF
|
| 101 |
+
if not self.orientation == Orientation.HORIZONTAL:
|
| 102 |
+
self.orientation = Orientation.HORIZONTAL
|
| 103 |
+
if not 0 <= self.absorptivity <= 1:
|
| 104 |
+
raise ValueError("Absorptivity must be between 0 and 1")
|
| 105 |
|
| 106 |
def to_dict(self) -> dict:
|
| 107 |
base_dict = super().to_dict()
|
| 108 |
+
base_dict.update({
|
| 109 |
+
"roof_type": self.roof_type, "roof_group": self.roof_group, "slope": self.slope, "absorptivity": self.absorptivity
|
| 110 |
+
})
|
| 111 |
return base_dict
|
| 112 |
|
| 113 |
@dataclass
|
| 114 |
class Floor(BuildingComponent):
|
| 115 |
floor_type: str = "Concrete"
|
| 116 |
+
ground_contact: bool = True
|
| 117 |
+
ground_temperature_c: float = 25.0
|
| 118 |
|
| 119 |
def __post_init__(self):
|
| 120 |
super().__post_init__()
|
| 121 |
self.component_type = ComponentType.FLOOR
|
| 122 |
+
self.orientation = Orientation.NOT_APPLICABLE
|
| 123 |
|
| 124 |
def to_dict(self) -> dict:
|
| 125 |
base_dict = super().to_dict()
|
| 126 |
+
base_dict.update({
|
| 127 |
+
"floor_type": self.floor_type, "ground_contact": self.ground_contact, "ground_temperature_c": self.ground_temperature_c
|
| 128 |
+
})
|
| 129 |
return base_dict
|
| 130 |
|
| 131 |
@dataclass
|
| 132 |
class Window(BuildingComponent):
|
| 133 |
shgc: float = 0.7
|
| 134 |
+
shading_device: str = "None"
|
| 135 |
+
shading_coefficient: float = 1.0
|
| 136 |
+
frame_type: str = "Aluminum"
|
| 137 |
+
frame_percentage: float = 20.0
|
| 138 |
+
infiltration_rate_cfm: float = 0.0
|
|
|
|
| 139 |
|
| 140 |
def __post_init__(self):
|
| 141 |
super().__post_init__()
|
| 142 |
self.component_type = ComponentType.WINDOW
|
| 143 |
if not 0 <= self.shgc <= 1:
|
| 144 |
raise ValueError("SHGC must be between 0 and 1")
|
| 145 |
+
if not 0 <= self.shading_coefficient <= 1:
|
| 146 |
+
raise ValueError("Shading coefficient must be between 0 and 1")
|
| 147 |
+
if not 0 <= self.frame_percentage <= 30:
|
| 148 |
+
raise ValueError("Frame percentage must be between 0 and 30")
|
| 149 |
+
if self.infiltration_rate_cfm < 0:
|
| 150 |
+
raise ValueError("Infiltration rate cannot be negative")
|
| 151 |
|
| 152 |
def to_dict(self) -> dict:
|
| 153 |
base_dict = super().to_dict()
|
| 154 |
base_dict.update({
|
| 155 |
+
"shgc": self.shgc, "shading_device": self.shading_device, "shading_coefficient": self.shading_coefficient,
|
| 156 |
+
"frame_type": self.frame_type, "frame_percentage": self.frame_percentage, "infiltration_rate_cfm": self.infiltration_rate_cfm
|
|
|
|
| 157 |
})
|
| 158 |
return base_dict
|
| 159 |
|
| 160 |
@dataclass
|
| 161 |
class Door(BuildingComponent):
|
| 162 |
door_type: str = "Solid Wood"
|
| 163 |
+
infiltration_rate_cfm: float = 0.0
|
| 164 |
|
| 165 |
def __post_init__(self):
|
| 166 |
super().__post_init__()
|
| 167 |
self.component_type = ComponentType.DOOR
|
| 168 |
+
if self.infiltration_rate_cfm < 0:
|
| 169 |
+
raise ValueError("Infiltration rate cannot be negative")
|
| 170 |
|
| 171 |
def to_dict(self) -> dict:
|
| 172 |
base_dict = super().to_dict()
|
| 173 |
+
base_dict.update({"door_type": self.door_type, "infiltration_rate_cfm": self.infiltration_rate_cfm})
|
| 174 |
return base_dict
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
# --- Reference Data ---
|
| 177 |
class ReferenceData:
|
| 178 |
def __init__(self):
|
| 179 |
self.data = {
|
| 180 |
"materials": {
|
| 181 |
+
"Concrete": {"conductivity": 1.4},
|
| 182 |
+
"Insulation": {"conductivity": 0.04},
|
| 183 |
+
"Brick": {"conductivity": 0.8},
|
| 184 |
+
"Glass": {"conductivity": 1.0},
|
| 185 |
+
"Wood": {"conductivity": 0.15}
|
| 186 |
},
|
| 187 |
"wall_types": {
|
| 188 |
+
"Brick Wall": {"u_value": 2.0, "absorptivity": 0.6},
|
| 189 |
+
"Insulated Brick": {"u_value": 0.5, "absorptivity": 0.6},
|
| 190 |
+
"Concrete Block": {"u_value": 1.8, "absorptivity": 0.6},
|
| 191 |
+
"Insulated Concrete": {"u_value": 0.4, "absorptivity": 0.6},
|
| 192 |
+
"Timber Frame": {"u_value": 0.3, "absorptivity": 0.6},
|
| 193 |
+
"Cavity Brick": {"u_value": 0.6, "absorptivity": 0.6},
|
| 194 |
+
"Lightweight Panel": {"u_value": 1.0, "absorptivity": 0.6},
|
| 195 |
+
"Reinforced Concrete": {"u_value": 1.5, "absorptivity": 0.6},
|
| 196 |
+
"SIP": {"u_value": 0.25, "absorptivity": 0.6},
|
| 197 |
+
"Custom": {"u_value": 0.5, "absorptivity": 0.6}
|
| 198 |
},
|
| 199 |
"roof_types": {
|
| 200 |
+
"Concrete Roof": {"u_value": 0.3, "absorptivity": 0.6, "group": "C"},
|
| 201 |
+
"Metal Roof": {"u_value": 1.0, "absorptivity": 0.75, "group": "B"}
|
| 202 |
+
},
|
| 203 |
+
"roof_ventilation_methods": {
|
| 204 |
+
"No Ventilation": 0.0,
|
| 205 |
+
"Natural Low": 0.1,
|
| 206 |
+
"Natural High": 0.5,
|
| 207 |
+
"Mechanical": 1.0
|
| 208 |
},
|
| 209 |
"floor_types": {
|
| 210 |
+
"Concrete Slab": {"u_value": 0.4, "ground_contact": True},
|
| 211 |
+
"Wood Floor": {"u_value": 0.8, "ground_contact": False}
|
| 212 |
},
|
| 213 |
"window_types": {
|
| 214 |
+
"Double Glazed": {"u_value": 2.8, "shgc": 0.7, "frame_type": "Aluminum"},
|
| 215 |
+
"Single Glazed": {"u_value": 5.0, "shgc": 0.9, "frame_type": "Wood"}
|
| 216 |
+
},
|
| 217 |
+
"shading_devices": {
|
| 218 |
+
"None": 1.0,
|
| 219 |
+
"Venetian Blinds": 0.6,
|
| 220 |
+
"Overhang": 0.4,
|
| 221 |
+
"Roller Shades": 0.5,
|
| 222 |
+
"Drapes": 0.7
|
| 223 |
},
|
| 224 |
"door_types": {
|
| 225 |
+
"Solid Wood": {"u_value": 2.0},
|
| 226 |
+
"Glass Door": {"u_value": 3.5}
|
| 227 |
}
|
| 228 |
}
|
| 229 |
|
| 230 |
def get_materials(self) -> List[Dict[str, Any]]:
|
| 231 |
+
return [{"name": k, "conductivity": v["conductivity"]} for k, v in self.data["materials"].items()]
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
reference_data = ReferenceData()
|
| 234 |
|
|
|
|
| 236 |
class ComponentLibrary:
|
| 237 |
def __init__(self):
|
| 238 |
self.components = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
def add_component(self, component: BuildingComponent):
|
| 241 |
self.components[component.id] = component
|
| 242 |
|
|
|
|
|
|
|
|
|
|
| 243 |
def remove_component(self, component_id: str):
|
| 244 |
+
if not component_id.startswith("preset_") and component_id in self.components:
|
|
|
|
|
|
|
| 245 |
del self.components[component_id]
|
| 246 |
|
| 247 |
component_library = ComponentLibrary()
|
|
|
|
| 251 |
def __init__(self):
|
| 252 |
self.materials = reference_data.get_materials()
|
| 253 |
|
| 254 |
+
def calculate_u_value(self, layers: List[Dict[str, float]], outside_resistance: float, inside_resistance: float) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
r_layers = sum(layer["thickness"] / 1000 / layer["conductivity"] for layer in layers)
|
| 256 |
+
r_total = outside_resistance + r_layers + inside_resistance
|
| 257 |
return 1 / r_total if r_total > 0 else 0
|
| 258 |
|
| 259 |
u_value_calculator = UValueCalculator()
|
|
|
|
| 263 |
def __init__(self):
|
| 264 |
self.component_library = component_library
|
| 265 |
self.u_value_calculator = u_value_calculator
|
|
|
|
| 266 |
self.reference_data = reference_data
|
| 267 |
|
| 268 |
def display_component_selection(self, session_state: Any) -> None:
|
|
|
|
| 270 |
|
| 271 |
if 'components' not in session_state:
|
| 272 |
session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []}
|
| 273 |
+
if 'roof_air_volume_m3' not in session_state:
|
| 274 |
+
session_state.roof_air_volume_m3 = 0.0
|
| 275 |
+
if 'roof_ventilation_ach' not in session_state:
|
| 276 |
+
session_state.roof_ventilation_ach = 0.0
|
| 277 |
+
|
| 278 |
tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"])
|
| 279 |
|
| 280 |
with tabs[0]:
|
|
|
|
| 290 |
with tabs[5]:
|
| 291 |
self._display_u_value_calculator_tab(session_state)
|
| 292 |
|
| 293 |
+
if st.button("Save Components"):
|
| 294 |
+
self._save_components(session_state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None:
|
| 297 |
type_name = component_type.value.lower()
|
| 298 |
st.subheader(f"{type_name.capitalize()} Components")
|
| 299 |
|
| 300 |
+
with st.expander(f"Add {type_name.capitalize()}", expanded=True):
|
| 301 |
if component_type == ComponentType.WALL:
|
| 302 |
self._display_add_wall_form(session_state)
|
| 303 |
+
elif component_type == ComponentType.ROOF:
|
| 304 |
+
self._display_add_roof_form(session_state)
|
| 305 |
+
elif component_type == ComponentType.FLOOR:
|
| 306 |
+
self._display_add_floor_form(session_state)
|
| 307 |
+
elif component_type == ComponentType.WINDOW:
|
| 308 |
+
self._display_add_window_form(session_state)
|
| 309 |
+
elif component_type == ComponentType.DOOR:
|
| 310 |
+
self._display_add_door_form(session_state)
|
| 311 |
|
| 312 |
components = session_state.components.get(type_name + 's', [])
|
| 313 |
+
if components or component_type == ComponentType.ROOF:
|
| 314 |
st.subheader(f"Existing {type_name.capitalize()} Components")
|
| 315 |
self._display_components_table(session_state, component_type, components)
|
| 316 |
|
| 317 |
def _display_add_wall_form(self, session_state: Any) -> None:
|
| 318 |
st.write("Add walls manually or upload a file.")
|
| 319 |
+
method = st.radio("Add Wall Method", ["Manual Entry", "File Upload"])
|
| 320 |
+
if "add_wall_submitted" not in session_state:
|
| 321 |
+
session_state.add_wall_submitted = False
|
| 322 |
+
|
| 323 |
+
if method == "Manual Entry":
|
|
|
|
| 324 |
with st.form("add_wall_form", clear_on_submit=True):
|
| 325 |
col1, col2 = st.columns(2)
|
| 326 |
with col1:
|
| 327 |
name = st.text_input("Name", "New Wall")
|
| 328 |
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 329 |
+
orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
|
| 330 |
with col2:
|
| 331 |
+
wall_options = self.reference_data.data["wall_types"]
|
| 332 |
+
selected_wall = st.selectbox("Wall Type", options=list(wall_options.keys()))
|
| 333 |
+
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(wall_options[selected_wall]["u_value"]), step=0.01)
|
| 334 |
+
absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2)
|
| 335 |
+
shading_coefficient = st.number_input("Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05)
|
| 336 |
+
infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
|
| 337 |
+
|
|
|
|
|
|
|
|
|
|
| 338 |
submitted = st.form_submit_button("Add Wall")
|
| 339 |
if submitted and not session_state.add_wall_submitted:
|
| 340 |
+
try:
|
| 341 |
+
absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
new_wall = Wall(
|
| 343 |
+
name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
|
| 344 |
+
wall_type=selected_wall, wall_group="Wall", absorptivity=absorptivity_value,
|
| 345 |
+
shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate
|
| 346 |
)
|
| 347 |
self.component_library.add_component(new_wall)
|
| 348 |
session_state.components['walls'].append(new_wall)
|
| 349 |
st.success(f"Added {new_wall.name}")
|
| 350 |
session_state.add_wall_submitted = True
|
| 351 |
st.rerun()
|
| 352 |
+
except ValueError as e:
|
| 353 |
+
st.error(f"Error: {str(e)}")
|
| 354 |
+
|
| 355 |
if session_state.add_wall_submitted:
|
| 356 |
session_state.add_wall_submitted = False
|
| 357 |
+
|
| 358 |
+
elif method == "File Upload":
|
| 359 |
+
uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key="wall_upload")
|
| 360 |
+
required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)"]
|
| 361 |
+
st.download_button(label="Download Wall Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="wall_template.csv", mime="text/csv")
|
| 362 |
+
if uploaded_file:
|
| 363 |
+
df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
|
| 364 |
+
if all(col in df.columns for col in required_cols):
|
| 365 |
+
for _, row in df.iterrows():
|
| 366 |
+
try:
|
| 367 |
+
new_wall = Wall(
|
| 368 |
+
name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
|
| 369 |
+
orientation=Orientation(row["Orientation"]), wall_type=str(row["Wall Type"]),
|
| 370 |
+
wall_group=str(row["Wall Group"]), absorptivity=float(row["Absorptivity"]),
|
| 371 |
+
shading_coefficient=float(row["Shading Coefficient"]), infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
|
| 372 |
+
)
|
| 373 |
+
self.component_library.add_component(new_wall)
|
| 374 |
+
session_state.components['walls'].append(new_wall)
|
| 375 |
+
except ValueError as e:
|
| 376 |
+
st.error(f"Error in row {row['Name']}: {str(e)}")
|
| 377 |
+
st.success("Walls uploaded successfully!")
|
| 378 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
else:
|
| 380 |
+
st.error(f"File must contain: {', '.join(required_cols)}")
|
| 381 |
+
|
| 382 |
+
def _display_add_roof_form(self, session_state: Any) -> None:
|
| 383 |
+
st.write("Add roofs manually or upload a file.")
|
| 384 |
+
method = st.radio("Add Roof Method", ["Manual Entry", "File Upload"])
|
| 385 |
+
if "add_roof_submitted" not in session_state:
|
| 386 |
+
session_state.add_roof_submitted = False
|
| 387 |
+
|
| 388 |
+
# Shared Roof-Level Fields
|
| 389 |
+
st.subheader("Roof System Ventilation")
|
| 390 |
+
air_volume = st.number_input("Air Volume (m³)", min_value=0.0, value=session_state.roof_air_volume_m3, step=1.0, help="Total volume between roof and ceiling")
|
| 391 |
+
vent_options = {f"{k} (ACH={v})": v for k, v in self.reference_data.data["roof_ventilation_methods"].items()}
|
| 392 |
+
vent_options["Custom"] = None
|
| 393 |
+
ventilation_method = st.selectbox("Ventilation Method", options=list(vent_options.keys()), index=0, help="Applies to entire roof system")
|
| 394 |
+
ventilation_ach = st.number_input("Custom Ventilation Rate (ACH)", min_value=0.0, max_value=10.0, value=0.0, step=0.1) if ventilation_method == "Custom" else vent_options[ventilation_method]
|
| 395 |
+
session_state.roof_air_volume_m3 = air_volume
|
| 396 |
+
session_state.roof_ventilation_ach = ventilation_ach
|
| 397 |
+
|
| 398 |
+
if method == "Manual Entry":
|
| 399 |
+
with st.form("add_roof_form", clear_on_submit=True):
|
| 400 |
+
col1, col2 = st.columns(2)
|
| 401 |
+
with col1:
|
| 402 |
+
name = st.text_input("Name", "New Roof")
|
| 403 |
+
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 404 |
orientation = Orientation.HORIZONTAL.value
|
| 405 |
+
with col2:
|
| 406 |
+
roof_options = self.reference_data.data["roof_types"]
|
| 407 |
+
selected_roof = st.selectbox("Roof Type", options=list(roof_options.keys()))
|
| 408 |
+
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(roof_options[selected_roof]["u_value"]), step=0.01)
|
| 409 |
+
roof_group = st.selectbox("Roof Group", ["A", "B", "C", "D", "E", "F", "G"], index=2)
|
| 410 |
+
slope = st.selectbox("Slope", ["Flat", "Pitched"], index=0)
|
| 411 |
+
absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2)
|
| 412 |
+
|
| 413 |
+
submitted = st.form_submit_button("Add Roof")
|
| 414 |
+
if submitted and not session_state.add_roof_submitted:
|
| 415 |
+
try:
|
| 416 |
+
absorptivity_value = float(absorptivity.split("(")[1].strip(")"))
|
| 417 |
+
new_roof = Roof(
|
| 418 |
+
name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
|
| 419 |
+
roof_type=selected_roof, roof_group=roof_group, slope=slope, absorptivity=absorptivity_value
|
| 420 |
+
)
|
| 421 |
+
self.component_library.add_component(new_roof)
|
| 422 |
+
session_state.components['roofs'].append(new_roof)
|
| 423 |
+
st.success(f"Added {new_roof.name}")
|
| 424 |
+
session_state.add_roof_submitted = True
|
| 425 |
+
st.rerun()
|
| 426 |
+
except ValueError as e:
|
| 427 |
+
st.error(f"Error: {str(e)}")
|
| 428 |
+
|
| 429 |
+
if session_state.add_roof_submitted:
|
| 430 |
+
session_state.add_roof_submitted = False
|
| 431 |
+
|
| 432 |
+
elif method == "File Upload":
|
| 433 |
+
uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload")
|
| 434 |
+
required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity"]
|
| 435 |
+
st.download_button(label="Download Roof Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="roof_template.csv", mime="text/csv")
|
| 436 |
+
if uploaded_file:
|
| 437 |
+
df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
|
| 438 |
+
if all(col in df.columns for col in required_cols):
|
| 439 |
+
for _, row in df.iterrows():
|
| 440 |
+
try:
|
| 441 |
+
new_roof = Roof(
|
| 442 |
+
name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
|
| 443 |
+
orientation=Orientation(row["Orientation"]), roof_type=str(row["Roof Type"]),
|
| 444 |
+
roof_group=str(row["Roof Group"]), slope=str(row["Slope"]), absorptivity=float(row["Absorptivity"])
|
| 445 |
+
)
|
| 446 |
+
self.component_library.add_component(new_roof)
|
| 447 |
+
session_state.components['roofs'].append(new_roof)
|
| 448 |
+
except ValueError as e:
|
| 449 |
+
st.error(f"Error in row {row['Name']}: {str(e)}")
|
| 450 |
+
st.success("Roofs uploaded successfully!")
|
| 451 |
+
st.rerun()
|
| 452 |
else:
|
| 453 |
+
st.error(f"File must contain: {', '.join(required_cols)}")
|
| 454 |
+
|
| 455 |
+
def _display_add_floor_form(self, session_state: Any) -> None:
|
| 456 |
+
st.write("Add floors manually or upload a file.")
|
| 457 |
+
method = st.radio("Add Floor Method", ["Manual Entry", "File Upload"])
|
| 458 |
+
if "add_floor_submitted" not in session_state:
|
| 459 |
+
session_state.add_floor_submitted = False
|
| 460 |
+
|
| 461 |
+
if method == "Manual Entry":
|
| 462 |
+
with st.form("add_floor_form", clear_on_submit=True):
|
| 463 |
+
col1, col2 = st.columns(2)
|
| 464 |
+
with col1:
|
| 465 |
+
name = st.text_input("Name", "New Floor")
|
| 466 |
+
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 467 |
+
with col2:
|
| 468 |
+
floor_options = self.reference_data.data["floor_types"]
|
| 469 |
+
selected_floor = st.selectbox("Floor Type", options=list(floor_options.keys()))
|
| 470 |
+
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(floor_options[selected_floor]["u_value"]), step=0.01)
|
| 471 |
+
ground_contact = st.selectbox("Ground Contact", ["Yes", "No"], index=0 if floor_options[selected_floor]["ground_contact"] else 1)
|
| 472 |
+
ground_temp = st.number_input("Ground Temperature (°C)", value=25.0, step=0.1) if ground_contact == "Yes" else 25.0
|
| 473 |
+
|
| 474 |
+
submitted = st.form_submit_button("Add Floor")
|
| 475 |
+
if submitted and not session_state.add_floor_submitted:
|
| 476 |
+
try:
|
| 477 |
+
new_floor = Floor(
|
| 478 |
+
name=name, u_value=u_value, area=area, floor_type=selected_floor,
|
| 479 |
+
ground_contact=(ground_contact == "Yes"), ground_temperature_c=ground_temp
|
| 480 |
+
)
|
| 481 |
+
self.component_library.add_component(new_floor)
|
| 482 |
+
session_state.components['floors'].append(new_floor)
|
| 483 |
+
st.success(f"Added {new_floor.name}")
|
| 484 |
+
session_state.add_floor_submitted = True
|
| 485 |
+
st.rerun()
|
| 486 |
+
except ValueError as e:
|
| 487 |
+
st.error(f"Error: {str(e)}")
|
| 488 |
+
|
| 489 |
+
if session_state.add_floor_submitted:
|
| 490 |
+
session_state.add_floor_submitted = False
|
| 491 |
+
|
| 492 |
+
elif method == "File Upload":
|
| 493 |
+
uploaded_file = st.file_uploader("Upload Floors File", type=["csv", "xlsx"], key="floor_upload")
|
| 494 |
+
required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)"]
|
| 495 |
+
st.download_button(label="Download Floor Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="floor_template.csv", mime="text/csv")
|
| 496 |
+
if uploaded_file:
|
| 497 |
+
df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
|
| 498 |
+
if all(col in df.columns for col in required_cols):
|
| 499 |
+
for _, row in df.iterrows():
|
| 500 |
+
try:
|
| 501 |
+
new_floor = Floor(
|
| 502 |
+
name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
|
| 503 |
+
floor_type=str(row["Floor Type"]), ground_contact=(str(row["Ground Contact"]).lower() == "yes"),
|
| 504 |
+
ground_temperature_c=float(row["Ground Temperature (°C)"])
|
| 505 |
+
)
|
| 506 |
+
self.component_library.add_component(new_floor)
|
| 507 |
+
session_state.components['floors'].append(new_floor)
|
| 508 |
+
except ValueError as e:
|
| 509 |
+
st.error(f"Error in row {row['Name']}: {str(e)}")
|
| 510 |
+
st.success("Floors uploaded successfully!")
|
| 511 |
+
st.rerun()
|
| 512 |
else:
|
| 513 |
+
st.error(f"File must contain: {', '.join(required_cols)}")
|
| 514 |
+
|
| 515 |
+
def _display_add_window_form(self, session_state: Any) -> None:
|
| 516 |
+
st.write("Add windows manually or upload a file.")
|
| 517 |
+
method = st.radio("Add Window Method", ["Manual Entry", "File Upload"])
|
| 518 |
+
if "add_window_submitted" not in session_state:
|
| 519 |
+
session_state.add_window_submitted = False
|
| 520 |
+
|
| 521 |
+
if method == "Manual Entry":
|
| 522 |
+
with st.form("add_window_form", clear_on_submit=True):
|
| 523 |
+
col1, col2 = st.columns(2)
|
| 524 |
+
with col1:
|
| 525 |
+
name = st.text_input("Name", "New Window")
|
| 526 |
+
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 527 |
+
orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
|
| 528 |
+
shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01)
|
| 529 |
+
with col2:
|
| 530 |
+
window_options = self.reference_data.data["window_types"]
|
| 531 |
+
selected_window = st.selectbox("Window Type", options=list(window_options.keys()))
|
| 532 |
+
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(window_options[selected_window]["u_value"]), step=0.01)
|
| 533 |
+
shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()}
|
| 534 |
+
shading_options["Custom"] = "Custom"
|
| 535 |
+
shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0)
|
| 536 |
+
shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]]
|
| 537 |
+
frame_type = st.selectbox("Frame Type", ["Aluminum", "Wood", "Vinyl"], index=0)
|
| 538 |
+
frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=20.0)
|
| 539 |
+
infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
|
| 540 |
+
|
| 541 |
+
submitted = st.form_submit_button("Add Window")
|
| 542 |
+
if submitted and not session_state.add_window_submitted:
|
| 543 |
try:
|
| 544 |
+
new_window = Window(
|
| 545 |
+
name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
|
| 546 |
+
shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient,
|
| 547 |
+
frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate
|
| 548 |
+
)
|
| 549 |
+
self.component_library.add_component(new_window)
|
| 550 |
+
session_state.components['windows'].append(new_window)
|
| 551 |
+
st.success(f"Added {new_window.name}")
|
| 552 |
+
session_state.add_window_submitted = True
|
| 553 |
+
st.rerun()
|
| 554 |
+
except ValueError as e:
|
| 555 |
+
st.error(f"Error: {str(e)}")
|
| 556 |
+
|
| 557 |
+
if session_state.add_window_submitted:
|
| 558 |
+
session_state.add_window_submitted = False
|
| 559 |
+
|
| 560 |
+
elif method == "File Upload":
|
| 561 |
+
uploaded_file = st.file_uploader("Upload Windows File", type=["csv", "xlsx"], key="window_upload")
|
| 562 |
+
required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)"]
|
| 563 |
+
st.download_button(label="Download Window Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="window_template.csv", mime="text/csv")
|
| 564 |
+
if uploaded_file:
|
| 565 |
+
df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
|
| 566 |
+
if all(col in df.columns for col in required_cols):
|
| 567 |
+
for _, row in df.iterrows():
|
| 568 |
+
try:
|
| 569 |
+
new_window = Window(
|
| 570 |
+
name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
|
| 571 |
+
orientation=Orientation(row["Orientation"]), shgc=float(row["SHGC"]),
|
| 572 |
+
shading_device=str(row["Shading Device"]), shading_coefficient=float(row["Shading Coefficient"]),
|
| 573 |
+
frame_type=str(row["Frame Type"]), frame_percentage=float(row["Frame Percentage"]),
|
| 574 |
+
infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
|
| 575 |
)
|
| 576 |
+
self.component_library.add_component(new_window)
|
| 577 |
+
session_state.components['windows'].append(new_window)
|
| 578 |
+
except ValueError as e:
|
| 579 |
+
st.error(f"Error in row {row['Name']}: {str(e)}")
|
| 580 |
+
st.success("Windows uploaded successfully!")
|
| 581 |
+
st.rerun()
|
| 582 |
+
else:
|
| 583 |
+
st.error(f"File must contain: {', '.join(required_cols)}")
|
| 584 |
+
|
| 585 |
+
def _display_add_door_form(self, session_state: Any) -> None:
|
| 586 |
+
st.write("Add doors manually or upload a file.")
|
| 587 |
+
method = st.radio("Add Door Method", ["Manual Entry", "File Upload"])
|
| 588 |
+
if "add_door_submitted" not in session_state:
|
| 589 |
+
session_state.add_door_submitted = False
|
| 590 |
+
|
| 591 |
+
if method == "Manual Entry":
|
| 592 |
+
with st.form("add_door_form", clear_on_submit=True):
|
| 593 |
+
col1, col2 = st.columns(2)
|
| 594 |
+
with col1:
|
| 595 |
+
name = st.text_input("Name", "New Door")
|
| 596 |
+
area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1)
|
| 597 |
+
orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0)
|
| 598 |
+
with col2:
|
| 599 |
+
door_options = self.reference_data.data["door_types"]
|
| 600 |
+
selected_door = st.selectbox("Door Type", options=list(door_options.keys()))
|
| 601 |
+
u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(door_options[selected_door]["u_value"]), step=0.01)
|
| 602 |
+
infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1)
|
| 603 |
+
|
| 604 |
+
submitted = st.form_submit_button("Add Door")
|
| 605 |
+
if submitted and not session_state.add_door_submitted:
|
| 606 |
+
try:
|
| 607 |
+
new_door = Door(
|
| 608 |
+
name=name, u_value=u_value, area=area, orientation=Orientation(orientation),
|
| 609 |
+
door_type=selected_door, infiltration_rate_cfm=infiltration_rate
|
| 610 |
+
)
|
| 611 |
+
self.component_library.add_component(new_door)
|
| 612 |
+
session_state.components['doors'].append(new_door)
|
| 613 |
+
st.success(f"Added {new_door.name}")
|
| 614 |
+
session_state.add_door_submitted = True
|
| 615 |
st.rerun()
|
| 616 |
except ValueError as e:
|
| 617 |
st.error(f"Error: {str(e)}")
|
| 618 |
+
|
| 619 |
+
if session_state.add_door_submitted:
|
| 620 |
+
session_state.add_door_submitted = False
|
| 621 |
+
|
| 622 |
+
elif method == "File Upload":
|
| 623 |
+
uploaded_file = st.file_uploader("Upload Doors File", type=["csv", "xlsx"], key="door_upload")
|
| 624 |
+
required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)"]
|
| 625 |
+
st.download_button(label="Download Door Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="door_template.csv", mime="text/csv")
|
| 626 |
+
if uploaded_file:
|
| 627 |
+
df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file)
|
| 628 |
+
if all(col in df.columns for col in required_cols):
|
| 629 |
+
for _, row in df.iterrows():
|
| 630 |
+
try:
|
| 631 |
+
new_door = Door(
|
| 632 |
+
name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]),
|
| 633 |
+
orientation=Orientation(row["Orientation"]), door_type=str(row["Door Type"]),
|
| 634 |
+
infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"])
|
| 635 |
+
)
|
| 636 |
+
self.component_library.add_component(new_door)
|
| 637 |
+
session_state.components['doors'].append(new_door)
|
| 638 |
+
except ValueError as e:
|
| 639 |
+
st.error(f"Error in row {row['Name']}: {str(e)}")
|
| 640 |
+
st.success("Doors uploaded successfully!")
|
| 641 |
+
st.rerun()
|
| 642 |
+
else:
|
| 643 |
+
st.error(f"File must contain: {', '.join(required_cols)}")
|
| 644 |
|
| 645 |
def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None:
|
| 646 |
type_name = component_type.value.lower()
|
| 647 |
+
if component_type == ComponentType.ROOF:
|
| 648 |
+
st.write(f"Roof Air Volume: {session_state.roof_air_volume_m3} m³, Ventilation Rate: {session_state.roof_ventilation_ach} ACH")
|
| 649 |
+
|
| 650 |
+
if components:
|
| 651 |
+
headers = {
|
| 652 |
+
ComponentType.WALL: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)", "Delete"],
|
| 653 |
+
ComponentType.ROOF: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity", "Delete"],
|
| 654 |
+
ComponentType.FLOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)", "Delete"],
|
| 655 |
+
ComponentType.WINDOW: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)", "Delete"],
|
| 656 |
+
ComponentType.DOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)", "Delete"]
|
| 657 |
+
}[component_type]
|
| 658 |
+
cols = st.columns([1] * len(headers))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
for i, header in enumerate(headers):
|
| 660 |
cols[i].write(f"**{header}**")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
|
| 662 |
+
for comp in components:
|
| 663 |
+
cols = st.columns([1] * len(headers))
|
| 664 |
+
cols[0].write(comp.name)
|
| 665 |
+
cols[1].write(comp.area)
|
| 666 |
+
cols[2].write(comp.u_value)
|
| 667 |
+
cols[3].write(comp.orientation.value)
|
| 668 |
+
if component_type == ComponentType.WALL:
|
| 669 |
+
cols[4].write(comp.wall_type)
|
| 670 |
+
cols[5].write(comp.wall_group)
|
| 671 |
+
cols[6].write(comp.absorptivity)
|
| 672 |
+
cols[7].write(comp.shading_coefficient)
|
| 673 |
+
cols[8].write(comp.infiltration_rate_cfm)
|
| 674 |
+
elif component_type == ComponentType.ROOF:
|
| 675 |
+
cols[4].write(comp.roof_type)
|
| 676 |
+
cols[5].write(comp.roof_group)
|
| 677 |
+
cols[6].write(comp.slope)
|
| 678 |
+
cols[7].write(comp.absorptivity)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
elif component_type == ComponentType.FLOOR:
|
| 680 |
+
cols[4].write(comp.floor_type)
|
| 681 |
+
cols[5].write("Yes" if comp.ground_contact else "No")
|
| 682 |
+
cols[6].write(comp.ground_temperature_c if comp.ground_contact else "N/A")
|
| 683 |
elif component_type == ComponentType.WINDOW:
|
| 684 |
+
cols[4].write(comp.shgc)
|
| 685 |
+
cols[5].write(comp.shading_device)
|
| 686 |
+
cols[6].write(comp.shading_coefficient)
|
| 687 |
+
cols[7].write(comp.frame_type)
|
| 688 |
+
cols[8].write(comp.frame_percentage)
|
| 689 |
+
cols[9].write(comp.infiltration_rate_cfm)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
elif component_type == ComponentType.DOOR:
|
| 691 |
+
cols[4].write(comp.door_type)
|
| 692 |
+
cols[5].write(comp.infiltration_rate_cfm)
|
| 693 |
+
if cols[-1].button("Delete", key=f"delete_{comp.id}"):
|
| 694 |
+
self.component_library.remove_component(comp.id)
|
| 695 |
+
session_state.components[type_name + 's'] = [c for c in components if c.id != comp.id]
|
| 696 |
+
st.success(f"Deleted {comp.name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
st.rerun()
|
| 698 |
|
| 699 |
def _display_u_value_calculator_tab(self, session_state: Any) -> None:
|
| 700 |
+
st.subheader("U-Value Calculator (Standalone)")
|
| 701 |
+
if "u_value_layers" not in session_state:
|
| 702 |
+
session_state.u_value_layers = []
|
| 703 |
+
|
| 704 |
+
if session_state.u_value_layers:
|
| 705 |
+
st.write("Material Layers (Outside to Inside):")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"],
|
| 707 |
+
"Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]}
|
| 708 |
+
for i, l in enumerate(session_state.u_value_layers)]
|
| 709 |
+
st.dataframe(pd.DataFrame(layer_data))
|
| 710 |
+
outside_resistance = st.selectbox("Outside Resistance (m²·K/W)", ["Summer (0.04)", "Winter (0.03)", "Custom"], index=0)
|
| 711 |
+
outside_r = float(st.number_input("Custom Outside Resistance", min_value=0.0, value=0.04, step=0.01)) if outside_resistance == "Custom" else (0.04 if outside_resistance.startswith("Summer") else 0.03)
|
| 712 |
+
inside_r = st.number_input("Inside Resistance (m²·K/W)", min_value=0.0, value=0.13, step=0.01)
|
| 713 |
+
u_value = self.u_value_calculator.calculate_u_value(session_state.u_value_layers, outside_r, inside_r)
|
| 714 |
+
st.metric("U-Value", f"{u_value:.3f} W/m²·K")
|
| 715 |
+
|
| 716 |
+
with st.form("u_value_form"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
col1, col2 = st.columns(2)
|
| 718 |
with col1:
|
| 719 |
+
material_options = {m["name"]: m["conductivity"] for m in self.u_value_calculator.materials}
|
| 720 |
+
material_name = st.selectbox("Material", options=list(material_options.keys()))
|
| 721 |
+
conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=material_options[material_name], step=0.01)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
with col2:
|
| 723 |
thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
submitted = st.form_submit_button("Add Layer")
|
| 725 |
if submitted:
|
| 726 |
+
session_state.u_value_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity})
|
| 727 |
+
st.rerun()
|
| 728 |
+
|
| 729 |
+
col1, col2 = st.columns(2)
|
| 730 |
+
with col1:
|
| 731 |
+
if st.button("Remove Last Layer"):
|
| 732 |
+
if session_state.u_value_layers:
|
| 733 |
+
session_state.u_value_layers.pop()
|
|
|
|
| 734 |
st.rerun()
|
| 735 |
+
with col2:
|
| 736 |
+
if st.button("Reset"):
|
| 737 |
+
session_state.u_value_layers = []
|
|
|
|
| 738 |
st.rerun()
|
| 739 |
|
| 740 |
def _save_components(self, session_state: Any) -> None:
|
|
|
|
| 743 |
"roofs": [c.to_dict() for c in session_state.components["roofs"]],
|
| 744 |
"floors": [c.to_dict() for c in session_state.components["floors"]],
|
| 745 |
"windows": [c.to_dict() for c in session_state.components["windows"]],
|
| 746 |
+
"doors": [c.to_dict() for c in session_state.components["doors"]],
|
| 747 |
+
"roof_air_volume_m3": session_state.roof_air_volume_m3,
|
| 748 |
+
"roof_ventilation_ach": session_state.roof_ventilation_ach
|
| 749 |
}
|
| 750 |
file_path = "components_export.json"
|
| 751 |
with open(file_path, 'w') as f:
|
|
|
|
| 754 |
st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json")
|
| 755 |
st.success("Components saved successfully.")
|
| 756 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
# --- Main Execution ---
|
| 758 |
if __name__ == "__main__":
|
| 759 |
interface = ComponentSelectionInterface()
|