|
|
""" |
|
|
Data validation module for HVAC Load Calculator. |
|
|
This module provides validation functions for user inputs. |
|
|
""" |
|
|
|
|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from typing import Dict, List, Any, Optional, Tuple, Callable |
|
|
import json |
|
|
import os |
|
|
|
|
|
|
|
|
class DataValidation: |
|
|
"""Class for data validation functionality.""" |
|
|
|
|
|
@staticmethod |
|
|
def validate_building_info(building_info: Dict[str, Any]) -> Tuple[bool, List[str]]: |
|
|
""" |
|
|
Validate building information inputs. |
|
|
|
|
|
Args: |
|
|
building_info: Dictionary with building information |
|
|
|
|
|
Returns: |
|
|
Tuple containing validation result (True if valid) and list of validation messages |
|
|
""" |
|
|
is_valid = True |
|
|
messages = [] |
|
|
|
|
|
|
|
|
required_fields = [ |
|
|
("project_name", "Project Name"), |
|
|
("building_name", "Building Name"), |
|
|
("location", "Location"), |
|
|
("climate_zone", "Climate Zone"), |
|
|
("building_type", "Building Type") |
|
|
] |
|
|
|
|
|
for field, display_name in required_fields: |
|
|
if field not in building_info or not building_info[field]: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} is required.") |
|
|
|
|
|
|
|
|
numeric_fields = [ |
|
|
("floor_area", "Floor Area", 0, None), |
|
|
("num_floors", "Number of Floors", 1, None), |
|
|
("floor_height", "Floor Height", 2.0, 10.0), |
|
|
("occupancy", "Occupancy", 0, None) |
|
|
] |
|
|
|
|
|
for field, display_name, min_val, max_val in numeric_fields: |
|
|
if field in building_info: |
|
|
try: |
|
|
value = float(building_info[field]) |
|
|
if min_val is not None and value < min_val: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be at least {min_val}.") |
|
|
if max_val is not None and value > max_val: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be at most {max_val}.") |
|
|
except (ValueError, TypeError): |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be a number.") |
|
|
|
|
|
|
|
|
if "design_conditions" in building_info: |
|
|
design_conditions = building_info["design_conditions"] |
|
|
|
|
|
|
|
|
summer_fields = [ |
|
|
("summer_outdoor_db", "Summer Outdoor Dry-Bulb", -10.0, 50.0), |
|
|
("summer_outdoor_wb", "Summer Outdoor Wet-Bulb", -10.0, 40.0), |
|
|
("summer_indoor_db", "Summer Indoor Dry-Bulb", 18.0, 30.0), |
|
|
("summer_indoor_rh", "Summer Indoor RH", 30.0, 70.0) |
|
|
] |
|
|
|
|
|
for field, display_name, min_val, max_val in summer_fields: |
|
|
if field in design_conditions: |
|
|
try: |
|
|
value = float(design_conditions[field]) |
|
|
if min_val is not None and value < min_val: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be at least {min_val}.") |
|
|
if max_val is not None and value > max_val: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be at most {max_val}.") |
|
|
except (ValueError, TypeError): |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be a number.") |
|
|
|
|
|
|
|
|
winter_fields = [ |
|
|
("winter_outdoor_db", "Winter Outdoor Dry-Bulb", -40.0, 20.0), |
|
|
("winter_outdoor_rh", "Winter Outdoor RH", 0.0, 100.0), |
|
|
("winter_indoor_db", "Winter Indoor Dry-Bulb", 18.0, 25.0), |
|
|
("winter_indoor_rh", "Winter Indoor RH", 20.0, 60.0) |
|
|
] |
|
|
|
|
|
for field, display_name, min_val, max_val in winter_fields: |
|
|
if field in design_conditions: |
|
|
try: |
|
|
value = float(design_conditions[field]) |
|
|
if min_val is not None and value < min_val: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be at least {min_val}.") |
|
|
if max_val is not None and value > max_val: |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be at most {max_val}.") |
|
|
except (ValueError, TypeError): |
|
|
is_valid = False |
|
|
messages.append(f"{display_name} must be a number.") |
|
|
|
|
|
|
|
|
if "summer_outdoor_db" in design_conditions and "summer_outdoor_wb" in design_conditions: |
|
|
try: |
|
|
db = float(design_conditions["summer_outdoor_db"]) |
|
|
wb = float(design_conditions["summer_outdoor_wb"]) |
|
|
if wb > db: |
|
|
is_valid = False |
|
|
messages.append("Summer Outdoor Wet-Bulb temperature must be less than or equal to Dry-Bulb temperature.") |
|
|
except (ValueError, TypeError): |
|
|
pass |
|
|
|
|
|
return is_valid, messages |
|
|
|
|
|
@staticmethod |
|
|
def validate_components(components: Dict[str, List[Any]]) -> Tuple[bool, List[str]]: |
|
|
""" |
|
|
Validate building components. |
|
|
|
|
|
Args: |
|
|
components: Dictionary with building components |
|
|
|
|
|
Returns: |
|
|
Tuple containing validation result (True if valid) and list of validation messages |
|
|
""" |
|
|
is_valid = True |
|
|
messages = [] |
|
|
|
|
|
|
|
|
if not any(components.values()): |
|
|
is_valid = False |
|
|
messages.append("At least one building component (wall, roof, floor, window, or door) is required.") |
|
|
|
|
|
|
|
|
for i, wall in enumerate(components.get("walls", [])): |
|
|
|
|
|
if not wall.name: |
|
|
is_valid = False |
|
|
messages.append(f"Wall #{i+1}: Name is required.") |
|
|
|
|
|
|
|
|
if wall.area <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Wall #{i+1}: Area must be greater than zero.") |
|
|
|
|
|
if wall.u_value <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Wall #{i+1}: U-value must be greater than zero.") |
|
|
|
|
|
|
|
|
for i, roof in enumerate(components.get("roofs", [])): |
|
|
|
|
|
if not roof.name: |
|
|
is_valid = False |
|
|
messages.append(f"Roof #{i+1}: Name is required.") |
|
|
|
|
|
|
|
|
if roof.area <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Roof #{i+1}: Area must be greater than zero.") |
|
|
|
|
|
if roof.u_value <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Roof #{i+1}: U-value must be greater than zero.") |
|
|
|
|
|
|
|
|
for i, floor in enumerate(components.get("floors", [])): |
|
|
|
|
|
if not floor.name: |
|
|
is_valid = False |
|
|
messages.append(f"Floor #{i+1}: Name is required.") |
|
|
|
|
|
|
|
|
if floor.area <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Floor #{i+1}: Area must be greater than zero.") |
|
|
|
|
|
if floor.u_value <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Floor #{i+1}: U-value must be greater than zero.") |
|
|
|
|
|
|
|
|
for i, window in enumerate(components.get("windows", [])): |
|
|
|
|
|
if not window.name: |
|
|
is_valid = False |
|
|
messages.append(f"Window #{i+1}: Name is required.") |
|
|
|
|
|
|
|
|
if window.area <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Window #{i+1}: Area must be greater than zero.") |
|
|
|
|
|
if window.u_value <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Window #{i+1}: U-value must be greater than zero.") |
|
|
|
|
|
if window.shgc <= 0 or window.shgc > 1: |
|
|
is_valid = False |
|
|
messages.append(f"Window #{i+1}: SHGC must be between 0 and 1.") |
|
|
|
|
|
|
|
|
for i, door in enumerate(components.get("doors", [])): |
|
|
|
|
|
if not door.name: |
|
|
is_valid = False |
|
|
messages.append(f"Door #{i+1}: Name is required.") |
|
|
|
|
|
|
|
|
if door.area <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Door #{i+1}: Area must be greater than zero.") |
|
|
|
|
|
if door.u_value <= 0: |
|
|
is_valid = False |
|
|
messages.append(f"Door #{i+1}: U-value must be greater than zero.") |
|
|
|
|
|
|
|
|
if not components.get("walls", []): |
|
|
messages.append("Warning: No walls defined. At least one wall is recommended.") |
|
|
|
|
|
if not components.get("roofs", []): |
|
|
messages.append("Warning: No roofs defined. At least one roof is recommended.") |
|
|
|
|
|
if not components.get("floors", []): |
|
|
messages.append("Warning: No floors defined. At least one floor is recommended.") |
|
|
|
|
|
return is_valid, messages |
|
|
|
|
|
def validate_calculation_inputs(self, session_state: Dict[str, Any]) -> bool: |
|
|
""" |
|
|
Validate inputs required for HVAC load calculations. |
|
|
|
|
|
Args: |
|
|
session_state: Streamlit session state containing all inputs |
|
|
|
|
|
Returns: |
|
|
Boolean indicating whether inputs are valid for calculations |
|
|
""" |
|
|
|
|
|
if not session_state.get("building_info"): |
|
|
st.error("Building information is required for calculations.") |
|
|
return False |
|
|
|
|
|
|
|
|
building_info_valid, building_info_messages = self.validate_building_info(session_state["building_info"]) |
|
|
if not building_info_valid: |
|
|
st.error("Building information is incomplete or invalid:") |
|
|
for message in building_info_messages: |
|
|
st.error(f"- {message}") |
|
|
return False |
|
|
|
|
|
|
|
|
if "design_conditions" not in session_state["building_info"]: |
|
|
st.error("Design conditions are required for calculations.") |
|
|
return False |
|
|
|
|
|
|
|
|
if not session_state.get("components"): |
|
|
st.error("Building components are required for calculations.") |
|
|
return False |
|
|
|
|
|
|
|
|
components_valid, components_messages = self.validate_components(session_state["components"]) |
|
|
if not components_valid: |
|
|
st.error("Building components are incomplete or invalid:") |
|
|
for message in components_messages: |
|
|
st.error(f"- {message}") |
|
|
return False |
|
|
|
|
|
|
|
|
if not any(session_state["components"].values()): |
|
|
st.error("At least one building component (wall, roof, floor, window, or door) is required.") |
|
|
return False |
|
|
|
|
|
|
|
|
return True |
|
|
|