Source code for twin4build.systems.fan.fan_torch_system

# Standard library imports
import datetime
from typing import 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


[docs] class FanTorchSystem(core.System, nn.Module): r""" A fan system model implemented with PyTorch for gradient-based optimization. This model represents a fan that controls air flow rate and temperature, considering both the power consumption and the heat added to the air stream. Args: nominalPowerRate : Nominal power rate [W] nominalAirFlowRate : Nominal air flow rate [m³/s] c1 : Constant term in power polynomial c2 : Linear term coefficient in power polynomial c3 : Quadratic term coefficient in power polynomial c4 : Cubic term coefficient in power polynomial f_total : Total fan efficiency factor (0-1) Mathematical Formulation ======================== The fan power is calculated using a polynomial equation: .. math:: P = P_{nom} \cdot \left(c_1 + c_2\frac{\dot{m}}{\dot{m}_{nom}} + c_3\left(\frac{\dot{m}}{\dot{m}_{nom}}\right)^2 + c_4\left(\frac{\dot{m}}{\dot{m}_{nom}}\right)^3\right) where: - :math:`P` is the fan power [W] - :math:`P_{nom}` is the nominal power [W] - :math:`\dot{m}` is the air mass flow rate [kg/s] - :math:`\dot{m}_{nom}` is the nominal air mass flow rate [kg/s] - :math:`c_1` to :math:`c_4` are polynomial coefficients that can be calibrated The outlet air temperature is calculated considering the heat added by the fan: .. math:: T_{out} = T_{in} + \frac{P \cdot f_{total}}{\dot{m} \cdot c_p} where: - :math:`T_{out}` is the outlet temperature [°C] - :math:`T_{in}` is the inlet temperature [°C] - :math:`f_{total}` is the fraction of power that is converted to heat and added to the air stream - :math:`c_p` is the specific heat capacity of air [J/(kg·K)] Notes ----- Model Assumptions: - Fan power follows polynomial relationship with flow rate - All heat from fan power is added to air stream - Constant air density and specific heat capacity - No mechanical losses considered separately Implementation Details: - Uses PyTorch for gradient-based optimization - Parameters are stored as trainable PyTorch parameters - Includes safety checks for numerical stability - All calculations performed in SI units """ def __init__( self, nominalPowerRate: float = None, nominalAirFlowRate: float = None, c1: float = None, c2: float = None, c3: float = None, c4: float = None, f_total: float = None, **kwargs, ): """ Initialize the fan system model. Args: nominalPowerRate: Nominal power rate [W] nominalAirFlowRate: Nominal air flow rate [m³/s] c1-c4: Polynomial coefficients for power calculation f_total: Total fan efficiency factor """ super().__init__(**kwargs) nn.Module.__init__(self) # Store parameters as tps.Parameters for gradient tracking self.nominalPowerRate = tps.Parameter( torch.tensor(nominalPowerRate, dtype=torch.float64), requires_grad=False ) self.nominalAirFlowRate = tps.Parameter( torch.tensor(nominalAirFlowRate, dtype=torch.float64), requires_grad=False ) self.c1 = tps.Parameter( torch.tensor(c1, dtype=torch.float64), requires_grad=False ) self.c2 = tps.Parameter( torch.tensor(c2, dtype=torch.float64), requires_grad=False ) self.c3 = tps.Parameter( torch.tensor(c3, dtype=torch.float64), requires_grad=False ) self.c4 = tps.Parameter( torch.tensor(c4, dtype=torch.float64), requires_grad=False ) self.f_total = tps.Parameter( torch.tensor(f_total, dtype=torch.float64), requires_grad=False ) # Define inputs and outputs as private variables self._input = {"airFlowRate": tps.Scalar(), "inletAirTemperature": tps.Scalar()} self._output = {"outletAirTemperature": tps.Scalar(), "Power": tps.Scalar()} # Define parameters for calibration self.parameter = { "nominalPowerRate": {"lb": 0.0, "ub": 10000.0}, "nominalAirFlowRate": {"lb": 0.0, "ub": 10.0}, "c1": {"lb": -10.0, "ub": 10.0}, "c2": {"lb": -10.0, "ub": 10.0}, "c3": {"lb": -10.0, "ub": 10.0}, "c4": {"lb": -10.0, "ub": 10.0}, "f_total": {"lb": 0.0, "ub": 1.0}, } self._config = {"parameters": list(self.parameter.keys())} self.INITIALIZED = False @property def config(self): """Get the configuration of the fan system.""" return self._config @property def input(self) -> dict: """ Get the input ports of the fan system. Returns: dict: Dictionary containing input ports: - "airFlowRate": Air flow rate [m³/s] - "inletAirTemperature": Inlet air temperature [°C] """ return self._input @property def output(self) -> dict: """ Get the output ports of the fan system. Returns: dict: Dictionary containing output ports: - "outletAirTemperature": Outlet air temperature [°C] - "Power": Fan power consumption [W] """ return self._output
[docs] def initialize( self, start_time: datetime.datetime, end_time: datetime.datetime, step_size: int, simulator: core.Simulator, ) -> None: """Initialize the fan system.""" # 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, ) self.INITIALIZED = True
[docs] def do_step( self, secondTime: float, dateTime: datetime.datetime, step_size: int, stepIndex: int, ) -> None: """ Perform one step of the fan system simulation. The fan power is calculated using a polynomial equation: P = P_nom * (c1 + c2*(m/m_nom) + c3*(m/m_nom)^2 + c4*(m/m_nom)^3) where: - P is the fan power - P_nom is the nominal power - m is the air flow rate - m_nom is the nominal air flow rate - c1-c4 are polynomial coefficients The outlet air temperature is calculated considering the heat added by the fan: T_out = T_in + (P * f_total) / (m * c_p) where: - T_out is the outlet temperature - T_in is the inlet temperature - f_total is the total fan efficiency - c_p is the specific heat capacity of air """ # Get inputs air_flow_rate = self.input["airFlowRate"].get() inlet_temp = self.input["inletAirTemperature"].get() # Convert to torch tensors if not already if not isinstance(air_flow_rate, torch.Tensor): air_flow_rate = torch.tensor(air_flow_rate, dtype=torch.float64) if not isinstance(inlet_temp, torch.Tensor): inlet_temp = torch.tensor(inlet_temp, dtype=torch.float64) # Calculate normalized flow rate m_norm = air_flow_rate / self.nominalAirFlowRate.get() # Calculate fan power using polynomial equation power = self.nominalPowerRate.get() * ( self.c1.get() + self.c2.get() * m_norm + self.c3.get() * m_norm**2 + self.c4.get() * m_norm**3 ) # Calculate outlet temperature # Using air properties at standard conditions c_p = 1005.0 # J/(kg·K) rho = 1.2 # kg/m³ # Convert volume flow rate to mass flow rate m_dot = air_flow_rate * rho # Calculate temperature rise delta_T = (power * self.f_total.get()) / (m_dot * c_p) outlet_temp = inlet_temp + delta_T # Update outputs self.output["outletAirTemperature"].set(outlet_temp, stepIndex) self.output["Power"].set(power, stepIndex)