OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
openswmm::transport::heat Namespace Reference

Classes

struct  BedCoupling
 One element's bed coupling, fully reduced to SI conductances. More...
 
struct  PairStep
 Simultaneous temperature increments for the coupled pair, °C. More...
 
struct  SolarPosition
 Sun geometry at one instant. The SPA swap point's return type. More...
 
struct  SolutePairStep
 Simultaneous concentration increments for the coupled pair. More...
 

Functions

PairStep relaxPair (const BedCoupling &g, double t_w, double t_b, double j0, double j1, double h, double area_m2, double dt) noexcept
 One exact step of the coupled (water, bed) pair.
 
SolutePairStep exchangePair (double c_w, double c_b, double vol_w, double vol_b, double q_exch, double dt) noexcept
 One exact step of the two-body solute exchange.
 
bool bedExchangeEnabled (const SimulationContext &ctx) noexcept
 True when [HEAT_FLUXES] SEDIMENT_EXCHANGE is on and heat transport is.
 
double groundTemperature (SimulationContext &ctx) noexcept
 
double linkBedAreaM2 (const SimulationContext &ctx, int link) noexcept
 
double linkFreeSurfaceFt2 (const SimulationContext &ctx, int link) noexcept
 
double bedExchangeQ (const SedimentConfig &cfg, double bed_m2) noexcept
 
double groundTempFor (const SimulationContext &ctx, const SedimentConfig &sc, double t_gr_global) noexcept
 
BedCoupling bedCouplingFromContact (const SimulationContext &ctx, const SedimentConfig &sc, double bed_m2, double vol_ft3, double t_gr) noexcept
 The material half of the coupling, geometry already folded in.
 
BedCoupling bedCouplingForLink (const SimulationContext &ctx, int link, double vol_ft3, double t_gr) noexcept
 Bed conductances for link j from the deck configuration and the link's current hydraulics.
 
void seedBedTemperature (SimulationContext &ctx)
 Size and seed the bed temperature array, idempotently.
 
void applyBedSoluteExchange (SimulationContext &ctx, double *link_conc, int n_arr, int offset, int n_total, double dt)
 Step every conduit's bed/channel solute exchange for one dt.
 
double netFluxOut (const SimulationContext &ctx, const HeatElement &elem, double t_w) noexcept
 Net outward surface flux at t_w, summed over every enabled module [W/m²].
 
void applyHeatFluxes (SimulationContext &ctx, double dt)
 Apply one step of surface heat exchange to every exchanging node and link.
 
std::vector< std::string > resolveHeatOverrides (SimulationContext &ctx)
 Materialise the dense per-element vectors from the parsed rows.
 
const RadiativeConfigradiativeFor (const SimulationContext &ctx, const HeatElement &e) noexcept
 The radiative attributes in force for e.
 
const SedimentConfigsedimentFor (const SimulationContext &ctx, const HeatElement &e) noexcept
 The bed attributes in force for e.
 
double netShortwave (double incoming_wm2, double albedo, double shade_factor) noexcept
 Net absorbed shortwave [W/m²], INTO the water.
 
double backLongwave (double t_water_c, double emiss_water) noexcept
 Back longwave emitted by the water [W/m²], OUT of the water.
 
double atmosphericEmissivity (double e_a_kpa, double atm_emiss_coeff, double cloud_factor) noexcept
 
double atmosphericLongwave (double t_air_c, double humidity_pct, double atm_emiss_coeff, double lw_reflection, double sky_view, double cloud_factor=1.0) noexcept
 Atmospheric longwave [W/m²], INTO the water.
 
double landCoverLongwave (double t_air_c, double emiss_landcover, double sky_view) noexcept
 Land-cover longwave [W/m²], INTO the water.
 
double netRadiativeFluxOut (double t_water_c, double t_air_c, double humidity_pct, const RadiativeConfig &cfg, double jin_wm2, double cloud_factor) noexcept
 
double radiativeFluxOut (const SimulationContext &ctx, const HeatElement &elem, double t_w) noexcept
 
