OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
SubsurfaceTransportState.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2//
3// Copyright 2026 Caleb Buahin
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
54
55#ifndef OPENSWMM_ENGINE_2D_SUBSURFACE_TRANSPORT_STATE_HPP
56#define OPENSWMM_ENGINE_2D_SUBSURFACE_TRANSPORT_STATE_HPP
57
58#include <cmath>
59#include <cstddef>
60#include <string>
61#include <string_view>
62#include <vector>
63
64namespace openswmm::twoD {
65
70 // ---- rows (TransportPolicy's layout, copied at initialize) ------------
71 double c_diff = 0.4;
72 int n_species = 0;
73 int n_cells = 0;
74 int n_pollut = 0;
75 int n_msx = 0;
76 int age_row = -1;
77 int temp_row = -1;
78 std::vector<std::string> row_names;
79
87 bool signedRow(int s) const noexcept { return s == temp_row; }
88
97 int rowIndex(std::string_view name) const noexcept {
98 for (std::size_t i = 0; i < row_names.size(); ++i)
99 if (row_names[i] == name)
100 return static_cast<int>(i);
101 return -1;
102 }
103
104 // ---- the two stores ---------------------------------------------------
105 // T7.2: a store holds the cell's TOTAL mass of a species — dissolved
106 // plus whatever is sorbed on the grains. Only the dissolved part
107 // travels, and `dissolvedFraction` is what every flux multiplies by, so
108 // retardation is a property of the CONCENTRATION the channels read
109 // rather than a factor sprinkled through them.
110 std::vector<double> sat_mass;
111 std::vector<double> unsat_mass;
112 // In LAYERED mode this is the canonical unsaturated inventory. The
113 // public bulk field above is its derived sum, never a second owner.
114 int n_layers = 0;
115 std::vector<double> layer_mass;
116 // Above-table receiving slots are fixed at producer time. Water is a
117 // routing split of nacc/lacc, not additional storage. The paired masses
118 // live here instead of nacc_mass/lacc_mass until the owner fires.
119 std::vector<double> contact_water;
120 std::vector<double> contact_node_out, contact_link_out;
132 std::size_t layerIdx(int s, int j, int c) const noexcept {
133 return (std::size_t(s) * n_cells + c) * n_layers + j;
134 }
135
145 void refreshUnsat(int s, int c) noexcept {
146 double sum = 0;
147 for (int j = 0; j < n_layers; ++j)
148 sum += layer_mass[layerIdx(s, j, c)];
149 unsat_mass[idx(s, c)] = sum;
150 }
151
152 // ---- T7.2 resolved parameters ----------------------------------------
154 std::vector<double> alpha_L;
155 std::vector<double> alpha_T;
156 std::vector<double> D_m;
157 std::vector<double> rho_b;
161 std::vector<double> kd;
162 std::vector<double> decay;
164 bool dispersion_on = true;
170
184 double dissolvedFraction(int s, int c, double theta) const noexcept {
185 if (s == age_row || s == temp_row)
186 return 1.0;
187 if (kd.empty() || !(theta > 0.0))
188 return 1.0;
189 const double k = kd[idx(s, c)];
190 if (!(k > 0.0))
191 return 1.0;
192 const double R = 1.0 + rho_b[static_cast<std::size_t>(c)] * k / theta;
193 return (R > 1.0) ? 1.0 / R : 1.0;
194 }
195
196 // ---- cross-cadence accumulators (paired with the water's) -------------
199 std::vector<double> sacc_L, sacc_R;
202 std::vector<double> xacc_from_surface;
206 std::vector<double> xacc_to_surface;
207
208 // ---- T7.4: the 1D seams ----------------------------------------------
212 std::vector<double> nacc_mass;
215 std::vector<double> lacc_mass;
220 std::vector<double> lout_mass;
223 std::vector<double> link_gain_shortfall;
227 std::vector<double> node_out_mass;
228
229 // ---- ledgers (per species, cumulative) --------------------------------
230 // Signs follow the water ledger they mirror: a "lost_" row is mass that
231 // left the aquifer, a "gained_" row is mass that arrived.
232 std::vector<double> lost_deep;
233 std::vector<double> lost_node;
234 std::vector<double> lost_link;
235 std::vector<double> lost_dunne;
236 std::vector<double> lost_et;
237 std::vector<double> gained_infil;
238 std::vector<double> gained_node;
239 std::vector<double> gained_link;
240 std::vector<double> net_lateral;
241 std::vector<double> gained_source, lost_source;
244 std::vector<double> lost_reaction;
245 std::vector<double> generated_age,
249 std::vector<double> init_mass;
252 std::vector<double> internal_recharge;
253
259 bool active() const noexcept { return n_species > 0 && n_cells > 0; }
260
268 std::size_t idx(int s, int c) const noexcept {
269 return static_cast<std::size_t>(s) * static_cast<std::size_t>(n_cells) +
270 static_cast<std::size_t>(c);
271 }
272
276 std::size_t lateralIndex(int species, std::size_t edge) const noexcept {
277 return edge * static_cast<std::size_t>(n_species) + static_cast<std::size_t>(species);
278 }
279
280
293 void resize(int n_spec, int n_cell, int n_edges) {
294 n_species = (n_spec > 0) ? n_spec : 0;
295 n_cells = (n_cell > 0) ? n_cell : 0;
296 const auto ns = static_cast<std::size_t>(n_species);
297 const auto nc = static_cast<std::size_t>(n_cells);
298 const auto ne = static_cast<std::size_t>(n_edges > 0 ? n_edges : 0);
299 sat_mass.assign(ns * nc, 0.0);
300 unsat_mass.assign(ns * nc, 0.0);
301 sacc_L.assign(ns * ne, 0.0);
302 sacc_R.assign(ns * ne, 0.0);
303 xacc_from_surface.assign(ns * nc, 0.0);
304 xacc_to_surface.assign(ns * nc, 0.0);
305 lacc_mass.assign(ns * nc, 0.0); // T7.4
306 lout_mass.assign(ns * nc, 0.0);
307 link_gain_shortfall.assign(ns, 0.0);
308 // nacc_mass and node_out_mass are sized by the solver, which knows
309 // the bed and node counts.
310 lost_deep.assign(ns, 0.0);
311 lost_node.assign(ns, 0.0);
312 lost_link.assign(ns, 0.0);
313 lost_dunne.assign(ns, 0.0);
314 lost_et.assign(ns, 0.0);
315 gained_infil.assign(ns, 0.0);
316 gained_node.assign(ns, 0.0);
317 gained_link.assign(ns, 0.0);
318 net_lateral.assign(ns, 0.0);
319 gained_source.assign(ns, 0.0);
320 lost_source.assign(ns, 0.0);
321 boundary_in.assign(ns, 0);
322 boundary_out.assign(ns, 0);
323 boundary_flux_in.assign(ns, 0);
324 boundary_flux_out.assign(ns, 0);
325 lost_reaction.assign(ns, 0.0); // T7.2
326 generated_age.clear();
327 gained_contact.clear();
328 init_mass.assign(ns, 0.0);
329 internal_recharge.assign(ns, 0.0);
330 alpha_L.assign(nc, 1.0); // T7.2: the row defaults
331 alpha_T.assign(nc, 0.1);
332 D_m.assign(nc, 1.0e-9);
333 rho_b.assign(nc, 0.0);
334 kd.assign(ns * nc, 0.0);
335 decay.assign(ns * nc, 0.0);
337 }
338
344 void clear() { *this = SubsurfaceTransportState{}; }
345
360 double inFlight1D(int s) const noexcept {
361 double m = 0.0;
362 const auto ns = static_cast<std::size_t>(n_species > 0 ? n_species : 1);
363 if (!nacc_mass.empty()) {
364 const auto nb = nacc_mass.size() / ns;
365 for (std::size_t b = 0; b < nb; ++b)
366 m += nacc_mass[static_cast<std::size_t>(s) * nb + b];
367 }
368 const auto base = static_cast<std::size_t>(s) * static_cast<std::size_t>(n_cells);
369 for (int c = 0; c < n_cells; ++c)
370 m += lacc_mass[base + static_cast<std::size_t>(c)];
371 for (int j = 0; j < n_layers; ++j)
372 for (int c = 0; c < n_cells; ++c) {
373 const auto k = layerIdx(s, j, c);
375 }
376 return m;
377 }
378
393 double storage(int s) const noexcept {
394 double m = 0.0;
395 const auto base = static_cast<std::size_t>(s) * static_cast<std::size_t>(n_cells);
396 for (int c = 0; c < n_cells; ++c) {
397 m += sat_mass[base + static_cast<std::size_t>(c)];
398 m += unsat_mass[base + static_cast<std::size_t>(c)];
399 m += xacc_from_surface[base + static_cast<std::size_t>(c)];
400 m += xacc_to_surface[base + static_cast<std::size_t>(c)];
401 }
402 const auto ne = sacc_L.size() / (n_species > 0 ? static_cast<std::size_t>(n_species) : 1);
403 for (std::size_t e = 0; e < ne; ++e) {
404 const auto k = lateralIndex(s, e);
405 m += sacc_L[k] + sacc_R[k];
406 }
407 return m + inFlight1D(s); // T7.4
408 }
409
427 double ledgeredStorage(int s) const noexcept {
428 double m = 0.0;
429 const auto base = static_cast<std::size_t>(s) * static_cast<std::size_t>(n_cells);
430 for (int c = 0; c < n_cells; ++c)
431 m += sat_mass[base + static_cast<std::size_t>(c)] +
432 unsat_mass[base + static_cast<std::size_t>(c)];
433 const auto ne = sacc_L.size() / (n_species > 0 ? static_cast<std::size_t>(n_species) : 1);
434 for (std::size_t e = 0; e < ne; ++e) {
435 const auto k = lateralIndex(s, e);
436 m += sacc_L[k] + sacc_R[k];
437 }
438 // T7.4: the two 1D arrival accumulators are booked into
439 // `gained_node` / `gained_link` only when the CELL gathers them, so
440 // until then they are storage the ledger has not yet named — the
441 // same phase rule the surface accumulators follow, and the reason
442 // they are counted here while `xacc_*` are not.
443 return m + inFlight1D(s);
444 }
445
465 double continuityDenominator(int s) const noexcept {
466 const auto us = static_cast<std::size_t>(s);
467 double total = std::fabs(init_mass[us]) + std::fabs(gained_infil[us]) +
468 std::fabs(gained_node[us]) + std::fabs(gained_link[us]) +
469 std::fabs(net_lateral[us]) + std::fabs(gained_source[us]);
470 if (!generated_age.empty())
471 total += std::abs(generated_age[us]) + std::abs(gained_contact[us]);
472 return total;
473 }
474
491 double residual(int s) const noexcept {
492 const auto us = static_cast<std::size_t>(s);
493 const double out = lost_deep[us] + lost_node[us] + lost_link[us] + lost_dunne[us] +
494 lost_et[us] + lost_reaction[us] + lost_source[us];
495 double in = gained_infil[us] + gained_node[us] + gained_link[us] + net_lateral[us] +
496 gained_source[us];
497 if (!generated_age.empty())
498 in += generated_age[us] + gained_contact[us];
499 return ledgeredStorage(s) + out - in - init_mass[us];
500 }
501};
502
503} // namespace openswmm::twoD
504
505#endif // OPENSWMM_ENGINE_2D_SUBSURFACE_TRANSPORT_STATE_HPP
Definition NodeCoupling2D.cpp:23
Definition ExchangeLaw.hpp:60
Species mass in both aquifer zones, and where mass crossed a boundary.
Definition SubsurfaceTransportState.hpp:69
std::vector< double > D_m
molecular diffusivity (m²/s)
Definition SubsurfaceTransportState.hpp:156
std::vector< double > nacc_mass
Definition SubsurfaceTransportState.hpp:212
std::vector< double > contact_link_mass
Definition SubsurfaceTransportState.hpp:121
std::vector< double > lacc_mass
Definition SubsurfaceTransportState.hpp:215
std::vector< double > sacc_L
Definition SubsurfaceTransportState.hpp:199
int n_layers
Definition SubsurfaceTransportState.hpp:114
std::vector< double > boundary_out
Definition SubsurfaceTransportState.hpp:242
std::vector< double > generated_age
Definition SubsurfaceTransportState.hpp:245
double dissolvedFraction(int s, int c, double theta) const noexcept
The share of a cell's stored mass that is DISSOLVED and therefore mobile: 1 / R with the retardation ...
Definition SubsurfaceTransportState.hpp:184
std::vector< double > sacc_R
Definition SubsurfaceTransportState.hpp:199
bool dispersion_on
Whether dispersion runs at all ([GW_TRANSPORT_OPTIONS] DISPERSION).
Definition SubsurfaceTransportState.hpp:164
std::vector< double > xacc_to_surface
Definition SubsurfaceTransportState.hpp:206
double ledgeredStorage(int s) const noexcept
The storage the LEDGER can account for: holdings less the two surface accumulators,...
Definition SubsurfaceTransportState.hpp:427
std::vector< double > unsat_mass
[s*n_cells+c] in the column store
Definition SubsurfaceTransportState.hpp:111
double inFlight1D(int s) const noexcept
T7.4: mass in flight on the two 1D seams — booked by the producer, not yet taken by the owner....
Definition SubsurfaceTransportState.hpp:360
std::vector< double > init_mass
Definition SubsurfaceTransportState.hpp:249
std::vector< double > lost_node
to a 1D node through its bed
Definition SubsurfaceTransportState.hpp:233
std::vector< double > net_lateral
net across the domain edge (0 on a closed mesh)
Definition SubsurfaceTransportState.hpp:240
long dispersion_limiter_binds
Definition SubsurfaceTransportState.hpp:169
int rowIndex(std::string_view name) const noexcept
Row index of a species by name, or −1 — the [GW_*] sections address species by name and resolve throu...
Definition SubsurfaceTransportState.hpp:97
int n_cells
Definition SubsurfaceTransportState.hpp:73
std::vector< double > node_out_mass
Definition SubsurfaceTransportState.hpp:227
std::vector< double > gained_source
Definition SubsurfaceTransportState.hpp:241
int age_row
Definition SubsurfaceTransportState.hpp:76
std::vector< double > gained_contact
Definition SubsurfaceTransportState.hpp:246
std::vector< std::string > row_names
Definition SubsurfaceTransportState.hpp:78
std::vector< double > link_gain_shortfall
Definition SubsurfaceTransportState.hpp:223
std::size_t idx(int s, int c) const noexcept
Map a species/cell pair into bulk species-major storage.
Definition SubsurfaceTransportState.hpp:268
std::vector< double > lost_reaction
T7.2: first-order decay.
Definition SubsurfaceTransportState.hpp:244
std::size_t lateralIndex(int species, std::size_t edge) const noexcept
Index a lateral species packet in edge-major owned storage. Requires valid species/edge indices and c...
Definition SubsurfaceTransportState.hpp:276
int n_pollut
Definition SubsurfaceTransportState.hpp:74
std::vector< double > alpha_L
[GW_TRANSPORT_PARAMS] per cell, after * < TAG < CELL resolution.
Definition SubsurfaceTransportState.hpp:154
std::vector< double > internal_recharge
Definition SubsurfaceTransportState.hpp:252
int n_species
Definition SubsurfaceTransportState.hpp:72
std::vector< double > gained_infil
from the surface's infiltration
Definition SubsurfaceTransportState.hpp:237
double continuityDenominator(int s) const noexcept
T7.4 (2026-09-21): the scale a residual is judged against — what the aquifer started with plus every ...
Definition SubsurfaceTransportState.hpp:465
std::vector< double > lost_deep
deep percolation
Definition SubsurfaceTransportState.hpp:232
std::vector< double > alpha_T
transverse dispersivity (m)
Definition SubsurfaceTransportState.hpp:155
std::vector< double > lost_et
the intensive rows ET carried off
Definition SubsurfaceTransportState.hpp:236
std::vector< double > lost_dunne
saturation excess + rejection, to the surface
Definition SubsurfaceTransportState.hpp:235
double storage(int s) const noexcept
Everything the aquifer HOLDS of one species, including mass in flight in a side accumulator (the wate...
Definition SubsurfaceTransportState.hpp:393
std::vector< double > lost_source
wells, native concentration x m3
Definition SubsurfaceTransportState.hpp:241
std::vector< double > decay
Definition SubsurfaceTransportState.hpp:162
std::vector< double > lout_mass
Definition SubsurfaceTransportState.hpp:220
std::vector< double > contact_water
[j*n_cells+c], SI m3
Definition SubsurfaceTransportState.hpp:119
std::size_t layerIdx(int s, int j, int c) const noexcept
< layerIdx
Definition SubsurfaceTransportState.hpp:132
bool signedRow(int s) const noexcept
A row whose mass may legitimately be negative — the temperature row (°C·m³ below zero is a state,...
Definition SubsurfaceTransportState.hpp:87
std::vector< double > lost_link
to a gaining conduit (G-X4)
Definition SubsurfaceTransportState.hpp:234
std::vector< double > contact_node_mass
Definition SubsurfaceTransportState.hpp:121
std::vector< double > boundary_in
Definition SubsurfaceTransportState.hpp:242
bool active() const noexcept
Test whether the subsurface transport layout is nonempty.
Definition SubsurfaceTransportState.hpp:259
std::vector< double > contact_link_out
gross promised water, per bed/cell
Definition SubsurfaceTransportState.hpp:120
std::vector< double > boundary_flux_in
Definition SubsurfaceTransportState.hpp:242
std::vector< double > contact_node_out
Definition SubsurfaceTransportState.hpp:120
void clear()
Reset the complete subsurface transport state to empty defaults.
Definition SubsurfaceTransportState.hpp:344
std::vector< double > rho_b
Definition SubsurfaceTransportState.hpp:157
int temp_row
Definition SubsurfaceTransportState.hpp:77
std::vector< double > kd
Definition SubsurfaceTransportState.hpp:161
void refreshUnsat(int s, int c) noexcept
Refresh one derived unsaturated bulk inventory from canonical layers.
Definition SubsurfaceTransportState.hpp:145
std::vector< double > xacc_from_surface
Definition SubsurfaceTransportState.hpp:202
std::vector< double > gained_link
from a leaking conduit (T7.4 seam)
Definition SubsurfaceTransportState.hpp:239
int n_msx
Definition SubsurfaceTransportState.hpp:75
std::vector< double > boundary_flux_out
gross split of net_lateral
Definition SubsurfaceTransportState.hpp:243
std::vector< double > gained_node
from a recharging node (T7.4 seam)
Definition SubsurfaceTransportState.hpp:238
double residual(int s) const noexcept
T7.1's conservation statement, the twin of SurfaceTransportState::totalIncludingLedgers: what the aqu...
Definition SubsurfaceTransportState.hpp:491
std::vector< double > layer_mass
[(s*n_cells+c)*n_layers+j]
Definition SubsurfaceTransportState.hpp:115
std::vector< double > sat_mass
[s*n_cells+c] in the saturated water
Definition SubsurfaceTransportState.hpp:110
void resize(int n_spec, int n_cell, int n_edges)
Allocate/reset the core two-zone transport arrays and ledgers.
Definition SubsurfaceTransportState.hpp:293
double c_diff
explicit solute diffusion safety factor
Definition SubsurfaceTransportState.hpp:71