# Standard library imports
import datetime
from typing import Any, Dict, List, Optional
# Third party imports
import numpy as np
import torch
import torch.nn as nn
# Local application imports
import twin4build.core as core
import twin4build.utils.types as tps
from twin4build.systems.utils.discrete_statespace_system import DiscreteStatespaceSystem
from twin4build.utils.constants import Constants
[docs]
class BuildingSpaceThermalTorchSystem(core.System, nn.Module):
r"""
Building Space Thermal Model using RC Network Dynamics.
This class implements a thermal model for building spaces using a network of thermal
resistances and capacitances (RC network). The model represents heat transfer between
indoor air, exterior walls, boundary walls, and adjacent zones using bilinear
state-space dynamics.
Args:
C_air: Thermal capacitance of indoor air [J/K]
C_wall: Thermal capacitance of exterior wall [J/K]
C_int: Thermal capacitance of internal structure [J/K]
C_boundary: Thermal capacitance of boundary wall [J/K]
R_out: Thermal resistance between wall and outdoor [K/W]
R_in: Thermal resistance between wall and indoor [K/W]
R_int: Thermal resistance between internal structure and indoor air [K/W]
R_boundary: Thermal resistance of boundary [K/W]
f_wall: Radiation factor for exterior wall
f_air: Radiation factor for air
Q_occ_gain: Heat gain per occupant [W]
Mathematical Formulation:
=========================
**Continuous-Time Differential Equations:**
The thermal dynamics are governed by energy balance equations for each thermal node:
*1. Indoor Air Temperature:*
.. math::
C_{air}\frac{dT_i}{dt} = \frac{T_w - T_i}{R_{in}} + \frac{T_{bw} - T_i}{R_{boundary}} + \sum_{j}\frac{T_{iw,j} - T_i}{R_{int}} + Q_{occ} N_{occ} + Q_{sh} + f_{air}\Phi_{sol} + c_p\dot{m}_{sup}(T_{sup} - T_i) - c_p\dot{m}_{exh}T_i
*2. Exterior Wall Temperature:*
.. math::
C_{wall}\frac{dT_w}{dt} = \frac{T_o - T_w}{R_{out}} + \frac{T_i - T_w}{R_{in}} + f_{wall}\Phi_{sol}
*3. Boundary Wall Temperature (if present):*
.. math::
C_{boundary}\frac{dT_{bw}}{dt} = \frac{T_i - T_{bw}}{R_{boundary}} + \frac{T_{bound} - T_{bw}}{R_{boundary}}
*4. Interior Wall Temperature (for each adjacent zone j):*
.. math::
C_{int}\frac{dT_{iw,j}}{dt} = \frac{T_i - T_{iw,j}}{R_{int}} + \frac{T_{adj,j} - T_{iw,j}}{R_{int}}
where:
- :math:`T_i`: Indoor air temperature [°C] (state)
- :math:`T_w`: Exterior wall temperature [°C] (state)
- :math:`T_{bw}`: Boundary wall temperature [°C] (state, optional)
- :math:`T_{iw,j}`: Interior wall temperature for zone j [°C] (state, optional)
- :math:`T_o`: Outdoor temperature [°C] (input)
- :math:`T_{sup}`: Supply air temperature [°C] (input)
- :math:`T_{bound}`: Boundary temperature [°C] (input, optional)
- :math:`T_{adj,j}`: Adjacent zone j temperature [°C] (input, optional)
- :math:`\dot{m}_{sup}`: Supply air flow rate [kg/s] (input)
- :math:`\dot{m}_{exh}`: Exhaust air flow rate [kg/s] (input)
- :math:`\Phi_{sol}`: Solar radiation [W/m²] (input)
- :math:`N_{occ}`: Number of occupants (input)
- :math:`Q_{sh}`: Space heater heat input [W] (input)
**State-Space Representation:**
The system is implemented using the DiscreteStatespaceSystem with matrices:
*State vector:* :math:`\mathbf{x} = \begin{bmatrix}T_i \\ T_w \\ T_{bw} \\ T_{iw,1} \\ \vdots \\ T_{iw,n}\end{bmatrix}`
*Input vector:* :math:`\mathbf{u} = \begin{bmatrix}T_o \\ \dot{m}_{sup} \\ \dot{m}_{exh} \\ T_{sup} \\ \Phi_{sol} \\ N_{occ} \\ Q_{sh} \\ T_{bound} \\ T_{adj,1} \\ \vdots \\ T_{adj,n}\end{bmatrix}`
*Base System Matrices:*
For a system with base thermal states (air, wall) + 1 boundary + 1 adjacent zone:
.. math::
\mathbf{A} = \begin{bmatrix}
-\frac{1}{R_{in}C_{air}} - \frac{1}{R_{boundary}C_{air}} - \frac{1}{R_{int}C_{air}} & \frac{1}{R_{in}C_{air}} & \frac{1}{R_{boundary}C_{air}} & \frac{1}{R_{int}C_{air}} \\
\frac{1}{R_{in}C_{wall}} & -\frac{1}{R_{in}C_{wall}} - \frac{1}{R_{out}C_{wall}} & 0 & 0 \\
\frac{1}{R_{boundary}C_{boundary}} & 0 & -\frac{2}{R_{boundary}C_{boundary}} & 0 \\
\frac{1}{R_{int}C_{int}} & 0 & 0 & -\frac{2}{R_{int}C_{int}}
\end{bmatrix}
\mathbf{B} = \begin{bmatrix}
0 & 0 & 0 & 0 & \frac{f_{air}}{C_{air}} & \frac{Q_{occ}}{C_{air}} & \frac{1}{C_{air}} & 0 & 0 \\
\frac{1}{R_{out}C_{wall}} & 0 & 0 & 0 & \frac{f_{wall}}{C_{wall}} & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & \frac{1}{R_{boundary}C_{boundary}} & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \frac{1}{R_{int}C_{int}}
\end{bmatrix}
\mathbf{C} = \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0
\end{bmatrix}
\mathbf{D} = \begin{bmatrix}
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix}
**Bilinear Coupling Matrices:**
*State-Input Coupling (E matrices):*
.. math::
\mathbf{E} \in \mathbb{R}^{9 \times 4 \times 4} = \begin{bmatrix}
\mathbf{0}_{4 \times 4} & \text{(outdoor temp)} \\
\mathbf{0}_{4 \times 4} & \text{(supply flow)} \\
\begin{bmatrix}
-\frac{c_p}{C_{air}} & 0 & 0 & 0 \\
0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0
\end{bmatrix} & \text{(exhaust flow)} \\
\mathbf{0}_{4 \times 4} & \text{(supply temp)} \\
\mathbf{0}_{4 \times 4} & \text{(solar)} \\
\mathbf{0}_{4 \times 4} & \text{(occupants)} \\
\mathbf{0}_{4 \times 4} & \text{(heater)} \\
\mathbf{0}_{4 \times 4} & \text{(boundary)} \\
\mathbf{0}_{4 \times 4} & \text{(adjacent)}
\end{bmatrix}
*Input-Input Coupling (F matrices):*
.. math::
\mathbf{F} \in \mathbb{R}^{9 \times 4 \times 9} = \begin{bmatrix}
\mathbf{0}_{4 \times 9} & \text{(outdoor temp)} \\
\begin{bmatrix}
0 & 0 & 0 & \frac{c_p}{C_{air}} & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix} & \text{(supply flow)} \\
\mathbf{0}_{4 \times 9} & \text{(exhaust flow)} \\
\mathbf{0}_{4 \times 9} & \text{(supply temp)} \\
\mathbf{0}_{4 \times 9} & \text{(solar)} \\
\mathbf{0}_{4 \times 9} & \text{(occupants)} \\
\mathbf{0}_{4 \times 9} & \text{(heater)} \\
\mathbf{0}_{4 \times 9} & \text{(boundary)} \\
\mathbf{0}_{4 \times 9} & \text{(adjacent)}
\end{bmatrix}
Input vector mapping: :math:`[T_o, \dot{m}_{sup}, \dot{m}_{exh}, T_{sup}, \Phi_{sol}, N_{occ}, Q_{sh}, T_{bound}, T_{adj,1}]^T`
*Bilinear Effects*
The bilinear terms handle specific flow-dependent heat transfer effects:
- :math:`\mathbf{E}[2,0,0] \cdot u_2 \cdot x_0 = -\frac{c_p}{C_{air}} \dot{m}_{exh} T_i`: Exhaust air removing heat
- :math:`\mathbf{F}[1,0,3] \cdot u_1 \cdot u_3 = \frac{c_p}{C_{air}} \dot{m}_{sup} T_{sup}`: Supply air bringing heat
Physical Interpretation:
======================
**Thermal Network:**
- RC network represents building thermal mass and resistances
- States capture temperature of air, walls, and structural elements
- Inputs represent weather, HVAC, occupancy, and heat sources
- Bilinear terms model flow-dependent heat transfer accurately
**Flow-Dependent Effects:**
- Supply air flow brings heat at supply temperature (F matrix coupling)
- Exhaust air flow removes heat at indoor temperature (E matrix coupling)
- These effects are critical for accurate HVAC modeling
Computational Features:
======================
- **Automatic Differentiation:** PyTorch tensors enable gradient computation
- **Adaptive Discretization:** Matrices updated when flows change significantly
- **Parameter Estimation:** All RC parameters available for calibration
Examples
--------
Basic thermal model:
>>> import twin4build as tb
>>>
>>> # Create thermal model with default RC parameters
>>> thermal_model = tb.BuildingSpaceThermalTorchSystem(
... C_air=2e6, # Higher air thermal mass
... C_wall=5e6, # Wall thermal mass
... R_out=0.1, # Outdoor thermal resistance
... R_in=0.05, # Indoor thermal resistance
... f_air=0.15, # Air radiation factor
... id="zone_1_thermal"
... )
Thermal model with specific boundary conditions:
>>> # Model with boundary wall and adjacent zones
>>> thermal_model = tb.BuildingSpaceThermalTorchSystem(
... C_air=1.5e6,
... C_boundary=3e6, # Boundary wall thermal mass
... R_boundary=0.02, # Low boundary resistance
... Q_occ_gain=120.0, # Higher occupant heat gain
... id="zone_thermal_with_boundary"
... )
"""
def __init__(
self,
# Thermal parameters
C_air: float = 1e6, # Thermal capacitance of indoor air [J/K]
C_wall: float = 1e6, # Thermal capacitance of exterior wall [J/K]
C_int: float = 1e5, # Thermal capacitance of internal structure [J/K]
C_boundary: float = 1e6, # Thermal capacitance of boundary wall [J/K]
R_out: float = 0.05, # Thermal resistance between wall and outdoor [K/W]
R_in: float = 0.05, # Thermal resistance between wall and indoor [K/W]
R_int: float = 0.01, # Thermal resistance between internal structure and indoor air [K/W]
R_boundary: float = 0.01, # Thermal resistance of boundary [K/W]
# Heat gain parameters
f_wall: float = 0.3, # Radiation factor for exterior wall
f_air: float = 0.1, # Radiation factor for air
Q_occ_gain: float = 100.0, # Heat gain per occupant [W]
**kwargs,
):
"""
Initialize the RC building space model with interior wall dynamics.
Args:
C_air: Thermal capacitance of indoor air [J/K]
C_wall: Thermal capacitance of exterior walls [J/K]
C_int: Thermal capacitance of internal mass [J/K]
C_boundary: Thermal capacitance of boundary wall [J/K]
R_out: Thermal resistance between exterior wall and outdoor [K/W]
R_in: Thermal resistance between exterior wall and indoor [K/W]
R_int: Thermal resistance between internal mass and indoor air [K/W]
R_boundary: Thermal resistance of boundary [K/W]
f_wall: Radiation factor for exterior wall
f_air: Radiation factor for air/internal mass
Q_occ_gain: Heat gain per occupant [W]
**kwargs: Additional keyword arguments passed to parent
"""
super().__init__(**kwargs)
nn.Module.__init__(self)
# Store thermal parameters as tps.Parameters
self.C_air = tps.Parameter(
torch.tensor(C_air, dtype=torch.float64), requires_grad=False
)
self.C_wall = tps.Parameter(
torch.tensor(C_wall, dtype=torch.float64), requires_grad=False
)
self.C_int = tps.Parameter(
torch.tensor(C_int, dtype=torch.float64), requires_grad=False
)
self.C_boundary = tps.Parameter(
torch.tensor(C_boundary, dtype=torch.float64), requires_grad=False
)
self.R_out = tps.Parameter(
torch.tensor(R_out, dtype=torch.float64), requires_grad=False
)
self.R_in = tps.Parameter(
torch.tensor(R_in, dtype=torch.float64), requires_grad=False
)
self.R_int = tps.Parameter(
torch.tensor(R_int, dtype=torch.float64), requires_grad=False
)
self.R_boundary = tps.Parameter(
torch.tensor(R_boundary, dtype=torch.float64), requires_grad=False
)
# Store other parameters as tps.Parameters
self.f_wall = tps.Parameter(
torch.tensor(f_wall, dtype=torch.float64), requires_grad=False
)
self.f_air = tps.Parameter(
torch.tensor(f_air, dtype=torch.float64), requires_grad=False
)
self.Q_occ_gain = tps.Parameter(
torch.tensor(Q_occ_gain, dtype=torch.float64), requires_grad=False
)
# Define inputs and outputs
self.input = {
"outdoorTemperature": tps.Scalar(), # Outdoor temperature [°C]
"supplyAirFlowRate": tps.Scalar(), # Supply air flow rate [kg/s]
"exhaustAirFlowRate": tps.Scalar(), # Exhaust air flow rate [kg/s]
"supplyAirTemperature": tps.Scalar(), # Supply air temperature [°C]
"globalIrradiation": tps.Scalar(), # Solar radiation [W/m²]
"numberOfPeople": tps.Scalar(), # Number of occupants
"heatGain": tps.Scalar(), # Space heater heat input [W]
"boundaryTemperature": tps.Scalar(
21, optional=True
), # Boundary temperature [°C], optional
"adjacentZoneTemperature": tps.Vector(
optional=True
), # Adjacent zone temperature [°C], optional
}
# Define outputs
self.output = {
"indoorTemperature": tps.Scalar(20), # Indoor air temperature [°C]
"wallTemperature": tps.Scalar(20), # Exterior wall temperature [°C]
}
# Define parameters for calibration
self.parameter = {
"C_air": {"lb": 1000.0, "ub": 1000000.0},
"C_wall": {"lb": 10000.0, "ub": 10000000.0},
"C_int": {"lb": 10000.0, "ub": 10000000.0},
"C_boundary": {"lb": 10000.0, "ub": 10000000.0},
"R_out": {"lb": 0.001, "ub": 1.0},
"R_in": {"lb": 0.001, "ub": 1.0},
"R_int": {"lb": 0.001, "ub": 1.0},
"R_boundary": {"lb": 0.001, "ub": 1.0},
"f_wall": {"lb": 0.0, "ub": 1.0},
"f_air": {"lb": 0.0, "ub": 1.0},
"Q_occ_gain": {"lb": 50.0, "ub": 200.0},
}
self._config = {"parameters": list(self.parameter.keys())}
self.INITIALIZED = False
self._n_adjacent_zones = 0
self._n_boundary_temperature = 0
self._manual_setup_n_adjacent_zones = False
self._manual_setup_n_boundary_temperature = False
@property
def n_adjacent_zones(self):
return self._n_adjacent_zones
@n_adjacent_zones.setter
def n_adjacent_zones(self, n_adjacent_zones: int):
self._manual_setup_n_adjacent_zones = True
self._n_adjacent_zones = n_adjacent_zones
@property
def n_boundary_temperature(self):
return self._n_boundary_temperature
@n_boundary_temperature.setter
def n_boundary_temperature(self, n_boundary_temperature: int):
self._manual_setup_n_boundary_temperature = True
self._n_boundary_temperature = n_boundary_temperature
@property
def manual_setup_n_adjacent_zones(self):
return self._manual_setup_n_adjacent_zones
@property
def manual_setup_n_boundary_temperature(self):
return self._manual_setup_n_boundary_temperature
[docs]
def initialize(
self,
start_time: datetime.datetime,
end_time: datetime.datetime,
step_size: int,
simulator: core.Simulator,
) -> None:
"""
Initialize the RC model by initializing the state space model.
Args:
start_time (datetime.datetime): Simulation start time.
end_time (datetime.datetime): Simulation end time.
step_size (int): Simulation step size.
simulator (core.Simulator): Reference to the simulation model.
"""
# Initialize I/O
for input in self.input.values():
input.initialize(
start_time=start_time,
end_time=end_time,
step_size=step_size,
simulator=simulator,
)
for output in self.output.values():
output.initialize(
start_time=start_time,
end_time=end_time,
step_size=step_size,
simulator=simulator,
)
if not self.INITIALIZED:
# First initialization
self._create_state_space_model()
# print("CREATED STATE SPACE MODEL 1")
# print("C_air: ", self.C_air.get().detach())
self.ss_model.initialize(start_time, end_time, step_size, simulator)
self.INITIALIZED = True
else:
# Re-initialize the state space model
self._create_state_space_model() # We need to re-create the model because the parameters have changed to create a new computation graph
# print("CREATED STATE SPACE MODEL 2")
# print("C_air: ", self.C_air.get().detach())
self.ss_model.initialize(start_time, end_time, step_size, simulator)
self._manual_setup_n_adjacent_zones = False
self._manual_setup_n_boundary_temperature = False
def _create_state_space_model(self):
"""
Create the state space model using PyTorch tensors.
This formulation directly constructs the state space matrices A and B
using PyTorch tensors for gradient tracking.
"""
if self.manual_setup_n_boundary_temperature == False:
# Find if boundary temperature is set as input
connection_point = [
cp for cp in self.connects_at if cp.inputPort == "boundaryTemperature"
]
n_boundary_temperature = (
len(connection_point[0].connects_system_through)
if connection_point
else 0
)
self.n_boundary_temperature = n_boundary_temperature
assert (
self.n_boundary_temperature == 0 or self.n_boundary_temperature == 1
), "Maximum one boundary temperature input is allowed"
if self.manual_setup_n_adjacent_zones == False:
# Find number of adjacent zones
connection_point = [
cp
for cp in self.connects_at
if cp.inputPort == "adjacentZoneTemperature"
]
n_adjacent_zones = (
len(connection_point[0].connects_system_through)
if connection_point
else 0
)
self.n_adjacent_zones = n_adjacent_zones
# Calculate number of states
n_states = 2 # Base states: air and wall temperature
n_states += self.n_boundary_temperature # Add boundary wall state
n_states += (
self.n_adjacent_zones
) # Add one state for each adjacent zone's interior wall
self.n_states = n_states
# Calculate number of inputs based on input dictionary
n_inputs = len(self.input) - 2 # Base inputs from input dictionary
n_inputs += (
self.n_adjacent_zones
) # Add one input for each adjacent zone temperature
n_inputs += (
self.n_boundary_temperature
) # Add one input for boundary temperature
self.n_inputs = n_inputs
# Initialize A and B matrices with zeros
A = torch.zeros((n_states, n_states), dtype=torch.float64)
B = torch.zeros((n_states, n_inputs), dtype=torch.float64)
# Air temperature equation coefficients
A[0, 0] = -1 / (self.R_in.get() * self.C_air.get())
A[0, 1] = 1 / (self.R_in.get() * self.C_air.get()) # T_wall coefficient
if self.n_boundary_temperature == 1:
# Add heat exchange with boundary wall
A[0, 0] -= 1 / (
self.R_boundary.get() * self.C_air.get()
) # T_bound_wall coefficient
A[0, 2] = 1 / (
self.R_boundary.get() * self.C_air.get()
) # T_bound_wall coefficient
A[2, 0] = 1 / (
self.R_boundary.get() * self.C_boundary.get()
) # T_air coefficient for boundary wall
A[2, 2] = -2 / (
self.R_boundary.get() * self.C_boundary.get()
) # T_bound_wall coefficient (heat exchange with both air and boundary)
# Add heat loss to interior walls of adjacent zones
A[0, 0] -= (
self.n_adjacent_zones * 1 / (self.R_int.get() * self.C_air.get())
) # Heat loss to interior walls
# Exterior wall temperature equation coefficients
A[1, 0] = 1 / (self.R_in.get() * self.C_wall.get()) # T_air coefficient
A[1, 1] = -1 / (self.R_in.get() * self.C_wall.get()) - 1 / (
self.R_out.get() * self.C_wall.get()
) # T_wall coefficient
# Add heat exchange with interior walls of adjacent zones
for i in range(self.n_adjacent_zones):
adj_wall_idx = (
n_states - self.n_adjacent_zones - self.n_boundary_temperature
) + i # Interior walls are after boundary wall
A[0, adj_wall_idx] = 1 / (
self.R_int.get() * self.C_air.get()
) # T_int_wall coefficient for each adjacent zone
A[adj_wall_idx, 0] = 1 / (
self.R_int.get() * self.C_int.get()
) # T_air coefficient for each interior wall
A[adj_wall_idx, adj_wall_idx] = -2 / (
self.R_int.get() * self.C_int.get()
) # T_int_wall coefficient for each interior wall (heat exchange with both air and adjacent zone)
# Input matrix B coefficients - match the order in do_step
# Outdoor temperature
B[1, 0] = 1 / (
self.R_out.get() * self.C_wall.get()
) # T_out coefficient for wall
# Solar radiation
B[0, 4] = self.f_air.get() / self.C_air.get() # Radiation coefficient for air
B[1, 4] = (
self.f_wall.get() / self.C_wall.get()
) # Radiation coefficient for wall
# Number of people
B[0, 5] = self.Q_occ_gain.get() / self.C_air.get() # N_people coefficient
# Space heater heat input
B[0, 6] = 1 / self.C_air.get() # Q_sh coefficient
if self.n_boundary_temperature == 1:
# Boundary temperature
B[2, 7] = 1 / (
self.R_boundary.get() * self.C_boundary.get()
) # T_bound coefficient
# Adjacent zone temperatures (at the end of the input vector)
for i in range(self.n_adjacent_zones):
adj_wall_state_idx = (
n_states - self.n_adjacent_zones - self.n_boundary_temperature
) + i # Interior walls are after boundary wall
adj_wall_input_idx = (
n_inputs - self.n_adjacent_zones - self.n_boundary_temperature
) + i # Adjacent zone temperatures are after boundary temperature
B[adj_wall_state_idx, adj_wall_input_idx] = 1 / (
self.R_int.get() * self.C_int.get()
) # T_adj coefficient for each adjacent zone
# Output matrix C - Identity matrix for direct observation of all states
C = torch.eye(n_states, dtype=torch.float64)
# Feedthrough matrix D (no direct feedthrough)
D = torch.zeros((n_states, n_inputs), dtype=torch.float64)
# Initial state
x0 = torch.zeros(n_states, dtype=torch.float64)
x0[0] = self.output["indoorTemperature"].get()
x0[1] = self.output["wallTemperature"].get()
if self.n_boundary_temperature == 1:
# Initialize boundary wall temperature with indoor temperature
x0[2] = self.output["indoorTemperature"].get()
# Initialize interior wall temperatures with indoor temperature
for i in range(self.n_adjacent_zones):
adj_wall_idx = (
n_states - self.n_adjacent_zones - self.n_boundary_temperature
) + i # Interior walls are after boundary wall
x0[adj_wall_idx] = self.output["indoorTemperature"].get()
# E matrix for input-state coupling: shape (n_inputs, n_states, n_states)
E = torch.zeros((n_inputs, n_states, n_states), dtype=torch.float64)
# -m_ex*cp*T_air (input 2, state 0)
E[2, 0, 0] = (
-Constants.specificHeatCapacity["air"] / self.C_air.get()
) # exhaustAirFlowRate * T_air
# Use E and F matrices for correct couplings
# F matrix for input-input coupling: shape (n_inputs, n_states, n_inputs)
F = torch.zeros((n_inputs, n_states, n_inputs), dtype=torch.float64)
# m_sup*cp*T_sup (inputs 1 and 3)
F[1, 0, 3] = (
Constants.specificHeatCapacity["air"] / self.C_air.get()
) # supplyAirFlowRate * supplyAirTemperature
# Pass E and F to DiscreteStatespaceSystem
self.ss_model = DiscreteStatespaceSystem(
A=A,
B=B,
C=C,
D=D,
x0=x0,
state_names=None,
add_noise=False,
id=f"ss_model_{self.id}",
E=E,
F=F,
)
# # Debug output for parameter validation
# if torch.any(torch.isnan(A)) or torch.any(torch.isinf(A)):
# print("WARNING: A matrix contains NaN or Inf values!")
# print("Parameters:")
# print(f"C_air: {self.C_air.get().item()}")
# print(f"C_wall: {self.C_wall.get().item()}")
# print(f"C_boundary: {self.C_boundary.get().item()}")
# print(f"R_out: {self.R_out.get().item()}")
# print(f"R_in: {self.R_in.get().item()}")
# print(f"R_boundary: {self.R_boundary.get().item()}")
# print("A matrix:", A)
# # Check for very small resistances that could cause numerical instability
# if self.R_boundary.get() < 1e-4:
# print(f"WARNING: R_boundary is very small ({self.R_boundary.get().item():.6f}), this may cause numerical instability!")
# if self.R_in.get() < 1e-4:
# print(f"WARNING: R_in is very small ({self.R_in.get().item():.6f}), this may cause numerical instability!")
# if self.R_out.get() < 1e-4:
# print(f"WARNING: R_out is very small ({self.R_out.get().item():.6f}), this may cause numerical instability!")
@property
def config(self):
"""Get the configuration of the RC model."""
return self._config
[docs]
def do_step(
self,
secondTime: Optional[float] = None,
dateTime: Optional[datetime.datetime] = None,
step_size: Optional[float] = None,
stepIndex: Optional[int] = None,
) -> None:
"""
Perform one step of the RC model simulation.
Args:
secondTime: Current simulation time in seconds.
dateTime: Current simulation date/time.
step_size: Current simulation step size.
"""
# Build input vector u with fixed inputs first
u = torch.stack(
[
self.input["outdoorTemperature"].get(),
self.input["supplyAirFlowRate"].get(),
self.input["exhaustAirFlowRate"].get(),
self.input["supplyAirTemperature"].get(),
self.input["globalIrradiation"].get(),
self.input["numberOfPeople"].get(),
self.input["heatGain"].get(),
]
).squeeze()
if self.n_boundary_temperature == 1:
u = torch.cat([u, self.input["boundaryTemperature"].get()])
# Add adjacent zone temperatures at the end
if self.n_adjacent_zones > 0:
u = torch.cat([u, self.input["adjacentZoneTemperature"].get()])
# Set the input vector
self.ss_model.input["u"].set(u, stepIndex)
# Execute state space model step
self.ss_model.do_step(secondTime, dateTime, step_size, stepIndex=stepIndex)
# Get the output vector
y = self.ss_model.output["y"].get()
# Update individual outputs from the output vector
self.output["indoorTemperature"].set(y[0], stepIndex)
self.output["wallTemperature"].set(y[1], stepIndex)
# if torch.any(torch.isnan(self.output["indoorTemperature"].get())):
# print("Parameters:")
# print(f"C_air: {self.C_air.get().item()}")
# print(f"C_wall: {self.C_wall.get().item()}")
# print(f"C_int: {self.C_int.get().item()}")
# print(f"C_boundary: {self.C_boundary.get().item()}")
# print(f"R_out: {self.R_out.get().item()}")
# print(f"R_in: {self.R_in.get().item()}")
# print(f"R_int: {self.R_int.get().item()}")
# print(f"R_boundary: {self.R_boundary.get().item()}")
# print(f"f_wall: {self.f_wall.get().item()}")
# print(f"f_air: {self.f_air.get().item()}")
# print(f"Q_occ_gain: {self.Q_occ_gain.get().item()}")
# print(f"Indoor temperature is NaN at step {stepIndex}")
# print(f"Input vector: {u}")
# print(f"Output vector: {y}")
# print(f"State vector: {self.ss_model.x}")
# raise ValueError("Indoor temperature is NaN")