SolarPosition solarPosition (int day_of_year, double hour_local, double lat_deg, double lon_deg, double tz_hours) noexcept
 Sun position by the Spencer (1971) / NOAA formulation.
 
double airMass (double zenith_deg) noexcept
 
double pressureFromElevation (double elevation_m) noexcept
 Station pressure [mb] from elevation [m], standard atmosphere.
 
double birdClearSkyGHI (const SolarPosition &pos, double pressure_mb, const SolarConfig &cfg) noexcept
 Bird & Hulstrom clear-sky global horizontal irradiance [W/m²].
 
double cloudShortwaveFactor (double cloud_fraction, double k, double n) noexcept
 Cloud attenuation of shortwave, 1 − k·C^n (Kasten–Czeplak).
 
double cloudLongwaveFactor (double cloud_fraction, double k_lw) noexcept
 Cloud enhancement of atmospheric emissivity, 1 + k_lw·C² (Bolz).
 
void updateSolarForcing (SimulationContext &ctx) noexcept
 Resolve Jin and C for this step into ctx.heat_state.
 
double airTempCelsius (const SimulationContext &ctx) noexcept
 
double relaxT (double j0, double j1, double h, double area_m2, double vol_m3, double dt, double rho, double cp) noexcept
 One element's temperature change, integrated SEMI-IMPLICITLY [°C].
 
double equilibriumT (double t0_c, double j0, double j1, double h) noexcept
 
double saturationVapourPressure (double t_c) noexcept
 Saturation vapour pressure over water at t_c degrees Celsius [kPa].
 
double latentHeatOfVaporization (double t_c) noexcept
 
double windFunction (double wind_ms, double a, double b) noexcept
 Mass-transfer wind function a + b·w, w in m/s [m/s/kPa].
 
double evaporationRate (double t_water_c, double t_air_c, double humidity_pct, double wind_ms, double a, double b) noexcept
 Evaporative mass-transfer rate [m/s]. Negative under condensation.
 
double latentFlux (double t_water_c, double t_air_c, double humidity_pct, double wind_ms, double a, double b, double water_density) noexcept
 Latent heat flux [W/m²], POSITIVE out of the water.
 
double bowenRatio (double t_water_c, double t_air_c, double humidity_pct, double pressure_ratio) noexcept
 
double sensibleFlux (double latent_flux, double bowen_ratio) noexcept
 Sensible heat flux [W/m²], POSITIVE out of the water.
 
double surfaceFluxOut (const SimulationContext &ctx, const HeatElement &elem, double t_w) noexcept
 This module's contribution to the net outward flux at t_w [W/m²].
 

Variables

constexpr double kStefanBoltzmann = 5.67e-8
 Stefan–Boltzmann constant, W/m²/K⁴ (rhemodel.cpp:46).
 
constexpr double kKelvinOffset = 273.15
 Celsius → Kelvin offset.
 
constexpr double kSolarConstant = 1367.0
 Solar constant, W/m² (Bird & Hulstrom 1981; WMO 1982).
 
constexpr double kStdPressureMb = 1013.25
 Sea-level standard pressure, millibars — Bird's pressure reference.
 
constexpr double kBowenCoeff = 0.061
 Bowen's coefficient, kPa/°C (CSH §4.5).
 
constexpr double kProbeC = 1.0e-3
 
constexpr double kSqFtToSqM = 0.09290304
 ft² → m². Exported with relaxT, which is useless without it.
 
constexpr double kCuFtToCuM = 0.028316846592
 ft³ → m³.
 
constexpr double kMphToMs = 0.44704
 mph → m/s. ClimateState::wind_speed is mph.
 

Function Documentation

◆ airMass()

double openswmm::transport::heat::airMass ( double zenith_deg)
noexcept

Kasten–Young relative optical air mass. Returns 0 below the horizon, which is what makes every Bird transmittance collapse to a dark result rather than to a domain error.

Here is the caller graph for this function:

◆ airTempCelsius()

double openswmm::transport::heat::airTempCelsius ( const SimulationContext & ctx)
noexcept

