# -*- coding: utf-8 -*-
"""
Environmental conditions module.
Defines the thermal and radiative properties of the external environment
used as boundary conditions in the borehole heat exchanger simulation.
"""
from dataclasses import dataclass, field
from pathlib import Path
from typing import Self
from numpy.typing import NDArray
import numpy as np
import pandas as pd
[docs]
@dataclass(frozen=True, slots=True)
class EnvironmentalProperties:
"""
Physical and thermal properties of the external environment.
Groups all site-specific parameters that characterize the surface
boundary condition: radiative exchange coefficients, surface optical
properties, and the seasonal air temperature signal.
Attributes
----------
R_ext : float
External thermal resistance [W / (K m²)].
absorptance : float
Surface absorptance for solar radiation [-].
eps : float
Surface emittance for longwave radiation [-].
At : float
Annual amplitude of monthly average air temperature [K].
tau : float
Current simulation time [s].
tau_y : float
Duration of one year (315,536,000 s) [s].
tau_shift : float
Time offset to account for the date of minimum surface temperature [s].
"""
R_ext: float # W / (K * m2)
absorptance: float # -, surface absorptance
eps: float # -, surface emittance
At: float # annual amplitude of monthly average air temperature
tau: float # s, time
tau_y: float # 315,536,000 s, time of 1 year
tau_shift: float # s, time to account for the date of minimum surface temperature
def __post_init__(self):
if self.R_ext < 0:
raise ValueError("R_ext value must be >= 0")
if self.absorptance < 0:
raise ValueError("absorptance value must be >= 0")
if self.eps < 0:
raise ValueError("eps value must be >= 0")
if self.At < 0:
raise ValueError("At value must be >= 0")
if self.tau < 0:
raise ValueError("tau value must be >= 0")
if self.tau_y < 0:
raise ValueError("tau_y value must be > 0")
if self.tau_shift < 0:
raise ValueError("tau_shift value must be > 0")
[docs]
@dataclass
class EnvironmentalTimeSeries:
"""
Time series of external environmental inputs.
Stores the external air temperature and solar irradiance arrays
used to drive the surface boundary condition over the simulation period.
Instances should be created via the class methods to ensure input validation.
Attributes
----------
Tm : float
Mean annual air temperature [°C].
T_ext : NDArray[np.float64]
External air temperature time series [°C].
SolarRad : NDArray[np.float64]
Solar irradiance time series [W/m²].
water_input : NDArray[np.float64] | None = None
Water input series by irrigation or
metherological phenomena [m3 / s].
"""
Tm: float
T_ext: NDArray[np.float64]
SolarRad: NDArray[np.float64]
water_input: NDArray[np.float64] | None = None
[docs]
@classmethod
def from_excel(cls, Tm: float, path: Path | str) -> Self:
"""
Construct an instance from an Excel file.
The file must contain columns named ``T_ext`` and ``SolarRad``.
All values must be finite (no NaN or Inf).
Parameters
----------
Tm : float
Mean annual air temperature [°C].
path : Path or str
Path to the Excel file (.xlsx).
Returns
-------
EnvironmentalTimeSeries
A validated instance populated from the file.
Raises
------
ValueError
If ``T_ext``, ``SolarRad``, or ``water_input`` contain non-finite values.
Examples
--------
>>> env = EnvironmentalTimeSeries.from_excel(12.0, "data/climate.xlsx")
>>> env.T_ext.shape
(8760,)
"""
df = pd.read_excel(path)
arrays = {"T_ext": df["T_ext"], "SolarRad": df["SolarRad"]}
if "water_input" in df.columns:
arrays["water_input"] = df["water_input"]
invalid = [name for name, arr in arrays.items() if not np.all(np.isfinite(arr))]
if invalid:
raise ValueError(f"{', '.join(invalid)} may contain invalid values")
T_ext = np.array(df["T_ext"])
SolarRad = np.array(df["SolarRad"])
water_input = (
np.array(df["water_input"]) if "water_input" in df.columns else None
)
return cls(Tm, T_ext, SolarRad, water_input)
[docs]
@classmethod
def from_array(
cls,
Tm: float,
T_ext: NDArray[np.float64],
SolarRad: NDArray[np.float64],
water_input: NDArray[np.float64] | None = None,
) -> Self:
"""
Construct an instance from NumPy arrays.
All values must be finite (no NaN or Inf).
Parameters
----------
Tm : float
Mean annual air temperature [°C].
T_ext : NDArray[np.float64]
External air temperature time series [°C].
SolarRad : NDArray[np.float64]
Solar irradiance time series [W/m²].
water_input : NDArray[np.float64] | None = None
Water input series by irrigation or
metherological phenomena [m3 / s].
Returns
-------
EnvironmentalTimeSeries
A validated instance populated from the arrays.
Raises
------
ValueError
If ``T_ext``, ``SolarRad``, or ``water_input`` contain non-finite values.
Examples
--------
>>> import numpy as np
>>> T = np.linspace(-5, 25, 8760)
>>> rad = np.abs(np.sin(np.linspace(0, 2 * np.pi, 8760))) * 600
>>> env = EnvironmentalTimeSeries.from_array(12.0, T, rad)
"""
arrays = {"T_ext": T_ext, "SolarRad": SolarRad}
if water_input is not None:
arrays["water_input"] = water_input
invalid = [name for name, arr in arrays.items() if not np.all(np.isfinite(arr))]
if invalid:
raise ValueError(f"{', '.join(invalid)} may contain invalid values")
return cls(Tm, T_ext, SolarRad, water_input)
[docs]
@dataclass
class ExternalEnvironment:
"""
Assembled external environment for simulation boundary conditions.
Combines physical properties and time-series inputs, and derives
the sky radiation temperature from the external air temperature
at construction time.
Attributes
----------
envprops : EnvironmentalProperties
Static physical and radiative properties of the site.
envinput : EnvironmentalTimeSeries
Time-varying environmental inputs (temperature and solar radiation).
T_sky : NDArray[np.float64]
Effective sky temperature time series, derived from ``T_ext`` [K].
boltzmann : float
Stefan-Boltzmann constant (5.670374419 × 10⁻⁸) [W / (m² K⁴)].
Examples
--------
>>> env = ExternalEnvironment(envprops=props, envinput=series)
>>> env.T_sky.shape
(8760,)
"""
envprops: EnvironmentalProperties
envinput: EnvironmentalTimeSeries
T_sky: NDArray[np.float64] = field(init=False)
boltzmann: float = field(default=5.670374419e-8, init=False)
def __post_init__(self) -> None:
# T_sky calculation
self._sky_temperature_calc()
def _sky_temperature_calc(self) -> None:
self.T_sky = 0.0552 * (self.envinput.T_ext + 273.15) ** 1.5