mabuseif commited on
Commit
60cca7c
·
verified ·
1 Parent(s): ef05671

Upload 14 files

Browse files
.streamlit/config.toml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#1E88E5"
3
+ backgroundColor = "#FFFFFF"
4
+ secondaryBackgroundColor = "#F0F2F6"
5
+ textColor = "#262730"
6
+ font = "sans serif"
README.md CHANGED
@@ -1,13 +1,66 @@
1
  ---
2
- title: HVAC
3
- emoji: 🏃
4
- colorFrom: red
5
- colorTo: purple
6
  sdk: streamlit
7
- sdk_version: 1.43.2
8
  app_file: app.py
9
  pinned: false
10
- short_description: HVAC tool
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: HVAC Load Calculator
3
+ emoji: 🔥❄️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: streamlit
7
+ sdk_version: 1.32.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
+ # HVAC Load Calculator
13
+
14
+ A modern web tool for calculating HVAC cooling and heating loads based on the ASHRAE method.
15
+
16
+ ## Features
17
+
18
+ - **Separate Calculators**: Independent cooling and heating load calculators
19
+ - **Step-by-Step Input Forms**: Guided process with validation
20
+ - **Reference Data**: Comprehensive material properties and location data
21
+ - **Visual Results**: Charts and tables for load components
22
+ - **Smart Validation**: Proceed with warnings rather than blocking progress
23
+ - **Downloadable Data**: Export results for student assignments
24
+ - **ASHRAE Method**: Implementation based on industry-standard calculation methods
25
+ - **Extensible Design**: Framework for adding other calculation methods or locations
26
+
27
+ ## How to Use
28
+
29
+ 1. Select either the Cooling Load Calculator or Heating Load Calculator from the sidebar
30
+ 2. Fill in the required information in each step
31
+ 3. Review any warnings that appear (you can proceed with warnings)
32
+ 4. Calculate results and analyze the output
33
+ 5. Export results for your assignments
34
+
35
+ ## Cooling Load Calculator
36
+
37
+ The cooling load calculator helps determine the amount of heat that needs to be removed from a space to maintain comfort conditions. It accounts for:
38
+
39
+ - Conduction through building envelope
40
+ - Solar radiation through windows
41
+ - Internal heat gains (people, equipment, lighting)
42
+ - Infiltration and ventilation
43
+
44
+ ## Heating Load Calculator
45
+
46
+ The heating load calculator helps determine the amount of heat that needs to be added to a space to maintain comfort conditions. It accounts for:
47
+
48
+ - Conduction through building envelope
49
+ - Infiltration and ventilation
50
+ - Annual heating energy requirements based on heating degree days
51
+
52
+ ## Technical Details
53
+
54
+ - Built with Python and Streamlit
55
+ - Modular design for extensibility
56
+ - Comprehensive reference data based on ASHRAE standards
57
+ - Visualization using Plotly
58
+ - Data export in CSV and JSON formats
59
+
60
+ ## Educational Purpose
61
+
62
+ This tool is designed for educational purposes to help students understand the factors that influence HVAC load calculations. It provides a practical way to apply theoretical knowledge and see how different building parameters affect heating and cooling requirements.
63
+
64
+ ## Acknowledgements
65
+
66
+ Based on ASHRAE calculation methods for heating and cooling loads.
app.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main application file for HVAC Load Calculator
3
+
4
+ This is the main entry point for the HVAC Load Calculator web application.
5
+ It sets up the Streamlit interface and navigation between different pages.
6
+ """
7
+
8
+ import streamlit as st
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Add the parent directory to sys.path to import modules
14
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
15
+
16
+ # Import pages
17
+ from pages.cooling_calculator import cooling_calculator
18
+ from pages.heating_calculator import heating_calculator
19
+
20
+ # Set page configuration
21
+ st.set_page_config(
22
+ page_title="HVAC Load Calculator",
23
+ page_icon="🔥❄️",
24
+ layout="wide",
25
+ initial_sidebar_state="expanded"
26
+ )
27
+
28
+ # Define main function
29
+ def main():
30
+ """Main function for the HVAC Load Calculator web application."""
31
+
32
+ # Add custom CSS
33
+ st.markdown("""
34
+ <style>
35
+ .main-header {
36
+ font-size: 2.5rem;
37
+ color: #1E88E5;
38
+ text-align: center;
39
+ margin-bottom: 1rem;
40
+ }
41
+ .sub-header {
42
+ font-size: 1.5rem;
43
+ color: #424242;
44
+ margin-bottom: 1rem;
45
+ }
46
+ .info-box {
47
+ background-color: #E3F2FD;
48
+ padding: 1rem;
49
+ border-radius: 0.5rem;
50
+ margin-bottom: 1rem;
51
+ }
52
+ </style>
53
+ """, unsafe_allow_html=True)
54
+
55
+ # Sidebar navigation
56
+ st.sidebar.title("HVAC Load Calculator")
57
+ st.sidebar.image("https://img.icons8.com/fluency/96/air-conditioner.png", width=100)
58
+
59
+ # Navigation options
60
+ page = st.sidebar.radio(
61
+ "Select Calculator",
62
+ ["Home", "Cooling Load Calculator", "Heating Load Calculator"]
63
+ )
64
+
65
+ # Display selected page
66
+ if page == "Home":
67
+ display_home_page()
68
+ elif page == "Cooling Load Calculator":
69
+ cooling_calculator()
70
+ elif page == "Heating Load Calculator":
71
+ heating_calculator()
72
+
73
+ # Footer
74
+ st.sidebar.markdown("---")
75
+ st.sidebar.info(
76
+ "HVAC Load Calculator v1.0\n\n"
77
+ "Based on ASHRAE calculation methods\n\n"
78
+ "© 2025"
79
+ )
80
+
81
+
82
+ def display_home_page():
83
+ """Display the home page."""
84
+
85
+ st.markdown('<h1 class="main-header">HVAC Load Calculator</h1>', unsafe_allow_html=True)
86
+ st.markdown('<h2 class="sub-header">A Modern Tool for HVAC Design</h2>', unsafe_allow_html=True)
87
+
88
+ # Introduction
89
+ st.markdown("""
90
+ <div class="info-box">
91
+ <p>Welcome to the HVAC Load Calculator! This tool helps you calculate cooling and heating loads for buildings
92
+ using the ASHRAE method. It's designed for educational purposes to help students understand the factors
93
+ that influence HVAC load calculations.</p>
94
+ </div>
95
+ """, unsafe_allow_html=True)
96
+
97
+ # Features
98
+ st.markdown("### Features")
99
+
100
+ col1, col2 = st.columns(2)
101
+
102
+ with col1:
103
+ st.markdown("""
104
+ #### Cooling Load Calculator
105
+ - Calculate sensible and latent cooling loads
106
+ - Account for conduction, solar radiation, infiltration, and internal gains
107
+ - Visualize load components with charts and tables
108
+ - Export results for assignments
109
+ """)
110
+
111
+ with col2:
112
+ st.markdown("""
113
+ #### Heating Load Calculator
114
+ - Calculate peak heating loads
115
+ - Account for conduction, infiltration, and ventilation
116
+ - Estimate annual heating energy requirements
117
+ - Visualize load components with charts and tables
118
+ """)
119
+
120
+ # How to use
121
+ st.markdown("### How to Use")
122
+ st.markdown("""
123
+ 1. Select either the Cooling Load Calculator or Heating Load Calculator from the sidebar
124
+ 2. Fill in the required information in each step
125
+ 3. Review any warnings that appear (you can proceed with warnings)
126
+ 4. Calculate results and analyze the output
127
+ 5. Export results for your assignments
128
+ """)
129
+
130
+ # Reference data
131
+ st.markdown("### Reference Data")
132
+ st.markdown("""
133
+ The calculator includes reference data for:
134
+ - Building materials (walls, roofs, floors)
135
+ - Glass types and shading coefficients
136
+ - Climate data for various locations
137
+ - Occupancy patterns and internal gains
138
+
139
+ This data is based on ASHRAE standards and guidelines.
140
+ """)
141
+
142
+ # Get started button
143
+ col1, col2, col3 = st.columns([1, 2, 1])
144
+ with col2:
145
+ st.markdown("### Get Started")
146
+ cooling_button = st.button("Go to Cooling Load Calculator")
147
+ heating_button = st.button("Go to Heating Load Calculator")
148
+
149
+ if cooling_button:
150
+ st.session_state.page = "Cooling Load Calculator"
151
+ st.experimental_rerun()
152
+
153
+ if heating_button:
154
+ st.session_state.page = "Heating Load Calculator"
155
+ st.experimental_rerun()
156
+
157
+
158
+ # Run the application
159
+ if __name__ == "__main__":
160
+ main()
application_design.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HVAC Load Calculator Web Application Design
2
+
3
+ ## Overview
4
+ This document outlines the structure and user flow for the HVAC Load Calculator web application. The application will be built using Python and deployed on Hugging Face Spaces, providing a user-friendly interface for calculating cooling and heating loads based on the ASHRAE method.
5
+
6
+ ## Application Structure
7
+
8
+ ### 1. Core Components
9
+ - **Backend Calculation Modules**
10
+ - `cooling_load.py`: Implements ASHRAE cooling load calculations
11
+ - `heating_load.py`: Implements ASHRAE heating load calculations
12
+ - `reference_data.py`: Contains material properties, climate data, and other reference information
13
+
14
+ - **Web Interface**
15
+ - `app.py`: Main Streamlit application entry point
16
+ - `pages/`: Directory containing individual calculator pages
17
+ - `cooling_calculator.py`: Cooling load calculator interface
18
+ - `heating_calculator.py`: Heating load calculator interface
19
+ - `about.py`: Information about the application and calculation methods
20
+
21
+ - **Utilities**
22
+ - `utils/`: Directory containing utility functions
23
+ - `validation.py`: Input validation functions
24
+ - `visualization.py`: Chart and table generation functions
25
+ - `export.py`: Data export functionality
26
+
27
+ ### 2. Data Flow
28
+ ```
29
+ User Input → Validation → Calculation → Results Visualization → Data Export
30
+ ```
31
+
32
+ ## User Flow
33
+
34
+ ### Home Page
35
+ - Introduction to the application
36
+ - Selection between cooling and heating load calculators
37
+ - Information about ASHRAE calculation methods
38
+ - Links to reference materials
39
+
40
+ ### Cooling Load Calculator
41
+ 1. **Building Information**
42
+ - Building location
43
+ - Indoor and outdoor design temperatures
44
+ - Building dimensions and volume
45
+
46
+ 2. **Building Envelope**
47
+ - Wall areas and construction types
48
+ - Roof/ceiling areas and construction types
49
+ - Floor areas and construction types
50
+
51
+ 3. **Windows and Doors**
52
+ - Window areas by orientation
53
+ - Glass types and shading information
54
+ - Door areas and types
55
+
56
+ 4. **Internal Loads**
57
+ - Number of occupants
58
+ - Lighting information
59
+ - Equipment and appliances
60
+
61
+ 5. **Ventilation and Infiltration**
62
+ - Air changes per hour
63
+ - Ventilation requirements
64
+
65
+ 6. **Results**
66
+ - Breakdown of cooling loads by component
67
+ - Total sensible and latent cooling loads
68
+ - Visualizations (charts and tables)
69
+ - Equipment sizing recommendations
70
+ - Option to download input and result data
71
+
72
+ ### Heating Load Calculator
73
+ 1. **Building Information**
74
+ - Building location
75
+ - Indoor and outdoor design temperatures
76
+ - Building dimensions and volume
77
+
78
+ 2. **Building Envelope**
79
+ - Wall areas and construction types
80
+ - Roof/ceiling areas and construction types
81
+ - Floor areas and construction types
82
+
83
+ 3. **Windows and Doors**
84
+ - Window areas by orientation
85
+ - Glass types
86
+ - Door areas and types
87
+
88
+ 4. **Ventilation and Infiltration**
89
+ - Air changes per hour
90
+ - Ventilation requirements
91
+
92
+ 5. **Occupancy Information**
93
+ - Occupancy type and schedule
94
+ - Heating degree days information
95
+
96
+ 6. **Results**
97
+ - Breakdown of heating loads by component
98
+ - Total peak heating load
99
+ - Annual heating energy requirement
100
+ - Visualizations (charts and tables)
101
+ - Equipment sizing recommendations
102
+ - Option to download input and result data
103
+
104
+ ## User Interface Design
105
+
106
+ ### General Principles
107
+ - Clean, modern interface with clear navigation
108
+ - Step-by-step input forms with progress indicators
109
+ - Immediate feedback on inputs with validation warnings
110
+ - Informative tooltips and help text for technical terms
111
+ - Responsive design for different screen sizes
112
+
113
+ ### Input Forms
114
+ - Grouped by logical sections
115
+ - Clear labels and units
116
+ - Default values where appropriate
117
+ - Input validation with warning messages
118
+ - Option to proceed with warnings rather than blocking progress
119
+ - Reference data selection for materials and locations
120
+
121
+ ### Results Display
122
+ - Clear summary of key results
123
+ - Detailed breakdown of load components
124
+ - Visual representations (charts and graphs)
125
+ - Tabular data for detailed analysis
126
+ - Equipment sizing recommendations
127
+ - Export options for reports and assignments
128
+
129
+ ## Validation System
130
+ - Input validation for required fields
131
+ - Range checking for numerical inputs
132
+ - Logical validation between related inputs
133
+ - Warning system that allows proceeding with caution
134
+ - Clear error messages with suggestions for correction
135
+
136
+ ## Data Export Functionality
137
+ - Export input data in JSON format
138
+ - Export results in CSV format
139
+ - Generate PDF reports with inputs and results
140
+ - Save charts and visualizations as images
141
+
142
+ ## Extensibility Features
143
+ - Modular code structure for easy addition of new calculation methods
144
+ - Configuration-based reference data for easy updates
145
+ - Pluggable visualization components
146
+ - Separation of UI and calculation logic
147
+
148
+ ## Technology Stack
149
+ - **Backend**: Python
150
+ - **Web Framework**: Streamlit
151
+ - **Data Processing**: Pandas, NumPy
152
+ - **Visualization**: Plotly, Matplotlib
153
+ - **Deployment**: Hugging Face Spaces
calculation_methods.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Calculation Method Interface for HVAC Load Calculator
3
+
4
+ This module defines the interface for calculation methods in the HVAC Load Calculator.
5
+ It provides a base class that all calculation methods should inherit from.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+
10
+
11
+ class CalculationMethod(ABC):
12
+ """
13
+ Abstract base class for HVAC load calculation methods.
14
+
15
+ All calculation methods should inherit from this class and implement
16
+ the required methods.
17
+ """
18
+
19
+ @property
20
+ @abstractmethod
21
+ def name(self):
22
+ """
23
+ Get the name of the calculation method.
24
+
25
+ Returns:
26
+ str: Name of the calculation method
27
+ """
28
+ pass
29
+
30
+ @property
31
+ @abstractmethod
32
+ def description(self):
33
+ """
34
+ Get the description of the calculation method.
35
+
36
+ Returns:
37
+ str: Description of the calculation method
38
+ """
39
+ pass
40
+
41
+ @property
42
+ @abstractmethod
43
+ def version(self):
44
+ """
45
+ Get the version of the calculation method.
46
+
47
+ Returns:
48
+ str: Version of the calculation method
49
+ """
50
+ pass
51
+
52
+ @abstractmethod
53
+ def calculate(self, input_data):
54
+ """
55
+ Perform the calculation.
56
+
57
+ Args:
58
+ input_data (dict): Input data for the calculation
59
+
60
+ Returns:
61
+ dict: Calculation results
62
+ """
63
+ pass
64
+
65
+ @abstractmethod
66
+ def get_input_schema(self):
67
+ """
68
+ Get the input schema for the calculation method.
69
+
70
+ Returns:
71
+ dict: JSON schema for input validation
72
+ """
73
+ pass
74
+
75
+ @abstractmethod
76
+ def get_output_schema(self):
77
+ """
78
+ Get the output schema for the calculation method.
79
+
80
+ Returns:
81
+ dict: JSON schema for output validation
82
+ """
83
+ pass
84
+
85
+
86
+ class ASHRAECoolingMethod(CalculationMethod):
87
+ """
88
+ ASHRAE method for cooling load calculation.
89
+ """
90
+
91
+ @property
92
+ def name(self):
93
+ return "ASHRAE Cooling Load Method"
94
+
95
+ @property
96
+ def description(self):
97
+ return "Calculates cooling loads using the ASHRAE method for residential buildings."
98
+
99
+ @property
100
+ def version(self):
101
+ return "1.0"
102
+
103
+ def calculate(self, input_data):
104
+ """
105
+ Calculate cooling load using the ASHRAE method.
106
+
107
+ Args:
108
+ input_data (dict): Input data for the calculation
109
+
110
+ Returns:
111
+ dict: Calculation results
112
+ """
113
+ from cooling_load import CoolingLoadCalculator
114
+
115
+ calculator = CoolingLoadCalculator()
116
+
117
+ # Extract input data
118
+ building_components = input_data.get('building_components', [])
119
+ windows = input_data.get('windows', [])
120
+ infiltration = input_data.get('infiltration', {})
121
+ internal_gains = input_data.get('internal_gains', {})
122
+
123
+ # Perform calculation
124
+ results = calculator.calculate_total_cooling_load(
125
+ building_components=building_components,
126
+ windows=windows,
127
+ infiltration=infiltration,
128
+ internal_gains=internal_gains
129
+ )
130
+
131
+ return results
132
+
133
+ def get_input_schema(self):
134
+ """
135
+ Get the input schema for the ASHRAE cooling load method.
136
+
137
+ Returns:
138
+ dict: JSON schema for input validation
139
+ """
140
+ return {
141
+ "type": "object",
142
+ "properties": {
143
+ "building_components": {
144
+ "type": "array",
145
+ "items": {
146
+ "type": "object",
147
+ "properties": {
148
+ "name": {"type": "string"},
149
+ "area": {"type": "number", "minimum": 0},
150
+ "u_value": {"type": "number", "minimum": 0},
151
+ "temp_diff": {"type": "number"}
152
+ },
153
+ "required": ["area", "u_value", "temp_diff"]
154
+ }
155
+ },
156
+ "windows": {
157
+ "type": "array",
158
+ "items": {
159
+ "type": "object",
160
+ "properties": {
161
+ "name": {"type": "string"},
162
+ "area": {"type": "number", "minimum": 0},
163
+ "u_value": {"type": "number", "minimum": 0},
164
+ "orientation": {"type": "string", "enum": ["north", "east", "south", "west", "horizontal"]},
165
+ "glass_type": {"type": "string"},
166
+ "shading": {"type": "string"},
167
+ "shade_factor": {"type": "number", "minimum": 0, "maximum": 1},
168
+ "temp_diff": {"type": "number"}
169
+ },
170
+ "required": ["area", "u_value", "orientation", "temp_diff"]
171
+ }
172
+ },
173
+ "infiltration": {
174
+ "type": "object",
175
+ "properties": {
176
+ "volume": {"type": "number", "minimum": 0},
177
+ "air_changes": {"type": "number", "minimum": 0},
178
+ "temp_diff": {"type": "number"}
179
+ },
180
+ "required": ["volume", "air_changes", "temp_diff"]
181
+ },
182
+ "internal_gains": {
183
+ "type": "object",
184
+ "properties": {
185
+ "num_people": {"type": "integer", "minimum": 0},
186
+ "has_kitchen": {"type": "boolean"},
187
+ "equipment_watts": {"type": "number", "minimum": 0}
188
+ },
189
+ "required": ["num_people"]
190
+ }
191
+ },
192
+ "required": ["building_components", "infiltration", "internal_gains"]
193
+ }
194
+
195
+ def get_output_schema(self):
196
+ """
197
+ Get the output schema for the ASHRAE cooling load method.
198
+
199
+ Returns:
200
+ dict: JSON schema for output validation
201
+ """
202
+ return {
203
+ "type": "object",
204
+ "properties": {
205
+ "conduction_gain": {"type": "number"},
206
+ "window_conduction_gain": {"type": "number"},
207
+ "window_solar_gain": {"type": "number"},
208
+ "infiltration_gain": {"type": "number"},
209
+ "internal_gain": {"type": "number"},
210
+ "sensible_load": {"type": "number"},
211
+ "latent_load": {"type": "number"},
212
+ "total_load": {"type": "number"}
213
+ },
214
+ "required": ["sensible_load", "latent_load", "total_load"]
215
+ }
216
+
217
+
218
+ class ASHRAEHeatingMethod(CalculationMethod):
219
+ """
220
+ ASHRAE method for heating load calculation.
221
+ """
222
+
223
+ @property
224
+ def name(self):
225
+ return "ASHRAE Heating Load Method"
226
+
227
+ @property
228
+ def description(self):
229
+ return "Calculates heating loads using the ASHRAE method for residential buildings."
230
+
231
+ @property
232
+ def version(self):
233
+ return "1.0"
234
+
235
+ def calculate(self, input_data):
236
+ """
237
+ Calculate heating load using the ASHRAE method.
238
+
239
+ Args:
240
+ input_data (dict): Input data for the calculation
241
+
242
+ Returns:
243
+ dict: Calculation results
244
+ """
245
+ from heating_load import HeatingLoadCalculator
246
+
247
+ calculator = HeatingLoadCalculator()
248
+
249
+ # Extract input data
250
+ building_components = input_data.get('building_components', [])
251
+ infiltration = input_data.get('infiltration', {})
252
+
253
+ # Perform calculation
254
+ results = calculator.calculate_total_heating_load(
255
+ building_components=building_components,
256
+ infiltration=infiltration
257
+ )
258
+
259
+ # Calculate annual heating requirement if location and occupancy data are provided
260
+ if 'location' in input_data and 'occupancy_type' in input_data:
261
+ location = input_data.get('location')
262
+ occupancy_type = input_data.get('occupancy_type')
263
+ base_temp = input_data.get('base_temp', 18)
264
+
265
+ annual_results = calculator.calculate_annual_heating_requirement(
266
+ results['total_load'],
267
+ location,
268
+ occupancy_type,
269
+ base_temp
270
+ )
271
+
272
+ # Combine results
273
+ results.update(annual_results)
274
+
275
+ return results
276
+
277
+ def get_input_schema(self):
278
+ """
279
+ Get the input schema for the ASHRAE heating load method.
280
+
281
+ Returns:
282
+ dict: JSON schema for input validation
283
+ """
284
+ return {
285
+ "type": "object",
286
+ "properties": {
287
+ "building_components": {
288
+ "type": "array",
289
+ "items": {
290
+ "type": "object",
291
+ "properties": {
292
+ "name": {"type": "string"},
293
+ "area": {"type": "number", "minimum": 0},
294
+ "u_value": {"type": "number", "minimum": 0},
295
+ "temp_diff": {"type": "number", "minimum": 0}
296
+ },
297
+ "required": ["area", "u_value", "temp_diff"]
298
+ }
299
+ },
300
+ "infiltration": {
301
+ "type": "object",
302
+ "properties": {
303
+ "volume": {"type": "number", "minimum": 0},
304
+ "air_changes": {"type": "number", "minimum": 0},
305
+ "temp_diff": {"type": "number", "minimum": 0}
306
+ },
307
+ "required": ["volume", "air_changes", "temp_diff"]
308
+ },
309
+ "location": {"type": "string"},
310
+ "occupancy_type": {"type": "string"},
311
+ "base_temp": {"type": "number"}
312
+ },
313
+ "required": ["building_components", "infiltration"]
314
+ }
315
+
316
+ def get_output_schema(self):
317
+ """
318
+ Get the output schema for the ASHRAE heating load method.
319
+
320
+ Returns:
321
+ dict: JSON schema for output validation
322
+ """
323
+ return {
324
+ "type": "object",
325
+ "properties": {
326
+ "component_losses": {
327
+ "type": "object",
328
+ "additionalProperties": {"type": "number"}
329
+ },
330
+ "total_conduction_loss": {"type": "number"},
331
+ "infiltration_loss": {"type": "number"},
332
+ "total_load": {"type": "number"},
333
+ "heating_degree_days": {"type": "number"},
334
+ "correction_factor": {"type": "number"},
335
+ "annual_energy_kwh": {"type": "number"},
336
+ "annual_energy_mj": {"type": "number"}
337
+ },
338
+ "required": ["total_load"]
339
+ }
340
+
341
+
342
+ class CalculationMethodRegistry:
343
+ """
344
+ Registry for calculation methods.
345
+
346
+ This class maintains a registry of available calculation methods
347
+ and provides methods to access them.
348
+ """
349
+
350
+ def __init__(self):
351
+ """Initialize the registry."""
352
+ self._methods = {}
353
+
354
+ def register_method(self, method_id, method_class):
355
+ """
356
+ Register a calculation method.
357
+
358
+ Args:
359
+ method_id (str): Unique identifier for the method
360
+ method_class (type): Class implementing the CalculationMethod interface
361
+
362
+ Returns:
363
+ bool: True if registration was successful, False otherwise
364
+ """
365
+ if method_id in self._methods:
366
+ return False
367
+
368
+ if not issubclass(method_class, CalculationMethod):
369
+ return False
370
+
371
+ self._methods[method_id] = method_class
372
+ return True
373
+
374
+ def get_method(self, method_id):
375
+ """
376
+ Get a calculation method by ID.
377
+
378
+ Args:
379
+ method_id (str): Unique identifier for the method
380
+
381
+ Returns:
382
+ CalculationMethod: Instance of the calculation method, or None if not found
383
+ """
384
+ if method_id not in self._methods:
385
+ return None
386
+
387
+ return self._methods[method_id]()
388
+
389
+ def get_available_methods(self):
390
+ """
391
+ Get a list of available calculation methods.
392
+
393
+ Returns:
394
+ list: List of dictionaries with method information
395
+ """
396
+ methods = []
397
+ for method_id, method_class in self._methods.items():
398
+ method = method_class()
399
+ methods.append({
400
+ 'id': method_id,
401
+ 'name': method.name,
402
+ 'description': method.description,
403
+ 'version': method.version
404
+ })
405
+
406
+ return methods
407
+
408
+
409
+ # Create a global registry instance
410
+ registry = CalculationMethodRegistry()
411
+
412
+ # Register the built-in calculation methods
413
+ registry.register_method('ashrae_cooling', ASHRAECoolingMethod)
414
+ registry.register_method('ashrae_heating', ASHRAEHeatingMethod)
415
+
416
+
417
+ # Example of how to add a new calculation method
418
+ """
419
+ class CustomCoolingMethod(CalculationMethod):
420
+ @property
421
+ def name(self):
422
+ return "Custom Cooling Method"
423
+
424
+ @property
425
+ def description(self):
426
+ return "A custom method for calculating cooling loads."
427
+
428
+ @property
429
+ def version(self):
430
+ return "1.0"
431
+
432
+ def calculate(self, input_data):
433
+ # Custom calculation logic
434
+ pass
435
+
436
+ def get_input_schema(self):
437
+ # Custom input schema
438
+ pass
439
+
440
+ def get_output_schema(self):
441
+ # Custom output schema
442
+ pass
443
+
444
+ # Register the custom method
445
+ registry.register_method('custom_cooling', CustomCoolingMethod)
446
+ """
cooling_load.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ASHRAE Cooling Load Calculation Module
3
+
4
+ This module implements the ASHRAE method for calculating cooling loads in residential buildings.
5
+ It calculates the sensible cooling load and then applies a factor of 1.3 to account for latent load.
6
+ """
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ class CoolingLoadCalculator:
13
+ """
14
+ A class to calculate cooling loads using the ASHRAE method.
15
+ """
16
+
17
+ def __init__(self):
18
+ """Initialize the cooling load calculator with default values."""
19
+ # Default values for internal heat gains (W)
20
+ self.heat_gain_per_person = 75
21
+ self.heat_gain_kitchen = 1000
22
+
23
+ # Specific heat capacity of air × density of air
24
+ self.air_heat_factor = 0.33
25
+
26
+ def calculate_conduction_heat_gain(self, area, u_value, temp_diff):
27
+ """
28
+ Calculate conduction heat gain through building components.
29
+
30
+ Args:
31
+ area (float): Area of the building component in m²
32
+ u_value (float): U-value of the component in W/m²°C
33
+ temp_diff (float): Temperature difference (outside - inside) in °C
34
+
35
+ Returns:
36
+ float: Heat gain in Watts
37
+ """
38
+ return area * u_value * temp_diff
39
+
40
+ def calculate_solar_heat_gain(self, area, shgf, shade_factor=1.0):
41
+ """
42
+ Calculate solar heat gain through glazing.
43
+
44
+ Args:
45
+ area (float): Area of the glazing in m²
46
+ shgf (float): Solar Heat Gain Factor based on orientation and climate
47
+ shade_factor (float): Factor to account for shading (1.0 = no shade, 0.0 = full shade)
48
+
49
+ Returns:
50
+ float: Heat gain in Watts
51
+ """
52
+ return area * shgf * shade_factor
53
+
54
+ def calculate_infiltration_heat_gain(self, volume, air_changes, temp_diff):
55
+ """
56
+ Calculate heat gain due to infiltration and ventilation.
57
+
58
+ Args:
59
+ volume (float): Volume of the space in m³
60
+ air_changes (float): Number of air changes per hour
61
+ temp_diff (float): Temperature difference (outside - inside) in °C
62
+
63
+ Returns:
64
+ float: Heat gain in Watts
65
+ """
66
+ return self.air_heat_factor * volume * air_changes * temp_diff
67
+
68
+ def calculate_internal_heat_gain(self, num_people, has_kitchen=False, equipment_watts=0):
69
+ """
70
+ Calculate internal heat gain from people, kitchen, and equipment.
71
+
72
+ Args:
73
+ num_people (int): Number of occupants
74
+ has_kitchen (bool): Whether the space includes a kitchen
75
+ equipment_watts (float): Additional equipment heat gain in Watts
76
+
77
+ Returns:
78
+ float: Heat gain in Watts
79
+ """
80
+ people_gain = num_people * self.heat_gain_per_person
81
+ kitchen_gain = self.heat_gain_kitchen if has_kitchen else 0
82
+ return people_gain + kitchen_gain + equipment_watts
83
+
84
+ def get_solar_heat_gain_factor(self, orientation, glass_type, daily_range, latitude='medium'):
85
+ """
86
+ Get the Solar Heat Gain Factor based on orientation, glass type, and climate.
87
+
88
+ Args:
89
+ orientation (str): Window orientation ('north', 'east', 'south', 'west')
90
+ glass_type (str): Type of glass ('single', 'double', 'low_e')
91
+ daily_range (str): Daily temperature range ('low', 'medium', 'high')
92
+ latitude (str): Latitude category ('low', 'medium', 'high')
93
+
94
+ Returns:
95
+ float: Solar Heat Gain Factor in W/m²
96
+ """
97
+ # This is a simplified version - in a real implementation, this would use lookup tables
98
+ # based on the ASHRAE data
99
+
100
+ # Base values for single glass at medium latitude
101
+ base_values = {
102
+ 'north': 200,
103
+ 'east': 550,
104
+ 'south': 350,
105
+ 'west': 550,
106
+ 'horizontal': 650
107
+ }
108
+
109
+ # Adjustments for glass type
110
+ glass_factors = {
111
+ 'single': 1.0,
112
+ 'double': 0.85,
113
+ 'low_e': 0.65
114
+ }
115
+
116
+ # Adjustments for latitude
117
+ latitude_factors = {
118
+ 'low': 1.1, # Closer to equator
119
+ 'medium': 1.0, # Mid latitudes
120
+ 'high': 0.9 # Closer to poles
121
+ }
122
+
123
+ # Adjustments for daily temperature range
124
+ range_factors = {
125
+ 'low': 0.95, # Less than 8.5°C
126
+ 'medium': 1.0, # Between 8.5°C and 14°C
127
+ 'high': 1.05 # Over 14°C
128
+ }
129
+
130
+ # Calculate the adjusted SHGF
131
+ base_value = base_values.get(orientation.lower(), 350) # Default to south if not found
132
+ glass_factor = glass_factors.get(glass_type.lower(), 1.0)
133
+ latitude_factor = latitude_factors.get(latitude.lower(), 1.0)
134
+ range_factor = range_factors.get(daily_range.lower(), 1.0)
135
+
136
+ return base_value * glass_factor * latitude_factor * range_factor
137
+
138
+ def calculate_total_cooling_load(self, building_components, windows, infiltration, internal_gains):
139
+ """
140
+ Calculate the total cooling load including latent load.
141
+
142
+ Args:
143
+ building_components (list): List of dicts with 'area', 'u_value', and 'temp_diff' for each component
144
+ windows (list): List of dicts with 'area', 'orientation', 'glass_type', 'shading', etc.
145
+ infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
146
+ internal_gains (dict): Dict with 'num_people', 'has_kitchen', and 'equipment_watts'
147
+
148
+ Returns:
149
+ dict: Dictionary with sensible load, latent load, and total cooling load in Watts
150
+ """
151
+ # Calculate conduction heat gain through building components
152
+ conduction_gain = sum(
153
+ self.calculate_conduction_heat_gain(comp['area'], comp['u_value'], comp['temp_diff'])
154
+ for comp in building_components
155
+ )
156
+
157
+ # Calculate solar and conduction heat gain through windows
158
+ window_conduction_gain = 0
159
+ window_solar_gain = 0
160
+
161
+ for window in windows:
162
+ # Conduction through glass
163
+ window_conduction_gain += self.calculate_conduction_heat_gain(
164
+ window['area'], window['u_value'], window['temp_diff']
165
+ )
166
+
167
+ # Solar radiation through glass
168
+ shgf = self.get_solar_heat_gain_factor(
169
+ window['orientation'],
170
+ window['glass_type'],
171
+ window.get('daily_range', 'medium'),
172
+ window.get('latitude', 'medium')
173
+ )
174
+
175
+ shading_value = window.get('shading', 0.0)
176
+ if shading_value == 'none' or shading_value == '':
177
+ shading_value = 0.0
178
+ shade_factor = 1.0 - float(shading_value)
179
+ window_solar_gain += self.calculate_solar_heat_gain(window['area'], shgf, shade_factor)
180
+
181
+ # Calculate infiltration heat gain
182
+ infiltration_gain = self.calculate_infiltration_heat_gain(
183
+ infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
184
+ )
185
+
186
+ # Calculate internal heat gain
187
+ internal_gain = self.calculate_internal_heat_gain(
188
+ internal_gains['num_people'],
189
+ internal_gains.get('has_kitchen', False),
190
+ internal_gains.get('equipment_watts', 0)
191
+ )
192
+
193
+ # Calculate sensible cooling load
194
+ sensible_load = conduction_gain + window_conduction_gain + window_solar_gain + infiltration_gain + internal_gain
195
+
196
+ # Calculate total cooling load (including latent load)
197
+ latent_load = sensible_load * 0.3 # 30% of sensible load for latent load
198
+ total_load = sensible_load * 1.3 # Factor of 1.3 to account for latent load
199
+
200
+ return {
201
+ 'conduction_gain': conduction_gain,
202
+ 'window_conduction_gain': window_conduction_gain,
203
+ 'window_solar_gain': window_solar_gain,
204
+ 'infiltration_gain': infiltration_gain,
205
+ 'internal_gain': internal_gain,
206
+ 'sensible_load': sensible_load,
207
+ 'latent_load': latent_load,
208
+ 'total_load': total_load
209
+ }
210
+
211
+
212
+ # Example usage
213
+ if __name__ == "__main__":
214
+ calculator = CoolingLoadCalculator()
215
+
216
+ # Example data for a simple room
217
+ building_components = [
218
+ {'area': 20, 'u_value': 0.6, 'temp_diff': 11}, # Floor
219
+ {'area': 50, 'u_value': 1.88, 'temp_diff': 11}, # Walls
220
+ {'area': 20, 'u_value': 0.46, 'temp_diff': 11} # Ceiling
221
+ ]
222
+
223
+ windows = [
224
+ {'area': 4, 'orientation': 'north', 'glass_type': 'single', 'u_value': 5.8, 'temp_diff': 11, 'shading': 0.5},
225
+ {'area': 4, 'orientation': 'east', 'glass_type': 'single', 'u_value': 5.8, 'temp_diff': 11, 'shading': 0.0},
226
+ {'area': 4, 'orientation': 'west', 'glass_type': 'single', 'u_value': 5.8, 'temp_diff': 11, 'shading': 0.0}
227
+ ]
228
+
229
+ infiltration = {'volume': 60, 'air_changes': 0.5, 'temp_diff': 11}
230
+
231
+ internal_gains = {'num_people': 4, 'has_kitchen': True, 'equipment_watts': 500}
232
+
233
+ result = calculator.calculate_total_cooling_load(building_components, windows, infiltration, internal_gains)
234
+
235
+ print("Cooling Load Calculation Results:")
236
+ for key, value in result.items():
237
+ print(f"{key}: {value:.2f} W")
heating_load.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ASHRAE Heating Load Calculation Module
3
+
4
+ This module implements the ASHRAE method for calculating heating loads in residential buildings.
5
+ It calculates the heat loss from the building envelope and unwanted ventilation/infiltration.
6
+ """
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ class HeatingLoadCalculator:
13
+ """
14
+ A class to calculate heating loads using the ASHRAE method.
15
+ """
16
+
17
+ def __init__(self):
18
+ """Initialize the heating load calculator with default values."""
19
+ # Specific heat capacity of air × density of air
20
+ self.air_heat_factor = 0.33
21
+
22
+ def calculate_conduction_heat_loss(self, area, u_value, temp_diff):
23
+ """
24
+ Calculate conduction heat loss through building components.
25
+
26
+ Args:
27
+ area (float): Area of the building component in m²
28
+ u_value (float): U-value of the component in W/m²°C
29
+ temp_diff (float): Temperature difference (inside - outside) in °C
30
+
31
+ Returns:
32
+ float: Heat loss in Watts
33
+ """
34
+ return area * u_value * temp_diff
35
+
36
+ def calculate_infiltration_heat_loss(self, volume, air_changes, temp_diff):
37
+ """
38
+ Calculate heat loss due to infiltration and ventilation.
39
+
40
+ Args:
41
+ volume (float): Volume of the space in m³
42
+ air_changes (float): Number of air changes per hour
43
+ temp_diff (float): Temperature difference (inside - outside) in °C
44
+
45
+ Returns:
46
+ float: Heat loss in Watts
47
+ """
48
+ return self.air_heat_factor * volume * air_changes * temp_diff
49
+
50
+ def calculate_annual_heating_energy(self, total_heat_loss, heating_degree_days, correction_factor=1.0):
51
+ """
52
+ Calculate annual heating energy requirement using heating degree days.
53
+
54
+ Args:
55
+ total_heat_loss (float): Total heat loss in Watts
56
+ heating_degree_days (float): Number of heating degree days
57
+ correction_factor (float): Correction factor for occupancy
58
+
59
+ Returns:
60
+ float: Annual heating energy in kWh
61
+ """
62
+ # Convert W to kW
63
+ heat_loss_kw = total_heat_loss / 1000
64
+
65
+ # Calculate annual heating energy (kWh)
66
+ # 24 hours in a day
67
+ annual_energy = heat_loss_kw * 24 * heating_degree_days * correction_factor
68
+
69
+ return annual_energy
70
+
71
+ def get_outdoor_design_temperature(self, location):
72
+ """
73
+ Get the outdoor design temperature for a location.
74
+
75
+ Args:
76
+ location (str): Location name
77
+
78
+ Returns:
79
+ float: Outdoor design temperature in °C
80
+ """
81
+ # This is a simplified version - in a real implementation, this would use lookup tables
82
+ # based on the AIRAH Design Data Manual
83
+
84
+ # Example data for Australian locations
85
+ temperatures = {
86
+ 'sydney': 7.0,
87
+ 'melbourne': 4.0,
88
+ 'brisbane': 9.0,
89
+ 'perth': 7.0,
90
+ 'adelaide': 5.0,
91
+ 'hobart': 2.0,
92
+ 'darwin': 15.0,
93
+ 'canberra': -1.0,
94
+ 'mildura': 4.5
95
+ }
96
+
97
+ return temperatures.get(location.lower(), 5.0) # Default to 5°C if location not found
98
+
99
+ def get_heating_degree_days(self, location, base_temp=18):
100
+ """
101
+ Get the heating degree days for a location.
102
+
103
+ Args:
104
+ location (str): Location name
105
+ base_temp (int): Base temperature for HDD calculation (default: 18°C)
106
+
107
+ Returns:
108
+ float: Heating degree days
109
+ """
110
+ # This is a simplified version - in a real implementation, this would use lookup tables
111
+ # or API data from Bureau of Meteorology
112
+
113
+ # Example data for Australian locations with base temperature of 18°C
114
+ hdd_data = {
115
+ 'sydney': 740,
116
+ 'melbourne': 1400,
117
+ 'brisbane': 320,
118
+ 'perth': 760,
119
+ 'adelaide': 1100,
120
+ 'hobart': 1800,
121
+ 'darwin': 0,
122
+ 'canberra': 2000,
123
+ 'mildura': 1200
124
+ }
125
+
126
+ return hdd_data.get(location.lower(), 1000) # Default to 1000 if location not found
127
+
128
+ def get_occupancy_correction_factor(self, occupancy_type):
129
+ """
130
+ Get the correction factor for occupancy type.
131
+
132
+ Args:
133
+ occupancy_type (str): Type of occupancy
134
+
135
+ Returns:
136
+ float: Correction factor
137
+ """
138
+ # Correction factors based on occupancy patterns
139
+ factors = {
140
+ 'continuous': 1.0, # Continuously heated
141
+ 'intermittent': 0.8, # Heated during occupied hours
142
+ 'night_setback': 0.9, # Temperature setback at night
143
+ 'weekend_off': 0.85, # Heating off during weekends
144
+ 'vacation_home': 0.6 # Occasionally occupied
145
+ }
146
+
147
+ return factors.get(occupancy_type.lower(), 1.0) # Default to continuous if not found
148
+
149
+ def calculate_total_heating_load(self, building_components, infiltration):
150
+ """
151
+ Calculate the total peak heating load.
152
+
153
+ Args:
154
+ building_components (list): List of dicts with 'area', 'u_value', and 'temp_diff' for each component
155
+ infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
156
+
157
+ Returns:
158
+ dict: Dictionary with component heat losses and total heating load in Watts
159
+ """
160
+ # Calculate conduction heat loss through building components
161
+ component_losses = {}
162
+ total_conduction_loss = 0
163
+
164
+ for comp in building_components:
165
+ name = comp.get('name', f"Component {len(component_losses) + 1}")
166
+ loss = self.calculate_conduction_heat_loss(comp['area'], comp['u_value'], comp['temp_diff'])
167
+ component_losses[name] = loss
168
+ total_conduction_loss += loss
169
+
170
+ # Calculate infiltration heat loss
171
+ infiltration_loss = self.calculate_infiltration_heat_loss(
172
+ infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
173
+ )
174
+
175
+ # Calculate total heating load
176
+ total_load = total_conduction_loss + infiltration_loss
177
+
178
+ return {
179
+ 'component_losses': component_losses,
180
+ 'total_conduction_loss': total_conduction_loss,
181
+ 'infiltration_loss': infiltration_loss,
182
+ 'total_load': total_load
183
+ }
184
+
185
+ def calculate_annual_heating_requirement(self, total_load, location, occupancy_type='continuous', base_temp=18):
186
+ """
187
+ Calculate the annual heating energy requirement.
188
+
189
+ Args:
190
+ total_load (float): Total heating load in Watts
191
+ location (str): Location name
192
+ occupancy_type (str): Type of occupancy
193
+ base_temp (int): Base temperature for HDD calculation
194
+
195
+ Returns:
196
+ dict: Dictionary with annual heating energy in kWh and related factors
197
+ """
198
+ # Get heating degree days for the location
199
+ hdd = self.get_heating_degree_days(location, base_temp)
200
+
201
+ # Get correction factor for occupancy
202
+ correction_factor = self.get_occupancy_correction_factor(occupancy_type)
203
+
204
+ # Calculate annual heating energy
205
+ annual_energy = self.calculate_annual_heating_energy(total_load, hdd, correction_factor)
206
+
207
+ return {
208
+ 'heating_degree_days': hdd,
209
+ 'correction_factor': correction_factor,
210
+ 'annual_energy_kwh': annual_energy,
211
+ 'annual_energy_mj': annual_energy * 3.6 # Convert kWh to MJ
212
+ }
213
+
214
+
215
+ # Example usage
216
+ if __name__ == "__main__":
217
+ calculator = HeatingLoadCalculator()
218
+
219
+ # Example data for a simple room in Mildura
220
+ building_components = [
221
+ {'name': 'Floor', 'area': 50, 'u_value': 1.47, 'temp_diff': 16.5}, # Concrete slab
222
+ {'name': 'Walls', 'area': 80, 'u_value': 1.5, 'temp_diff': 16.5}, # External walls
223
+ {'name': 'Ceiling', 'area': 50, 'u_value': 0.9, 'temp_diff': 16.5}, # Ceiling
224
+ {'name': 'Windows', 'area': 8, 'u_value': 5.8, 'temp_diff': 16.5} # Windows
225
+ ]
226
+
227
+ infiltration = {'volume': 125, 'air_changes': 0.5, 'temp_diff': 16.5}
228
+
229
+ # Calculate peak heating load
230
+ result = calculator.calculate_total_heating_load(building_components, infiltration)
231
+
232
+ print("Heating Load Calculation Results:")
233
+ for key, value in result.items():
234
+ if key == 'component_losses':
235
+ print("Component Losses:")
236
+ for comp, loss in value.items():
237
+ print(f" {comp}: {loss:.2f} W")
238
+ else:
239
+ print(f"{key}: {value:.2f} W")
240
+
241
+ # Calculate annual heating requirement
242
+ annual_result = calculator.calculate_annual_heating_requirement(result['total_load'], 'mildura')
243
+
244
+ print("\nAnnual Heating Requirement:")
245
+ for key, value in annual_result.items():
246
+ print(f"{key}: {value:.2f}")
pages/cooling_calculator.py ADDED
@@ -0,0 +1,1636 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cooling Load Calculator Page
3
+
4
+ This module implements the cooling load calculator interface for the HVAC Load Calculator web application.
5
+ It provides a step-by-step form for inputting building information and calculates cooling loads
6
+ using the ASHRAE method.
7
+ """
8
+
9
+ import streamlit as st
10
+ import pandas as pd
11
+ import numpy as np
12
+ import plotly.express as px
13
+ import plotly.graph_objects as go
14
+ import json
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+
20
+ # Add the parent directory to sys.path to import modules
21
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
+
23
+ # Import custom modules
24
+ from cooling_load import CoolingLoadCalculator
25
+ from reference_data import ReferenceData
26
+ from utils.validation import validate_input, ValidationWarning
27
+ from utils.export import export_data
28
+
29
+
30
+ def load_session_state():
31
+ """Initialize or load session state variables."""
32
+ # Initialize session state for form data
33
+ if 'cooling_form_data' not in st.session_state:
34
+ st.session_state.cooling_form_data = {
35
+ 'building_info': {},
36
+ 'building_envelope': {},
37
+ 'windows': {},
38
+ 'internal_loads': {},
39
+ 'ventilation': {},
40
+ 'results': {}
41
+ }
42
+
43
+ # Initialize session state for validation warnings
44
+ if 'cooling_warnings' not in st.session_state:
45
+ st.session_state.cooling_warnings = {
46
+ 'building_info': [],
47
+ 'building_envelope': [],
48
+ 'windows': [],
49
+ 'internal_loads': [],
50
+ 'ventilation': []
51
+ }
52
+
53
+ # Initialize session state for form completion status
54
+ if 'cooling_completed' not in st.session_state:
55
+ st.session_state.cooling_completed = {
56
+ 'building_info': False,
57
+ 'building_envelope': False,
58
+ 'windows': False,
59
+ 'internal_loads': False,
60
+ 'ventilation': False
61
+ }
62
+
63
+ # Initialize session state for calculation results
64
+ if 'cooling_results' not in st.session_state:
65
+ st.session_state.cooling_results = None
66
+
67
+
68
+ def building_info_form(ref_data):
69
+ """
70
+ Form for building information.
71
+
72
+ Args:
73
+ ref_data: Reference data object
74
+ """
75
+ st.subheader("Building Information")
76
+ st.write("Enter general building information, location, and design temperatures.")
77
+
78
+ # Get location options from reference data
79
+ location_options = {loc_id: loc_data['name'] for loc_id, loc_data in ref_data.locations.items()}
80
+
81
+ col1, col2 = st.columns(2)
82
+
83
+ with col1:
84
+ # Building name
85
+ building_name = st.text_input(
86
+ "Building Name",
87
+ value=st.session_state.cooling_form_data['building_info'].get('building_name', ''),
88
+ help="Enter a name for this building or project"
89
+ )
90
+
91
+ # Location selection
92
+ location = st.selectbox(
93
+ "Location",
94
+ options=list(location_options.keys()),
95
+ format_func=lambda x: location_options[x],
96
+ index=list(location_options.keys()).index(st.session_state.cooling_form_data['building_info'].get('location', 'sydney')) if st.session_state.cooling_form_data['building_info'].get('location') in location_options else 0,
97
+ help="Select the location of the building"
98
+ )
99
+
100
+ # Get climate data for selected location
101
+ location_data = ref_data.get_location_data(location)
102
+
103
+ # Indoor design temperature
104
+ indoor_temp = st.number_input(
105
+ "Indoor Design Temperature (°C)",
106
+ value=float(st.session_state.cooling_form_data['building_info'].get('indoor_temp', 24.0)),
107
+ min_value=18.0,
108
+ max_value=30.0,
109
+ step=0.5,
110
+ help="Recommended indoor design temperature for cooling is 24°C"
111
+ )
112
+
113
+ with col2:
114
+ # Building type
115
+ building_type = st.selectbox(
116
+ "Building Type",
117
+ options=["Residential", "Small Office", "Educational", "Other"],
118
+ index=["Residential", "Small Office", "Educational", "Other"].index(st.session_state.cooling_form_data['building_info'].get('building_type', 'Residential')),
119
+ help="Select the type of building"
120
+ )
121
+
122
+ # Outdoor design temperature (with default from location data)
123
+ outdoor_temp = st.number_input(
124
+ "Outdoor Design Temperature (°C)",
125
+ value=float(st.session_state.cooling_form_data['building_info'].get('outdoor_temp', location_data['summer_design_temp'])),
126
+ min_value=25.0,
127
+ max_value=45.0,
128
+ step=0.5,
129
+ help=f"Default value is based on selected location ({location_data['name']})"
130
+ )
131
+
132
+ # Daily temperature range
133
+ daily_range_options = {
134
+ "low": "Low (< 8.5°C)",
135
+ "medium": "Medium (8.5-14°C)",
136
+ "high": "High (> 14°C)"
137
+ }
138
+ daily_range = st.selectbox(
139
+ "Daily Temperature Range",
140
+ options=list(daily_range_options.keys()),
141
+ format_func=lambda x: daily_range_options[x],
142
+ index=list(daily_range_options.keys()).index(st.session_state.cooling_form_data['building_info'].get('daily_range', location_data['daily_temp_range'])),
143
+ help="Daily temperature range affects solar heat gain calculations"
144
+ )
145
+
146
+ # Building dimensions
147
+ st.subheader("Building Dimensions")
148
+
149
+ col1, col2, col3 = st.columns(3)
150
+
151
+ with col1:
152
+ length = st.number_input(
153
+ "Length (m)",
154
+ value=float(st.session_state.cooling_form_data['building_info'].get('length', 10.0)),
155
+ min_value=1.0,
156
+ step=0.1,
157
+ help="Building length in meters"
158
+ )
159
+
160
+ with col2:
161
+ width = st.number_input(
162
+ "Width (m)",
163
+ value=float(st.session_state.cooling_form_data['building_info'].get('width', 8.0)),
164
+ min_value=1.0,
165
+ step=0.1,
166
+ help="Building width in meters"
167
+ )
168
+
169
+ with col3:
170
+ height = st.number_input(
171
+ "Height (m)",
172
+ value=float(st.session_state.cooling_form_data['building_info'].get('height', 2.7)),
173
+ min_value=1.0,
174
+ step=0.1,
175
+ help="Floor-to-ceiling height in meters"
176
+ )
177
+
178
+ # Calculate floor area and volume
179
+ floor_area = length * width
180
+ volume = floor_area * height
181
+
182
+ st.info(f"Floor Area: {floor_area:.2f} m² | Volume: {volume:.2f} m³")
183
+
184
+ # Save form data to session state
185
+ form_data = {
186
+ 'building_name': building_name,
187
+ 'building_type': building_type,
188
+ 'location': location,
189
+ 'location_name': location_data['name'],
190
+ 'indoor_temp': indoor_temp,
191
+ 'outdoor_temp': outdoor_temp,
192
+ 'daily_range': daily_range,
193
+ 'length': length,
194
+ 'width': width,
195
+ 'height': height,
196
+ 'floor_area': floor_area,
197
+ 'volume': volume,
198
+ 'temp_diff': outdoor_temp - indoor_temp
199
+ }
200
+
201
+ # Validate inputs
202
+ warnings = []
203
+
204
+ # Check if building name is provided
205
+ if not building_name:
206
+ warnings.append(ValidationWarning("Building name is empty", "Consider adding a building name for reference"))
207
+
208
+ # Check if temperature difference is reasonable
209
+ if form_data['temp_diff'] <= 0:
210
+ warnings.append(ValidationWarning(
211
+ "Invalid temperature difference",
212
+ "Outdoor temperature should be higher than indoor temperature for cooling load calculation",
213
+ is_critical=True
214
+ ))
215
+
216
+ # Check if dimensions are reasonable
217
+ if floor_area > 500:
218
+ warnings.append(ValidationWarning(
219
+ "Large floor area",
220
+ "Floor area exceeds 500 m², verify if this is correct for a residential building"
221
+ ))
222
+
223
+ if height < 2.4 or height > 3.5:
224
+ warnings.append(ValidationWarning(
225
+ "Unusual ceiling height",
226
+ "Typical residential ceiling heights are between 2.4m and 3.5m"
227
+ ))
228
+
229
+ # Save warnings to session state
230
+ st.session_state.cooling_warnings['building_info'] = warnings
231
+
232
+ # Display warnings if any
233
+ if warnings:
234
+ st.warning("Please review the following warnings:")
235
+ for warning in warnings:
236
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
237
+ st.write(f" Suggestion: {warning.suggestion}")
238
+
239
+ # Save form data regardless of warnings
240
+ st.session_state.cooling_form_data['building_info'] = form_data
241
+
242
+ # Mark this step as completed if there are no critical warnings
243
+ st.session_state.cooling_completed['building_info'] = not any(w.is_critical for w in warnings)
244
+
245
+ # Navigation buttons
246
+ col1, col2 = st.columns([1, 1])
247
+
248
+ with col2:
249
+ next_button = st.button("Next: Building Envelope →", key="building_info_next")
250
+ if next_button:
251
+ st.session_state.cooling_active_tab = "building_envelope"
252
+ st.experimental_rerun()
253
+
254
+
255
+ def building_envelope_form(ref_data):
256
+ """
257
+ Form for building envelope information.
258
+
259
+ Args:
260
+ ref_data: Reference data object
261
+ """
262
+ st.subheader("Building Envelope")
263
+ st.write("Enter information about walls, roof, and floor construction.")
264
+
265
+ # Get building dimensions from previous step
266
+ building_info = st.session_state.cooling_form_data['building_info']
267
+ length = building_info.get('length', 10.0)
268
+ width = building_info.get('width', 8.0)
269
+ height = building_info.get('height', 2.7)
270
+ temp_diff = building_info.get('temp_diff', 11.0)
271
+
272
+ # Calculate default areas
273
+ default_wall_area = 2 * (length + width) * height
274
+ default_roof_area = length * width
275
+ default_floor_area = length * width
276
+
277
+ # Initialize envelope data if not already in session state
278
+ if 'walls' not in st.session_state.cooling_form_data['building_envelope']:
279
+ st.session_state.cooling_form_data['building_envelope']['walls'] = []
280
+
281
+ if 'roof' not in st.session_state.cooling_form_data['building_envelope']:
282
+ st.session_state.cooling_form_data['building_envelope']['roof'] = {}
283
+
284
+ if 'floor' not in st.session_state.cooling_form_data['building_envelope']:
285
+ st.session_state.cooling_form_data['building_envelope']['floor'] = {}
286
+
287
+ # Walls section
288
+ st.write("### Walls")
289
+
290
+ # Get wall material options from reference data
291
+ wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
292
+
293
+ # Display existing wall entries
294
+ if st.session_state.cooling_form_data['building_envelope']['walls']:
295
+ st.write("Current walls:")
296
+ walls_df = pd.DataFrame(st.session_state.cooling_form_data['building_envelope']['walls'])
297
+ walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
298
+ walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
299
+ walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
300
+ st.dataframe(walls_df)
301
+
302
+ # Add new wall form
303
+ st.write("Add a new wall:")
304
+
305
+ col1, col2 = st.columns(2)
306
+
307
+ with col1:
308
+ wall_name = st.text_input("Wall Name", value="", key="new_wall_name")
309
+ wall_material = st.selectbox(
310
+ "Wall Material",
311
+ options=list(wall_material_options.keys()),
312
+ format_func=lambda x: wall_material_options[x],
313
+ key="new_wall_material"
314
+ )
315
+
316
+ # Get material properties
317
+ material_data = ref_data.get_material_by_type("walls", wall_material)
318
+ u_value = material_data['u_value']
319
+
320
+ with col2:
321
+ wall_area = st.number_input(
322
+ "Wall Area (m²)",
323
+ value=default_wall_area / 4, # Default to 1/4 of total wall area as a starting point
324
+ min_value=0.1,
325
+ step=0.1,
326
+ key="new_wall_area"
327
+ )
328
+
329
+ st.write(f"Material U-Value: {u_value} W/m²°C")
330
+ st.write(f"Heat Transfer: {u_value * wall_area * temp_diff:.2f} W")
331
+
332
+ # Add wall button
333
+ if st.button("Add Wall"):
334
+ new_wall = {
335
+ 'name': wall_name if wall_name else f"Wall {len(st.session_state.cooling_form_data['building_envelope']['walls']) + 1}",
336
+ 'material_id': wall_material,
337
+ 'area': wall_area,
338
+ 'u_value': u_value,
339
+ 'temp_diff': temp_diff
340
+ }
341
+ st.session_state.cooling_form_data['building_envelope']['walls'].append(new_wall)
342
+ st.experimental_rerun()
343
+
344
+ # Roof section
345
+ st.write("### Roof")
346
+
347
+ # Get roof material options from reference data
348
+ roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
349
+
350
+ col1, col2 = st.columns(2)
351
+
352
+ with col1:
353
+ roof_material = st.selectbox(
354
+ "Roof Material",
355
+ options=list(roof_material_options.keys()),
356
+ format_func=lambda x: roof_material_options[x],
357
+ index=list(roof_material_options.keys()).index(st.session_state.cooling_form_data['building_envelope'].get('roof', {}).get('material_id', 'metal_deck_insulated')) if st.session_state.cooling_form_data['building_envelope'].get('roof', {}).get('material_id') in roof_material_options else 0
358
+ )
359
+
360
+ # Get material properties
361
+ material_data = ref_data.get_material_by_type("roofs", roof_material)
362
+ roof_u_value = material_data['u_value']
363
+
364
+ with col2:
365
+ roof_area = st.number_input(
366
+ "Roof Area (m²)",
367
+ value=float(st.session_state.cooling_form_data['building_envelope'].get('roof', {}).get('area', default_roof_area)),
368
+ min_value=0.1,
369
+ step=0.1
370
+ )
371
+
372
+ st.write(f"Material U-Value: {roof_u_value} W/m²°C")
373
+ st.write(f"Heat Transfer: {roof_u_value * roof_area * temp_diff:.2f} W")
374
+
375
+ # Save roof data
376
+ st.session_state.cooling_form_data['building_envelope']['roof'] = {
377
+ 'material_id': roof_material,
378
+ 'area': roof_area,
379
+ 'u_value': roof_u_value,
380
+ 'temp_diff': temp_diff
381
+ }
382
+
383
+ # Floor section
384
+ st.write("### Floor")
385
+
386
+ # Get floor material options from reference data
387
+ floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
388
+
389
+ col1, col2 = st.columns(2)
390
+
391
+ with col1:
392
+ floor_material = st.selectbox(
393
+ "Floor Material",
394
+ options=list(floor_material_options.keys()),
395
+ format_func=lambda x: floor_material_options[x],
396
+ index=list(floor_material_options.keys()).index(st.session_state.cooling_form_data['building_envelope'].get('floor', {}).get('material_id', 'concrete_slab_ground')) if st.session_state.cooling_form_data['building_envelope'].get('floor', {}).get('material_id') in floor_material_options else 0
397
+ )
398
+
399
+ # Get material properties
400
+ material_data = ref_data.get_material_by_type("floors", floor_material)
401
+ floor_u_value = material_data['u_value']
402
+
403
+ with col2:
404
+ floor_area = st.number_input(
405
+ "Floor Area (m²)",
406
+ value=float(st.session_state.cooling_form_data['building_envelope'].get('floor', {}).get('area', default_floor_area)),
407
+ min_value=0.1,
408
+ step=0.1
409
+ )
410
+
411
+ st.write(f"Material U-Value: {floor_u_value} W/m²°C")
412
+ st.write(f"Heat Transfer: {floor_u_value * floor_area * temp_diff:.2f} W")
413
+
414
+ # Save floor data
415
+ st.session_state.cooling_form_data['building_envelope']['floor'] = {
416
+ 'material_id': floor_material,
417
+ 'area': floor_area,
418
+ 'u_value': floor_u_value,
419
+ 'temp_diff': temp_diff
420
+ }
421
+
422
+ # Validate inputs
423
+ warnings = []
424
+
425
+ # Check if walls are defined
426
+ if not st.session_state.cooling_form_data['building_envelope']['walls']:
427
+ warnings.append(ValidationWarning(
428
+ "No walls defined",
429
+ "Add at least one wall to continue",
430
+ is_critical=True
431
+ ))
432
+
433
+ # Check if total wall area is reasonable
434
+ total_wall_area = sum(wall['area'] for wall in st.session_state.cooling_form_data['building_envelope']['walls'])
435
+ expected_wall_area = 2 * (length + width) * height
436
+
437
+ if total_wall_area < expected_wall_area * 0.8 or total_wall_area > expected_wall_area * 1.2:
438
+ warnings.append(ValidationWarning(
439
+ "Unusual wall area",
440
+ f"Total wall area ({total_wall_area:.2f} m²) differs significantly from the expected area ({expected_wall_area:.2f} m²) based on building dimensions"
441
+ ))
442
+
443
+ # Check if roof area matches floor area
444
+ if abs(roof_area - floor_area) > 1.0:
445
+ warnings.append(ValidationWarning(
446
+ "Roof area doesn't match floor area",
447
+ "For a simple building, roof area should approximately match floor area"
448
+ ))
449
+
450
+ # Save warnings to session state
451
+ st.session_state.cooling_warnings['building_envelope'] = warnings
452
+
453
+ # Display warnings if any
454
+ if warnings:
455
+ st.warning("Please review the following warnings:")
456
+ for warning in warnings:
457
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
458
+ st.write(f" Suggestion: {warning.suggestion}")
459
+
460
+ # Mark this step as completed if there are no critical warnings
461
+ st.session_state.cooling_completed['building_envelope'] = not any(w.is_critical for w in warnings)
462
+
463
+ # Navigation buttons
464
+ col1, col2 = st.columns([1, 1])
465
+
466
+ with col1:
467
+ prev_button = st.button("← Back: Building Information", key="building_envelope_prev")
468
+ if prev_button:
469
+ st.session_state.cooling_active_tab = "building_info"
470
+ st.experimental_rerun()
471
+
472
+ with col2:
473
+ next_button = st.button("Next: Windows & Doors →", key="building_envelope_next")
474
+ if next_button:
475
+ st.session_state.cooling_active_tab = "windows"
476
+ st.experimental_rerun()
477
+
478
+
479
+ def windows_form(ref_data):
480
+ """
481
+ Form for windows and doors information.
482
+
483
+ Args:
484
+ ref_data: Reference data object
485
+ """
486
+ st.subheader("Windows & Doors")
487
+ st.write("Enter information about windows and doors.")
488
+
489
+ # Get temperature difference from building info
490
+ temp_diff = st.session_state.cooling_form_data['building_info'].get('temp_diff', 11.0)
491
+ daily_range = st.session_state.cooling_form_data['building_info'].get('daily_range', 'medium')
492
+
493
+ # Initialize windows data if not already in session state
494
+ if 'windows' not in st.session_state.cooling_form_data['windows']:
495
+ st.session_state.cooling_form_data['windows']['windows'] = []
496
+
497
+ if 'doors' not in st.session_state.cooling_form_data['windows']:
498
+ st.session_state.cooling_form_data['windows']['doors'] = []
499
+
500
+ # Windows section
501
+ st.write("### Windows")
502
+
503
+ # Get glass type options from reference data
504
+ glass_type_options = {glass_id: glass_data['name'] for glass_id, glass_data in ref_data.glass_types.items()}
505
+
506
+ # Get shading options from reference data
507
+ shading_options = {shade_id: shade_data['name'] for shade_id, shade_data in ref_data.shading_factors.items()}
508
+
509
+ # Display existing window entries
510
+ if st.session_state.cooling_form_data['windows']['windows']:
511
+ st.write("Current windows:")
512
+ windows_df = pd.DataFrame(st.session_state.cooling_form_data['windows']['windows'])
513
+ windows_df['Glass Type'] = windows_df['glass_type'].map(lambda x: glass_type_options.get(x, "Unknown"))
514
+ windows_df['Shading'] = windows_df['shading'].map(lambda x: shading_options.get(x, "Unknown"))
515
+ windows_df = windows_df[['name', 'orientation', 'Glass Type', 'Shading', 'area', 'u_value']]
516
+ windows_df.columns = ['Name', 'Orientation', 'Glass Type', 'Shading', 'Area (m²)', 'U-Value (W/m²°C)']
517
+ st.dataframe(windows_df)
518
+
519
+ # Add new window form
520
+ st.write("Add a new window:")
521
+
522
+ col1, col2 = st.columns(2)
523
+
524
+ with col1:
525
+ window_name = st.text_input("Window Name", value="", key="new_window_name")
526
+
527
+ orientation = st.selectbox(
528
+ "Orientation",
529
+ options=["north", "east", "south", "west", "horizontal"],
530
+ key="new_window_orientation"
531
+ )
532
+
533
+ glass_type = st.selectbox(
534
+ "Glass Type",
535
+ options=list(glass_type_options.keys()),
536
+ format_func=lambda x: glass_type_options[x],
537
+ key="new_window_glass_type"
538
+ )
539
+
540
+ # Get glass properties
541
+ glass_data = ref_data.get_glass_type(glass_type)
542
+ window_u_value = glass_data['u_value']
543
+
544
+ with col2:
545
+ window_area = st.number_input(
546
+ "Window Area (m²)",
547
+ value=2.0,
548
+ min_value=0.1,
549
+ step=0.1,
550
+ key="new_window_area"
551
+ )
552
+
553
+ shading = st.selectbox(
554
+ "Shading",
555
+ options=list(shading_options.keys()),
556
+ format_func=lambda x: shading_options[x],
557
+ key="new_window_shading"
558
+ )
559
+
560
+ # Get shading factor
561
+ shading_data = ref_data.get_shading_factor(shading)
562
+ shade_factor = shading_data['factor']
563
+
564
+ st.write(f"Glass U-Value: {window_u_value} W/m²°C")
565
+ st.write(f"Conduction Heat Transfer: {window_u_value * window_area * temp_diff:.2f} W")
566
+
567
+ # Add window button
568
+ if st.button("Add Window"):
569
+ # Calculate solar heat gain factor
570
+ calculator = CoolingLoadCalculator()
571
+ shgf = calculator.get_solar_heat_gain_factor(
572
+ orientation=orientation,
573
+ glass_type=glass_type,
574
+ daily_range=daily_range
575
+ )
576
+
577
+ new_window = {
578
+ 'name': window_name if window_name else f"Window {len(st.session_state.cooling_form_data['windows']['windows']) + 1}",
579
+ 'orientation': orientation,
580
+ 'glass_type': glass_type,
581
+ 'shading': shading,
582
+ 'area': window_area,
583
+ 'u_value': window_u_value,
584
+ 'shgf': shgf,
585
+ 'shade_factor': shade_factor,
586
+ 'temp_diff': temp_diff
587
+ }
588
+ st.session_state.cooling_form_data['windows']['windows'].append(new_window)
589
+ st.experimental_rerun()
590
+
591
+ # Doors section
592
+ st.write("### Doors")
593
+
594
+ # Display existing door entries
595
+ if st.session_state.cooling_form_data['windows']['doors']:
596
+ st.write("Current doors:")
597
+ doors_df = pd.DataFrame(st.session_state.cooling_form_data['windows']['doors'])
598
+ doors_df = doors_df[['name', 'type', 'area', 'u_value']]
599
+ doors_df.columns = ['Name', 'Type', 'Area (m²)', 'U-Value (W/m²°C)']
600
+ st.dataframe(doors_df)
601
+
602
+ # Add new door form
603
+ st.write("Add a new door:")
604
+
605
+ col1, col2 = st.columns(2)
606
+
607
+ with col1:
608
+ door_name = st.text_input("Door Name", value="", key="new_door_name")
609
+
610
+ door_type = st.selectbox(
611
+ "Door Type",
612
+ options=["Solid wood", "Hollow core", "Glass", "Insulated"],
613
+ key="new_door_type"
614
+ )
615
+
616
+ # Set U-value based on door type
617
+ door_u_values = {
618
+ "Solid wood": 2.0,
619
+ "Hollow core": 2.5,
620
+ "Glass": 5.0,
621
+ "Insulated": 1.2
622
+ }
623
+ door_u_value = door_u_values[door_type]
624
+
625
+ with col2:
626
+ door_area = st.number_input(
627
+ "Door Area (m²)",
628
+ value=2.0,
629
+ min_value=0.1,
630
+ step=0.1,
631
+ key="new_door_area"
632
+ )
633
+
634
+ st.write(f"Door U-Value: {door_u_value} W/m²°C")
635
+ st.write(f"Heat Transfer: {door_u_value * door_area * temp_diff:.2f} W")
636
+
637
+ # Add door button
638
+ if st.button("Add Door"):
639
+ new_door = {
640
+ 'name': door_name if door_name else f"Door {len(st.session_state.cooling_form_data['windows']['doors']) + 1}",
641
+ 'type': door_type,
642
+ 'area': door_area,
643
+ 'u_value': door_u_value,
644
+ 'temp_diff': temp_diff
645
+ }
646
+ st.session_state.cooling_form_data['windows']['doors'].append(new_door)
647
+ st.experimental_rerun()
648
+
649
+ # Validate inputs
650
+ warnings = []
651
+
652
+ # Check if windows are defined
653
+ if not st.session_state.cooling_form_data['windows']['windows']:
654
+ warnings.append(ValidationWarning(
655
+ "No windows defined",
656
+ "Add at least one window to continue"
657
+ ))
658
+
659
+ # Check window-to-wall ratio
660
+ if st.session_state.cooling_form_data['windows']['windows']:
661
+ total_window_area = sum(window['area'] for window in st.session_state.cooling_form_data['windows']['windows'])
662
+ total_wall_area = sum(wall['area'] for wall in st.session_state.cooling_form_data['building_envelope']['walls'])
663
+ window_wall_ratio = total_window_area / total_wall_area if total_wall_area > 0 else 0
664
+
665
+ if window_wall_ratio > 0.6:
666
+ warnings.append(ValidationWarning(
667
+ "High window-to-wall ratio",
668
+ f"Window-to-wall ratio is {window_wall_ratio:.2f}, which is unusually high. Typical ratios are 0.2-0.4."
669
+ ))
670
+
671
+ # Save warnings to session state
672
+ st.session_state.cooling_warnings['windows'] = warnings
673
+
674
+ # Display warnings if any
675
+ if warnings:
676
+ st.warning("Please review the following warnings:")
677
+ for warning in warnings:
678
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
679
+ st.write(f" Suggestion: {warning.suggestion}")
680
+
681
+ # Mark this step as completed if there are no critical warnings
682
+ st.session_state.cooling_completed['windows'] = not any(w.is_critical for w in warnings)
683
+
684
+ # Navigation buttons
685
+ col1, col2 = st.columns([1, 1])
686
+
687
+ with col1:
688
+ prev_button = st.button("← Back: Building Envelope", key="windows_prev")
689
+ if prev_button:
690
+ st.session_state.cooling_active_tab = "building_envelope"
691
+ st.experimental_rerun()
692
+
693
+ with col2:
694
+ next_button = st.button("Next: Internal Loads →", key="windows_next")
695
+ if next_button:
696
+ st.session_state.cooling_active_tab = "internal_loads"
697
+ st.experimental_rerun()
698
+
699
+
700
+ def internal_loads_form(ref_data):
701
+ """
702
+ Form for internal loads information.
703
+
704
+ Args:
705
+ ref_data: Reference data object
706
+ """
707
+ st.subheader("Internal Loads")
708
+ st.write("Enter information about occupants, lighting, and equipment.")
709
+
710
+ # Initialize internal loads data if not already in session state
711
+ if 'occupants' not in st.session_state.cooling_form_data['internal_loads']:
712
+ st.session_state.cooling_form_data['internal_loads']['occupants'] = {
713
+ 'count': 4,
714
+ 'activity_level': 'seated_resting'
715
+ }
716
+
717
+ if 'lighting' not in st.session_state.cooling_form_data['internal_loads']:
718
+ st.session_state.cooling_form_data['internal_loads']['lighting'] = {
719
+ 'type': 'led',
720
+ 'power_density': 5.0 # W/m²
721
+ }
722
+
723
+ if 'appliances' not in st.session_state.cooling_form_data['internal_loads']:
724
+ st.session_state.cooling_form_data['internal_loads']['appliances'] = {
725
+ 'kitchen': True,
726
+ 'living_room': True,
727
+ 'bedroom': True,
728
+ 'office': False
729
+ }
730
+
731
+ # Occupants section
732
+ st.write("### Occupants")
733
+
734
+ col1, col2 = st.columns(2)
735
+
736
+ with col1:
737
+ occupant_count = st.number_input(
738
+ "Number of Occupants",
739
+ value=int(st.session_state.cooling_form_data['internal_loads']['occupants'].get('count', 4)),
740
+ min_value=1,
741
+ step=1
742
+ )
743
+
744
+ with col2:
745
+ # Get activity level options from reference data
746
+ activity_options = {act_id: act_data['name'] for act_id, act_data in ref_data.internal_loads['people'].items()}
747
+
748
+ activity_level = st.selectbox(
749
+ "Activity Level",
750
+ options=list(activity_options.keys()),
751
+ format_func=lambda x: activity_options[x],
752
+ index=list(activity_options.keys()).index(st.session_state.cooling_form_data['internal_loads']['occupants'].get('activity_level', 'seated_resting')) if st.session_state.cooling_form_data['internal_loads']['occupants'].get('activity_level') in activity_options else 0
753
+ )
754
+
755
+ # Get heat gain per person
756
+ activity_data = ref_data.get_internal_load('people', activity_level)
757
+ sensible_heat_pp = activity_data['sensible_heat']
758
+ latent_heat_pp = activity_data['latent_heat']
759
+ total_heat_pp = sensible_heat_pp + latent_heat_pp
760
+
761
+ st.write(f"Heat gain per person: {total_heat_pp} W ({sensible_heat_pp} W sensible + {latent_heat_pp} W latent)")
762
+ st.write(f"Total occupant heat gain: {total_heat_pp * occupant_count} W")
763
+
764
+ # Save occupants data
765
+ st.session_state.cooling_form_data['internal_loads']['occupants'] = {
766
+ 'count': occupant_count,
767
+ 'activity_level': activity_level,
768
+ 'sensible_heat_pp': sensible_heat_pp,
769
+ 'latent_heat_pp': latent_heat_pp,
770
+ 'total_heat_gain': total_heat_pp * occupant_count
771
+ }
772
+
773
+ # Lighting section
774
+ st.write("### Lighting")
775
+
776
+ col1, col2 = st.columns(2)
777
+
778
+ with col1:
779
+ # Get lighting type options from reference data
780
+ lighting_options = {light_id: light_data['name'] for light_id, light_data in ref_data.internal_loads['lighting'].items()}
781
+
782
+ lighting_type = st.selectbox(
783
+ "Lighting Type",
784
+ options=list(lighting_options.keys()),
785
+ format_func=lambda x: lighting_options[x],
786
+ index=list(lighting_options.keys()).index(st.session_state.cooling_form_data['internal_loads']['lighting'].get('type', 'led')) if st.session_state.cooling_form_data['internal_loads']['lighting'].get('type') in lighting_options else 0
787
+ )
788
+
789
+ with col2:
790
+ lighting_power_density = st.number_input(
791
+ "Lighting Power Density (W/m²)",
792
+ value=float(st.session_state.cooling_form_data['internal_loads']['lighting'].get('power_density', 5.0)),
793
+ min_value=1.0,
794
+ max_value=20.0,
795
+ step=0.5,
796
+ help="Typical values: Residential 5-10 W/m², Office 10-15 W/m²"
797
+ )
798
+
799
+ # Get lighting heat factor
800
+ lighting_data = ref_data.get_internal_load('lighting', lighting_type)
801
+ lighting_heat_factor = lighting_data['heat_factor']
802
+
803
+ # Calculate lighting heat gain
804
+ floor_area = st.session_state.cooling_form_data['building_info'].get('floor_area', 80.0)
805
+ lighting_heat_gain = lighting_power_density * floor_area * lighting_heat_factor
806
+
807
+ st.write(f"Lighting heat factor: {lighting_heat_factor}")
808
+ st.write(f"Total lighting heat gain: {lighting_heat_gain:.2f} W")
809
+
810
+ # Save lighting data
811
+ st.session_state.cooling_form_data['internal_loads']['lighting'] = {
812
+ 'type': lighting_type,
813
+ 'power_density': lighting_power_density,
814
+ 'heat_factor': lighting_heat_factor,
815
+ 'total_heat_gain': lighting_heat_gain
816
+ }
817
+
818
+ # Appliances section
819
+ st.write("### Appliances")
820
+
821
+ # Get appliance options from reference data
822
+ appliance_options = {app_id: app_data for app_id, app_data in ref_data.internal_loads['appliances'].items()}
823
+
824
+ col1, col2 = st.columns(2)
825
+
826
+ with col1:
827
+ has_kitchen = st.checkbox(
828
+ "Kitchen Appliances",
829
+ value=st.session_state.cooling_form_data['internal_loads']['appliances'].get('kitchen', True),
830
+ help=f"Heat gain: {appliance_options['kitchen']['heat_gain']} W"
831
+ )
832
+
833
+ has_living_room = st.checkbox(
834
+ "Living Room Appliances",
835
+ value=st.session_state.cooling_form_data['internal_loads']['appliances'].get('living_room', True),
836
+ help=f"Heat gain: {appliance_options['living_room']['heat_gain']} W"
837
+ )
838
+
839
+ with col2:
840
+ has_bedroom = st.checkbox(
841
+ "Bedroom Appliances",
842
+ value=st.session_state.cooling_form_data['internal_loads']['appliances'].get('bedroom', True),
843
+ help=f"Heat gain: {appliance_options['bedroom']['heat_gain']} W"
844
+ )
845
+
846
+ has_office = st.checkbox(
847
+ "Home Office Equipment",
848
+ value=st.session_state.cooling_form_data['internal_loads']['appliances'].get('office', False),
849
+ help=f"Heat gain: {appliance_options['office']['heat_gain']} W"
850
+ )
851
+
852
+ # Calculate appliance heat gain
853
+ appliance_heat_gain = 0
854
+ if has_kitchen:
855
+ appliance_heat_gain += appliance_options['kitchen']['heat_gain']
856
+ if has_living_room:
857
+ appliance_heat_gain += appliance_options['living_room']['heat_gain']
858
+ if has_bedroom:
859
+ appliance_heat_gain += appliance_options['bedroom']['heat_gain']
860
+ if has_office:
861
+ appliance_heat_gain += appliance_options['office']['heat_gain']
862
+
863
+ st.write(f"Total appliance heat gain: {appliance_heat_gain} W")
864
+
865
+ # Save appliances data
866
+ st.session_state.cooling_form_data['internal_loads']['appliances'] = {
867
+ 'kitchen': has_kitchen,
868
+ 'living_room': has_living_room,
869
+ 'bedroom': has_bedroom,
870
+ 'office': has_office,
871
+ 'total_heat_gain': appliance_heat_gain
872
+ }
873
+
874
+ # Calculate total internal heat gain
875
+ total_internal_gain = (
876
+ st.session_state.cooling_form_data['internal_loads']['occupants']['total_heat_gain'] +
877
+ st.session_state.cooling_form_data['internal_loads']['lighting']['total_heat_gain'] +
878
+ st.session_state.cooling_form_data['internal_loads']['appliances']['total_heat_gain']
879
+ )
880
+
881
+ st.info(f"Total Internal Heat Gain: {total_internal_gain:.2f} W")
882
+
883
+ # Save total internal gain
884
+ st.session_state.cooling_form_data['internal_loads']['total_internal_gain'] = total_internal_gain
885
+
886
+ # Validate inputs
887
+ warnings = []
888
+
889
+ # Check if occupant count is reasonable for the floor area
890
+ floor_area = st.session_state.cooling_form_data['building_info'].get('floor_area', 80.0)
891
+ area_per_person = floor_area / occupant_count if occupant_count > 0 else float('inf')
892
+
893
+ if area_per_person < 10:
894
+ warnings.append(ValidationWarning(
895
+ "High occupant density",
896
+ f"Area per person ({area_per_person:.2f} m²) is low. Typical residential values are 20-30 m² per person."
897
+ ))
898
+
899
+ # Check if lighting power density is reasonable
900
+ if lighting_power_density > 15:
901
+ warnings.append(ValidationWarning(
902
+ "High lighting power density",
903
+ "Lighting power density exceeds 15 W/m², which is high for residential buildings."
904
+ ))
905
+
906
+ # Save warnings to session state
907
+ st.session_state.cooling_warnings['internal_loads'] = warnings
908
+
909
+ # Display warnings if any
910
+ if warnings:
911
+ st.warning("Please review the following warnings:")
912
+ for warning in warnings:
913
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
914
+ st.write(f" Suggestion: {warning.suggestion}")
915
+
916
+ # Mark this step as completed if there are no critical warnings
917
+ st.session_state.cooling_completed['internal_loads'] = not any(w.is_critical for w in warnings)
918
+
919
+ # Navigation buttons
920
+ col1, col2 = st.columns([1, 1])
921
+
922
+ with col1:
923
+ prev_button = st.button("← Back: Windows & Doors", key="internal_loads_prev")
924
+ if prev_button:
925
+ st.session_state.cooling_active_tab = "windows"
926
+ st.experimental_rerun()
927
+
928
+ with col2:
929
+ next_button = st.button("Next: Ventilation →", key="internal_loads_next")
930
+ if next_button:
931
+ st.session_state.cooling_active_tab = "ventilation"
932
+ st.experimental_rerun()
933
+
934
+
935
+ def ventilation_form(ref_data):
936
+ """
937
+ Form for ventilation and infiltration information.
938
+
939
+ Args:
940
+ ref_data: Reference data object
941
+ """
942
+ st.subheader("Ventilation & Infiltration")
943
+ st.write("Enter information about ventilation and infiltration rates.")
944
+
945
+ # Get building info
946
+ building_info = st.session_state.cooling_form_data['building_info']
947
+ volume = building_info.get('volume', 216.0)
948
+ temp_diff = building_info.get('temp_diff', 11.0)
949
+
950
+ # Initialize ventilation data if not already in session state
951
+ if 'infiltration' not in st.session_state.cooling_form_data['ventilation']:
952
+ st.session_state.cooling_form_data['ventilation']['infiltration'] = {
953
+ 'air_changes': 0.5
954
+ }
955
+
956
+ if 'ventilation' not in st.session_state.cooling_form_data['ventilation']:
957
+ st.session_state.cooling_form_data['ventilation']['ventilation'] = {
958
+ 'type': 'natural',
959
+ 'air_changes': 0.0
960
+ }
961
+
962
+ # Infiltration section
963
+ st.write("### Infiltration")
964
+ st.write("Infiltration is the unintended air leakage through the building envelope.")
965
+
966
+ infiltration_ach = st.slider(
967
+ "Infiltration Rate (air changes per hour)",
968
+ value=float(st.session_state.cooling_form_data['ventilation']['infiltration'].get('air_changes', 0.5)),
969
+ min_value=0.1,
970
+ max_value=2.0,
971
+ step=0.1,
972
+ help="Typical values: 0.5 ACH for modern construction, 1.0 ACH for average construction, 1.5+ ACH for older buildings"
973
+ )
974
+
975
+ # Calculate infiltration heat gain
976
+ infiltration_heat_gain = 0.33 * volume * infiltration_ach * temp_diff
977
+
978
+ st.write(f"Infiltration heat gain: {infiltration_heat_gain:.2f} W")
979
+
980
+ # Save infiltration data
981
+ st.session_state.cooling_form_data['ventilation']['infiltration'] = {
982
+ 'air_changes': infiltration_ach,
983
+ 'volume': volume,
984
+ 'temp_diff': temp_diff,
985
+ 'heat_gain': infiltration_heat_gain
986
+ }
987
+
988
+ # Ventilation section
989
+ st.write("### Ventilation")
990
+ st.write("Ventilation is the intentional introduction of outside air into the building.")
991
+
992
+ col1, col2 = st.columns(2)
993
+
994
+ with col1:
995
+ ventilation_type = st.selectbox(
996
+ "Ventilation Type",
997
+ options=["natural", "mechanical", "mixed"],
998
+ format_func=lambda x: x.capitalize(),
999
+ index=["natural", "mechanical", "mixed"].index(st.session_state.cooling_form_data['ventilation']['ventilation'].get('type', 'natural'))
1000
+ )
1001
+
1002
+ with col2:
1003
+ ventilation_ach = st.number_input(
1004
+ "Ventilation Rate (air changes per hour)",
1005
+ value=float(st.session_state.cooling_form_data['ventilation']['ventilation'].get('air_changes', 0.0)),
1006
+ min_value=0.0,
1007
+ max_value=5.0,
1008
+ step=0.1,
1009
+ help="Typical values: 0.35-1.0 ACH for residential buildings"
1010
+ )
1011
+
1012
+ # Calculate ventilation heat gain
1013
+ ventilation_heat_gain = 0.33 * volume * ventilation_ach * temp_diff
1014
+
1015
+ st.write(f"Ventilation heat gain: {ventilation_heat_gain:.2f} W")
1016
+
1017
+ # Save ventilation data
1018
+ st.session_state.cooling_form_data['ventilation']['ventilation'] = {
1019
+ 'type': ventilation_type,
1020
+ 'air_changes': ventilation_ach,
1021
+ 'volume': volume,
1022
+ 'temp_diff': temp_diff,
1023
+ 'heat_gain': ventilation_heat_gain
1024
+ }
1025
+
1026
+ # Calculate total ventilation and infiltration heat gain
1027
+ total_ventilation_gain = infiltration_heat_gain + ventilation_heat_gain
1028
+
1029
+ st.info(f"Total Ventilation & Infiltration Heat Gain: {total_ventilation_gain:.2f} W")
1030
+
1031
+ # Save total ventilation gain
1032
+ st.session_state.cooling_form_data['ventilation']['total_gain'] = total_ventilation_gain
1033
+
1034
+ # Validate inputs
1035
+ warnings = []
1036
+
1037
+ # Check if infiltration rate is reasonable
1038
+ if infiltration_ach < 0.3:
1039
+ warnings.append(ValidationWarning(
1040
+ "Low infiltration rate",
1041
+ "Infiltration rate below 0.3 ACH is unusually low for most buildings."
1042
+ ))
1043
+ elif infiltration_ach > 1.5:
1044
+ warnings.append(ValidationWarning(
1045
+ "High infiltration rate",
1046
+ "Infiltration rate above 1.5 ACH indicates a leaky building envelope."
1047
+ ))
1048
+
1049
+ # Check if ventilation rate is reasonable
1050
+ if ventilation_ach > 0 and ventilation_ach < 0.35:
1051
+ warnings.append(ValidationWarning(
1052
+ "Low ventilation rate",
1053
+ "Ventilation rate below 0.35 ACH may not provide adequate fresh air."
1054
+ ))
1055
+ elif ventilation_ach > 2.0:
1056
+ warnings.append(ValidationWarning(
1057
+ "High ventilation rate",
1058
+ "Ventilation rate above 2.0 ACH is unusually high for residential buildings."
1059
+ ))
1060
+
1061
+ # Save warnings to session state
1062
+ st.session_state.cooling_warnings['ventilation'] = warnings
1063
+
1064
+ # Display warnings if any
1065
+ if warnings:
1066
+ st.warning("Please review the following warnings:")
1067
+ for warning in warnings:
1068
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
1069
+ st.write(f" Suggestion: {warning.suggestion}")
1070
+
1071
+ # Mark this step as completed if there are no critical warnings
1072
+ st.session_state.cooling_completed['ventilation'] = not any(w.is_critical for w in warnings)
1073
+
1074
+ # Navigation buttons
1075
+ col1, col2 = st.columns([1, 1])
1076
+
1077
+ with col1:
1078
+ prev_button = st.button("← Back: Internal Loads", key="ventilation_prev")
1079
+ if prev_button:
1080
+ st.session_state.cooling_active_tab = "internal_loads"
1081
+ st.experimental_rerun()
1082
+
1083
+ with col2:
1084
+ calculate_button = st.button("Calculate Results →", key="ventilation_calculate")
1085
+ if calculate_button:
1086
+ # Calculate cooling load
1087
+ calculate_cooling_load()
1088
+ st.session_state.cooling_active_tab = "results"
1089
+ st.experimental_rerun()
1090
+
1091
+
1092
+ def calculate_cooling_load():
1093
+ """Calculate cooling load based on input data."""
1094
+ # Create calculator instance
1095
+ calculator = CoolingLoadCalculator()
1096
+
1097
+ # Get form data
1098
+ form_data = st.session_state.cooling_form_data
1099
+
1100
+ # Prepare building components for calculation
1101
+ building_components = []
1102
+
1103
+ # Add walls
1104
+ for wall in form_data['building_envelope'].get('walls', []):
1105
+ building_components.append({
1106
+ 'name': wall['name'],
1107
+ 'area': wall['area'],
1108
+ 'u_value': wall['u_value'],
1109
+ 'temp_diff': wall['temp_diff']
1110
+ })
1111
+
1112
+ # Add roof
1113
+ roof = form_data['building_envelope'].get('roof', {})
1114
+ if roof:
1115
+ building_components.append({
1116
+ 'name': 'Roof',
1117
+ 'area': roof['area'],
1118
+ 'u_value': roof['u_value'],
1119
+ 'temp_diff': roof['temp_diff']
1120
+ })
1121
+
1122
+ # Add floor
1123
+ floor = form_data['building_envelope'].get('floor', {})
1124
+ if floor:
1125
+ building_components.append({
1126
+ 'name': 'Floor',
1127
+ 'area': floor['area'],
1128
+ 'u_value': floor['u_value'],
1129
+ 'temp_diff': floor['temp_diff']
1130
+ })
1131
+
1132
+ # Prepare windows for calculation
1133
+ windows = []
1134
+ for window in form_data['windows'].get('windows', []):
1135
+ windows.append({
1136
+ 'name': window['name'],
1137
+ 'area': window['area'],
1138
+ 'u_value': window['u_value'],
1139
+ 'orientation': window['orientation'],
1140
+ 'glass_type': window['glass_type'],
1141
+ 'shading': window['shading'],
1142
+ 'shgf': window['shgf'],
1143
+ 'shade_factor': 1.0 - window['shade_factor'],
1144
+ 'temp_diff': window['temp_diff']
1145
+ })
1146
+
1147
+ # Add doors to building components
1148
+ for door in form_data['windows'].get('doors', []):
1149
+ building_components.append({
1150
+ 'name': door['name'],
1151
+ 'area': door['area'],
1152
+ 'u_value': door['u_value'],
1153
+ 'temp_diff': door['temp_diff']
1154
+ })
1155
+
1156
+ # Prepare infiltration data
1157
+ infiltration = form_data['ventilation'].get('infiltration', {})
1158
+ ventilation = form_data['ventilation'].get('ventilation', {})
1159
+
1160
+ infiltration_data = {
1161
+ 'volume': infiltration.get('volume', 0),
1162
+ 'air_changes': infiltration.get('air_changes', 0) + ventilation.get('air_changes', 0),
1163
+ 'temp_diff': infiltration.get('temp_diff', 0)
1164
+ }
1165
+
1166
+ # Prepare internal gains data
1167
+ internal_gains = {
1168
+ 'num_people': form_data['internal_loads'].get('occupants', {}).get('count', 0),
1169
+ 'has_kitchen': form_data['internal_loads'].get('appliances', {}).get('kitchen', False),
1170
+ 'equipment_watts': (
1171
+ form_data['internal_loads'].get('lighting', {}).get('total_heat_gain', 0) +
1172
+ form_data['internal_loads'].get('appliances', {}).get('total_heat_gain', 0) -
1173
+ (1000 if form_data['internal_loads'].get('appliances', {}).get('kitchen', False) else 0) # Subtract kitchen heat gain if included
1174
+ )
1175
+ }
1176
+
1177
+ # Calculate cooling load
1178
+ results = calculator.calculate_total_cooling_load(
1179
+ building_components=building_components,
1180
+ windows=windows,
1181
+ infiltration=infiltration_data,
1182
+ internal_gains=internal_gains
1183
+ )
1184
+
1185
+ # Save results to session state
1186
+ st.session_state.cooling_results = results
1187
+
1188
+ # Add timestamp
1189
+ st.session_state.cooling_results['timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1190
+
1191
+ # Add building info
1192
+ st.session_state.cooling_results['building_info'] = form_data['building_info']
1193
+
1194
+ return results
1195
+
1196
+
1197
+ def results_page():
1198
+ """Display calculation results."""
1199
+ st.subheader("Cooling Load Calculation Results")
1200
+
1201
+ # Check if results are available
1202
+ if not st.session_state.cooling_results:
1203
+ st.warning("No calculation results available. Please complete the input forms and calculate results.")
1204
+ return
1205
+
1206
+ # Get results
1207
+ results = st.session_state.cooling_results
1208
+
1209
+ # Display summary
1210
+ st.write("### Summary")
1211
+
1212
+ col1, col2 = st.columns(2)
1213
+
1214
+ with col1:
1215
+ st.metric("Sensible Cooling Load", f"{results['sensible_load']:.2f} W")
1216
+ st.metric("Total Cooling Load", f"{results['total_load']:.2f} W")
1217
+
1218
+ # Convert to kW
1219
+ total_load_kw = results['total_load'] / 1000
1220
+ st.metric("Total Cooling Load", f"{total_load_kw:.2f} kW")
1221
+
1222
+ with col2:
1223
+ st.metric("Latent Cooling Load", f"{results['latent_load']:.2f} W")
1224
+
1225
+ # Calculate cooling load per area
1226
+ floor_area = results['building_info'].get('floor_area', 80.0)
1227
+ cooling_load_per_area = results['total_load'] / floor_area
1228
+ st.metric("Cooling Load per Area", f"{cooling_load_per_area:.2f} W/m²")
1229
+
1230
+ # Equipment sizing recommendation
1231
+ # Add 10% safety factor
1232
+ recommended_size = total_load_kw * 1.1
1233
+ st.metric("Recommended Equipment Size", f"{recommended_size:.2f} kW")
1234
+
1235
+ # Display load breakdown
1236
+ st.write("### Load Breakdown")
1237
+
1238
+ # Prepare data for pie chart
1239
+ load_components = {
1240
+ 'Conduction (Opaque Surfaces)': results['conduction_gain'],
1241
+ 'Conduction (Windows)': results['window_conduction_gain'],
1242
+ 'Solar Radiation (Windows)': results['window_solar_gain'],
1243
+ 'Infiltration & Ventilation': results['infiltration_gain'],
1244
+ 'Internal Gains': results['internal_gain']
1245
+ }
1246
+
1247
+ # Create pie chart
1248
+ fig = px.pie(
1249
+ values=list(load_components.values()),
1250
+ names=list(load_components.keys()),
1251
+ title="Cooling Load Components",
1252
+ color_discrete_sequence=px.colors.qualitative.Set2
1253
+ )
1254
+
1255
+ st.plotly_chart(fig)
1256
+
1257
+ # Display load components in a table
1258
+ load_df = pd.DataFrame({
1259
+ 'Component': list(load_components.keys()),
1260
+ 'Load (W)': list(load_components.values()),
1261
+ 'Percentage (%)': [value / results['sensible_load'] * 100 for value in load_components.values()]
1262
+ })
1263
+
1264
+ st.dataframe(load_df.style.format({
1265
+ 'Load (W)': '{:.2f}',
1266
+ 'Percentage (%)': '{:.2f}'
1267
+ }))
1268
+
1269
+ # Display detailed results
1270
+ st.write("### Detailed Results")
1271
+
1272
+ # Create tabs for different result sections
1273
+ tabs = st.tabs([
1274
+ "Building Envelope",
1275
+ "Windows & Doors",
1276
+ "Internal Loads",
1277
+ "Ventilation"
1278
+ ])
1279
+
1280
+ with tabs[0]:
1281
+ st.subheader("Building Envelope Heat Gains")
1282
+
1283
+ # Get building components
1284
+ building_components = []
1285
+
1286
+ # Add walls
1287
+ for wall in st.session_state.cooling_form_data['building_envelope'].get('walls', []):
1288
+ building_components.append({
1289
+ 'Component': wall['name'],
1290
+ 'Area (m²)': wall['area'],
1291
+ 'U-Value (W/m²°C)': wall['u_value'],
1292
+ 'Temperature Difference (°C)': wall['temp_diff'],
1293
+ 'Heat Gain (W)': wall['area'] * wall['u_value'] * wall['temp_diff']
1294
+ })
1295
+
1296
+ # Add roof
1297
+ roof = st.session_state.cooling_form_data['building_envelope'].get('roof', {})
1298
+ if roof:
1299
+ building_components.append({
1300
+ 'Component': 'Roof',
1301
+ 'Area (m²)': roof['area'],
1302
+ 'U-Value (W/m²°C)': roof['u_value'],
1303
+ 'Temperature Difference (°C)': roof['temp_diff'],
1304
+ 'Heat Gain (W)': roof['area'] * roof['u_value'] * roof['temp_diff']
1305
+ })
1306
+
1307
+ # Add floor
1308
+ floor = st.session_state.cooling_form_data['building_envelope'].get('floor', {})
1309
+ if floor:
1310
+ building_components.append({
1311
+ 'Component': 'Floor',
1312
+ 'Area (m²)': floor['area'],
1313
+ 'U-Value (W/m²°C)': floor['u_value'],
1314
+ 'Temperature Difference (°C)': floor['temp_diff'],
1315
+ 'Heat Gain (W)': floor['area'] * floor['u_value'] * floor['temp_diff']
1316
+ })
1317
+
1318
+ # Create dataframe
1319
+ envelope_df = pd.DataFrame(building_components)
1320
+
1321
+ # Display table
1322
+ st.dataframe(envelope_df.style.format({
1323
+ 'Area (m²)': '{:.2f}',
1324
+ 'U-Value (W/m²°C)': '{:.2f}',
1325
+ 'Temperature Difference (°C)': '{:.2f}',
1326
+ 'Heat Gain (W)': '{:.2f}'
1327
+ }))
1328
+
1329
+ # Create bar chart
1330
+ fig = px.bar(
1331
+ envelope_df,
1332
+ x='Component',
1333
+ y='Heat Gain (W)',
1334
+ title="Heat Gain by Building Component",
1335
+ color='Component',
1336
+ color_discrete_sequence=px.colors.qualitative.Set3
1337
+ )
1338
+
1339
+ st.plotly_chart(fig)
1340
+
1341
+ with tabs[1]:
1342
+ st.subheader("Windows & Doors Heat Gains")
1343
+
1344
+ # Windows section
1345
+ st.write("#### Windows")
1346
+
1347
+ # Get windows
1348
+ windows_data = []
1349
+ for window in st.session_state.cooling_form_data['windows'].get('windows', []):
1350
+ windows_data.append({
1351
+ 'Component': window['name'],
1352
+ 'Orientation': window['orientation'].capitalize(),
1353
+ 'Area (m²)': window['area'],
1354
+ 'U-Value (W/m²°C)': window['u_value'],
1355
+ 'Temperature Difference (°C)': window['temp_diff'],
1356
+ 'Conduction Heat Gain (W)': window['area'] * window['u_value'] * window['temp_diff'],
1357
+ 'Solar Heat Gain Factor (W/m²)': window['shgf'],
1358
+ 'Shading Factor': 1.0 - window['shade_factor'],
1359
+ 'Solar Heat Gain (W)': window['area'] * window['shgf'] * (1.0 - window['shade_factor']),
1360
+ 'Total Heat Gain (W)': (window['area'] * window['u_value'] * window['temp_diff']) +
1361
+ (window['area'] * window['shgf'] * (1.0 - window['shade_factor']))
1362
+ })
1363
+
1364
+ if windows_data:
1365
+ # Create dataframe
1366
+ windows_df = pd.DataFrame(windows_data)
1367
+
1368
+ # Display table
1369
+ st.dataframe(windows_df.style.format({
1370
+ 'Area (m²)': '{:.2f}',
1371
+ 'U-Value (W/m²°C)': '{:.2f}',
1372
+ 'Temperature Difference (°C)': '{:.2f}',
1373
+ 'Conduction Heat Gain (W)': '{:.2f}',
1374
+ 'Solar Heat Gain Factor (W/m²)': '{:.2f}',
1375
+ 'Shading Factor': '{:.2f}',
1376
+ 'Solar Heat Gain (W)': '{:.2f}',
1377
+ 'Total Heat Gain (W)': '{:.2f}'
1378
+ }))
1379
+
1380
+ # Create grouped bar chart
1381
+ fig = go.Figure()
1382
+
1383
+ fig.add_trace(go.Bar(
1384
+ x=windows_df['Component'],
1385
+ y=windows_df['Conduction Heat Gain (W)'],
1386
+ name='Conduction Heat Gain',
1387
+ marker_color='indianred'
1388
+ ))
1389
+
1390
+ fig.add_trace(go.Bar(
1391
+ x=windows_df['Component'],
1392
+ y=windows_df['Solar Heat Gain (W)'],
1393
+ name='Solar Heat Gain',
1394
+ marker_color='lightsalmon'
1395
+ ))
1396
+
1397
+ fig.update_layout(
1398
+ title="Window Heat Gains",
1399
+ xaxis_title="Window",
1400
+ yaxis_title="Heat Gain (W)",
1401
+ barmode='stack'
1402
+ )
1403
+
1404
+ st.plotly_chart(fig)
1405
+ else:
1406
+ st.write("No windows defined.")
1407
+
1408
+ # Doors section
1409
+ st.write("#### Doors")
1410
+
1411
+ # Get doors
1412
+ doors_data = []
1413
+ for door in st.session_state.cooling_form_data['windows'].get('doors', []):
1414
+ doors_data.append({
1415
+ 'Component': door['name'],
1416
+ 'Type': door['type'],
1417
+ 'Area (m²)': door['area'],
1418
+ 'U-Value (W/m²°C)': door['u_value'],
1419
+ 'Temperature Difference (°C)': door['temp_diff'],
1420
+ 'Heat Gain (W)': door['area'] * door['u_value'] * door['temp_diff']
1421
+ })
1422
+
1423
+ if doors_data:
1424
+ # Create dataframe
1425
+ doors_df = pd.DataFrame(doors_data)
1426
+
1427
+ # Display table
1428
+ st.dataframe(doors_df.style.format({
1429
+ 'Area (m²)': '{:.2f}',
1430
+ 'U-Value (W/m²°C)': '{:.2f}',
1431
+ 'Temperature Difference (°C)': '{:.2f}',
1432
+ 'Heat Gain (W)': '{:.2f}'
1433
+ }))
1434
+
1435
+ # Create bar chart
1436
+ fig = px.bar(
1437
+ doors_df,
1438
+ x='Component',
1439
+ y='Heat Gain (W)',
1440
+ title="Door Heat Gains",
1441
+ color='Type',
1442
+ color_discrete_sequence=px.colors.qualitative.Pastel
1443
+ )
1444
+
1445
+ st.plotly_chart(fig)
1446
+ else:
1447
+ st.write("No doors defined.")
1448
+
1449
+ with tabs[2]:
1450
+ st.subheader("Internal Heat Gains")
1451
+
1452
+ # Get internal loads data
1453
+ internal_loads = st.session_state.cooling_form_data['internal_loads']
1454
+
1455
+ # Create dataframe
1456
+ internal_loads_data = [
1457
+ {
1458
+ 'Source': 'Occupants',
1459
+ 'Details': f"{internal_loads['occupants']['count']} people",
1460
+ 'Heat Gain (W)': internal_loads['occupants']['total_heat_gain']
1461
+ },
1462
+ {
1463
+ 'Source': 'Lighting',
1464
+ 'Details': f"{internal_loads['lighting']['type']} lighting",
1465
+ 'Heat Gain (W)': internal_loads['lighting']['total_heat_gain']
1466
+ },
1467
+ {
1468
+ 'Source': 'Appliances',
1469
+ 'Details': ', '.join([k for k, v in internal_loads['appliances'].items() if v and k != 'total_heat_gain']),
1470
+ 'Heat Gain (W)': internal_loads['appliances']['total_heat_gain']
1471
+ }
1472
+ ]
1473
+
1474
+ internal_loads_df = pd.DataFrame(internal_loads_data)
1475
+
1476
+ # Display table
1477
+ st.dataframe(internal_loads_df.style.format({
1478
+ 'Heat Gain (W)': '{:.2f}'
1479
+ }))
1480
+
1481
+ # Create bar chart
1482
+ fig = px.bar(
1483
+ internal_loads_df,
1484
+ x='Source',
1485
+ y='Heat Gain (W)',
1486
+ title="Internal Heat Gains",
1487
+ color='Source',
1488
+ color_discrete_sequence=px.colors.qualitative.Pastel1
1489
+ )
1490
+
1491
+ st.plotly_chart(fig)
1492
+
1493
+ with tabs[3]:
1494
+ st.subheader("Ventilation & Infiltration Heat Gains")
1495
+
1496
+ # Get ventilation data
1497
+ ventilation_data = st.session_state.cooling_form_data['ventilation']
1498
+
1499
+ # Create dataframe
1500
+ ventilation_df = pd.DataFrame([
1501
+ {
1502
+ 'Source': 'Infiltration',
1503
+ 'Air Changes per Hour': ventilation_data['infiltration']['air_changes'],
1504
+ 'Volume (m³)': ventilation_data['infiltration']['volume'],
1505
+ 'Temperature Difference (°C)': ventilation_data['infiltration']['temp_diff'],
1506
+ 'Heat Gain (W)': ventilation_data['infiltration']['heat_gain']
1507
+ },
1508
+ {
1509
+ 'Source': 'Ventilation',
1510
+ 'Air Changes per Hour': ventilation_data['ventilation']['air_changes'],
1511
+ 'Volume (m³)': ventilation_data['ventilation']['volume'],
1512
+ 'Temperature Difference (°C)': ventilation_data['ventilation']['temp_diff'],
1513
+ 'Heat Gain (W)': ventilation_data['ventilation']['heat_gain']
1514
+ }
1515
+ ])
1516
+
1517
+ # Display table
1518
+ st.dataframe(ventilation_df.style.format({
1519
+ 'Air Changes per Hour': '{:.2f}',
1520
+ 'Volume (m³)': '{:.2f}',
1521
+ 'Temperature Difference (°C)': '{:.2f}',
1522
+ 'Heat Gain (W)': '{:.2f}'
1523
+ }))
1524
+
1525
+ # Create bar chart
1526
+ fig = px.bar(
1527
+ ventilation_df,
1528
+ x='Source',
1529
+ y='Heat Gain (W)',
1530
+ title="Ventilation & Infiltration Heat Gains",
1531
+ color='Source',
1532
+ color_discrete_sequence=px.colors.qualitative.Pastel2
1533
+ )
1534
+
1535
+ st.plotly_chart(fig)
1536
+
1537
+ # Export options
1538
+ st.write("### Export Options")
1539
+
1540
+ col1, col2 = st.columns(2)
1541
+
1542
+ with col1:
1543
+ if st.button("Export Results as CSV"):
1544
+ # Create a CSV file with results
1545
+ csv_data = export_data(st.session_state.cooling_form_data, st.session_state.cooling_results, format='csv')
1546
+
1547
+ # Provide download link
1548
+ st.download_button(
1549
+ label="Download CSV",
1550
+ data=csv_data,
1551
+ file_name=f"cooling_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
1552
+ mime="text/csv"
1553
+ )
1554
+
1555
+ with col2:
1556
+ if st.button("Export Results as JSON"):
1557
+ # Create a JSON file with results
1558
+ json_data = export_data(st.session_state.cooling_form_data, st.session_state.cooling_results, format='json')
1559
+
1560
+ # Provide download link
1561
+ st.download_button(
1562
+ label="Download JSON",
1563
+ data=json_data,
1564
+ file_name=f"cooling_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1565
+ mime="application/json"
1566
+ )
1567
+
1568
+ # Navigation buttons
1569
+ col1, col2 = st.columns([1, 1])
1570
+
1571
+ with col1:
1572
+ prev_button = st.button("← Back: Ventilation", key="results_prev")
1573
+ if prev_button:
1574
+ st.session_state.cooling_active_tab = "ventilation"
1575
+ st.experimental_rerun()
1576
+
1577
+ with col2:
1578
+ recalculate_button = st.button("Recalculate", key="results_recalculate")
1579
+ if recalculate_button:
1580
+ # Recalculate cooling load
1581
+ calculate_cooling_load()
1582
+ st.experimental_rerun()
1583
+
1584
+
1585
+ def cooling_calculator():
1586
+ """Main function for the cooling load calculator page."""
1587
+ st.title("Cooling Load Calculator")
1588
+
1589
+ # Initialize reference data
1590
+ ref_data = ReferenceData()
1591
+
1592
+ # Initialize session state
1593
+ load_session_state()
1594
+
1595
+ # Initialize active tab if not already set
1596
+ if 'cooling_active_tab' not in st.session_state:
1597
+ st.session_state.cooling_active_tab = "building_info"
1598
+
1599
+ # Create tabs for different steps
1600
+ tabs = st.tabs([
1601
+ "1. Building Information",
1602
+ "2. Building Envelope",
1603
+ "3. Windows & Doors",
1604
+ "4. Internal Loads",
1605
+ "5. Ventilation",
1606
+ "6. Results"
1607
+ ])
1608
+
1609
+ # Display the active tab
1610
+ with tabs[0]:
1611
+ if st.session_state.cooling_active_tab == "building_info":
1612
+ building_info_form(ref_data)
1613
+
1614
+ with tabs[1]:
1615
+ if st.session_state.cooling_active_tab == "building_envelope":
1616
+ building_envelope_form(ref_data)
1617
+
1618
+ with tabs[2]:
1619
+ if st.session_state.cooling_active_tab == "windows":
1620
+ windows_form(ref_data)
1621
+
1622
+ with tabs[3]:
1623
+ if st.session_state.cooling_active_tab == "internal_loads":
1624
+ internal_loads_form(ref_data)
1625
+
1626
+ with tabs[4]:
1627
+ if st.session_state.cooling_active_tab == "ventilation":
1628
+ ventilation_form(ref_data)
1629
+
1630
+ with tabs[5]:
1631
+ if st.session_state.cooling_active_tab == "results":
1632
+ results_page()
1633
+
1634
+
1635
+ if __name__ == "__main__":
1636
+ cooling_calculator()
pages/heating_calculator.py ADDED
@@ -0,0 +1,1435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Heating Load Calculator Page
3
+
4
+ This module implements the heating load calculator interface for the HVAC Load Calculator web application.
5
+ It provides a step-by-step form for inputting building information and calculates heating loads
6
+ using the ASHRAE method.
7
+ """
8
+
9
+ import streamlit as st
10
+ import pandas as pd
11
+ import numpy as np
12
+ import plotly.express as px
13
+ import plotly.graph_objects as go
14
+ import json
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+
20
+ # Add the parent directory to sys.path to import modules
21
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
+
23
+ # Import custom modules
24
+ from heating_load import HeatingLoadCalculator
25
+ from reference_data import ReferenceData
26
+ from utils.validation import validate_input, ValidationWarning
27
+ from utils.export import export_data
28
+
29
+
30
+ def load_session_state():
31
+ """Initialize or load session state variables."""
32
+ # Initialize session state for form data
33
+ if 'heating_form_data' not in st.session_state:
34
+ st.session_state.heating_form_data = {
35
+ 'building_info': {},
36
+ 'building_envelope': {},
37
+ 'windows': {},
38
+ 'ventilation': {},
39
+ 'occupancy': {},
40
+ 'results': {}
41
+ }
42
+
43
+ # Initialize session state for validation warnings
44
+ if 'heating_warnings' not in st.session_state:
45
+ st.session_state.heating_warnings = {
46
+ 'building_info': [],
47
+ 'building_envelope': [],
48
+ 'windows': [],
49
+ 'ventilation': [],
50
+ 'occupancy': []
51
+ }
52
+
53
+ # Initialize session state for form completion status
54
+ if 'heating_completed' not in st.session_state:
55
+ st.session_state.heating_completed = {
56
+ 'building_info': False,
57
+ 'building_envelope': False,
58
+ 'windows': False,
59
+ 'ventilation': False,
60
+ 'occupancy': False
61
+ }
62
+
63
+ # Initialize session state for calculation results
64
+ if 'heating_results' not in st.session_state:
65
+ st.session_state.heating_results = None
66
+
67
+
68
+ def building_info_form(ref_data):
69
+ """
70
+ Form for building information.
71
+
72
+ Args:
73
+ ref_data: Reference data object
74
+ """
75
+ st.subheader("Building Information")
76
+ st.write("Enter general building information, location, and design temperatures.")
77
+
78
+ # Get location options from reference data
79
+ location_options = {loc_id: loc_data['name'] for loc_id, loc_data in ref_data.locations.items()}
80
+
81
+ col1, col2 = st.columns(2)
82
+
83
+ with col1:
84
+ # Building name
85
+ building_name = st.text_input(
86
+ "Building Name",
87
+ value=st.session_state.heating_form_data['building_info'].get('building_name', ''),
88
+ help="Enter a name for this building or project"
89
+ )
90
+
91
+ # Location selection
92
+ location = st.selectbox(
93
+ "Location",
94
+ options=list(location_options.keys()),
95
+ format_func=lambda x: location_options[x],
96
+ index=list(location_options.keys()).index(st.session_state.heating_form_data['building_info'].get('location', 'sydney')) if st.session_state.heating_form_data['building_info'].get('location') in location_options else 0,
97
+ help="Select the location of the building"
98
+ )
99
+
100
+ # Get climate data for selected location
101
+ location_data = ref_data.get_location_data(location)
102
+
103
+ # Indoor design temperature
104
+ indoor_temp = st.number_input(
105
+ "Indoor Design Temperature (°C)",
106
+ value=float(st.session_state.heating_form_data['building_info'].get('indoor_temp', 21.0)),
107
+ min_value=15.0,
108
+ max_value=25.0,
109
+ step=0.5,
110
+ help="Recommended indoor design temperature for heating is 21°C for living areas and 17°C for bedrooms"
111
+ )
112
+
113
+ with col2:
114
+ # Building type
115
+ building_type = st.selectbox(
116
+ "Building Type",
117
+ options=["Residential", "Small Office", "Educational", "Other"],
118
+ index=["Residential", "Small Office", "Educational", "Other"].index(st.session_state.heating_form_data['building_info'].get('building_type', 'Residential')),
119
+ help="Select the type of building"
120
+ )
121
+
122
+ # Outdoor design temperature (with default from location data)
123
+ outdoor_temp = st.number_input(
124
+ "Outdoor Design Temperature (°C)",
125
+ value=float(st.session_state.heating_form_data['building_info'].get('outdoor_temp', location_data['winter_design_temp'])),
126
+ min_value=-10.0,
127
+ max_value=15.0,
128
+ step=0.5,
129
+ help=f"Default value is based on selected location ({location_data['name']})"
130
+ )
131
+
132
+ # Building dimensions
133
+ st.subheader("Building Dimensions")
134
+
135
+ col1, col2, col3 = st.columns(3)
136
+
137
+ with col1:
138
+ length = st.number_input(
139
+ "Length (m)",
140
+ value=float(st.session_state.heating_form_data['building_info'].get('length', 10.0)),
141
+ min_value=1.0,
142
+ step=0.1,
143
+ help="Building length in meters"
144
+ )
145
+
146
+ with col2:
147
+ width = st.number_input(
148
+ "Width (m)",
149
+ value=float(st.session_state.heating_form_data['building_info'].get('width', 8.0)),
150
+ min_value=1.0,
151
+ step=0.1,
152
+ help="Building width in meters"
153
+ )
154
+
155
+ with col3:
156
+ height = st.number_input(
157
+ "Height (m)",
158
+ value=float(st.session_state.heating_form_data['building_info'].get('height', 2.7)),
159
+ min_value=1.0,
160
+ step=0.1,
161
+ help="Floor-to-ceiling height in meters"
162
+ )
163
+
164
+ # Calculate floor area and volume
165
+ floor_area = length * width
166
+ volume = floor_area * height
167
+
168
+ st.info(f"Floor Area: {floor_area:.2f} m² | Volume: {volume:.2f} m³")
169
+
170
+ # Save form data to session state
171
+ form_data = {
172
+ 'building_name': building_name,
173
+ 'building_type': building_type,
174
+ 'location': location,
175
+ 'location_name': location_data['name'],
176
+ 'indoor_temp': indoor_temp,
177
+ 'outdoor_temp': outdoor_temp,
178
+ 'length': length,
179
+ 'width': width,
180
+ 'height': height,
181
+ 'floor_area': floor_area,
182
+ 'volume': volume,
183
+ 'temp_diff': indoor_temp - outdoor_temp
184
+ }
185
+
186
+ # Validate inputs
187
+ warnings = []
188
+
189
+ # Check if building name is provided
190
+ if not building_name:
191
+ warnings.append(ValidationWarning("Building name is empty", "Consider adding a building name for reference"))
192
+
193
+ # Check if temperature difference is reasonable
194
+ if form_data['temp_diff'] <= 0:
195
+ warnings.append(ValidationWarning(
196
+ "Invalid temperature difference",
197
+ "Indoor temperature should be higher than outdoor temperature for heating load calculation",
198
+ is_critical=True
199
+ ))
200
+
201
+ # Check if dimensions are reasonable
202
+ if floor_area > 500:
203
+ warnings.append(ValidationWarning(
204
+ "Large floor area",
205
+ "Floor area exceeds 500 m², verify if this is correct for a residential building"
206
+ ))
207
+
208
+ if height < 2.4 or height > 3.5:
209
+ warnings.append(ValidationWarning(
210
+ "Unusual ceiling height",
211
+ "Typical residential ceiling heights are between 2.4m and 3.5m"
212
+ ))
213
+
214
+ # Save warnings to session state
215
+ st.session_state.heating_warnings['building_info'] = warnings
216
+
217
+ # Display warnings if any
218
+ if warnings:
219
+ st.warning("Please review the following warnings:")
220
+ for warning in warnings:
221
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
222
+ st.write(f" Suggestion: {warning.suggestion}")
223
+
224
+ # Save form data regardless of warnings
225
+ st.session_state.heating_form_data['building_info'] = form_data
226
+
227
+ # Mark this step as completed if there are no critical warnings
228
+ st.session_state.heating_completed['building_info'] = not any(w.is_critical for w in warnings)
229
+
230
+ # Navigation buttons
231
+ col1, col2 = st.columns([1, 1])
232
+
233
+ with col2:
234
+ next_button = st.button("Next: Building Envelope →", key="heating_building_info_next")
235
+ if next_button:
236
+ st.session_state.heating_active_tab = "building_envelope"
237
+ st.experimental_rerun()
238
+
239
+
240
+ def building_envelope_form(ref_data):
241
+ """
242
+ Form for building envelope information.
243
+
244
+ Args:
245
+ ref_data: Reference data object
246
+ """
247
+ st.subheader("Building Envelope")
248
+ st.write("Enter information about walls, roof, and floor construction.")
249
+
250
+ # Get building dimensions from previous step
251
+ building_info = st.session_state.heating_form_data['building_info']
252
+ length = building_info.get('length', 10.0)
253
+ width = building_info.get('width', 8.0)
254
+ height = building_info.get('height', 2.7)
255
+ temp_diff = building_info.get('temp_diff', 16.5)
256
+
257
+ # Calculate default areas
258
+ default_wall_area = 2 * (length + width) * height
259
+ default_roof_area = length * width
260
+ default_floor_area = length * width
261
+
262
+ # Initialize envelope data if not already in session state
263
+ if 'walls' not in st.session_state.heating_form_data['building_envelope']:
264
+ st.session_state.heating_form_data['building_envelope']['walls'] = []
265
+
266
+ if 'roof' not in st.session_state.heating_form_data['building_envelope']:
267
+ st.session_state.heating_form_data['building_envelope']['roof'] = {}
268
+
269
+ if 'floor' not in st.session_state.heating_form_data['building_envelope']:
270
+ st.session_state.heating_form_data['building_envelope']['floor'] = {}
271
+
272
+ # Walls section
273
+ st.write("### Walls")
274
+
275
+ # Get wall material options from reference data
276
+ wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
277
+
278
+ # Display existing wall entries
279
+ if st.session_state.heating_form_data['building_envelope']['walls']:
280
+ st.write("Current walls:")
281
+ walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls'])
282
+ walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
283
+ walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
284
+ walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
285
+ st.dataframe(walls_df)
286
+
287
+ # Add new wall form
288
+ st.write("Add a new wall:")
289
+
290
+ col1, col2 = st.columns(2)
291
+
292
+ with col1:
293
+ wall_name = st.text_input("Wall Name", value="", key="new_wall_name_heating")
294
+ wall_material = st.selectbox(
295
+ "Wall Material",
296
+ options=list(wall_material_options.keys()),
297
+ format_func=lambda x: wall_material_options[x],
298
+ key="new_wall_material_heating"
299
+ )
300
+
301
+ # Get material properties
302
+ material_data = ref_data.get_material_by_type("walls", wall_material)
303
+ u_value = material_data['u_value']
304
+
305
+ with col2:
306
+ wall_area = st.number_input(
307
+ "Wall Area (m²)",
308
+ value=default_wall_area / 4, # Default to 1/4 of total wall area as a starting point
309
+ min_value=0.1,
310
+ step=0.1,
311
+ key="new_wall_area_heating"
312
+ )
313
+
314
+ st.write(f"Material U-Value: {u_value} W/m²°C")
315
+ st.write(f"Heat Loss: {u_value * wall_area * temp_diff:.2f} W")
316
+
317
+ # Add wall button
318
+ if st.button("Add Wall", key="add_wall_heating"):
319
+ new_wall = {
320
+ 'name': wall_name if wall_name else f"Wall {len(st.session_state.heating_form_data['building_envelope']['walls']) + 1}",
321
+ 'material_id': wall_material,
322
+ 'area': wall_area,
323
+ 'u_value': u_value,
324
+ 'temp_diff': temp_diff
325
+ }
326
+ st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall)
327
+ st.experimental_rerun()
328
+
329
+ # Roof section
330
+ st.write("### Roof")
331
+
332
+ # Get roof material options from reference data
333
+ roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
334
+
335
+ col1, col2 = st.columns(2)
336
+
337
+ with col1:
338
+ roof_material = st.selectbox(
339
+ "Roof Material",
340
+ options=list(roof_material_options.keys()),
341
+ format_func=lambda x: roof_material_options[x],
342
+ index=list(roof_material_options.keys()).index(st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('material_id', 'metal_deck_insulated')) if st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('material_id') in roof_material_options else 0
343
+ )
344
+
345
+ # Get material properties
346
+ material_data = ref_data.get_material_by_type("roofs", roof_material)
347
+ roof_u_value = material_data['u_value']
348
+
349
+ with col2:
350
+ roof_area = st.number_input(
351
+ "Roof Area (m²)",
352
+ value=float(st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('area', default_roof_area)),
353
+ min_value=0.1,
354
+ step=0.1,
355
+ key="roof_area_heating"
356
+ )
357
+
358
+ st.write(f"Material U-Value: {roof_u_value} W/m²°C")
359
+ st.write(f"Heat Loss: {roof_u_value * roof_area * temp_diff:.2f} W")
360
+
361
+ # Save roof data
362
+ st.session_state.heating_form_data['building_envelope']['roof'] = {
363
+ 'material_id': roof_material,
364
+ 'area': roof_area,
365
+ 'u_value': roof_u_value,
366
+ 'temp_diff': temp_diff
367
+ }
368
+
369
+ # Floor section
370
+ st.write("### Floor")
371
+
372
+ # Get floor material options from reference data
373
+ floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
374
+
375
+ col1, col2 = st.columns(2)
376
+
377
+ with col1:
378
+ floor_material = st.selectbox(
379
+ "Floor Material",
380
+ options=list(floor_material_options.keys()),
381
+ format_func=lambda x: floor_material_options[x],
382
+ index=list(floor_material_options.keys()).index(st.session_state.heating_form_data['building_envelope'].get('floor', {}).get('material_id', 'concrete_slab_ground')) if st.session_state.heating_form_data['building_envelope'].get('floor', {}).get('material_id') in floor_material_options else 0
383
+ )
384
+
385
+ # Get material properties
386
+ material_data = ref_data.get_material_by_type("floors", floor_material)
387
+ floor_u_value = material_data['u_value']
388
+
389
+ with col2:
390
+ floor_area = st.number_input(
391
+ "Floor Area (m²)",
392
+ value=float(st.session_state.heating_form_data['building_envelope'].get('floor', {}).get('area', default_floor_area)),
393
+ min_value=0.1,
394
+ step=0.1,
395
+ key="floor_area_heating"
396
+ )
397
+
398
+ st.write(f"Material U-Value: {floor_u_value} W/m²°C")
399
+ st.write(f"Heat Loss: {floor_u_value * floor_area * temp_diff:.2f} W")
400
+
401
+ # Save floor data
402
+ st.session_state.heating_form_data['building_envelope']['floor'] = {
403
+ 'material_id': floor_material,
404
+ 'area': floor_area,
405
+ 'u_value': floor_u_value,
406
+ 'temp_diff': temp_diff
407
+ }
408
+
409
+ # Validate inputs
410
+ warnings = []
411
+
412
+ # Check if walls are defined
413
+ if not st.session_state.heating_form_data['building_envelope']['walls']:
414
+ warnings.append(ValidationWarning(
415
+ "No walls defined",
416
+ "Add at least one wall to continue",
417
+ is_critical=True
418
+ ))
419
+
420
+ # Check if total wall area is reasonable
421
+ total_wall_area = sum(wall['area'] for wall in st.session_state.heating_form_data['building_envelope']['walls'])
422
+ expected_wall_area = 2 * (length + width) * height
423
+
424
+ if total_wall_area < expected_wall_area * 0.8 or total_wall_area > expected_wall_area * 1.2:
425
+ warnings.append(ValidationWarning(
426
+ "Unusual wall area",
427
+ f"Total wall area ({total_wall_area:.2f} m²) differs significantly from the expected area ({expected_wall_area:.2f} m²) based on building dimensions"
428
+ ))
429
+
430
+ # Check if roof area matches floor area
431
+ if abs(roof_area - floor_area) > 1.0:
432
+ warnings.append(ValidationWarning(
433
+ "Roof area doesn't match floor area",
434
+ "For a simple building, roof area should approximately match floor area"
435
+ ))
436
+
437
+ # Save warnings to session state
438
+ st.session_state.heating_warnings['building_envelope'] = warnings
439
+
440
+ # Display warnings if any
441
+ if warnings:
442
+ st.warning("Please review the following warnings:")
443
+ for warning in warnings:
444
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
445
+ st.write(f" Suggestion: {warning.suggestion}")
446
+
447
+ # Mark this step as completed if there are no critical warnings
448
+ st.session_state.heating_completed['building_envelope'] = not any(w.is_critical for w in warnings)
449
+
450
+ # Navigation buttons
451
+ col1, col2 = st.columns([1, 1])
452
+
453
+ with col1:
454
+ prev_button = st.button("← Back: Building Information", key="heating_building_envelope_prev")
455
+ if prev_button:
456
+ st.session_state.heating_active_tab = "building_info"
457
+ st.experimental_rerun()
458
+
459
+ with col2:
460
+ next_button = st.button("Next: Windows & Doors →", key="heating_building_envelope_next")
461
+ if next_button:
462
+ st.session_state.heating_active_tab = "windows"
463
+ st.experimental_rerun()
464
+
465
+
466
+ def windows_form(ref_data):
467
+ """
468
+ Form for windows and doors information.
469
+
470
+ Args:
471
+ ref_data: Reference data object
472
+ """
473
+ st.subheader("Windows & Doors")
474
+ st.write("Enter information about windows and doors.")
475
+
476
+ # Get temperature difference from building info
477
+ temp_diff = st.session_state.heating_form_data['building_info'].get('temp_diff', 16.5)
478
+
479
+ # Initialize windows data if not already in session state
480
+ if 'windows' not in st.session_state.heating_form_data['windows']:
481
+ st.session_state.heating_form_data['windows']['windows'] = []
482
+
483
+ if 'doors' not in st.session_state.heating_form_data['windows']:
484
+ st.session_state.heating_form_data['windows']['doors'] = []
485
+
486
+ # Windows section
487
+ st.write("### Windows")
488
+
489
+ # Get glass type options from reference data
490
+ glass_type_options = {glass_id: glass_data['name'] for glass_id, glass_data in ref_data.glass_types.items()}
491
+
492
+ # Display existing window entries
493
+ if st.session_state.heating_form_data['windows']['windows']:
494
+ st.write("Current windows:")
495
+ windows_df = pd.DataFrame(st.session_state.heating_form_data['windows']['windows'])
496
+ windows_df['Glass Type'] = windows_df['glass_type'].map(lambda x: glass_type_options.get(x, "Unknown"))
497
+ windows_df = windows_df[['name', 'orientation', 'Glass Type', 'area', 'u_value']]
498
+ windows_df.columns = ['Name', 'Orientation', 'Glass Type', 'Area (m²)', 'U-Value (W/m²°C)']
499
+ st.dataframe(windows_df)
500
+
501
+ # Add new window form
502
+ st.write("Add a new window:")
503
+
504
+ col1, col2 = st.columns(2)
505
+
506
+ with col1:
507
+ window_name = st.text_input("Window Name", value="", key="new_window_name_heating")
508
+
509
+ orientation = st.selectbox(
510
+ "Orientation",
511
+ options=["north", "east", "south", "west", "horizontal"],
512
+ key="new_window_orientation_heating"
513
+ )
514
+
515
+ glass_type = st.selectbox(
516
+ "Glass Type",
517
+ options=list(glass_type_options.keys()),
518
+ format_func=lambda x: glass_type_options[x],
519
+ key="new_window_glass_type_heating"
520
+ )
521
+
522
+ # Get glass properties
523
+ glass_data = ref_data.get_glass_type(glass_type)
524
+ window_u_value = glass_data['u_value']
525
+
526
+ with col2:
527
+ window_area = st.number_input(
528
+ "Window Area (m²)",
529
+ value=2.0,
530
+ min_value=0.1,
531
+ step=0.1,
532
+ key="new_window_area_heating"
533
+ )
534
+
535
+ st.write(f"Glass U-Value: {window_u_value} W/m²°C")
536
+ st.write(f"Heat Loss: {window_u_value * window_area * temp_diff:.2f} W")
537
+
538
+ # Add window button
539
+ if st.button("Add Window", key="add_window_heating"):
540
+ new_window = {
541
+ 'name': window_name if window_name else f"Window {len(st.session_state.heating_form_data['windows']['windows']) + 1}",
542
+ 'orientation': orientation,
543
+ 'glass_type': glass_type,
544
+ 'area': window_area,
545
+ 'u_value': window_u_value,
546
+ 'temp_diff': temp_diff
547
+ }
548
+ st.session_state.heating_form_data['windows']['windows'].append(new_window)
549
+ st.experimental_rerun()
550
+
551
+ # Doors section
552
+ st.write("### Doors")
553
+
554
+ # Display existing door entries
555
+ if st.session_state.heating_form_data['windows']['doors']:
556
+ st.write("Current doors:")
557
+ doors_df = pd.DataFrame(st.session_state.heating_form_data['windows']['doors'])
558
+ doors_df = doors_df[['name', 'type', 'area', 'u_value']]
559
+ doors_df.columns = ['Name', 'Type', 'Area (m²)', 'U-Value (W/m²°C)']
560
+ st.dataframe(doors_df)
561
+
562
+ # Add new door form
563
+ st.write("Add a new door:")
564
+
565
+ col1, col2 = st.columns(2)
566
+
567
+ with col1:
568
+ door_name = st.text_input("Door Name", value="", key="new_door_name_heating")
569
+
570
+ door_type = st.selectbox(
571
+ "Door Type",
572
+ options=["Solid wood", "Hollow core", "Glass", "Insulated"],
573
+ key="new_door_type_heating"
574
+ )
575
+
576
+ # Set U-value based on door type
577
+ door_u_values = {
578
+ "Solid wood": 2.0,
579
+ "Hollow core": 2.5,
580
+ "Glass": 5.0,
581
+ "Insulated": 1.2
582
+ }
583
+ door_u_value = door_u_values[door_type]
584
+
585
+ with col2:
586
+ door_area = st.number_input(
587
+ "Door Area (m²)",
588
+ value=2.0,
589
+ min_value=0.1,
590
+ step=0.1,
591
+ key="new_door_area_heating"
592
+ )
593
+
594
+ st.write(f"Door U-Value: {door_u_value} W/m²°C")
595
+ st.write(f"Heat Loss: {door_u_value * door_area * temp_diff:.2f} W")
596
+
597
+ # Add door button
598
+ if st.button("Add Door", key="add_door_heating"):
599
+ new_door = {
600
+ 'name': door_name if door_name else f"Door {len(st.session_state.heating_form_data['windows']['doors']) + 1}",
601
+ 'type': door_type,
602
+ 'area': door_area,
603
+ 'u_value': door_u_value,
604
+ 'temp_diff': temp_diff
605
+ }
606
+ st.session_state.heating_form_data['windows']['doors'].append(new_door)
607
+ st.experimental_rerun()
608
+
609
+ # Validate inputs
610
+ warnings = []
611
+
612
+ # Check if windows are defined
613
+ if not st.session_state.heating_form_data['windows']['windows']:
614
+ warnings.append(ValidationWarning(
615
+ "No windows defined",
616
+ "Add at least one window to continue"
617
+ ))
618
+
619
+ # Check window-to-wall ratio
620
+ if st.session_state.heating_form_data['windows']['windows']:
621
+ total_window_area = sum(window['area'] for window in st.session_state.heating_form_data['windows']['windows'])
622
+ total_wall_area = sum(wall['area'] for wall in st.session_state.heating_form_data['building_envelope']['walls'])
623
+ window_wall_ratio = total_window_area / total_wall_area if total_wall_area > 0 else 0
624
+
625
+ if window_wall_ratio > 0.6:
626
+ warnings.append(ValidationWarning(
627
+ "High window-to-wall ratio",
628
+ f"Window-to-wall ratio is {window_wall_ratio:.2f}, which is unusually high. Typical ratios are 0.2-0.4."
629
+ ))
630
+
631
+ # Save warnings to session state
632
+ st.session_state.heating_warnings['windows'] = warnings
633
+
634
+ # Display warnings if any
635
+ if warnings:
636
+ st.warning("Please review the following warnings:")
637
+ for warning in warnings:
638
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
639
+ st.write(f" Suggestion: {warning.suggestion}")
640
+
641
+ # Mark this step as completed if there are no critical warnings
642
+ st.session_state.heating_completed['windows'] = not any(w.is_critical for w in warnings)
643
+
644
+ # Navigation buttons
645
+ col1, col2 = st.columns([1, 1])
646
+
647
+ with col1:
648
+ prev_button = st.button("← Back: Building Envelope", key="heating_windows_prev")
649
+ if prev_button:
650
+ st.session_state.heating_active_tab = "building_envelope"
651
+ st.experimental_rerun()
652
+
653
+ with col2:
654
+ next_button = st.button("Next: Ventilation →", key="heating_windows_next")
655
+ if next_button:
656
+ st.session_state.heating_active_tab = "ventilation"
657
+ st.experimental_rerun()
658
+
659
+
660
+ def ventilation_form(ref_data):
661
+ """
662
+ Form for ventilation and infiltration information.
663
+
664
+ Args:
665
+ ref_data: Reference data object
666
+ """
667
+ st.subheader("Ventilation & Infiltration")
668
+ st.write("Enter information about ventilation and infiltration rates.")
669
+
670
+ # Get building info
671
+ building_info = st.session_state.heating_form_data['building_info']
672
+ volume = building_info.get('volume', 216.0)
673
+ temp_diff = building_info.get('temp_diff', 16.5)
674
+
675
+ # Initialize ventilation data if not already in session state
676
+ if 'infiltration' not in st.session_state.heating_form_data['ventilation']:
677
+ st.session_state.heating_form_data['ventilation']['infiltration'] = {
678
+ 'air_changes': 0.5
679
+ }
680
+
681
+ if 'ventilation' not in st.session_state.heating_form_data['ventilation']:
682
+ st.session_state.heating_form_data['ventilation']['ventilation'] = {
683
+ 'type': 'natural',
684
+ 'air_changes': 0.0
685
+ }
686
+
687
+ # Infiltration section
688
+ st.write("### Infiltration")
689
+ st.write("Infiltration is the unintended air leakage through the building envelope.")
690
+
691
+ infiltration_ach = st.slider(
692
+ "Infiltration Rate (air changes per hour)",
693
+ value=float(st.session_state.heating_form_data['ventilation']['infiltration'].get('air_changes', 0.5)),
694
+ min_value=0.1,
695
+ max_value=2.0,
696
+ step=0.1,
697
+ help="Typical values: 0.5 ACH for modern construction, 1.0 ACH for average construction, 1.5+ ACH for older buildings",
698
+ key="infiltration_ach_heating"
699
+ )
700
+
701
+ # Calculate infiltration heat loss
702
+ infiltration_heat_loss = 0.33 * volume * infiltration_ach * temp_diff
703
+
704
+ st.write(f"Infiltration heat loss: {infiltration_heat_loss:.2f} W")
705
+
706
+ # Save infiltration data
707
+ st.session_state.heating_form_data['ventilation']['infiltration'] = {
708
+ 'air_changes': infiltration_ach,
709
+ 'volume': volume,
710
+ 'temp_diff': temp_diff,
711
+ 'heat_loss': infiltration_heat_loss
712
+ }
713
+
714
+ # Ventilation section
715
+ st.write("### Ventilation")
716
+ st.write("Ventilation is the intentional introduction of outside air into the building.")
717
+
718
+ col1, col2 = st.columns(2)
719
+
720
+ with col1:
721
+ ventilation_type = st.selectbox(
722
+ "Ventilation Type",
723
+ options=["natural", "mechanical", "mixed"],
724
+ format_func=lambda x: x.capitalize(),
725
+ index=["natural", "mechanical", "mixed"].index(st.session_state.heating_form_data['ventilation']['ventilation'].get('type', 'natural')),
726
+ key="ventilation_type_heating"
727
+ )
728
+
729
+ with col2:
730
+ ventilation_ach = st.number_input(
731
+ "Ventilation Rate (air changes per hour)",
732
+ value=float(st.session_state.heating_form_data['ventilation']['ventilation'].get('air_changes', 0.0)),
733
+ min_value=0.0,
734
+ max_value=5.0,
735
+ step=0.1,
736
+ help="Typical values: 0.35-1.0 ACH for residential buildings",
737
+ key="ventilation_ach_heating"
738
+ )
739
+
740
+ # Calculate ventilation heat loss
741
+ ventilation_heat_loss = 0.33 * volume * ventilation_ach * temp_diff
742
+
743
+ st.write(f"Ventilation heat loss: {ventilation_heat_loss:.2f} W")
744
+
745
+ # Save ventilation data
746
+ st.session_state.heating_form_data['ventilation']['ventilation'] = {
747
+ 'type': ventilation_type,
748
+ 'air_changes': ventilation_ach,
749
+ 'volume': volume,
750
+ 'temp_diff': temp_diff,
751
+ 'heat_loss': ventilation_heat_loss
752
+ }
753
+
754
+ # Calculate total ventilation and infiltration heat loss
755
+ total_ventilation_loss = infiltration_heat_loss + ventilation_heat_loss
756
+
757
+ st.info(f"Total Ventilation & Infiltration Heat Loss: {total_ventilation_loss:.2f} W")
758
+
759
+ # Save total ventilation loss
760
+ st.session_state.heating_form_data['ventilation']['total_loss'] = total_ventilation_loss
761
+
762
+ # Validate inputs
763
+ warnings = []
764
+
765
+ # Check if infiltration rate is reasonable
766
+ if infiltration_ach < 0.3:
767
+ warnings.append(ValidationWarning(
768
+ "Low infiltration rate",
769
+ "Infiltration rate below 0.3 ACH is unusually low for most buildings."
770
+ ))
771
+ elif infiltration_ach > 1.5:
772
+ warnings.append(ValidationWarning(
773
+ "High infiltration rate",
774
+ "Infiltration rate above 1.5 ACH indicates a leaky building envelope."
775
+ ))
776
+
777
+ # Check if ventilation rate is reasonable
778
+ if ventilation_ach > 0 and ventilation_ach < 0.35:
779
+ warnings.append(ValidationWarning(
780
+ "Low ventilation rate",
781
+ "Ventilation rate below 0.35 ACH may not provide adequate fresh air."
782
+ ))
783
+ elif ventilation_ach > 2.0:
784
+ warnings.append(ValidationWarning(
785
+ "High ventilation rate",
786
+ "Ventilation rate above 2.0 ACH is unusually high for residential buildings."
787
+ ))
788
+
789
+ # Save warnings to session state
790
+ st.session_state.heating_warnings['ventilation'] = warnings
791
+
792
+ # Display warnings if any
793
+ if warnings:
794
+ st.warning("Please review the following warnings:")
795
+ for warning in warnings:
796
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
797
+ st.write(f" Suggestion: {warning.suggestion}")
798
+
799
+ # Mark this step as completed if there are no critical warnings
800
+ st.session_state.heating_completed['ventilation'] = not any(w.is_critical for w in warnings)
801
+
802
+ # Navigation buttons
803
+ col1, col2 = st.columns([1, 1])
804
+
805
+ with col1:
806
+ prev_button = st.button("← Back: Windows & Doors", key="heating_ventilation_prev")
807
+ if prev_button:
808
+ st.session_state.heating_active_tab = "windows"
809
+ st.experimental_rerun()
810
+
811
+ with col2:
812
+ next_button = st.button("Next: Occupancy →", key="heating_ventilation_next")
813
+ if next_button:
814
+ st.session_state.heating_active_tab = "occupancy"
815
+ st.experimental_rerun()
816
+
817
+
818
+ def occupancy_form(ref_data):
819
+ """
820
+ Form for occupancy information.
821
+
822
+ Args:
823
+ ref_data: Reference data object
824
+ """
825
+ st.subheader("Occupancy Information")
826
+ st.write("Enter information about occupancy patterns and heating degree days.")
827
+
828
+ # Get location from building info
829
+ location = st.session_state.heating_form_data['building_info'].get('location', 'sydney')
830
+ location_name = st.session_state.heating_form_data['building_info'].get('location_name', 'Sydney')
831
+
832
+ # Initialize occupancy data if not already in session state
833
+ if 'occupancy_type' not in st.session_state.heating_form_data['occupancy']:
834
+ st.session_state.heating_form_data['occupancy']['occupancy_type'] = 'continuous'
835
+
836
+ if 'heating_degree_days' not in st.session_state.heating_form_data['occupancy']:
837
+ # Get default HDD from reference data
838
+ calculator = HeatingLoadCalculator()
839
+ default_hdd = calculator.get_heating_degree_days(location)
840
+ st.session_state.heating_form_data['occupancy']['heating_degree_days'] = default_hdd
841
+
842
+ # Occupancy section
843
+ st.write("### Occupancy Pattern")
844
+
845
+ # Get occupancy options from reference data
846
+ occupancy_options = {occ_id: occ_data['name'] for occ_id, occ_data in ref_data.occupancy_factors.items()}
847
+
848
+ occupancy_type = st.selectbox(
849
+ "Occupancy Type",
850
+ options=list(occupancy_options.keys()),
851
+ format_func=lambda x: occupancy_options[x],
852
+ index=list(occupancy_options.keys()).index(st.session_state.heating_form_data['occupancy'].get('occupancy_type', 'continuous')) if st.session_state.heating_form_data['occupancy'].get('occupancy_type') in occupancy_options else 0,
853
+ help="Select the occupancy pattern that best describes how the building is used"
854
+ )
855
+
856
+ # Get occupancy factor
857
+ occupancy_data = ref_data.get_occupancy_factor(occupancy_type)
858
+ occupancy_factor = occupancy_data['factor']
859
+
860
+ st.write(f"Occupancy correction factor: {occupancy_factor}")
861
+ st.write(f"Description: {occupancy_data['description']}")
862
+
863
+ # Save occupancy data
864
+ st.session_state.heating_form_data['occupancy']['occupancy_type'] = occupancy_type
865
+ st.session_state.heating_form_data['occupancy']['occupancy_factor'] = occupancy_factor
866
+
867
+ # Heating degree days section
868
+ st.write("### Heating Degree Days")
869
+ st.write("Heating degree days are used to estimate annual heating energy requirements.")
870
+
871
+ col1, col2 = st.columns(2)
872
+
873
+ with col1:
874
+ base_temp = st.selectbox(
875
+ "Base Temperature",
876
+ options=[18, 15.5, 12],
877
+ index=[18, 15.5, 12].index(st.session_state.heating_form_data['occupancy'].get('base_temp', 18)) if st.session_state.heating_form_data['occupancy'].get('base_temp') in [18, 15.5, 12] else 0,
878
+ help="Base temperature for heating degree days calculation"
879
+ )
880
+
881
+ with col2:
882
+ # Get default HDD from reference data
883
+ calculator = HeatingLoadCalculator()
884
+ default_hdd = calculator.get_heating_degree_days(location, base_temp)
885
+
886
+ heating_degree_days = st.number_input(
887
+ "Heating Degree Days",
888
+ value=float(st.session_state.heating_form_data['occupancy'].get('heating_degree_days', default_hdd)),
889
+ min_value=0.0,
890
+ step=10.0,
891
+ help=f"Default value for {location_name} at base {base_temp}°C: {default_hdd}"
892
+ )
893
+
894
+ st.write(f"Heating degree days represent the sum of daily temperature differences between the base temperature and the average daily temperature when it falls below the base temperature.")
895
+
896
+ # Save heating degree days data
897
+ st.session_state.heating_form_data['occupancy']['base_temp'] = base_temp
898
+ st.session_state.heating_form_data['occupancy']['heating_degree_days'] = heating_degree_days
899
+
900
+ # Validate inputs
901
+ warnings = []
902
+
903
+ # Check if heating degree days are reasonable
904
+ if heating_degree_days == 0:
905
+ warnings.append(ValidationWarning(
906
+ "Zero heating degree days",
907
+ "With zero heating degree days, annual heating energy will be zero."
908
+ ))
909
+ elif heating_degree_days < 100 and base_temp == 18:
910
+ warnings.append(ValidationWarning(
911
+ "Very low heating degree days",
912
+ f"Heating degree days below 100 at base {base_temp}°C is unusually low for most locations."
913
+ ))
914
+ elif heating_degree_days > 3000:
915
+ warnings.append(ValidationWarning(
916
+ "Very high heating degree days",
917
+ "Heating degree days above 3000 is unusually high for most locations."
918
+ ))
919
+
920
+ # Save warnings to session state
921
+ st.session_state.heating_warnings['occupancy'] = warnings
922
+
923
+ # Display warnings if any
924
+ if warnings:
925
+ st.warning("Please review the following warnings:")
926
+ for warning in warnings:
927
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
928
+ st.write(f" Suggestion: {warning.suggestion}")
929
+
930
+ # Mark this step as completed if there are no critical warnings
931
+ st.session_state.heating_completed['occupancy'] = not any(w.is_critical for w in warnings)
932
+
933
+ # Navigation buttons
934
+ col1, col2 = st.columns([1, 1])
935
+
936
+ with col1:
937
+ prev_button = st.button("← Back: Ventilation", key="heating_occupancy_prev")
938
+ if prev_button:
939
+ st.session_state.heating_active_tab = "ventilation"
940
+ st.experimental_rerun()
941
+
942
+ with col2:
943
+ calculate_button = st.button("Calculate Results →", key="heating_occupancy_calculate")
944
+ if calculate_button:
945
+ # Calculate heating load
946
+ calculate_heating_load()
947
+ st.session_state.heating_active_tab = "results"
948
+ st.experimental_rerun()
949
+
950
+
951
+ def calculate_heating_load():
952
+ """Calculate heating load based on input data."""
953
+ # Create calculator instance
954
+ calculator = HeatingLoadCalculator()
955
+
956
+ # Get form data
957
+ form_data = st.session_state.heating_form_data
958
+
959
+ # Prepare building components for calculation
960
+ building_components = []
961
+
962
+ # Add walls
963
+ for wall in form_data['building_envelope'].get('walls', []):
964
+ building_components.append({
965
+ 'name': wall['name'],
966
+ 'area': wall['area'],
967
+ 'u_value': wall['u_value'],
968
+ 'temp_diff': wall['temp_diff']
969
+ })
970
+
971
+ # Add roof
972
+ roof = form_data['building_envelope'].get('roof', {})
973
+ if roof:
974
+ building_components.append({
975
+ 'name': 'Roof',
976
+ 'area': roof['area'],
977
+ 'u_value': roof['u_value'],
978
+ 'temp_diff': roof['temp_diff']
979
+ })
980
+
981
+ # Add floor
982
+ floor = form_data['building_envelope'].get('floor', {})
983
+ if floor:
984
+ building_components.append({
985
+ 'name': 'Floor',
986
+ 'area': floor['area'],
987
+ 'u_value': floor['u_value'],
988
+ 'temp_diff': floor['temp_diff']
989
+ })
990
+
991
+ # Add windows
992
+ for window in form_data['windows'].get('windows', []):
993
+ building_components.append({
994
+ 'name': window['name'],
995
+ 'area': window['area'],
996
+ 'u_value': window['u_value'],
997
+ 'temp_diff': window['temp_diff']
998
+ })
999
+
1000
+ # Add doors
1001
+ for door in form_data['windows'].get('doors', []):
1002
+ building_components.append({
1003
+ 'name': door['name'],
1004
+ 'area': door['area'],
1005
+ 'u_value': door['u_value'],
1006
+ 'temp_diff': door['temp_diff']
1007
+ })
1008
+
1009
+ # Prepare infiltration data
1010
+ infiltration = form_data['ventilation'].get('infiltration', {})
1011
+ ventilation = form_data['ventilation'].get('ventilation', {})
1012
+
1013
+ infiltration_data = {
1014
+ 'volume': infiltration.get('volume', 0),
1015
+ 'air_changes': infiltration.get('air_changes', 0) + ventilation.get('air_changes', 0),
1016
+ 'temp_diff': infiltration.get('temp_diff', 0)
1017
+ }
1018
+
1019
+ # Calculate heating load
1020
+ results = calculator.calculate_total_heating_load(
1021
+ building_components=building_components,
1022
+ infiltration=infiltration_data
1023
+ )
1024
+
1025
+ # Calculate annual heating requirement
1026
+ location = form_data['building_info'].get('location', 'sydney')
1027
+ occupancy_type = form_data['occupancy'].get('occupancy_type', 'continuous')
1028
+ base_temp = form_data['occupancy'].get('base_temp', 18)
1029
+
1030
+ annual_results = calculator.calculate_annual_heating_requirement(
1031
+ results['total_load'],
1032
+ location,
1033
+ occupancy_type,
1034
+ base_temp
1035
+ )
1036
+
1037
+ # Combine results
1038
+ combined_results = {**results, **annual_results}
1039
+
1040
+ # Save results to session state
1041
+ st.session_state.heating_results = combined_results
1042
+
1043
+ # Add timestamp
1044
+ st.session_state.heating_results['timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1045
+
1046
+ # Add building info
1047
+ st.session_state.heating_results['building_info'] = form_data['building_info']
1048
+
1049
+ return combined_results
1050
+
1051
+
1052
+ def results_page():
1053
+ """Display calculation results."""
1054
+ st.subheader("Heating Load Calculation Results")
1055
+
1056
+ # Check if results are available
1057
+ if not st.session_state.heating_results:
1058
+ st.warning("No calculation results available. Please complete the input forms and calculate results.")
1059
+ return
1060
+
1061
+ # Get results
1062
+ results = st.session_state.heating_results
1063
+
1064
+ # Display summary
1065
+ st.write("### Summary")
1066
+
1067
+ col1, col2 = st.columns(2)
1068
+
1069
+ with col1:
1070
+ st.metric("Total Heating Load", f"{results['total_load']:.2f} W")
1071
+
1072
+ # Convert to kW
1073
+ total_load_kw = results['total_load'] / 1000
1074
+ st.metric("Total Heating Load", f"{total_load_kw:.2f} kW")
1075
+
1076
+ # Annual heating energy
1077
+ st.metric("Annual Heating Energy", f"{results['annual_energy_kwh']:.2f} kWh")
1078
+
1079
+ with col2:
1080
+ # Calculate heating load per area
1081
+ floor_area = results['building_info'].get('floor_area', 80.0)
1082
+ heating_load_per_area = results['total_load'] / floor_area
1083
+ st.metric("Heating Load per Area", f"{heating_load_per_area:.2f} W/m²")
1084
+
1085
+ # Annual heating energy per area
1086
+ annual_energy_per_area = results['annual_energy_kwh'] / floor_area
1087
+ st.metric("Annual Heating Energy per Area", f"{annual_energy_per_area:.2f} kWh/m²")
1088
+
1089
+ # Equipment sizing recommendation
1090
+ # Add 10% safety factor
1091
+ recommended_size = total_load_kw * 1.1
1092
+ st.metric("Recommended Equipment Size", f"{recommended_size:.2f} kW")
1093
+
1094
+ # Display load breakdown
1095
+ st.write("### Load Breakdown")
1096
+
1097
+ # Prepare data for pie chart
1098
+ component_losses = results['component_losses']
1099
+
1100
+ # Create pie chart for component losses
1101
+ fig = px.pie(
1102
+ values=list(component_losses.values()),
1103
+ names=list(component_losses.keys()),
1104
+ title="Heating Load Components",
1105
+ color_discrete_sequence=px.colors.qualitative.Set2
1106
+ )
1107
+
1108
+ st.plotly_chart(fig)
1109
+
1110
+ # Display load components in a table
1111
+ load_components = {
1112
+ 'Conduction (Building Envelope)': results['total_conduction_loss'] - results.get('infiltration_loss', 0),
1113
+ 'Infiltration & Ventilation': results.get('infiltration_loss', 0)
1114
+ }
1115
+
1116
+ load_df = pd.DataFrame({
1117
+ 'Component': list(load_components.keys()),
1118
+ 'Load (W)': list(load_components.values()),
1119
+ 'Percentage (%)': [value / results['total_load'] * 100 for value in load_components.values()]
1120
+ })
1121
+
1122
+ st.dataframe(load_df.style.format({
1123
+ 'Load (W)': '{:.2f}',
1124
+ 'Percentage (%)': '{:.2f}'
1125
+ }))
1126
+
1127
+ # Display detailed results
1128
+ st.write("### Detailed Results")
1129
+
1130
+ # Create tabs for different result sections
1131
+ tabs = st.tabs([
1132
+ "Building Components",
1133
+ "Ventilation",
1134
+ "Annual Energy"
1135
+ ])
1136
+
1137
+ with tabs[0]:
1138
+ st.subheader("Building Component Heat Losses")
1139
+
1140
+ # Create dataframe from component losses
1141
+ components_data = []
1142
+ for name, loss in component_losses.items():
1143
+ # Find the component in the original data to get area and U-value
1144
+ component = None
1145
+ for comp in st.session_state.heating_form_data['building_envelope'].get('walls', []):
1146
+ if comp['name'] == name:
1147
+ component = comp
1148
+ break
1149
+
1150
+ if name == 'Roof':
1151
+ component = st.session_state.heating_form_data['building_envelope'].get('roof', {})
1152
+ elif name == 'Floor':
1153
+ component = st.session_state.heating_form_data['building_envelope'].get('floor', {})
1154
+
1155
+ # Check windows and doors
1156
+ if not component:
1157
+ for window in st.session_state.heating_form_data['windows'].get('windows', []):
1158
+ if window['name'] == name:
1159
+ component = window
1160
+ break
1161
+
1162
+ if not component:
1163
+ for door in st.session_state.heating_form_data['windows'].get('doors', []):
1164
+ if door['name'] == name:
1165
+ component = door
1166
+ break
1167
+
1168
+ if component:
1169
+ components_data.append({
1170
+ 'Component': name,
1171
+ 'Area (m²)': component.get('area', 0),
1172
+ 'U-Value (W/m²°C)': component.get('u_value', 0),
1173
+ 'Temperature Difference (°C)': component.get('temp_diff', 0),
1174
+ 'Heat Loss (W)': loss
1175
+ })
1176
+ else:
1177
+ components_data.append({
1178
+ 'Component': name,
1179
+ 'Area (m²)': 0,
1180
+ 'U-Value (W/m²°C)': 0,
1181
+ 'Temperature Difference (°C)': 0,
1182
+ 'Heat Loss (W)': loss
1183
+ })
1184
+
1185
+ # Create dataframe
1186
+ components_df = pd.DataFrame(components_data)
1187
+
1188
+ # Display table
1189
+ st.dataframe(components_df.style.format({
1190
+ 'Area (m²)': '{:.2f}',
1191
+ 'U-Value (W/m²°C)': '{:.2f}',
1192
+ 'Temperature Difference (°C)': '{:.2f}',
1193
+ 'Heat Loss (W)': '{:.2f}'
1194
+ }))
1195
+
1196
+ # Create bar chart
1197
+ fig = px.bar(
1198
+ components_df,
1199
+ x='Component',
1200
+ y='Heat Loss (W)',
1201
+ title="Heat Loss by Building Component",
1202
+ color='Component',
1203
+ color_discrete_sequence=px.colors.qualitative.Set3
1204
+ )
1205
+
1206
+ st.plotly_chart(fig)
1207
+
1208
+ with tabs[1]:
1209
+ st.subheader("Ventilation & Infiltration Heat Losses")
1210
+
1211
+ # Get ventilation data
1212
+ ventilation_data = st.session_state.heating_form_data['ventilation']
1213
+
1214
+ # Create dataframe
1215
+ ventilation_df = pd.DataFrame([
1216
+ {
1217
+ 'Source': 'Infiltration',
1218
+ 'Air Changes per Hour': ventilation_data['infiltration']['air_changes'],
1219
+ 'Volume (m³)': ventilation_data['infiltration']['volume'],
1220
+ 'Temperature Difference (°C)': ventilation_data['infiltration']['temp_diff'],
1221
+ 'Heat Loss (W)': ventilation_data['infiltration']['heat_loss']
1222
+ },
1223
+ {
1224
+ 'Source': 'Ventilation',
1225
+ 'Air Changes per Hour': ventilation_data['ventilation']['air_changes'],
1226
+ 'Volume (m³)': ventilation_data['ventilation']['volume'],
1227
+ 'Temperature Difference (°C)': ventilation_data['ventilation']['temp_diff'],
1228
+ 'Heat Loss (W)': ventilation_data['ventilation']['heat_loss']
1229
+ }
1230
+ ])
1231
+
1232
+ # Display table
1233
+ st.dataframe(ventilation_df.style.format({
1234
+ 'Air Changes per Hour': '{:.2f}',
1235
+ 'Volume (m³)': '{:.2f}',
1236
+ 'Temperature Difference (°C)': '{:.2f}',
1237
+ 'Heat Loss (W)': '{:.2f}'
1238
+ }))
1239
+
1240
+ # Create bar chart
1241
+ fig = px.bar(
1242
+ ventilation_df,
1243
+ x='Source',
1244
+ y='Heat Loss (W)',
1245
+ title="Ventilation & Infiltration Heat Losses",
1246
+ color='Source',
1247
+ color_discrete_sequence=px.colors.qualitative.Pastel2
1248
+ )
1249
+
1250
+ st.plotly_chart(fig)
1251
+
1252
+ with tabs[2]:
1253
+ st.subheader("Annual Heating Energy")
1254
+
1255
+ # Get occupancy data
1256
+ occupancy_data = st.session_state.heating_form_data['occupancy']
1257
+
1258
+ # Create dataframe
1259
+ annual_data = pd.DataFrame([
1260
+ {
1261
+ 'Parameter': 'Heating Degree Days',
1262
+ 'Value': results['heating_degree_days'],
1263
+ 'Unit': 'HDD'
1264
+ },
1265
+ {
1266
+ 'Parameter': 'Base Temperature',
1267
+ 'Value': occupancy_data['base_temp'],
1268
+ 'Unit': '°C'
1269
+ },
1270
+ {
1271
+ 'Parameter': 'Occupancy Type',
1272
+ 'Value': occupancy_data['occupancy_type'].capitalize(),
1273
+ 'Unit': ''
1274
+ },
1275
+ {
1276
+ 'Parameter': 'Correction Factor',
1277
+ 'Value': results['correction_factor'],
1278
+ 'Unit': ''
1279
+ },
1280
+ {
1281
+ 'Parameter': 'Annual Heating Energy',
1282
+ 'Value': results['annual_energy_kwh'],
1283
+ 'Unit': 'kWh'
1284
+ },
1285
+ {
1286
+ 'Parameter': 'Annual Heating Energy',
1287
+ 'Value': results['annual_energy_mj'],
1288
+ 'Unit': 'MJ'
1289
+ }
1290
+ ])
1291
+
1292
+ # Display table
1293
+ st.dataframe(annual_data.style.format({
1294
+ 'Value': lambda x: f"{x:.2f}" if isinstance(x, (int, float)) else str(x)
1295
+ }))
1296
+
1297
+ # Create bar chart for monthly distribution (estimated)
1298
+ # This is a simplified distribution based on heating degree days
1299
+ months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
1300
+
1301
+ # Get location
1302
+ location = st.session_state.heating_form_data['building_info'].get('location', 'sydney')
1303
+
1304
+ # Simplified monthly distribution factors based on hemisphere
1305
+ # Southern hemisphere: winter is June-August
1306
+ # Northern hemisphere: winter is December-February
1307
+ southern_hemisphere = ['sydney', 'melbourne', 'brisbane', 'perth', 'adelaide', 'hobart', 'darwin', 'canberra', 'mildura']
1308
+
1309
+ if location.lower() in southern_hemisphere:
1310
+ # Southern hemisphere distribution
1311
+ monthly_factors = [0.02, 0.01, 0.03, 0.08, 0.12, 0.16, 0.18, 0.16, 0.12, 0.08, 0.03, 0.01]
1312
+ else:
1313
+ # Northern hemisphere distribution
1314
+ monthly_factors = [0.18, 0.16, 0.12, 0.08, 0.03, 0.01, 0.01, 0.01, 0.03, 0.08, 0.12, 0.17]
1315
+
1316
+ # Calculate monthly energy
1317
+ monthly_energy = [results['annual_energy_kwh'] * factor for factor in monthly_factors]
1318
+
1319
+ # Create dataframe
1320
+ monthly_df = pd.DataFrame({
1321
+ 'Month': months,
1322
+ 'Energy (kWh)': monthly_energy
1323
+ })
1324
+
1325
+ # Create bar chart
1326
+ fig = px.bar(
1327
+ monthly_df,
1328
+ x='Month',
1329
+ y='Energy (kWh)',
1330
+ title="Estimated Monthly Heating Energy Distribution",
1331
+ color_discrete_sequence=['indianred']
1332
+ )
1333
+
1334
+ st.plotly_chart(fig)
1335
+
1336
+ # Export options
1337
+ st.write("### Export Options")
1338
+
1339
+ col1, col2 = st.columns(2)
1340
+
1341
+ with col1:
1342
+ if st.button("Export Results as CSV", key="export_csv_heating"):
1343
+ # Create a CSV file with results
1344
+ csv_data = export_data(st.session_state.heating_form_data, st.session_state.heating_results, format='csv')
1345
+
1346
+ # Provide download link
1347
+ st.download_button(
1348
+ label="Download CSV",
1349
+ data=csv_data,
1350
+ file_name=f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
1351
+ mime="text/csv"
1352
+ )
1353
+
1354
+ with col2:
1355
+ if st.button("Export Results as JSON", key="export_json_heating"):
1356
+ # Create a JSON file with results
1357
+ json_data = export_data(st.session_state.heating_form_data, st.session_state.heating_results, format='json')
1358
+
1359
+ # Provide download link
1360
+ st.download_button(
1361
+ label="Download JSON",
1362
+ data=json_data,
1363
+ file_name=f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1364
+ mime="application/json"
1365
+ )
1366
+
1367
+ # Navigation buttons
1368
+ col1, col2 = st.columns([1, 1])
1369
+
1370
+ with col1:
1371
+ prev_button = st.button("← Back: Occupancy", key="heating_results_prev")
1372
+ if prev_button:
1373
+ st.session_state.heating_active_tab = "occupancy"
1374
+ st.experimental_rerun()
1375
+
1376
+ with col2:
1377
+ recalculate_button = st.button("Recalculate", key="heating_results_recalculate")
1378
+ if recalculate_button:
1379
+ # Recalculate heating load
1380
+ calculate_heating_load()
1381
+ st.experimental_rerun()
1382
+
1383
+
1384
+ def heating_calculator():
1385
+ """Main function for the heating load calculator page."""
1386
+ st.title("Heating Load Calculator")
1387
+
1388
+ # Initialize reference data
1389
+ ref_data = ReferenceData()
1390
+
1391
+ # Initialize session state
1392
+ load_session_state()
1393
+
1394
+ # Initialize active tab if not already set
1395
+ if 'heating_active_tab' not in st.session_state:
1396
+ st.session_state.heating_active_tab = "building_info"
1397
+
1398
+ # Create tabs for different steps
1399
+ tabs = st.tabs([
1400
+ "1. Building Information",
1401
+ "2. Building Envelope",
1402
+ "3. Windows & Doors",
1403
+ "4. Ventilation",
1404
+ "5. Occupancy",
1405
+ "6. Results"
1406
+ ])
1407
+
1408
+ # Display the active tab
1409
+ with tabs[0]:
1410
+ if st.session_state.heating_active_tab == "building_info":
1411
+ building_info_form(ref_data)
1412
+
1413
+ with tabs[1]:
1414
+ if st.session_state.heating_active_tab == "building_envelope":
1415
+ building_envelope_form(ref_data)
1416
+
1417
+ with tabs[2]:
1418
+ if st.session_state.heating_active_tab == "windows":
1419
+ windows_form(ref_data)
1420
+
1421
+ with tabs[3]:
1422
+ if st.session_state.heating_active_tab == "ventilation":
1423
+ ventilation_form(ref_data)
1424
+
1425
+ with tabs[4]:
1426
+ if st.session_state.heating_active_tab == "occupancy":
1427
+ occupancy_form(ref_data)
1428
+
1429
+ with tabs[5]:
1430
+ if st.session_state.heating_active_tab == "results":
1431
+ results_page()
1432
+
1433
+
1434
+ if __name__ == "__main__":
1435
+ heating_calculator()
reference_data.py ADDED
@@ -0,0 +1,616 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reference Data Module for HVAC Load Calculator
3
+
4
+ This module provides reference data for materials, locations, and other parameters
5
+ needed for HVAC load calculations.
6
+ """
7
+
8
+ import pandas as pd
9
+ import json
10
+ from pathlib import Path
11
+
12
+
13
+ class ReferenceData:
14
+ """
15
+ A class to manage reference data for HVAC load calculations.
16
+ """
17
+
18
+ def __init__(self):
19
+ """Initialize the reference data."""
20
+ self.materials = self._load_materials()
21
+ self.locations = self._load_locations()
22
+ self.glass_types = self._load_glass_types()
23
+ self.shading_factors = self._load_shading_factors()
24
+ self.internal_loads = self._load_internal_loads()
25
+ self.occupancy_factors = self._load_occupancy_factors()
26
+
27
+ def _load_materials(self):
28
+ """
29
+ Load building material properties.
30
+
31
+ Returns:
32
+ dict: Dictionary of material properties
33
+ """
34
+ # This would typically load from a JSON or CSV file
35
+ # For now, we'll define it directly
36
+
37
+ materials = {
38
+ "walls": {
39
+ "brick_veneer": {
40
+ "name": "Brick veneer with insulation",
41
+ "u_value": 0.5, # W/m²°C
42
+ "r_value": 2.0, # m²°C/W
43
+ "description": "Brick veneer with timber frame and insulation"
44
+ },
45
+ "double_brick": {
46
+ "name": "Double brick",
47
+ "u_value": 1.88, # W/m²°C
48
+ "r_value": 0.53, # m²°C/W
49
+ "description": "Double brick wall without insulation"
50
+ },
51
+ "double_brick_insulated": {
52
+ "name": "Double brick with insulation",
53
+ "u_value": 0.6, # W/m²°C
54
+ "r_value": 1.67, # m²°C/W
55
+ "description": "Double brick wall with insulation"
56
+ },
57
+ "timber_frame": {
58
+ "name": "Timber frame",
59
+ "u_value": 0.8, # W/m²°C
60
+ "r_value": 1.25, # m²°C/W
61
+ "description": "Timber frame wall with insulation"
62
+ },
63
+ "concrete_block": {
64
+ "name": "Concrete block",
65
+ "u_value": 2.3, # W/m²°C
66
+ "r_value": 0.43, # m²°C/W
67
+ "description": "Concrete block wall without insulation"
68
+ },
69
+ "concrete_block_insulated": {
70
+ "name": "Concrete block with insulation",
71
+ "u_value": 0.7, # W/m²°C
72
+ "r_value": 1.43, # m²°C/W
73
+ "description": "Concrete block wall with insulation"
74
+ }
75
+ },
76
+ "roofs": {
77
+ "metal_deck_insulated": {
78
+ "name": "Metal deck with insulation",
79
+ "u_value": 0.46, # W/m²°C
80
+ "r_value": 2.17, # m²°C/W
81
+ "description": "Metal deck roof with insulation and plasterboard ceiling"
82
+ },
83
+ "metal_deck_uninsulated": {
84
+ "name": "Metal deck without insulation",
85
+ "u_value": 2.2, # W/m²°C
86
+ "r_value": 0.45, # m²°C/W
87
+ "description": "Metal deck roof without insulation"
88
+ },
89
+ "concrete_slab_roof": {
90
+ "name": "Concrete slab roof",
91
+ "u_value": 3.1, # W/m²°C
92
+ "r_value": 0.32, # m²°C/W
93
+ "description": "Concrete slab roof without insulation"
94
+ },
95
+ "concrete_slab_insulated": {
96
+ "name": "Concrete slab roof with insulation",
97
+ "u_value": 0.5, # W/m²°C
98
+ "r_value": 2.0, # m²°C/W
99
+ "description": "Concrete slab roof with insulation"
100
+ },
101
+ "tiled_roof_insulated": {
102
+ "name": "Tiled roof with insulation",
103
+ "u_value": 0.4, # W/m²°C
104
+ "r_value": 2.5, # m²°C/W
105
+ "description": "Tiled roof with insulation and plasterboard ceiling"
106
+ },
107
+ "tiled_roof_uninsulated": {
108
+ "name": "Tiled roof without insulation",
109
+ "u_value": 2.0, # W/m²°C
110
+ "r_value": 0.5, # m²°C/W
111
+ "description": "Tiled roof without insulation"
112
+ }
113
+ },
114
+ "floors": {
115
+ "concrete_slab_ground": {
116
+ "name": "Concrete slab on ground",
117
+ "u_value": 0.6, # W/m²°C
118
+ "r_value": 1.67, # m²°C/W
119
+ "description": "Concrete slab directly on ground"
120
+ },
121
+ "concrete_slab_insulated": {
122
+ "name": "Concrete slab with insulation",
123
+ "u_value": 0.3, # W/m²°C
124
+ "r_value": 3.33, # m²°C/W
125
+ "description": "Concrete slab with insulation"
126
+ },
127
+ "suspended_timber": {
128
+ "name": "Suspended timber floor",
129
+ "u_value": 1.5, # W/m²°C
130
+ "r_value": 0.67, # m²°C/W
131
+ "description": "Suspended timber floor without insulation"
132
+ },
133
+ "suspended_timber_insulated": {
134
+ "name": "Suspended timber floor with insulation",
135
+ "u_value": 0.4, # W/m²°C
136
+ "r_value": 2.5, # m²°C/W
137
+ "description": "Suspended timber floor with insulation"
138
+ }
139
+ }
140
+ }
141
+
142
+ return materials
143
+
144
+ def _load_locations(self):
145
+ """
146
+ Load climate data for different locations.
147
+
148
+ Returns:
149
+ dict: Dictionary of location climate data
150
+ """
151
+ # This would typically load from a JSON or CSV file
152
+ # For now, we'll define it directly
153
+
154
+ locations = {
155
+ "sydney": {
156
+ "name": "Sydney",
157
+ "state": "NSW",
158
+ "summer_design_temp": 32.0, # °C
159
+ "winter_design_temp": 7.0, # °C
160
+ "daily_temp_range": "medium", # 8.5-14°C
161
+ "heating_degree_days": 740, # Base 18°C
162
+ "cooling_degree_days": 350, # Base 18°C
163
+ "latitude": -33.87,
164
+ "longitude": 151.21
165
+ },
166
+ "melbourne": {
167
+ "name": "Melbourne",
168
+ "state": "VIC",
169
+ "summer_design_temp": 35.0, # °C
170
+ "winter_design_temp": 4.0, # °C
171
+ "daily_temp_range": "medium", # 8.5-14°C
172
+ "heating_degree_days": 1400, # Base 18°C
173
+ "cooling_degree_days": 200, # Base 18°C
174
+ "latitude": -37.81,
175
+ "longitude": 144.96
176
+ },
177
+ "brisbane": {
178
+ "name": "Brisbane",
179
+ "state": "QLD",
180
+ "summer_design_temp": 32.0, # °C
181
+ "winter_design_temp": 9.0, # °C
182
+ "daily_temp_range": "medium", # 8.5-14°C
183
+ "heating_degree_days": 320, # Base 18°C
184
+ "cooling_degree_days": 750, # Base 18°C
185
+ "latitude": -27.47,
186
+ "longitude": 153.03
187
+ },
188
+ "perth": {
189
+ "name": "Perth",
190
+ "state": "WA",
191
+ "summer_design_temp": 37.0, # °C
192
+ "winter_design_temp": 7.0, # °C
193
+ "daily_temp_range": "high", # >14°C
194
+ "heating_degree_days": 760, # Base 18°C
195
+ "cooling_degree_days": 600, # Base 18°C
196
+ "latitude": -31.95,
197
+ "longitude": 115.86
198
+ },
199
+ "adelaide": {
200
+ "name": "Adelaide",
201
+ "state": "SA",
202
+ "summer_design_temp": 38.0, # °C
203
+ "winter_design_temp": 5.0, # °C
204
+ "daily_temp_range": "high", # >14°C
205
+ "heating_degree_days": 1100, # Base 18°C
206
+ "cooling_degree_days": 500, # Base 18°C
207
+ "latitude": -34.93,
208
+ "longitude": 138.60
209
+ },
210
+ "hobart": {
211
+ "name": "Hobart",
212
+ "state": "TAS",
213
+ "summer_design_temp": 28.0, # °C
214
+ "winter_design_temp": 2.0, # °C
215
+ "daily_temp_range": "medium", # 8.5-14°C
216
+ "heating_degree_days": 1800, # Base 18°C
217
+ "cooling_degree_days": 50, # Base 18°C
218
+ "latitude": -42.88,
219
+ "longitude": 147.33
220
+ },
221
+ "darwin": {
222
+ "name": "Darwin",
223
+ "state": "NT",
224
+ "summer_design_temp": 34.0, # °C
225
+ "winter_design_temp": 15.0, # °C
226
+ "daily_temp_range": "low", # <8.5°C
227
+ "heating_degree_days": 0, # Base 18°C
228
+ "cooling_degree_days": 3500, # Base 18°C
229
+ "latitude": -12.46,
230
+ "longitude": 130.84
231
+ },
232
+ "canberra": {
233
+ "name": "Canberra",
234
+ "state": "ACT",
235
+ "summer_design_temp": 35.0, # °C
236
+ "winter_design_temp": -1.0, # °C
237
+ "daily_temp_range": "high", # >14°C
238
+ "heating_degree_days": 2000, # Base 18°C
239
+ "cooling_degree_days": 150, # Base 18°C
240
+ "latitude": -35.28,
241
+ "longitude": 149.13
242
+ },
243
+ "mildura": {
244
+ "name": "Mildura",
245
+ "state": "VIC",
246
+ "summer_design_temp": 38.0, # °C
247
+ "winter_design_temp": 4.5, # °C
248
+ "daily_temp_range": "high", # >14°C
249
+ "heating_degree_days": 1200, # Base 18°C
250
+ "cooling_degree_days": 700, # Base 18°C
251
+ "latitude": -34.21,
252
+ "longitude": 142.14
253
+ }
254
+ }
255
+
256
+ return locations
257
+
258
+ def _load_glass_types(self):
259
+ """
260
+ Load glass type properties.
261
+
262
+ Returns:
263
+ dict: Dictionary of glass type properties
264
+ """
265
+ # This would typically load from a JSON or CSV file
266
+ # For now, we'll define it directly
267
+
268
+ glass_types = {
269
+ "single": {
270
+ "name": "Single glazing",
271
+ "u_value": 5.8, # W/m²°C
272
+ "shgc": 0.85, # Solar Heat Gain Coefficient
273
+ "description": "Standard single glazed window"
274
+ },
275
+ "double": {
276
+ "name": "Double glazing",
277
+ "u_value": 2.9, # W/m²°C
278
+ "shgc": 0.75, # Solar Heat Gain Coefficient
279
+ "description": "Standard double glazed window"
280
+ },
281
+ "low_e": {
282
+ "name": "Low-E double glazing",
283
+ "u_value": 1.8, # W/m²°C
284
+ "shgc": 0.65, # Solar Heat Gain Coefficient
285
+ "description": "Double glazed window with low-emissivity coating"
286
+ },
287
+ "triple": {
288
+ "name": "Triple glazing",
289
+ "u_value": 1.2, # W/m²°C
290
+ "shgc": 0.6, # Solar Heat Gain Coefficient
291
+ "description": "Triple glazed window"
292
+ },
293
+ "tinted": {
294
+ "name": "Tinted single glazing",
295
+ "u_value": 5.8, # W/m²°C
296
+ "shgc": 0.65, # Solar Heat Gain Coefficient
297
+ "description": "Single glazed window with tinting"
298
+ },
299
+ "tinted_double": {
300
+ "name": "Tinted double glazing",
301
+ "u_value": 2.9, # W/m²°C
302
+ "shgc": 0.55, # Solar Heat Gain Coefficient
303
+ "description": "Double glazed window with tinting"
304
+ }
305
+ }
306
+
307
+ return glass_types
308
+
309
+ def _load_shading_factors(self):
310
+ """
311
+ Load shading factors for different shading devices.
312
+
313
+ Returns:
314
+ dict: Dictionary of shading factors
315
+ """
316
+ # This would typically load from a JSON or CSV file
317
+ # For now, we'll define it directly
318
+
319
+ shading_factors = {
320
+ "none": {
321
+ "name": "No shading",
322
+ "factor": 0.0,
323
+ "description": "No shading devices"
324
+ },
325
+ "internal_blinds": {
326
+ "name": "Internal venetian blinds",
327
+ "factor": 0.4,
328
+ "description": "Internal venetian blinds"
329
+ },
330
+ "internal_drapes": {
331
+ "name": "Internal drapes",
332
+ "factor": 0.3,
333
+ "description": "Internal drapes or curtains"
334
+ },
335
+ "external_awning": {
336
+ "name": "External awning",
337
+ "factor": 0.7,
338
+ "description": "External awning"
339
+ },
340
+ "external_shutters": {
341
+ "name": "External shutters",
342
+ "factor": 0.8,
343
+ "description": "External shutters"
344
+ },
345
+ "eaves": {
346
+ "name": "Eaves or overhang",
347
+ "factor": 0.5,
348
+ "description": "Eaves or overhang"
349
+ },
350
+ "pergola": {
351
+ "name": "Pergola with vegetation",
352
+ "factor": 0.6,
353
+ "description": "Pergola with vegetation"
354
+ }
355
+ }
356
+
357
+ return shading_factors
358
+
359
+ def _load_internal_loads(self):
360
+ """
361
+ Load internal load data.
362
+
363
+ Returns:
364
+ dict: Dictionary of internal load data
365
+ """
366
+ # This would typically load from a JSON or CSV file
367
+ # For now, we'll define it directly
368
+
369
+ internal_loads = {
370
+ "people": {
371
+ "seated_resting": {
372
+ "name": "Seated, resting",
373
+ "sensible_heat": 75, # W per person
374
+ "latent_heat": 30 # W per person
375
+ },
376
+ "seated_light_work": {
377
+ "name": "Seated, light work",
378
+ "sensible_heat": 85, # W per person
379
+ "latent_heat": 40 # W per person
380
+ },
381
+ "standing_light_work": {
382
+ "name": "Standing, light work",
383
+ "sensible_heat": 90, # W per person
384
+ "latent_heat": 50 # W per person
385
+ },
386
+ "light_activity": {
387
+ "name": "Light activity",
388
+ "sensible_heat": 100, # W per person
389
+ "latent_heat": 60 # W per person
390
+ },
391
+ "medium_activity": {
392
+ "name": "Medium activity",
393
+ "sensible_heat": 120, # W per person
394
+ "latent_heat": 80 # W per person
395
+ }
396
+ },
397
+ "lighting": {
398
+ "incandescent": {
399
+ "name": "Incandescent",
400
+ "heat_factor": 1.0 # 100% of wattage becomes heat
401
+ },
402
+ "fluorescent": {
403
+ "name": "Fluorescent",
404
+ "heat_factor": 1.2 # 120% of wattage becomes heat (includes ballast)
405
+ },
406
+ "led": {
407
+ "name": "LED",
408
+ "heat_factor": 0.8 # 80% of wattage becomes heat
409
+ }
410
+ },
411
+ "appliances": {
412
+ "kitchen": {
413
+ "name": "Kitchen",
414
+ "heat_gain": 1000 # W
415
+ },
416
+ "living_room": {
417
+ "name": "Living room",
418
+ "heat_gain": 300 # W
419
+ },
420
+ "bedroom": {
421
+ "name": "Bedroom",
422
+ "heat_gain": 150 # W
423
+ },
424
+ "office": {
425
+ "name": "Home office",
426
+ "heat_gain": 450 # W
427
+ }
428
+ }
429
+ }
430
+
431
+ return internal_loads
432
+
433
+ def _load_occupancy_factors(self):
434
+ """
435
+ Load occupancy correction factors.
436
+
437
+ Returns:
438
+ dict: Dictionary of occupancy correction factors
439
+ """
440
+ # This would typically load from a JSON or CSV file
441
+ # For now, we'll define it directly
442
+
443
+ occupancy_factors = {
444
+ "continuous": {
445
+ "name": "Continuous",
446
+ "factor": 1.0,
447
+ "description": "Continuously heated"
448
+ },
449
+ "intermittent": {
450
+ "name": "Intermittent",
451
+ "factor": 0.8,
452
+ "description": "Heated during occupied hours"
453
+ },
454
+ "night_setback": {
455
+ "name": "Night setback",
456
+ "factor": 0.9,
457
+ "description": "Temperature setback at night"
458
+ },
459
+ "weekend_off": {
460
+ "name": "Weekend off",
461
+ "factor": 0.85,
462
+ "description": "Heating off during weekends"
463
+ },
464
+ "vacation_home": {
465
+ "name": "Vacation home",
466
+ "factor": 0.6,
467
+ "description": "Occasionally occupied"
468
+ }
469
+ }
470
+
471
+ return occupancy_factors
472
+
473
+ def get_material_by_type(self, material_type, material_id):
474
+ """
475
+ Get material properties by type and ID.
476
+
477
+ Args:
478
+ material_type (str): Type of material ('walls', 'roofs', 'floors')
479
+ material_id (str): ID of the material
480
+
481
+ Returns:
482
+ dict: Material properties
483
+ """
484
+ if material_type in self.materials and material_id in self.materials[material_type]:
485
+ return self.materials[material_type][material_id]
486
+ return None
487
+
488
+ def get_location_data(self, location_id):
489
+ """
490
+ Get climate data for a location.
491
+
492
+ Args:
493
+ location_id (str): ID of the location
494
+
495
+ Returns:
496
+ dict: Location climate data
497
+ """
498
+ if location_id in self.locations:
499
+ return self.locations[location_id]
500
+ return None
501
+
502
+ def get_glass_type(self, glass_id):
503
+ """
504
+ Get glass type properties.
505
+
506
+ Args:
507
+ glass_id (str): ID of the glass type
508
+
509
+ Returns:
510
+ dict: Glass type properties
511
+ """
512
+ if glass_id in self.glass_types:
513
+ return self.glass_types[glass_id]
514
+ return None
515
+
516
+ def get_shading_factor(self, shading_id):
517
+ """
518
+ Get shading factor.
519
+
520
+ Args:
521
+ shading_id (str): ID of the shading type
522
+
523
+ Returns:
524
+ dict: Shading factor data
525
+ """
526
+ if shading_id in self.shading_factors:
527
+ return self.shading_factors[shading_id]
528
+ return None
529
+
530
+ def get_internal_load(self, load_type, load_id):
531
+ """
532
+ Get internal load data.
533
+
534
+ Args:
535
+ load_type (str): Type of internal load ('people', 'lighting', 'appliances')
536
+ load_id (str): ID of the internal load
537
+
538
+ Returns:
539
+ dict: Internal load data
540
+ """
541
+ if load_type in self.internal_loads and load_id in self.internal_loads[load_type]:
542
+ return self.internal_loads[load_type][load_id]
543
+ return None
544
+
545
+ def get_occupancy_factor(self, occupancy_id):
546
+ """
547
+ Get occupancy correction factor.
548
+
549
+ Args:
550
+ occupancy_id (str): ID of the occupancy type
551
+
552
+ Returns:
553
+ dict: Occupancy correction factor data
554
+ """
555
+ if occupancy_id in self.occupancy_factors:
556
+ return self.occupancy_factors[occupancy_id]
557
+ return None
558
+
559
+ def export_to_json(self, output_dir):
560
+ """
561
+ Export all reference data to JSON files.
562
+
563
+ Args:
564
+ output_dir (str): Directory to save JSON files
565
+
566
+ Returns:
567
+ bool: True if successful, False otherwise
568
+ """
569
+ try:
570
+ output_path = Path(output_dir)
571
+ output_path.mkdir(parents=True, exist_ok=True)
572
+
573
+ # Export materials
574
+ with open(output_path / "materials.json", "w") as f:
575
+ json.dump(self.materials, f, indent=2)
576
+
577
+ # Export locations
578
+ with open(output_path / "locations.json", "w") as f:
579
+ json.dump(self.locations, f, indent=2)
580
+
581
+ # Export glass types
582
+ with open(output_path / "glass_types.json", "w") as f:
583
+ json.dump(self.glass_types, f, indent=2)
584
+
585
+ # Export shading factors
586
+ with open(output_path / "shading_factors.json", "w") as f:
587
+ json.dump(self.shading_factors, f, indent=2)
588
+
589
+ # Export internal loads
590
+ with open(output_path / "internal_loads.json", "w") as f:
591
+ json.dump(self.internal_loads, f, indent=2)
592
+
593
+ # Export occupancy factors
594
+ with open(output_path / "occupancy_factors.json", "w") as f:
595
+ json.dump(self.occupancy_factors, f, indent=2)
596
+
597
+ return True
598
+ except Exception as e:
599
+ print(f"Error exporting reference data: {e}")
600
+ return False
601
+
602
+
603
+ # Example usage
604
+ if __name__ == "__main__":
605
+ ref_data = ReferenceData()
606
+
607
+ # Example: Get wall material properties
608
+ brick_veneer = ref_data.get_material_by_type("walls", "brick_veneer")
609
+ print("Brick Veneer Wall Properties:", brick_veneer)
610
+
611
+ # Example: Get location climate data
612
+ sydney_data = ref_data.get_location_data("sydney")
613
+ print("Sydney Climate Data:", sydney_data)
614
+
615
+ # Example: Export all data to JSON
616
+ ref_data.export_to_json("reference_data")
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ pandas==2.0.0
2
+ streamlit==1.32.0
3
+ plotly==5.18.0
4
+ numpy==1.24.3
runtime.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [build]
2
+ python_version = "3.10"
utils/export.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Export utilities for HVAC Load Calculator
3
+
4
+ This module provides functions for exporting data from the HVAC Load Calculator.
5
+ """
6
+
7
+ import json
8
+ import csv
9
+ import io
10
+ import pandas as pd
11
+ from datetime import datetime
12
+
13
+
14
+ def export_data(form_data, results, format='json'):
15
+ """
16
+ Export form data and calculation results.
17
+
18
+ Args:
19
+ form_data (dict): Form input data
20
+ results (dict): Calculation results
21
+ format (str): Export format ('json' or 'csv')
22
+
23
+ Returns:
24
+ str: Exported data as string
25
+ """
26
+ if format == 'json':
27
+ return export_as_json(form_data, results)
28
+ elif format == 'csv':
29
+ return export_as_csv(form_data, results)
30
+ else:
31
+ raise ValueError(f"Unsupported export format: {format}")
32
+
33
+
34
+ def export_as_json(form_data, results):
35
+ """
36
+ Export data as JSON.
37
+
38
+ Args:
39
+ form_data (dict): Form input data
40
+ results (dict): Calculation results
41
+
42
+ Returns:
43
+ str: JSON string
44
+ """
45
+ # Combine form data and results
46
+ export_data = {
47
+ 'form_data': form_data,
48
+ 'results': results,
49
+ 'export_timestamp': datetime.now().isoformat()
50
+ }
51
+
52
+ # Convert to JSON string
53
+ return json.dumps(export_data, indent=2)
54
+
55
+
56
+ def export_as_csv(form_data, results):
57
+ """
58
+ Export data as CSV.
59
+
60
+ Args:
61
+ form_data (dict): Form input data
62
+ results (dict): Calculation results
63
+
64
+ Returns:
65
+ str: CSV string
66
+ """
67
+ # Create a buffer for CSV data
68
+ output = io.StringIO()
69
+ writer = csv.writer(output)
70
+
71
+ # Write header
72
+ writer.writerow(['HVAC Load Calculator Results', datetime.now().isoformat()])
73
+ writer.writerow([])
74
+
75
+ # Write building information
76
+ writer.writerow(['Building Information'])
77
+ building_info = form_data.get('building_info', {})
78
+ for key, value in building_info.items():
79
+ writer.writerow([key, value])
80
+ writer.writerow([])
81
+
82
+ # Write calculation results
83
+ writer.writerow(['Calculation Results'])
84
+ for key, value in results.items():
85
+ if key not in ['building_info', 'timestamp'] and not isinstance(value, dict):
86
+ writer.writerow([key, value])
87
+ writer.writerow([])
88
+
89
+ # Write load components
90
+ writer.writerow(['Load Components'])
91
+ writer.writerow(['Component', 'Load (W)', 'Percentage (%)'])
92
+
93
+ # Calculate percentages
94
+ sensible_load = results.get('sensible_load', 1) # Avoid division by zero
95
+
96
+ components = {
97
+ 'Conduction (Opaque Surfaces)': results.get('conduction_gain', 0),
98
+ 'Conduction (Windows)': results.get('window_conduction_gain', 0),
99
+ 'Solar Radiation (Windows)': results.get('window_solar_gain', 0),
100
+ 'Infiltration & Ventilation': results.get('infiltration_gain', 0),
101
+ 'Internal Gains': results.get('internal_gain', 0)
102
+ }
103
+
104
+ for component, load in components.items():
105
+ percentage = (load / sensible_load) * 100 if sensible_load > 0 else 0
106
+ writer.writerow([component, f"{load:.2f}", f"{percentage:.2f}"])
107
+
108
+ # Get CSV content
109
+ return output.getvalue()
110
+
111
+
112
+ def generate_report(form_data, results, calculation_type='cooling'):
113
+ """
114
+ Generate a formatted report of calculation results.
115
+
116
+ Args:
117
+ form_data (dict): Form input data
118
+ results (dict): Calculation results
119
+ calculation_type (str): Type of calculation ('cooling' or 'heating')
120
+
121
+ Returns:
122
+ str: Formatted report as HTML
123
+ """
124
+ # Create a DataFrame for the report
125
+ report_data = []
126
+
127
+ # Add building information
128
+ building_info = form_data.get('building_info', {})
129
+ report_data.append({
130
+ 'Section': 'Building Information',
131
+ 'Item': 'Building Name',
132
+ 'Value': building_info.get('building_name', 'N/A')
133
+ })
134
+ report_data.append({
135
+ 'Section': 'Building Information',
136
+ 'Item': 'Location',
137
+ 'Value': building_info.get('location_name', 'N/A')
138
+ })
139
+ report_data.append({
140
+ 'Section': 'Building Information',
141
+ 'Item': 'Floor Area',
142
+ 'Value': f"{building_info.get('floor_area', 0):.2f} m²"
143
+ })
144
+ report_data.append({
145
+ 'Section': 'Building Information',
146
+ 'Item': 'Volume',
147
+ 'Value': f"{building_info.get('volume', 0):.2f} m³"
148
+ })
149
+
150
+ # Add calculation results
151
+ if calculation_type == 'cooling':
152
+ report_data.append({
153
+ 'Section': 'Results',
154
+ 'Item': 'Sensible Cooling Load',
155
+ 'Value': f"{results.get('sensible_load', 0):.2f} W"
156
+ })
157
+ report_data.append({
158
+ 'Section': 'Results',
159
+ 'Item': 'Latent Cooling Load',
160
+ 'Value': f"{results.get('latent_load', 0):.2f} W"
161
+ })
162
+ report_data.append({
163
+ 'Section': 'Results',
164
+ 'Item': 'Total Cooling Load',
165
+ 'Value': f"{results.get('total_load', 0):.2f} W"
166
+ })
167
+ report_data.append({
168
+ 'Section': 'Results',
169
+ 'Item': 'Cooling Load per Area',
170
+ 'Value': f"{results.get('total_load', 0) / building_info.get('floor_area', 1):.2f} W/m²"
171
+ })
172
+ else: # heating
173
+ report_data.append({
174
+ 'Section': 'Results',
175
+ 'Item': 'Total Heating Load',
176
+ 'Value': f"{results.get('total_load', 0):.2f} W"
177
+ })
178
+ report_data.append({
179
+ 'Section': 'Results',
180
+ 'Item': 'Heating Load per Area',
181
+ 'Value': f"{results.get('total_load', 0) / building_info.get('floor_area', 1):.2f} W/m²"
182
+ })
183
+ if 'annual_energy_kwh' in results:
184
+ report_data.append({
185
+ 'Section': 'Results',
186
+ 'Item': 'Annual Heating Energy',
187
+ 'Value': f"{results.get('annual_energy_kwh', 0):.2f} kWh"
188
+ })
189
+
190
+ # Create DataFrame
191
+ df = pd.DataFrame(report_data)
192
+
193
+ # Convert to HTML
194
+ html = df.to_html(index=False)
195
+
196
+ return html
utils/validation.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Validation utilities for HVAC Load Calculator
3
+
4
+ This module provides validation functions for input data in the HVAC Load Calculator.
5
+ """
6
+
7
+ class ValidationWarning:
8
+ """
9
+ A class to represent validation warnings.
10
+ """
11
+
12
+ def __init__(self, message, suggestion, is_critical=False):
13
+ """
14
+ Initialize a validation warning.
15
+
16
+ Args:
17
+ message (str): Warning message
18
+ suggestion (str): Suggestion for fixing the warning
19
+ is_critical (bool): Whether the warning is critical (prevents proceeding)
20
+ """
21
+ self.message = message
22
+ self.suggestion = suggestion
23
+ self.is_critical = is_critical
24
+
25
+
26
+ def validate_input(input_value, validation_type, min_value=None, max_value=None, required=False):
27
+ """
28
+ Validate an input value.
29
+
30
+ Args:
31
+ input_value: Value to validate
32
+ validation_type (str): Type of validation ('number', 'text', etc.)
33
+ min_value: Minimum allowed value (for numeric inputs)
34
+ max_value: Maximum allowed value (for numeric inputs)
35
+ required (bool): Whether the input is required
36
+
37
+ Returns:
38
+ tuple: (is_valid, warnings)
39
+ """
40
+ warnings = []
41
+ is_valid = True
42
+
43
+ # Check if required
44
+ if required and (input_value is None or input_value == "" or (isinstance(input_value, (int, float)) and input_value == 0)):
45
+ warnings.append(ValidationWarning(
46
+ "Required field is empty",
47
+ "Please provide a value for this field",
48
+ is_critical=True
49
+ ))
50
+ is_valid = False
51
+
52
+ # Skip further validation if value is empty and not required
53
+ if input_value is None or input_value == "":
54
+ return is_valid, warnings
55
+
56
+ # Validate based on type
57
+ if validation_type == 'number':
58
+ try:
59
+ # Convert to float if it's a string
60
+ if isinstance(input_value, str):
61
+ input_value = float(input_value)
62
+
63
+ # Check min value
64
+ if min_value is not None and input_value < min_value:
65
+ warnings.append(ValidationWarning(
66
+ f"Value is below minimum ({min_value})",
67
+ f"Please enter a value greater than or equal to {min_value}",
68
+ is_critical=True
69
+ ))
70
+ is_valid = False
71
+
72
+ # Check max value
73
+ if max_value is not None and input_value > max_value:
74
+ warnings.append(ValidationWarning(
75
+ f"Value exceeds maximum ({max_value})",
76
+ f"Please enter a value less than or equal to {max_value}",
77
+ is_critical=True
78
+ ))
79
+ is_valid = False
80
+
81
+ except ValueError:
82
+ warnings.append(ValidationWarning(
83
+ "Invalid number format",
84
+ "Please enter a valid number",
85
+ is_critical=True
86
+ ))
87
+ is_valid = False
88
+
89
+ elif validation_type == 'text':
90
+ # Add text validation if needed
91
+ pass
92
+
93
+ return is_valid, warnings