Air temperature in °C. ClimateState carries °F (a legacy convention); every formulation in this program is Celsius. Exported at H5a so the watershed binding cannot acquire a second, drifting copy of the conversion — there is exactly one place that knows the units.

Here is the caller graph for this function:

◆ applyBedSoluteExchange()

void openswmm::transport::heat::applyBedSoluteExchange ( SimulationContext & ctx,
double * link_conc,
int n_arr,
int offset,
int n_total,
double dt )

Step every conduit's bed/channel solute exchange for one dt.

Parameters
link_conc[link * n_arr + s] channel concentrations, updated in place. The caller owns the layout because the engines hold their channel state differently; what they share is this function and the bed array it moves mass into.
n_arrSpecies carried by link_conc.
offsetWhere link_conc's rows start inside the bed store. The bed carries ONE species set — pollutants then MSX — and LEGACY holds those in two arrays (links.conc, msx_link_conc), so it calls twice: (conc, np, 0) and (msx, nm, np). Splitting the bed store instead would give the two halves independent sizing paths and a seam where one resizes and the other does not.
n_totalTotal species the bed store carries (np + nm). Sizing is by n_total, so the two LEGACY calls agree about the store's shape by construction.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ applyHeatFluxes()

void openswmm::transport::heat::applyHeatFluxes ( SimulationContext & ctx,
double dt )

Apply one step of surface heat exchange to every exchanging node and link.

Storage nodes (node::getSurfArea) and open conduits (top width × length × barrels) — the engine's own evaporation surfaces, so heat crosses the free surface exactly where water leaves it. Junctions, outfalls, dividers and closed conduits have no free surface and no exchange.

Modifies heat_state.node_temp and link_temp in place. No-op unless HEAT_TRANSPORT is on and at least one flux module is enabled.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ atmosphericEmissivity()

double openswmm::transport::heat::atmosphericEmissivity ( double e_a_kpa,
double atm_emiss_coeff,
double cloud_factor = 1.0 )
noexcept

Brunt atmospheric emissivity [-]. e_a_kpa is converted to Pa inside.

H6a: cloud_factor is Bolz's 1 + k_lw C² from SolarRadiation::cloudLongwaveFactor. It DEFAULTS TO 1.0 and the default is not decorative — every H3 gate calls the two-argument form, so the clear-sky value is unchanged by construction rather than by arithmetic that happens to round the same way.

Here is the caller graph for this function:

◆ atmosphericLongwave()

double openswmm::transport::heat::atmosphericLongwave ( double t_air_c,
double humidity_pct,
double atm_emiss_coeff,
double lw_reflection,
double sky_view,
double cloud_factor )
noexcept

Atmospheric longwave [W/m²], INTO the water.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ backLongwave()

double openswmm::transport::heat::backLongwave ( double t_water_c,
double emiss_water )
noexcept

Back longwave emitted by the water [W/m²], OUT of the water.

Here is the caller graph for this function:

◆ bedCouplingForLink()

BedCoupling openswmm::transport::heat::bedCouplingForLink ( const SimulationContext & ctx,
int link,
double vol_ft3,
double t_gr )
noexcept

Bed conductances for link j from the deck configuration and the link's current hydraulics.

Contact area is the WETTED PERIMETER times length times barrels — see BedZoneData.hpp divergence 1. Returns a non-viable coupling (which relaxPair treats as a no-op) for a dry link, a non-conduit, or a link whose perimeter cannot be formed. t_gr is passed in rather than resolved here because the TIMESERIES lookup is GLOBAL scope: one lookup per STEP, not one per link. That is updateSolarForcing's pattern and the reason it exists. PE2: the link's own SedimentConfig (per-element overrides included) is resolved INSIDE, so every caller of the link form gets per-element bed attributes without threading them.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ bedCouplingFromContact()

BedCoupling openswmm::transport::heat::bedCouplingFromContact ( const SimulationContext & ctx,
const SedimentConfig & sc,
double bed_m2,
double vol_ft3,
double t_gr )
noexcept

The material half of the coupling, geometry already folded in.

