OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
LagrangianSolver.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
94
95#ifndef OPENSWMM_QUALITY_LARD_LAGRANGIAN_SOLVER_HPP
96#define OPENSWMM_QUALITY_LARD_LAGRANGIAN_SOLVER_HPP
97
98#include <algorithm>
99#include <cmath>
100#include <limits>
101#include <cstdint>
102#include <vector>
103
106#include "../NegativeSources.hpp"
107#include "../QualityRouting.hpp"
110#include "RwptDispersion.hpp"
111#include "SegmentStore.hpp"
119#include "../QualityRouting.hpp"
120
121namespace openswmm {
122namespace lard {
123
124constexpr double kTinyFlow = 1.0e-8;
125
152 int np = 0;
153 int age_row = -1;
154 int temp_row = -1;
155 int msx_first = -1;
157 int ns = 0;
158};
159
164 // E2: the class ENABLES come from the one policy
165 // (transport::network1DEnables — allocation-free, this runs per step).
166 // Identical to the pre-E2 reads: LARD only runs with IGNORE_QUALITY NO,
167 // where the policy's gate is a no-op. The row ORDER below is deliberately
168 // LARD's own (pollutants, age, temperature, MSX) — see the struct note;
169 // E2 unifies the decision, not the index arithmetic.
172 L.np = en.n_pollut;
173 L.ns = L.np;
174 if (en.age) L.age_row = L.ns++;
175 if (en.temperature) L.temp_row = L.ns++; // H7b
176 // L3: MSX species ride the segments after the reserved rows —
177 // stable across the run (the compiled species table is fixed at
178 // open). n_msx == 0 leaves every index above IDENTICAL to H7b's
179 // layout, which is the bit-inertness claim the corpus checks.
181 L.msx_first = L.ns;
182 L.ns += en.n_msx;
183 }
184 return L;
185}
186
188public:
193 void step(SimulationContext& ctx, double dt_routing) {
194 // X4/H7b: the age and temperature rows ride the segments after
195 // the pollutants — age published to water_age_state (seconds),
196 // temperature to heat_state (degC) — rather than the np-strided
197 // conc arrays.
198 const SpeciesRowLayout L = rowLayout(ctx);
199 const int np = L.np;
200 const int ns = L.ns;
201 if (ns <= 0) return;
202 if (!initialized_) init(ctx);
203
204 const int nl = ctx.n_links();
205 auto& links = ctx.links;
206
207 // ---- 0. Flow reversal (§4.4) — once per routing step: the flow
208 // solution is constant within it. -----------------------------
209 bool topo_dirty = false;
210 for (int l = 0; l < nl; ++l) {
211 const auto ul = static_cast<std::size_t>(l);
212 const double q = links.flow[ul];
213 if (std::abs(q) <= kTinyFlow) continue;
214 const std::int8_t sign = (q >= 0.0) ? 1 : -1;
215 if (sign != flow_sign_[ul]) {
216 if (links.type[ul] == LinkType::CONDUIT) store_.reverse(l);
217 flow_sign_[ul] = sign;
218 topo_dirty = true;
219 }
220 }
221 if (topo_dirty || topo_.empty()) computeTopoOrder(ctx);
222
223 // ---- X3a: QUALITY_STEP substepping (strategy §4.2). Equal
224 // substeps; mass/age external loads are RATES and scale
225 // through dt, the per-routing-step external VOLUME
226 // (qual_vol_in) scales through frac. dtq absent or >= the
227 // routing step degenerates to one substep — bit-identical to
228 // the pre-X3a engine by construction.
229 const double dtq = ctx.options.quality_step;
230 const int nsub = (dtq > 0.0 && dtq < dt_routing)
231 ? static_cast<int>(std::ceil(dt_routing / dtq))
232 : 1;
233 const double dt = dt_routing / static_cast<double>(nsub);
234 const double frac = 1.0 / static_cast<double>(nsub);
235 for (int sub = 0; sub < nsub; ++sub) substep(ctx, dt, frac);
236
237 // H2/H3/H6b under LARD: surface + radiative fluxes and the bed
238 // pair, once per routing step (the ARD slot), applied to the
239 // segment field before publication.
240 applyFluxesAndBed(ctx, dt_routing);
241
242 publish(ctx, dt_routing);
243 }
244
252 void substep(SimulationContext& ctx, double dt, double frac) {
253 const SpeciesRowLayout L = rowLayout(ctx);
254 const int np = L.np;
255 const int ns = L.ns;
256 // Derived from the layout rather than re-read from options, so the
257 // "is there a reserved row" question has exactly one answer per call.
258 const bool age = (L.age_row >= 0);
259 const bool heat = (L.temp_row >= 0);
260 const int nn = ctx.n_nodes();
261 const int nl = ctx.n_links();
262 auto& nodes = ctx.nodes;
263 auto& links = ctx.links;
264 auto& ws = ctx.water_age_state;
265 auto& hs = ctx.heat_state;
266
267 // ---- AGE (before transport): every parcel ages by exactly dt, and
268 // the aged value is this substep's "old" state — the plan §1
269 // convention routeLegacyAge follows (age then mix). ------------
270 if (age) {
271 store_.add_species(L.age_row, dt);
272 for (int n = 0; n < nn; ++n)
273 ws.node_age[static_cast<std::size_t>(n)] += dt;
274 }
275
276 // ---- 1. DRAIN (all conduits, before any node mixes) ---------------
277 scratch_.assign(static_cast<std::size_t>(ns), 0.0);
278 treat_cin_.assign(static_cast<std::size_t>(np > 0 ? np : 0), 0.0);
279 for (int l = 0; l < nl; ++l) {
280 const auto ul = static_cast<std::size_t>(l);
281 if (links.type[ul] != LinkType::CONDUIT) continue;
282 const double q = std::abs(links.flow[ul]);
283 const int dn = downstreamNode(ctx, l);
284 const int up = upstreamNode(ctx, l);
285
286 if (q > kTinyFlow && dn >= 0) {
287 std::fill(scratch_.begin(), scratch_.end(), 0.0);
288 const double drained = store_.drain_back(l, q * dt,
289 scratch_.data());
290 addToLedger(dn, drained, scratch_.data(), ns);
291 }
292 // Volume reconciliation: the slab must sum to links.volume.
293 // A shortfall is filled at RELEASE with upstream water; an
294 // excess left after the outflow drain leaves through the FRONT
295 // to the upstream ledger — booked, not rescaled (see header).
296 const double v_new = links.volume[ul];
297 const double v_rem = store_.total_volume(l);
298 if (v_rem > v_new && up >= 0) {
299 std::fill(scratch_.begin(), scratch_.end(), 0.0);
300 const double shed = store_.drain_front(l, v_rem - v_new,
301 scratch_.data());
302 addToLedger(up, shed, scratch_.data(), ns);
303 }
304 }
305
306 // ---- 2. MIX in topo order, passthrough zero-volume links ----------
307 for (const int n : topo_) {
308 const auto un = static_cast<std::size_t>(n);
309 const double v_old = nodes.old_volume[un];
310 const double v_in = node_vol_in_[un];
311 // OUTFALL_BACKFLOW_QUALITY ZERO: an outfall taking no volume
312 // inflow this substep is a fresh boundary — its held state
313 // (pollutant rows AND the age row) reads zero, so the RELEASE
314 // seeding and zero-volume passthrough below draw clean water.
315 const bool zero_bf =
317 nodes.type[un] == NodeType::OUTFALL &&
318 v_in + nodes.qual_vol_in[un] * frac <= 0.0;
319
320 for (int s = 0; s < ns; ++s) {
321 // H7a: identity by INDEX, not by threshold. `s >= np` was
322 // correct only while age was the sole reserved row; it would
323 // silently capture the temperature row too (H7b).
324 const bool is_age = (s == L.age_row);
325 const bool is_temp = (s == L.temp_row);
326 // L3: species rows — state lives in msx_node_conc
327 // ([node*nsp + sp]); U2 gave them the [INFLOWS] pathway
328 // (msx_ext_mass_in, same index).
329 const bool is_msx =
330 (L.msx_first >= 0 && s >= L.msx_first);
331 const auto xi =
332 is_msx ? un * static_cast<std::size_t>(
333 ns - L.msx_first) +
334 static_cast<std::size_t>(s - L.msx_first)
335 : 0;
336 const auto li = un * static_cast<std::size_t>(ns) +
337 static_cast<std::size_t>(s); // ledger index
338 // State and external load per row. Pollutants: nodes.conc +
339 // qual_mass_in (rate × dt — the mixAtNodes convention). Age:
340 // water_age_state.node_age (already aged +dt this step) +
341 // node_age_vol_in (age·ft³/s rate, the D-UT10 parallel
342 // accumulator filled by all seven loader pathways).
343 const auto ci = un * static_cast<std::size_t>(np) +
344 static_cast<std::size_t>(s);
345 // Temperature mirrors age one row over: state in
346 // heat_state.node_temp (degC), external load from
347 // node_temp_vol_in (degC.ft3/s, the D-UT10 twin filled by
348 // the same seven loaders) -- H1's convention, consumed here
349 // instead of by routeLegacyHeat.
350 const double st_old =
351 is_age ? ws.node_age[un]
352 : is_temp ? hs.node_temp[un]
353 : is_msx ? ctx.reactions.msx_node_conc[xi]
354 : nodes.conc[ci];
355 // U2: species rows take the [INFLOWS] species loads
356 // (msx_ext_mass_in, a rate; empty when no such row).
357 const double m_ext =
358 is_age ? ws.node_age_vol_in[un] * dt
359 : is_temp ? hs.node_temp_vol_in[un] * dt
360 : is_msx ? ((xi < ctx.reactions.msx_ext_mass_in.size())
361 ? ctx.reactions.msx_ext_mass_in[xi] * dt : 0.0)
362 : nodes.qual_mass_in[ci] * dt;
363 double m = st_old * v_old + node_mass_in_[li] + m_ext;
364 // P2.3: stash the ARRIVING mass for the treatment cin below
365 // (pollutant rows only; s == p for rows 0..np-1).
366 if (!is_age && !is_temp && !is_msx &&
367 s < static_cast<int>(treat_cin_.size()))
368 treat_cin_[static_cast<std::size_t>(s)] =
369 node_mass_in_[li] + m_ext;
370 // D-NS1 (X6, now observable): extraction beyond the
371 // store's mass clamps to available — counted, warned
372 // once, and (pollutant rows) un-booked so the ledger
373 // carries what actually left.
374 // The non-negativity clamp does NOT apply to temperature:
375 // degC water below zero is an ordinary state (the freezing
376 // gate in heat_watershed exists to keep it so), where a
377 // negative mass or age is a defect. Same reasoning as the
378 // report boundary's deliberate no-mask on temperature.
379 if (m < 0.0 && !is_temp) {
380 if (is_age)
382 else if (!is_msx) // L3: species have no ledger row
384 m = 0.0;
385 }
386 const double denom =
387 v_old + v_in + nodes.qual_vol_in[un] * frac;
388 // ALWAYS divide by the full denominator. m/denom is a convex
389 // combination of st_old and the arriving values, so it can
390 // never exceed its inputs; the fallback this replaces
391 // divided a mass that included st_old*v_old by a divisor
392 // that EXCLUDED v_old, and at a nearly-dry junction that
393 // quotient amplified step over step -- measured on a
394 // receding-flow deck (inflow stops at 1 h): node
395 // concentrations reached 2.7e30, 2.0e281, then inf, and the
396 // final-storage row went NaN. Below 1e-12 ft^3 there is no
397 // meaningful water and the store keeps its value.
398 const double st_new =
399 zero_bf ? 0.0
400 : ((denom > 1.0e-12) ? m / denom : st_old);
401 if (is_age) ws.node_age[un] = st_new;
402 else if (is_temp) hs.node_temp[un] = st_new;
403 else if (is_msx) ctx.reactions.msx_node_conc[xi] = st_new;
404 else nodes.conc[ci] = st_new;
405 node_mass_in_[li] = 0.0; // consumed; cycle residue carries
406 }
407 node_vol_in_[un] = 0.0;
408
409 // P2.3: [TREATMENT] applies HERE — to the freshly mixed node
410 // state, BEFORE the passthrough and RELEASE below draw it: the
411 // LEGACY ordering (mix → treat → links), transplanted. The
412 // handoff's end-of-step application outside the solver was a
413 // no-op twice over: the evaluator's cin read the LEGACY
414 // accumulators (external loads only — 0 at an interior node,
415 // and an R-typed expression keeps c_node when cin reads 0),
416 // and a junction's ~zero stored volume gave the late write no
417 // weight in the next mix. Fed with THIS substep's inflow
418 // figures instead; nodes with no treatment pay one flag test.
419 if (np > 0 && dt > 0.0 &&
420 un < ctx.treatment.has_treatment.size() &&
421 ctx.treatment.has_treatment[un]) {
422 const double v_in_total =
423 v_in + nodes.qual_vol_in[un] * frac;
424 for (int p = 0; p < np; ++p)
425 treat_cin_[static_cast<std::size_t>(p)] =
426 (v_in_total > 1.0e-12)
427 ? std::max(0.0,
428 treat_cin_[
429 static_cast<std::size_t>(p)]) /
430 v_in_total
431 : 0.0;
432 quality::applyNodeTreatment(ctx, n, dt, v_in_total / dt,
433 treat_cin_.data());
434 }
435
436 // Zero-volume passthrough (§2.4): outgoing pump/orifice/weir/
437 // outlet links deliver the node's NEW concentration downstream
438 // within this step — the property the topo order exists for.
439 for (const int l : node_out_links_[un]) {
440 const auto ul = static_cast<std::size_t>(l);
441 if (links.type[ul] == LinkType::CONDUIT) continue;
442 const double q = std::abs(links.flow[ul]);
443 if (q <= kTinyFlow) continue;
444 const int dn = downstreamNode(ctx, l);
445 if (dn < 0) continue;
446 for (int p = 0; p < np; ++p) {
447 const auto ni = un * static_cast<std::size_t>(np) +
448 static_cast<std::size_t>(p);
449 const auto lp = ul * static_cast<std::size_t>(np) +
450 static_cast<std::size_t>(p);
451 scratch_[static_cast<std::size_t>(p)] = nodes.conc[ni];
452 links.conc[lp] = nodes.conc[ni];
453 }
454 if (age) {
455 scratch_[static_cast<std::size_t>(L.age_row)] =
456 ws.node_age[un];
457 ws.link_age[ul] = ws.node_age[un];
458 }
459 if (heat) {
460 scratch_[static_cast<std::size_t>(L.temp_row)] =
461 hs.node_temp[un];
462 hs.link_temp[ul] = hs.node_temp[un];
463 }
464 if (L.msx_first >= 0) { // L3
465 const int nsp = ns - L.msx_first;
466 for (int sp = 0; sp < nsp; ++sp) {
467 const double c = ctx.reactions.msx_node_conc[
468 un * static_cast<std::size_t>(nsp) +
469 static_cast<std::size_t>(sp)];
470 scratch_[static_cast<std::size_t>(
471 L.msx_first + sp)] = c;
473 ul * static_cast<std::size_t>(nsp) +
474 static_cast<std::size_t>(sp)] = c;
475 }
476 }
477 addToLedgerRate(dn, q * dt, scratch_.data(), ns);
478 }
479 }
480
481 // ---- 3. RELEASE new front segments --------------------------------
482 for (int l = 0; l < nl; ++l) {
483 const auto ul = static_cast<std::size_t>(l);
484 if (links.type[ul] != LinkType::CONDUIT) continue;
485 const int up = upstreamNode(ctx, l);
486 const double need = links.volume[ul] - store_.total_volume(l);
487 release_vol_[ul] = (need > 0.0) ? need : 0.0; // X3b: RWPT's V_in
488 if (need <= 0.0 || up < 0) continue;
489 const auto uu = static_cast<std::size_t>(up);
490 for (int p = 0; p < np; ++p)
491 scratch_[static_cast<std::size_t>(p)] =
492 nodes.conc[uu * static_cast<std::size_t>(np) +
493 static_cast<std::size_t>(p)];
494 if (age)
495 scratch_[static_cast<std::size_t>(L.age_row)] =
496 ws.node_age[uu];
497 if (heat)
498 scratch_[static_cast<std::size_t>(L.temp_row)] =
499 hs.node_temp[uu];
500 if (L.msx_first >= 0) { // L3
501 const int nsp = ns - L.msx_first;
502 for (int sp = 0; sp < nsp; ++sp)
503 scratch_[static_cast<std::size_t>(
504 L.msx_first + sp)] =
506 uu * static_cast<std::size_t>(nsp) +
507 static_cast<std::size_t>(sp)];
508 }
509 store_.push_front(l, need, scratch_.data());
510 }
511
512 // ---- 3b. RWPT dispersion (X3b) — on the substep's FINAL segment
513 // field, per link, resolved vertical shear + walk. The age row
514 // disperses with the water like every other species — mixing
515 // moves age, physically. H7b: so does TEMPERATURE, at the same
516 // coefficient as a solute — the ARD engine's deliberate choice
517 // for its temperature row, adopted here for cross-engine
518 // consistency (decision 2026-08-30). ---------------------------
519 if (ctx.options.lard_rwpt) {
520 ++substep_counter_;
521 const auto& cond = ctx.link_subtypes.conduits;
522 for (int l = 0; l < nl; ++l) {
523 const auto ul = static_cast<std::size_t>(l);
524 if (links.type[ul] != LinkType::CONDUIT) continue;
525 const double q = std::abs(links.flow[ul]);
526 if (q <= kTinyFlow) continue;
527 const int row = ctx.link_subtypes.conduit_row(l);
528 if (row < 0) continue;
529 const auto ur = static_cast<std::size_t>(row);
530 const double len = cond.length[ur];
531 const double vol = links.volume[ul];
532 if (len <= 0.0 || vol <= 0.0) continue;
533 const double a_flow = vol / len;
534 const double ubar = q / a_flow;
535 const double h = links.depth[ul];
536 const bool circ =
537 links.xsect_shape[ul] == XsectShape::CIRCULAR;
538 const double rh =
539 rwpt_hyd_radius(a_flow, h, links.xsect_geom1[ul], circ);
540 rwpt_.disperse(ctx, store_, l, ubar, h, rh,
541 cond.roughness[ur], release_vol_[ul],
542 q * dt, dt, substep_counter_,
543 static_cast<std::uint64_t>(
544 ctx.options.rwpt_seed),
545 scratch_);
546 }
547 }
548
549 // ---- 4. DECAY (exact exponential; species-major stripes) ----------
550 for (int p = 0; p < np; ++p) {
551 const double k = ctx.pollutants.k_decay[static_cast<std::size_t>(p)];
552 if (k == 0.0) continue;
553 const double f = std::exp(-k * dt);
554 double removed = store_.decay_species(p, f);
555 for (int n = 0; n < nn; ++n) {
556 const auto idx = static_cast<std::size_t>(n) *
557 static_cast<std::size_t>(np) +
558 static_cast<std::size_t>(p);
559 const double v = nodes.volume[static_cast<std::size_t>(n)];
560 removed += nodes.conc[idx] * (1.0 - f) * v;
561 nodes.conc[idx] *= f;
562 }
563 if (static_cast<std::size_t>(p) <
566 static_cast<std::size_t>(p)] += removed;
567 }
568
569 // ---- 4b. REACT (L3): MSX species integrate per SEGMENT (pipe
570 // scope, the segment's own pollutant rows as context) and
571 // per NODE store (tank scope, the node's HRT) — D-L1's
572 // gather/scatter through the shared integrator. Pollutant
573 // kdecay is NOT re-applied here: stage 4 owns it (the
574 // integrator handles only MSX kinetics), so there is no
575 // double-decay the way the legacy binding had to avoid.
576 if (L.msx_first >= 0) {
577 auto& rx = ctx.reactions;
578 const int nsp = ns - L.msx_first;
579 if (static_cast<int>(msx_block_.size()) < nsp)
580 msx_block_.assign(static_cast<std::size_t>(nsp), 0.0);
581 if (static_cast<int>(msx_poll_.size()) < np && np > 0)
582 msx_poll_.assign(static_cast<std::size_t>(np), 0.0);
583 for (int l = 0; l < nl; ++l) {
584 const auto ul = static_cast<std::size_t>(l);
585 if (links.type[ul] != LinkType::CONDUIT) continue;
586 const int cnt = store_.count(l);
587 for (int i = 0; i < cnt; ++i) {
588 for (int sp = 0; sp < nsp; ++sp)
589 msx_block_[static_cast<std::size_t>(sp)] =
590 store_.seg_conc(l, i, L.msx_first + sp);
591 for (int p = 0; p < np; ++p)
592 msx_poll_[static_cast<std::size_t>(p)] =
593 store_.seg_conc(l, i, p);
594 // TEMP: the segment's own temperature row when heat
595 // rides the store (H7b); NaN defers to the
596 // [REACTION_OPTIONS] TEMPERATURE constant.
597 const double seg_temp_c =
598 (L.temp_row >= 0)
599 ? store_.seg_conc(l, i, L.temp_row)
600 : std::numeric_limits<double>::quiet_NaN();
602 ctx, /*tank=*/false, dt, msx_block_.data(),
603 np > 0 ? msx_poll_.data() : nullptr, 0.0,
604 seg_temp_c);
605 for (int sp = 0; sp < nsp; ++sp)
606 store_.set_seg_conc(l, i, L.msx_first + sp,
607 msx_block_[
608 static_cast<std::size_t>(
609 sp)]);
610 }
611 }
612 for (int n = 0; n < nn; ++n) {
613 const auto un = static_cast<std::size_t>(n);
614 for (int sp = 0; sp < nsp; ++sp)
615 msx_block_[static_cast<std::size_t>(sp)] =
616 rx.msx_node_conc[un * static_cast<std::size_t>(
617 nsp) +
618 static_cast<std::size_t>(sp)];
619 for (int p = 0; p < np; ++p)
620 msx_poll_[static_cast<std::size_t>(p)] =
621 nodes.conc[un * static_cast<std::size_t>(np) +
622 static_cast<std::size_t>(p)];
623 const double hrt =
624 (un < nodes.hrt.size()) ? nodes.hrt[un] : 0.0;
625 const double node_temp_c =
626 (L.temp_row >= 0 &&
627 un < ctx.heat_state.node_temp.size())
628 ? ctx.heat_state.node_temp[un]
629 : std::numeric_limits<double>::quiet_NaN();
631 ctx, /*tank=*/true, dt, msx_block_.data(),
632 np > 0 ? msx_poll_.data() : nullptr, hrt,
633 node_temp_c);
634 for (int sp = 0; sp < nsp; ++sp)
635 rx.msx_node_conc[un * static_cast<std::size_t>(nsp) +
636 static_cast<std::size_t>(sp)] =
637 msx_block_[static_cast<std::size_t>(sp)];
638 }
639 }
640 }
641
644 void publish(SimulationContext& ctx, double dt_routing) {
645 // H7b: the fourth layout-aware site joins rowLayout() (H7a
646 // converted step/substep/init and flagged this one).
647 const SpeciesRowLayout L = rowLayout(ctx);
648 const int np = L.np;
649 const bool age = (L.age_row >= 0);
650 const bool heat = (L.temp_row >= 0);
651 const int nl = ctx.n_links();
652 auto& nodes = ctx.nodes;
653 auto& links = ctx.links;
654 auto& ws = ctx.water_age_state;
655 auto& hs = ctx.heat_state;
656
657 // ---- 5. PUBLISH ---------------------------------------------------
658 for (int l = 0; l < nl; ++l) {
659 const auto ul = static_cast<std::size_t>(l);
660 if (links.type[ul] != LinkType::CONDUIT) continue;
661 store_.mean_conc(l, scratch_.data());
662 for (int p = 0; p < np; ++p)
663 links.conc[ul * static_cast<std::size_t>(np) +
664 static_cast<std::size_t>(p)] =
665 scratch_[static_cast<std::size_t>(p)];
666 // Age: volume-weighted mean over the same segments, seconds.
667 // The dry-element report mask (A2b) is at the report boundary,
668 // engine-side — state keeps aging here regardless. An EMPTY
669 // slab holds no parcels to average, so the link's held age
670 // ages in place instead of resetting to 0 — the state/report
671 // separation the dry-mask round established (a dry element's
672 // STATE keeps aging; only the report masks it).
673 if (age) {
674 if (store_.count(l) > 0)
675 ws.link_age[ul] =
676 scratch_[static_cast<std::size_t>(L.age_row)];
677 else
678 ws.link_age[ul] += dt_routing;
679 }
680 // Temperature: volume-weighted mean over the same segments. An
681 // EMPTY slab HOLDS its temperature — unlike age it does not
682 // grow, and unlike the age report it is not masked when dry
683 // (0 degC is an ordinary temperature; the no-mask call is
684 // documented at the snapshot builder).
685 if (heat) {
686 if (store_.count(l) > 0)
687 hs.link_temp[ul] =
688 scratch_[static_cast<std::size_t>(L.temp_row)];
689 }
690 // L3: species rows publish like temperature — an EMPTY
691 // slab HOLDS its concentration.
692 if (L.msx_first >= 0 && store_.count(l) > 0) {
693 const int nsp = L.ns - L.msx_first;
694 for (int sp = 0; sp < nsp; ++sp)
696 ul * static_cast<std::size_t>(nsp) +
697 static_cast<std::size_t>(sp)] =
698 scratch_[static_cast<std::size_t>(
699 L.msx_first + sp)];
700 }
701 }
702 // conc_old bookkeeping matches the ARD/legacy convention.
703 links.conc_old = links.conc;
704 nodes.conc_old = nodes.conc;
705 }
706
707private:
730 void applyFluxesAndBed(SimulationContext& ctx, double dt) {
731 const SpeciesRowLayout L = rowLayout(ctx);
732 const bool heat_on = (L.temp_row >= 0);
733 const bool bed_on = transport::heat::bedExchangeEnabled(ctx);
734 const bool flux_on =
735 heat_on && (ctx.heat_config.surface_exchange ||
737 if ((!flux_on && !bed_on) || !(dt > 0.0)) return;
738 namespace th = transport::heat;
739
740 if (flux_on) th::updateSolarForcing(ctx);
741 if (bed_on) th::seedBedTemperature(ctx);
742 const double t_gr = bed_on ? th::groundTemperature(ctx) : 0.0;
743 const double rho = ctx.options.water_density;
744 const double cp = ctx.options.water_specific_heat;
745 constexpr double kSqFt = 0.09290304;
746 constexpr double kCuFt = 0.028316846592;
747
748 auto& hs = ctx.heat_state;
749 const int nl = ctx.n_links();
750 const int nn = ctx.n_nodes();
751
752 // ---- Nodes: storage surfaces only, HeatFluxes.cpp's convention. --
753 if (flux_on) {
754 const int unit_sys = ucf::getUnitSystem(
755 static_cast<int>(ctx.options.flow_units));
756 for (int n = 0; n < nn; ++n) {
757 const auto un = static_cast<std::size_t>(n);
758 if (un >= hs.node_temp.size()) break;
759 const double vol = ctx.nodes.volume[un];
760 if (!(vol > 0.0)) continue;
761 const double a = node::getSurfArea(
762 ctx.nodes, n, ctx.nodes.depth[un], &ctx.tables,
763 unit_sys, &ctx.node_subtypes);
764 if (!(a > 0.0)) continue;
765 const double t = hs.node_temp[un];
766 const HeatElement ne = HeatElement::node(n);
767 hs.node_temp[un] += th::relaxT(
768 th::netFluxOut(ctx, ne, t),
769 th::netFluxOut(ctx, ne, t + th::kProbeC), th::kProbeC,
770 a * kSqFt, vol * kCuFt, dt, rho, cp);
771 }
772 }
773
774 // ---- Links: segment field, uniform increments. -------------------
775 // Bed rows: pollutants then MSX — every row before age/temp.
776 const int n_bed = L.np + ((L.msx_first >= 0) ? (L.ns - L.msx_first)
777 : 0);
778 auto& bed = ctx.bed_state;
779 if (bed_on && n_bed > 0 &&
780 (bed.n_species != n_bed ||
781 bed.link_conc.size() != static_cast<std::size_t>(nl) *
782 static_cast<std::size_t>(n_bed))) {
783 bed.link_conc.assign(static_cast<std::size_t>(nl) *
784 static_cast<std::size_t>(n_bed),
785 0.0);
786 bed.n_species = n_bed;
787 }
788
789 for (int l = 0; l < nl; ++l) {
790 const auto ul = static_cast<std::size_t>(l);
791 if (ctx.links.type[ul] != LinkType::CONDUIT) continue;
792 const int cnt = store_.count(l);
793 if (cnt <= 0) continue;
794 const double vol_ft3 = store_.total_volume(l);
795 if (!(vol_ft3 > 0.0)) continue;
796
797 store_.mean_conc(l, scratch_.data());
798 // PE1: every parcel of a conduit shares the conduit's
799 // attributes (D-PE1) — the bed and the shading belong to the
800 // pipe, not to the water passing through it.
801 const HeatElement le = HeatElement::link(l);
802
803 // Heat: one pair/relaxation against the mean, uniform dT.
804 if (heat_on) {
805 const double t_mean =
806 scratch_[static_cast<std::size_t>(L.temp_row)];
807 const double surf_m2 =
808 flux_on ? th::linkFreeSurfaceFt2(ctx, l) * kSqFt : 0.0;
809 double d_tw = 0.0;
810 bool bed_stepped = false;
811 if (bed_on && ul < bed.link_temp.size()) {
812 // PE2: this link's own bed material and boundary.
813 const auto& sd = th::sedimentFor(ctx, le);
814 const th::BedCoupling g = th::bedCouplingFromContact(
815 ctx, sd, th::linkBedAreaM2(ctx, l), vol_ft3,
816 th::groundTempFor(ctx, sd, t_gr));
817 if (g.viable()) {
818 const th::PairStep ps = th::relaxPair(
819 g, t_mean, bed.link_temp[ul],
820 flux_on ? th::netFluxOut(ctx, le, t_mean) : 0.0,
821 flux_on ? th::netFluxOut(ctx, le,
822 t_mean + th::kProbeC)
823 : 0.0,
824 flux_on ? th::kProbeC : 0.0, surf_m2, dt);
825 d_tw = ps.dt_w;
826 bed.link_temp[ul] += ps.dt_b;
827 bed_stepped = true;
828 }
829 }
830 if (!bed_stepped && flux_on && surf_m2 > 0.0) {
831 d_tw = th::relaxT(
832 th::netFluxOut(ctx, le, t_mean),
833 th::netFluxOut(ctx, le, t_mean + th::kProbeC),
834 th::kProbeC, surf_m2, vol_ft3 * kCuFt, dt, rho, cp);
835 }
836 if (d_tw != 0.0)
837 for (int i = 0; i < cnt; ++i)
838 store_.set_seg_conc(
839 l, i, L.temp_row,
840 store_.seg_conc(l, i, L.temp_row) + d_tw);
841 }
842
843 // Solutes: pair against the mean, uniform dc, per bed row.
844 if (bed_on && n_bed > 0) {
845 // PE2: same per-link config the heat pair above used, so the
846 // two halves of one bed cannot disagree about its material.
847 const auto& sd = th::sedimentFor(ctx, le);
848 const double bed_m2 = th::linkBedAreaM2(ctx, l);
849 const double vol_b = bed_m2 * sd.bed_thickness;
850 const double q_exch = th::bedExchangeQ(sd, bed_m2);
851 if (q_exch > 0.0 && vol_b > 0.0) {
852 for (int b = 0; b < n_bed; ++b) {
853 // Bed row b: pollutants 0..np-1 map directly, MSX
854 // rows at msx_first + (b - np).
855 const int row = (b < L.np)
856 ? b
857 : L.msx_first + (b - L.np);
858 const double c_mean =
859 scratch_[static_cast<std::size_t>(row)];
860 double& cb = bed.link_conc[
861 ul * static_cast<std::size_t>(n_bed) +
862 static_cast<std::size_t>(b)];
863 const th::SolutePairStep st = th::exchangePair(
864 c_mean, cb, vol_ft3 * kCuFt, vol_b, q_exch,
865 dt);
866 if (st.dc_w != 0.0)
867 for (int i = 0; i < cnt; ++i)
868 store_.set_seg_conc(
869 l, i, row,
870 store_.seg_conc(l, i, row) + st.dc_w);
871 cb += st.dc_b;
872 }
873 }
874 }
875 }
876 }
877
878 void init(SimulationContext& ctx) {
879
880 const SpeciesRowLayout L = rowLayout(ctx);
881 const int np = L.np;
882 const int ns = L.ns;
883 // Derived from the layout rather than re-read from options, so the
884 // "is there a reserved row" question has exactly one answer per call.
885 const bool age = (L.age_row >= 0);
886 const bool heat = (L.temp_row >= 0);
887 const int nn = ctx.n_nodes();
888 const int nl = ctx.n_links();
889 // X3a: slab capacity from [OPTIONS] MAX_SEGMENTS_PER_LINK, floored
890 // at 2 (one segment to hold, one to receive).
891 store_.resize(nl, ns,
892 std::max(2, ctx.options.max_segments_per_link));
893 flow_sign_.assign(static_cast<std::size_t>(nl), 1);
894 node_mass_in_.assign(
895 static_cast<std::size_t>(nn) * static_cast<std::size_t>(ns), 0.0);
896 node_vol_in_.assign(static_cast<std::size_t>(nn), 0.0);
897 scratch_.assign(static_cast<std::size_t>(ns), 0.0);
898 treat_cin_.assign(static_cast<std::size_t>(np > 0 ? np : 0), 0.0);
899 node_out_links_.assign(static_cast<std::size_t>(nn), {});
900 release_vol_.assign(static_cast<std::size_t>(nl), 0.0);
901 if (ctx.options.lard_rwpt) rwpt_.resize(nl);
902
903 // L3: size + seed the shared MSX element state (GLOBAL fill +
904 // [REACTION_QUALITY] overrides) with the SAME spelling the
905 // LEGACY dispatch uses, then let the seeded link values seed
906 // the segments below like every other row.
907 if (L.msx_first >= 0) transport::ensureMsxState(ctx);
908
909 // X4 age seeding, the ARD precedent: a hotstart-loaded state wins
910 // (node_age/link_age already carry the restored values, A2a);
911 // otherwise a configured INITIAL_STATE age fills the network.
912 // Restored/seeded link ages then seed the segments below — the
913 // same within-link profile collapse A2a recorded for ARD
914 // (lesson 37): continuous, not bit-continuous.
915 if (age) {
916 auto& ws = ctx.water_age_state;
917 if (ws.node_age.size() != static_cast<std::size_t>(nn))
918 ws.resize(nn, nl, ctx.n_subcatches());
919 if (!ws.hotstart_loaded) {
920 const double a0 = ctx.water_age_config.global_age[
921 static_cast<int>(WaterAgeSource::INITIAL_STATE)];
922 if (a0 > 0.0) {
923 std::fill(ws.node_age.begin(), ws.node_age.end(), a0);
924 std::fill(ws.link_age.begin(), ws.link_age.end(), a0);
925 }
926 }
927 // E-A3: [INITIAL_QUALITY] __WATER_AGE__ rows override the
928 // global fill; the helper no-ops under hotstart (D-IQ7). The
929 // per-link values then seed the segments below.
931 }
932
933 // H7b temperature seeding, the routeLegacyHeat convention: size the
934 // state if the loaders have not already, fill with the configured
935 // INITIAL_STATE once (legacy_seeded — shared with the LEGACY mirror
936 // so a fallback path never re-seeds), then let per-element
937 // [INITIAL_QUALITY] __TEMPERATURE__ rows override. The seeded link
938 // temperatures seed the segments below, the same within-link
939 // profile collapse the age row records (lesson 37): continuous,
940 // not bit-continuous. Hotstart does NOT restore temperature — no
941 // engine's does, the record has no field for it (owed; see the
942 // H7b round record) — so a restarted run re-seeds from
943 // INITIAL_STATE exactly as LEGACY and ARD do.
944 if (heat) {
945 auto& hstate = ctx.heat_state;
946 const double t0 = ctx.heat_config.global_temp[
947 static_cast<int>(HeatSource::INITIAL_STATE)];
948 if (hstate.node_temp.size() != static_cast<std::size_t>(nn))
949 hstate.resize(nn, nl, t0);
950 if (!hstate.legacy_seeded) {
951 std::fill(hstate.node_temp.begin(), hstate.node_temp.end(),
952 t0);
953 std::fill(hstate.link_temp.begin(), hstate.link_temp.end(),
954 t0);
956 hstate.legacy_seeded = true;
957 }
958 }
959
960 // Seed: one segment per conduit at the link's current volume and
961 // (initQuality-seeded) concentration — a dry link seeds nothing.
962 for (int l = 0; l < nl; ++l) {
963 const auto ul = static_cast<std::size_t>(l);
964 store_.clear_link(l);
965 if (ctx.links.type[ul] != LinkType::CONDUIT) continue;
966 const double v = ctx.links.volume[ul];
967 if (v <= 0.0) continue;
968 for (int p = 0; p < np; ++p)
969 scratch_[static_cast<std::size_t>(p)] =
970 ctx.links.conc[ul * static_cast<std::size_t>(np) +
971 static_cast<std::size_t>(p)];
972 if (age)
973 scratch_[static_cast<std::size_t>(L.age_row)] =
974 ctx.water_age_state.link_age[ul];
975 if (heat)
976 scratch_[static_cast<std::size_t>(L.temp_row)] =
977 ctx.heat_state.link_temp[ul];
978 if (L.msx_first >= 0) { // L3
979 const int nsp = ns - L.msx_first;
980 for (int sp = 0; sp < nsp; ++sp)
981 scratch_[static_cast<std::size_t>(
982 L.msx_first + sp)] =
983 ctx.reactions.msx_link_conc[
984 ul * static_cast<std::size_t>(nsp) +
985 static_cast<std::size_t>(sp)];
986 }
987 store_.push_front(l, v, scratch_.data());
988 flow_sign_[ul] = (ctx.links.flow[ul] >= 0.0) ? 1 : -1;
989 }
990 computeTopoOrder(ctx);
991 initialized_ = true;
992 }
993
994 int upstreamNode(const SimulationContext& ctx, int l) const {
995 const auto ul = static_cast<std::size_t>(l);
996 return (flow_sign_[ul] >= 0) ? ctx.links.node1[ul]
997 : ctx.links.node2[ul];
998 }
999 int downstreamNode(const SimulationContext& ctx, int l) const {
1000 const auto ul = static_cast<std::size_t>(l);
1001 return (flow_sign_[ul] >= 0) ? ctx.links.node2[ul]
1002 : ctx.links.node1[ul];
1003 }
1004
1005 void addToLedger(int n, double vol, const double* mass, int np) {
1006 const auto un = static_cast<std::size_t>(n);
1007 node_vol_in_[un] += vol;
1008 for (int p = 0; p < np; ++p)
1009 node_mass_in_[un * static_cast<std::size_t>(np) +
1010 static_cast<std::size_t>(p)] +=
1011 mass[static_cast<std::size_t>(p)];
1012 }
1014 void addToLedgerRate(int n, double vol, const double* conc, int np) {
1015 const auto un = static_cast<std::size_t>(n);
1016 node_vol_in_[un] += vol;
1017 for (int p = 0; p < np; ++p)
1018 node_mass_in_[un * static_cast<std::size_t>(np) +
1019 static_cast<std::size_t>(p)] +=
1020 vol * conc[static_cast<std::size_t>(p)];
1021 }
1022
1027 void computeTopoOrder(SimulationContext& ctx) {
1028 const int nn = ctx.n_nodes();
1029 const int nl = ctx.n_links();
1030 std::vector<int> indeg(static_cast<std::size_t>(nn), 0);
1031 for (auto& v : node_out_links_) v.clear();
1032 for (int l = 0; l < nl; ++l) {
1033 const auto ul = static_cast<std::size_t>(l);
1034 const int up = upstreamNode(ctx, l);
1035 const int dn = downstreamNode(ctx, l);
1036 if (up < 0 || dn < 0) continue;
1037 node_out_links_[static_cast<std::size_t>(up)].push_back(l);
1038 if (std::abs(ctx.links.flow[ul]) > kTinyFlow)
1039 indeg[static_cast<std::size_t>(dn)] += 1;
1040 }
1041 topo_.clear();
1042 topo_.reserve(static_cast<std::size_t>(nn));
1043 std::vector<int> q;
1044 for (int n = 0; n < nn; ++n)
1045 if (indeg[static_cast<std::size_t>(n)] == 0) q.push_back(n);
1046 std::vector<char> seen(static_cast<std::size_t>(nn), 0);
1047 std::size_t qi = 0;
1048 while (qi < q.size()) {
1049 const int n = q[qi++];
1050 if (seen[static_cast<std::size_t>(n)]) continue;
1051 seen[static_cast<std::size_t>(n)] = 1;
1052 topo_.push_back(n);
1053 for (const int l : node_out_links_[static_cast<std::size_t>(n)]) {
1054 if (std::abs(ctx.links.flow[static_cast<std::size_t>(l)]) <=
1055 kTinyFlow)
1056 continue;
1057 const int dn = downstreamNode(ctx, l);
1058 if (dn >= 0 && --indeg[static_cast<std::size_t>(dn)] == 0)
1059 q.push_back(dn);
1060 }
1061 }
1062 for (int n = 0; n < nn; ++n) // cycle leftovers
1063 if (!seen[static_cast<std::size_t>(n)]) topo_.push_back(n);
1064 }
1065
1066 SegmentStore store_;
1067 RwptDispersion rwpt_;
1068 std::vector<double> release_vol_;
1069 std::uint64_t substep_counter_ = 0;
1070 std::vector<int> topo_;
1071 std::vector<std::vector<int>> node_out_links_;
1072 std::vector<std::int8_t> flow_sign_;
1073 std::vector<double> node_mass_in_;
1074 std::vector<double> node_vol_in_;
1075 std::vector<double> scratch_;
1079 std::vector<double> treat_cin_;
1080 std::vector<double> msx_block_;
1081 std::vector<double> msx_poll_;
1082 bool initialized_ = false;
1083};
1084
1085} // namespace lard
1086} // namespace openswmm
1087
1088#endif // OPENSWMM_QUALITY_LARD_LAGRANGIAN_SOLVER_HPP
Plan H6b — bed conduction, deep-ground conduction, and hyporheic exchange, integrated SIMULTANEOUSLY ...
Plan D-H5e — the single node/link surface-flux binding.
Plan PE — resolve per-element attributes, and read them.
[INITIAL_QUALITY] reserved-species (WATER_AGE/__TEMPERATURE__) helpers for the per-engine seed sites ...
D-NS1: one clamp-bookkeeping seam for all three quality engines.
Node hydraulics — volume/depth/head conversions, surface area, overflow.
Water quality routing — constituent transport, mixing, decay.
Phase R4 — reaction binding for the LEGACY (CSTR) quality engine.
X3b: RWPT longitudinal dispersion on LARD segments.
LARD segment slabs — per-link ring buffers of plug-flow segments.
The central, reentrant simulation context for the new engine.
Phase H6a — where incoming shortwave Jin comes from (heat plan §2.5, D-H6a).
Phase H2 — latent and sensible heat exchange at the water surface (heat plan §2.1; CSHComponent §4....
The Domain × Species-class transport contract (OPT plan §5, E2).
Global unit conversion factors — matching legacy SWMM Ucf[]/Qcf[].
Definition LagrangianSolver.hpp:187
void publish(SimulationContext &ctx, double dt_routing)
Definition LagrangianSolver.hpp:644
void step(SimulationContext &ctx, double dt_routing)
One routing step of LTD transport. Lazily initializes on the first call (needs router-set volumes,...
Definition LagrangianSolver.hpp:193
void substep(SimulationContext &ctx, double dt, double frac)
One LTD substep: AGE → DRAIN → MIX(+passthrough) → RELEASE → DECAY.
Definition LagrangianSolver.hpp:252
Definition LagrangianSolver.hpp:122
double rwpt_hyd_radius(double area, double depth, double diam, bool circular)
Definition RwptDispersion.hpp:156
constexpr double kTinyFlow
cfs; below this a link moves nothing
Definition LagrangianSolver.hpp:124
SpeciesRowLayout rowLayout(const SimulationContext &ctx)
Definition LagrangianSolver.hpp:163
double getSurfArea(const NodeData &nodes, int idx, double depth, TableData *tables, int unit_sys, const NodeSubtypes *subs)
Compute surface area at a given depth for a single node.
Definition Node.cpp:272
void applyNodeTreatment(SimulationContext &ctx, int j, double dt, double q_raw, const double *cin)
One node's [TREATMENT] application, against the CALLER's inflow figures — the seam the LEGACY pass an...
Definition QualityRouting.cpp:1102
void bookNegativeAgeClamp(SimulationContext &ctx, int node)
Definition NegativeSources.hpp:83
void bookNegativeSourceClamp(SimulationContext &ctx, int node, int p, double shortfall)
Book one pollutant-row clamp: count it and un-book the shortfall from the external-extraction ledger ...
Definition NegativeSources.hpp:60
Definition BedExchange.cpp:37
bool bedExchangeEnabled(const SimulationContext &ctx) noexcept
True when [HEAT_FLUXES] SEDIMENT_EXCHANGE is on and heat transport is.
Definition BedExchange.cpp:176
ClassEnables network1DEnables(const SimulationContext &ctx) noexcept
1D network enables: LEGACY / ARD / LARD all size from this.
Definition TransportPolicy.cpp:78
void ensureMsxState(SimulationContext &ctx)
Definition ReactionLegacyBinding.cpp:65
void applyInitialTempOverrides(SimulationContext &ctx)
Apply TEMPERATURE rows onto heat_state.node_temp/link_temp (degC). No-op on unsized arrays.
Definition InitialQualitySeeds.hpp:109
void applyInitialAgeOverrides(SimulationContext &ctx)
Apply WATER_AGE rows onto water_age_state.node_age/link_age (hours -> seconds). No-op when a hotstart...
Definition InitialQualitySeeds.hpp:90
void reactSpeciesBlock(SimulationContext &ctx, bool tank, double dt, double *species_block, const double *pollut, double hrt_seconds, double temp_c)
Definition ReactionLegacyBinding.cpp:195
bool legacyReactionsActive(const SimulationContext &ctx)
Definition ReactionLegacyBinding.cpp:224
Counters g
Definition KokkosPerfCounters.hpp:90
int getUnitSystem(int flow_units)
Determine unit system (0=US, 1=SI) from flow units.
Definition UnitConversion.cpp:33
Definition NodeCoupling.cpp:16
@ CONDUIT
Definition LinkData.hpp:59
@ OUTFALL
Definition NodeData.hpp:61
@ INITIAL_STATE
water in the network at t = 0
Definition WaterAgeData.hpp:59
@ CIRCULAR
Definition LinkData.hpp:71
@ INITIAL_STATE
water in the network at t = 0
Definition HeatData.hpp:79
bool radiative_exchange
Definition HeatData.hpp:323
bool surface_exchange
[HEAT_FLUXES] SURFACE_EXCHANGE ON — latent + sensible exchange at the free surface (plan §2....
Definition HeatData.hpp:338
Which element a flux evaluator is being called for.
Definition HeatOverrideData.hpp:96
static HeatElement link(int i)
Definition HeatOverrideData.hpp:101
static HeatElement node(int i)
Definition HeatOverrideData.hpp:100
std::vector< double > node_temp
[node], °C
Definition HeatData.hpp:423
std::vector< double > depth
Current water depth above invert (project length units).
Definition NodeData.hpp:227
std::vector< double > volume
Current water volume (project volume units).
Definition NodeData.hpp:239
std::vector< double > k_decay
First-order decay coefficient (1/sec). The [POLLUTANTS] Kdecay column is 1/day: the parser divides by...
Definition PollutantData.hpp:96
std::vector< double > msx_ext_mass_in
Definition ReactionData.hpp:157
std::vector< double > msx_node_conc
Definition ReactionData.hpp:146
std::vector< double > msx_link_conc
Definition ReactionData.hpp:147
std::vector< double > qual_routing_reacted
Quality mass lost to decay.
Definition SimulationContext.hpp:1151
Central, reentrant simulation context.
Definition SimulationContext.hpp:353
struct openswmm::SimulationContext::MassBalance mass_balance
LinkSubtypes link_subtypes
Relational per-subtype link side-tables (Phase 6) — the link analogue of node_subtypes....
Definition SimulationContext.hpp:521
ReactionData reactions
Multispecies reaction system (EPANET-MSX conventions), parsed from the reactions component's config f...
Definition SimulationContext.hpp:557
int n_nodes() const noexcept
Number of nodes.
Definition SimulationContext.hpp:1868
NodeData nodes
All node state and properties.
Definition SimulationContext.hpp:496
TableData tables
All time series and rating curves.
Definition SimulationContext.hpp:629
BedZoneState bed_state
The bed / hyporheic transient-storage zone (phase H6b).
Definition SimulationContext.hpp:601
NodeSubtypes node_subtypes
Relational side-tables for node subtypes (storage/outfall/divider).
Definition SimulationContext.hpp:507
WaterAgeState water_age_state
Definition SimulationContext.hpp:573
TreatmentData treatment
Definition SimulationContext.hpp:679
LinkData links
All link state and properties.
Definition SimulationContext.hpp:513
int n_links() const noexcept
Number of links.
Definition SimulationContext.hpp:1871
HeatState heat_state
Definition SimulationContext.hpp:589
HeatConfigData heat_config
Heat transport (phase H1): per-source inlet temperatures parsed from the heat component (model....
Definition SimulationContext.hpp:588
SimulationOptions options
Parsed simulation options (from [OPTIONS] section).
Definition SimulationContext.hpp:398
PollutantData pollutants
Pollutant definitions and per-object quality state.
Definition SimulationContext.hpp:539
double water_density
Water density, kg/m³ ([OPTIONS] WATER_DENSITY; CSH Table 4.1).
Definition SimulationOptions.hpp:369
bool outfall_backflow_zero
[OPTIONS] OUTFALL_BACKFLOW_QUALITY LAST|ZERO — quality carried by reverse flow at outfalls (false = L...
Definition SimulationOptions.hpp:319
FlowUnits flow_units
Flow units system.
Definition SimulationOptions.hpp:232
int rwpt_seed
[OPTIONS] RWPT_SEED — deterministic counter-RNG seed (D-L6). Same seed ⇒ bit-identical runs at any th...
Definition SimulationOptions.hpp:287
double water_specific_heat
Water specific heat capacity, J/kg/°C (WATER_SPECIFIC_HEAT_CAPACITY).
Definition SimulationOptions.hpp:372
bool lard_rwpt
[OPTIONS] DISPERSION RWPT|OFF — LARD RWPT dispersion (X3b; strategy §5; the GUI plan's Lagrangian-gro...
Definition SimulationOptions.hpp:281
double quality_step
[OPTIONS] QUALITY_STEP — transport substep, seconds (HH:MM:SS or seconds; 0 = follow ROUTING_STEP).
Definition SimulationOptions.hpp:261
std::vector< bool > has_treatment
Per-node flag: true if any pollutant has a treatment expression.
Definition QualityData.hpp:160
The segment store's species-row layout, computed in ONE place.
Definition LagrangianSolver.hpp:151
int np
pollutant rows occupy [0, np)
Definition LagrangianSolver.hpp:152
int temp_row
temperature row index, or -1 (H7b)
Definition LagrangianSolver.hpp:154
int ns
total rows the store carries
Definition LagrangianSolver.hpp:157
int age_row
water-age row index, or -1 when absent
Definition LagrangianSolver.hpp:153
int msx_first
Definition LagrangianSolver.hpp:155
Allocation-free class enables for one domain — what the engines size from. n_msx is the reactions com...
Definition TransportPolicy.hpp:102
bool temperature
Definition TransportPolicy.hpp:106
bool age
Definition TransportPolicy.hpp:105
int n_pollut
Definition TransportPolicy.hpp:103
int n_msx
Definition TransportPolicy.hpp:104