Split out of bedCouplingForLink for the ARD binding: a CELL has its own contact area and volume but the same material math, and a second hand-written copy of that math is how the two engines' beds would come to disagree about the same sediment. LEGACY and LARD go through bedCouplingForLink (link geometry); ARD computes its per-cell bed_m2/vol_ft3 and calls this directly.

PE2: sc is the SedimentConfig in force for the element — sedimentFor(ctx, elem), or the global for a caller with no element. The check found the handoff's draft resolving the per-element table and never passing it to ANY bed reader: both links of the per-tag gate cooled to the identical bit.

Here is the caller graph for this function:

◆ bedExchangeEnabled()

bool openswmm::transport::heat::bedExchangeEnabled ( const SimulationContext & ctx)
noexcept

True when [HEAT_FLUXES] SEDIMENT_EXCHANGE is on and heat transport is.

Here is the caller graph for this function:

◆ bedExchangeQ()

double openswmm::transport::heat::bedExchangeQ ( const SedimentConfig & cfg,
double bed_m2 )
noexcept

Total solute exchange discharge across a bed interface of bed_m2, m³/s: D_sed·A/Y + v_hyp·A. One spelling, used by every engine's solute loop.

Here is the caller graph for this function:

◆ birdClearSkyGHI()

double openswmm::transport::heat::birdClearSkyGHI ( const SolarPosition & pos,
double pressure_mb,
const SolarConfig & cfg )
noexcept

Bird & Hulstrom clear-sky global horizontal irradiance [W/m²].

Parameters
posSun geometry from solarPosition.
pressure_mbStation pressure.
cfgAerosol / water / ozone / ground albedo.
Returns
0 when the sun is at or below the horizon.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ bowenRatio()

double openswmm::transport::heat::bowenRatio ( double t_water_c,
double t_air_c,
double humidity_pct,
double pressure_ratio )
noexcept

Bowen ratio [-]. Returns 0 when the vapour-pressure deficit vanishes, which is the removable singularity in Br's definition, not an error.

Here is the call graph for this function:

◆ cloudLongwaveFactor()

double openswmm::transport::heat::cloudLongwaveFactor ( double cloud_fraction,
double k_lw )
noexcept

Cloud enhancement of atmospheric emissivity, 1 + k_lw·C² (Bolz).

Warning
This multiplies into H3's RHE-gated Brunt term. It returns a LITERAL 1.0 when cloud_fraction <= 0, rather than evaluating 1 + k·0², so the clear-sky path is bit-identical to H3 rather than merely equal to it. Plan §2.5 makes that a verify criterion.
Here is the caller graph for this function:

◆ cloudShortwaveFactor()

double openswmm::transport::heat::cloudShortwaveFactor ( double cloud_fraction,
double k,
double n )
noexcept

Cloud attenuation of shortwave, 1 − k·C^n (Kasten–Czeplak).

Clamped at 0: an over-unity k·C^n blocks completely rather than turning into negative insolation — the same guard netShortwave applies to an over-unity shade factor.

Here is the caller graph for this function:

◆ equilibriumT()

double openswmm::transport::heat::equilibriumT ( double t0_c,
double j0,
double j1,
double h )
noexcept

The equilibrium temperature the linearization relaxes toward, °C — T₀ − J₀/J′. Exported so a gate can assert the no-overshoot property against it directly rather than recomputing the slope by hand. Returns t0_c when J′ ≤ 0, matching relaxT's fallback.

◆ evaporationRate()

double openswmm::transport::heat::evaporationRate ( double t_water_c,
double t_air_c,
double humidity_pct,
double wind_ms,
double a,
double b )
noexcept

Evaporative mass-transfer rate [m/s]. Negative under condensation.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ exchangePair()

SolutePairStep openswmm::transport::heat::exchangePair ( double c_w,
double c_b,
double vol_w,
double vol_b,
double q_exch,
double dt )
noexcept

One exact step of the two-body solute exchange.

Parameters
c_wChannel concentration.
c_bBed concentration.
vol_wChannel water volume, m³.
vol_bBed water volume, m³ (bed volume x porosity is the honest reading; the reference uses the bulk volume and so does this, see the header note on porosity).
q_exchTotal exchange discharge, m³/s: D_sed*A/Y + v_hyp*A.
dtStep, seconds.
Returns
Increments satisfying vol_w*dc_w + vol_b*dc_b == 0 to relative round-off. See the header on why this is not bit-exact.
Here is the caller graph for this function:

◆ groundTemperature()

double openswmm::transport::heat::groundTemperature ( SimulationContext & ctx)
noexcept

The deep-ground temperature in force this step, °C — resolves the GROUND_TEMPERATURE TIMESERIES spelling against the simulation clock. Non-const because the timeseries lookup advances the table's own cursor, exactly as table_tseries_lookup_cursor does everywhere else.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ groundTempFor()

double openswmm::transport::heat::groundTempFor ( const SimulationContext & ctx,
const SedimentConfig & sc,
double t_gr_global )
noexcept

PE2: the deep-ground temperature in force for ONE element. The global GROUND_TEMPERATURE TIMESERIES wins when configured — the TIMESERIES spelling is GLOBAL-only by grammar (per-element series are PE3/D-PE7) — otherwise the element's own resolved constant. For a model with no overrides sc.ground_temp IS the global constant, so this returns exactly what groundTemperature returned, by construction.

Here is the caller graph for this function:

◆ landCoverLongwave()

double openswmm::transport::heat::landCoverLongwave ( double t_air_c,
double emiss_landcover,
double sky_view )
noexcept

Land-cover longwave [W/m²], INTO the water.

Here is the caller graph for this function:

◆ latentFlux()

double openswmm::transport::heat::latentFlux ( double t_water_c,
double t_air_c,
double humidity_pct,
double wind_ms,
double a,
double b,
double water_density )
noexcept

Latent heat flux [W/m²], POSITIVE out of the water.

Here is the call graph for this function:

◆ latentHeatOfVaporization()

double openswmm::transport::heat::latentHeatOfVaporization ( double t_c)
noexcept

Latent heat of vaporization at t_c degrees Celsius [J/kg] (Martin & McCutcheon 1998).

Here is the caller graph for this function:

◆ linkBedAreaM2()

double openswmm::transport::heat::linkBedAreaM2 ( const SimulationContext & ctx,
int link )
noexcept

Bed contact area for link j, m² — wetted perimeter × length × barrels (divergence 1). 0 for a dry link or a non-conduit.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ linkFreeSurfaceFt2()

double openswmm::transport::heat::linkFreeSurfaceFt2 ( const SimulationContext & ctx,
int link )
noexcept

Free-surface area for link j, ft² — top width × length × barrels for an OPEN conduit, 0 otherwise. The Routing.cpp:597 evaporation-area expression, exported so the LARD binding does not grow a fourth copy of the XSectParams builder.

Here is the call graph for this function:

◆ netFluxOut()

double openswmm::transport::heat::netFluxOut ( const SimulationContext & ctx,
const HeatElement & elem,
double t_w )
noexcept

Net outward surface flux at t_w, summed over every enabled module [W/m²].

Each module's evaluator returns 0 when its own [HEAT_FLUXES] toggle is off, so this sums unconditionally and no caller has to know which families exist. This is the extension point: a new flux family is one added term here and nowhere else.

Shared with the ARD mesh and watershed bindings, which have their own element geometry but must compose the flux identically — the alternative being four hand-rolled copies of the same sum, which is the duplication that produced the defect above.

PE1: elem names WHICH element is being evaluated, so a module can read per-element attributes (shading, sky view, bed properties) instead of one global block. Passing a default- constructed HeatElement yields the global config — which is exactly what every caller got before PE — so a binding that forgets to pass its element degrades to the old behaviour rather than reading element 0's attributes.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ netRadiativeFluxOut()

double openswmm::transport::heat::netRadiativeFluxOut ( double t_water_c,
double t_air_c,
double humidity_pct,
const RadiativeConfig & cfg,
double jin_wm2 = -1.0,
double cloud_factor = 1.0 )
noexcept

Net radiative flux [W/m²], positive OUT of the water so it sums with SurfaceExchange's Je + Jc.

H6a adds two optional arguments and changes nothing without them:

  • jin_wm2 < 0 (the default) means "use `cfg.shortwave_wm2`", the H3 behaviour. A caller with a resolved per-step Jin passes it here. The sentinel is negative rather than NaN because a deck may legally mean 0 W/m² (night), and 0 must not be mistaken for "unset".
  • cloud_factor defaults to 1.0 — see atmosphericEmissivity.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ netShortwave()

double openswmm::transport::heat::netShortwave ( double incoming_wm2,
double albedo,
double shade_factor )
noexcept

Net absorbed shortwave [W/m²], INTO the water.

Here is the caller graph for this function:

◆ pressureFromElevation()

double openswmm::transport::heat::pressureFromElevation ( double elevation_m)
noexcept

Station pressure [mb] from elevation [m], standard atmosphere.

Here is the caller graph for this function:

◆ radiativeFluxOut()

double openswmm::transport::heat::radiativeFluxOut ( const SimulationContext & ctx,
const HeatElement & elem,
double t_w )
noexcept

This module's contribution to the net outward flux at t_w [W/m²]. Returns 0 unless HEAT_TRANSPORT is on AND [HEAT_FLUXES] / RADIATIVE_EXCHANGE is enabled, so a caller sums it unconditionally.

D-H5e: this module no longer has a binding of its own. It had one, with
its own element traversal and its own explicit conversion, and that is
how it came to relax separately from SurfaceExchange toward a different
equilibrium — making the answer depend on which module ran last. 
Here is the call graph for this function:
Here is the caller graph for this function:

◆ radiativeFor()

const RadiativeConfig & openswmm::transport::heat::radiativeFor ( const SimulationContext & ctx,
const HeatElement & e )
noexcept

The radiative attributes in force for e.

Returns the GLOBAL config when no override targets this family, when the element index is out of range, or when the element is not a kind that carries radiative overrides. The empty-vector path returns the very same object the pre-PE engine used, which is what makes a non-PE model bit-identical by construction rather than by tolerance.

Here is the caller graph for this function:

◆ relaxPair()

PairStep openswmm::transport::heat::relaxPair ( const BedCoupling & g,
double t_w,
double t_b,
double j0,
double j1,
double h,
double area_m2,
double dt )
noexcept

One exact step of the coupled (water, bed) pair.

Parameters
gConductances and capacities for this element.
t_wCurrent channel water temperature, °C.
t_bCurrent bed temperature, °C.
j0Net outward SURFACE flux at t_w, W/m² (netFluxOut).
j1The same flux at t_w + h — the probe relaxT already takes, passed in rather than recomputed so the pair is stepped against exactly the function the single body was.
hProbe offset, °C. <= 0 disables the linearization and takes j0 as a constant forcing.
area_m2Free-surface area the surface flux acts on, m². May be 0 for a closed conduit — the bed terms still apply.
dtStep, seconds.
Returns
Increments for both bodies. {0,0} when g is not viable.
Note
Reduces to relaxT when g_wb == g_bg == 0, to within 1-2 ULP — the two take different but algebraically identical routes to the same scalar, so the last bit can differ. Measured over dt in {1, 60, 900, 86400}: 1 ULP at every step, well inside the 4 ULP EXPECT_DOUBLE_EQ allows. The reduction is what keeps pre-H6b answers unchanged, and it holds only because the module is OFF by default — a deck without SEDIMENT_EXCHANGE never enters relaxPair at all, so byte-identity is exact for the reason that matters, not for this one.
Here is the caller graph for this function:

◆ relaxT()

double openswmm::transport::heat::relaxT ( double j0,
double j1,
double h,
double area_m2,
double vol_m3,
double dt,
double rho,
double cp )
noexcept

One element's temperature change, integrated SEMI-IMPLICITLY [°C].

Plan D-H5d. The energy balance ρ cp V dT/dt = −A·J(T) is linearized about the current temperature and the resulting linear ODE is integrated exactly:

k = A·J′ / (ρ cp V) [1/s] T_eq = T₀ − J₀/J′ ΔT = (J₀/J′)·expm1(−k·dt)

This replaces a forward-Euler step that diverged to NaN. Heat capacity is ρ cp V, so a thin film has almost none: a 0.52 ft³ film over 27,226 ft² took a +862 °C step in 60 s and the sequence ran 5 → 182 → −1.8e4 → −3.9e9 → inf → NaN. The explicit form had simply stopped representing the ODE.

Three properties earned this scheme its place, and each is gated:

  • |ΔT| ≤ |T_eq − T₀| always — the step cannot overshoot equilibrium, whatever dt is. That is an assertion needing no reference value, so no deck or timestep change can stale it.
  • No iteration, hence no cap to tune and no failure mode when the cap is reached.
  • Degrades to forward Euler as dt → 0 (expm1(−x) ≈ −x), so every small-step answer is unchanged from H2/H3.

The flux FORMULATIONS of H2 and H3 were never wrong; only the stepping was. CSH runs these same formulas at a 1e-4 s timestep with a selectable ODE solver — we took its physics and not its integrator.

Parameters
j0Net flux OUT of the water at T₀, W/m² (sum of every enabled module: Je + Jc from here, Jr from RadiativeExchange — all signed positive out, so they add).
j1The same sum re-evaluated at T₀ + h.
hThe probe offset used for j1; kProbeC unless testing.
area_m2Exchange area.
vol_m3Element volume — the thermal mass, and the reason this function exists.
Returns
ΔT in °C. Zero for a volume-less or area-less element. Falls back to the explicit step when J′ ≤ 0 (anti-damping: the linearized system has no fixed point to relax onto, so there is nothing to be stable about).
Here is the caller graph for this function:

◆ resolveHeatOverrides()

std::vector< std::string > openswmm::transport::heat::resolveHeatOverrides ( SimulationContext & ctx)

Materialise the dense per-element vectors from the parsed rows.

Called ONCE from SWMMEngine::open, after every process component has applied — the D-RQ1 timing, so link_names, node_names and the [TAGS] columns are all complete and an unknown element name can be reported with the row that named it.

Precedence is applied by CONSTRUCTION rather than by comparison: rows are swept in scope order (GLOBAL is already in the base config, then TAG, then element), so a later write simply wins. A comparison-based resolver would have to encode the ordering a second time and the two spellings could disagree.

Returns
Diagnostics; non-empty means the model must not open. Unknown element names are FATAL (D-PE5): a silently ignored override produces a plausible answer, which is the failure mode lessons 10/20 are about.
Here is the call graph for this function:

◆ saturationVapourPressure()

double openswmm::transport::heat::saturationVapourPressure ( double t_c)
noexcept

Saturation vapour pressure over water at t_c degrees Celsius [kPa].

Here is the caller graph for this function:

◆ sedimentFor()

const SedimentConfig & openswmm::transport::heat::sedimentFor ( const SimulationContext & ctx,
const HeatElement & e )
noexcept

The bed attributes in force for e.

Bed overrides are LINK-scoped only — the bed zone itself is conduits only (BedZoneState), so a node-scoped bed attribute would describe a body that does not exist. A NODE element therefore always reads the global here, and the parser refuses NODE scope on a [SEDIMENT_EXCHANGE] row rather than accepting a row that could never take effect.

Here is the caller graph for this function:

◆ seedBedTemperature()

void openswmm::transport::heat::seedBedTemperature ( SimulationContext & ctx)

Size and seed the bed temperature array, idempotently.

There is deliberately no applyBedHeatExchange. A function by that name would be called beside applyHeatFluxes and would step the bed in its own relaxation — which is D-H5e's defect, one phase after D-H5e removed it. The heat coupling instead happens INSIDE applyHeatFluxes's link loop, which calls relaxPair with the real j0/j1/area so the surface families and the bed are one simultaneous step. This function is only the state setup that loop needs first.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ sensibleFlux()

double openswmm::transport::heat::sensibleFlux ( double latent_flux,
double bowen_ratio )
noexcept

Sensible heat flux [W/m²], POSITIVE out of the water.

◆ solarPosition()

SolarPosition openswmm::transport::heat::solarPosition ( int day_of_year,
double hour_local,
double lat_deg,
double lon_deg,
double tz_hours )
noexcept

Sun position by the Spencer (1971) / NOAA formulation.

Parameters
day_of_year1–366.
hour_localLocal clock hour, decimal (13.5 = 13:30).
lat_degLatitude, +N.
lon_degLongitude, +E.
tz_hoursOffset from UTC, +E (MST = −7).
Note
This is the function SPA replaces. Nothing else in this module knows how a position is obtained.
Here is the caller graph for this function:

◆ surfaceFluxOut()

double openswmm::transport::heat::surfaceFluxOut ( const SimulationContext & ctx,
const HeatElement & elem,
double t_w )
noexcept

This module's contribution to the net outward flux at t_w [W/m²].

Returns 0 unless HEAT_TRANSPORT is on AND [HEAT_FLUXES] SURFACE_EXCHANGE is enabled, so a caller sums it unconditionally and the toggle stays this module's business.

Why this module no longer has a binding of its own (D-H5e)
It did, and so did RadiativeExchange, and each relaxed FULLY toward its own equilibrium. Under forward Euler the two increments were linear and added exactly; under relaxation they do NOT commute, so with both modules on the pair overshot the true combined equilibrium and the answer depended on module order — at large k·dt it landed on whichever module ran last. Every flux family now sums into one J(T) before a single relaxation, in HeatFluxes.cpp.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ updateSolarForcing()

void openswmm::transport::heat::updateSolarForcing ( SimulationContext & ctx)
noexcept

Resolve Jin and C for this step into ctx.heat_state.

Call at the PROLOGUE of every binding that will go on to call netFluxOut. There are four (applyHeatFluxes, the ARD binding, the watershed binding, the LID binding) and they run on two different clocks, so a single call site upstream of all of them does not exist. The function is idempotent within a step and costs ~40 flops, which is why repeating it is cheaper than inventing a scheduler to avoid repeating it.

Note
No-op unless HEAT_TRANSPORT is on and RADIATIVE_EXCHANGE is enabled — under any other configuration shortwave_now is never read, and computing it would be the kind of unobserved work lesson 39 is about.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ windFunction()

double openswmm::transport::heat::windFunction ( double wind_ms,
double a,
double b )
noexcept

Mass-transfer wind function a + b·w, w in m/s [m/s/kPa].

Here is the caller graph for this function:

Variable Documentation

◆ kBowenCoeff

double openswmm::transport::heat::kBowenCoeff = 0.061
inlineconstexpr

Bowen's coefficient, kPa/°C (CSH §4.5).

◆ kCuFtToCuM

double openswmm::transport::heat::kCuFtToCuM = 0.028316846592
inlineconstexpr

ft³ → m³.

◆ kKelvinOffset

double openswmm::transport::heat::kKelvinOffset = 273.15
inlineconstexpr

Celsius → Kelvin offset.

◆ kMphToMs

double openswmm::transport::heat::kMphToMs = 0.44704
inlineconstexpr

mph → m/s. ClimateState::wind_speed is mph.

◆ kProbeC

double openswmm::transport::heat::kProbeC = 1.0e-3
inlineconstexpr

Probe offset for the flux derivative, °C. Small enough that J′ is the local slope, large enough that the difference is not float noise.

◆ kSolarConstant

double openswmm::transport::heat::kSolarConstant = 1367.0
inlineconstexpr

Solar constant, W/m² (Bird & Hulstrom 1981; WMO 1982).

◆ kSqFtToSqM

double openswmm::transport::heat::kSqFtToSqM = 0.09290304
inlineconstexpr

ft² → m². Exported with relaxT, which is useless without it.

◆ kStdPressureMb

double openswmm::transport::heat::kStdPressureMb = 1013.25
inlineconstexpr

Sea-level standard pressure, millibars — Bird's pressure reference.

◆ kStefanBoltzmann

double openswmm::transport::heat::kStefanBoltzmann = 5.67e-8
inlineconstexpr

Stefan–Boltzmann constant, W/m²/K⁴ (rhemodel.cpp:46).