OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
RwptDispersion.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
76
77#ifndef OPENSWMM_QUALITY_LARD_RWPT_DISPERSION_HPP
78#define OPENSWMM_QUALITY_LARD_RWPT_DISPERSION_HPP
80
81#include <algorithm>
82#include <cmath>
83#include <cstdint>
84#include <vector>
85
87#include "SegmentStore.hpp"
88
89namespace openswmm {
90namespace lard {
91
92constexpr int kRwptParticlesPerLink = 2000;
93constexpr double kKappa = 0.41;
94constexpr double kNuWater = 1.05e-5;
95constexpr double kDm = 1.3454e-8;
96constexpr double kEtaMin = 1.0e-3;
97constexpr double kReTurb = 2000.0;
98
99// ---------------------------------------------------------------------------
100// D-L6 counter-based RNG: pure function of the key, no state anywhere.
101// ---------------------------------------------------------------------------
107inline std::uint64_t rwpt_hash(std::uint64_t z) {
108 z += 0x9e3779b97f4a7c15ULL;
109 z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL;
110 z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL;
111 return z ^ (z >> 31);
112}
113
127inline double rwpt_uniform(std::uint64_t seed, std::uint64_t link, std::uint64_t stepc,
128 std::uint64_t particle, std::uint64_t draw) {
129 std::uint64_t k = rwpt_hash(seed ^ rwpt_hash(link));
130 k = rwpt_hash(k ^ rwpt_hash(stepc));
131 k = rwpt_hash(k ^ rwpt_hash(particle * 8ULL + draw));
132 // 53-bit mantissa, strictly inside (0,1).
133 return (static_cast<double>(k >> 11) + 0.5) * (1.0 / 9007199254740992.0);
134}
135
148inline double rwpt_normal(std::uint64_t seed, std::uint64_t link, std::uint64_t stepc,
149 std::uint64_t particle, std::uint64_t draw) {
150 const double u1 = rwpt_uniform(seed, link, stepc, particle, draw);
151 const double u2 = rwpt_uniform(seed, link, stepc, particle, draw + 1);
152 return std::sqrt(-2.0 * std::log(u1)) * std::cos(6.283185307179586 * u2);
153}
154
155// ---------------------------------------------------------------------------
156// Profile kernels — exposed free functions so the unit gates can pin the
157// math without an engine run.
158// ---------------------------------------------------------------------------
170inline double rwpt_u_dev(double eta, double ubar, double ustar, bool turbulent) {
171 // The kEtaMin floor belongs to the LOG-LAW only (ln 0 = -inf); clamping
172 // the laminar parabola too biased its mean by ~1.5e-6*ubar — gate 4's
173 // mean-free quadrature caught it (X3b round).
174 if (turbulent)
175 return (ustar / kKappa) * (1.0 + std::log(std::max(eta, kEtaMin)));
176 return 1.5 * ubar * eta * (2.0 - eta) - ubar; // parabola, mean-free
177}
178
191inline double rwpt_d_eta(double eta, double h, double ustar, bool turbulent) {
192 if (!turbulent)
193 return kDm / (h * h);
194 const double e = std::min(std::max(eta, 0.0), 1.0);
195 return kKappa * ustar * e * (1.0 - e) / h; // (κ u* h η(1-η))/h²
196}
197
207inline double rwpt_d_eta_grad(double eta, double h, double ustar, bool turbulent) {
208 if (!turbulent)
209 return 0.0;
210 return kKappa * ustar * (1.0 - 2.0 * eta) / h;
211}
212
226inline double rwpt_hyd_radius(double area, double depth, double diam, bool circular) {
227 if (!circular || diam <= 0.0 || depth <= 0.0)
228 return std::max(depth, 0.0);
229 const double ratio = std::min(std::max(1.0 - 2.0 * depth / diam, -1.0), 1.0);
230 const double theta = 2.0 * std::acos(ratio);
231 if (theta <= 1.0e-6)
232 return std::max(depth, 0.0);
233 const double a = diam * diam / 8.0 * (theta - std::sin(theta));
234 const double p = diam * theta / 2.0;
235 (void)area;
236 return (p > 0.0) ? a / p : std::max(depth, 0.0);
237}
238
243public:
256 a.fixed(zeta_, eta_, seg_idx_);
257 for (double v : zeta_)
258 if (!std::isfinite(v))
259 throw std::runtime_error("Invalid LARD particle position.");
260 for (double v : eta_)
261 if (!std::isfinite(v) || v < 0 || v > 1)
262 throw std::runtime_error("Invalid LARD particle ordinate.");
263 }
264
273 void resize(int n_links, int n_per_link = kRwptParticlesPerLink) {
274 nl_ = n_links;
275 npart_ = n_per_link;
276 const std::size_t total = static_cast<std::size_t>(nl_) * static_cast<std::size_t>(npart_);
277 zeta_.assign(total, -1.0); // <0 marks "not yet seeded"
278 eta_.assign(total, 0.5);
279 seg_idx_.assign(total, 0);
280 }
281
321 void disperse(SimulationContext& ctx, SegmentStore& segs, int l, double ubar, double h,
322 double rh, double n_manning, double v_in, double q_dt, double dt,
323 std::uint64_t stepc, std::uint64_t seed, std::vector<double>& delta) {
324 (void)ctx;
325 const int nseg = segs.count(l);
326 if (nseg < 2 || h <= 1.0e-3 || ubar <= 0.0)
327 return;
328 const double vol = segs.total_volume(l);
329 if (vol <= 0.0)
330 return;
331 const int ns = segs.species();
332
333 // Friction: Manning S_f at the link's own n → u*. Regime by Re.
334 const double sf = (n_manning * ubar) * (n_manning * ubar) /
335 (2.208 * std::pow(std::max(rh, 1.0e-3), 4.0 / 3.0));
336 const double ustar = std::sqrt(32.174 * std::max(rh, 1.0e-3) * sf);
337 const bool turbulent = (ubar * 4.0 * rh / kNuWater) >= kReTurb;
338
339 // Segment boundaries in volume coordinates, front → back.
340 bounds_.assign(static_cast<std::size_t>(nseg) + 1, 0.0);
341 for (int j = 0; j < nseg; ++j)
342 bounds_[static_cast<std::size_t>(j) + 1] =
343 bounds_[static_cast<std::size_t>(j)] + segs.seg_volume(l, j);
344
345 const std::size_t base = static_cast<std::size_t>(l) * static_cast<std::size_t>(npart_);
346 for (int p = 0; p < npart_; ++p) {
347 const std::size_t ip = base + static_cast<std::size_t>(p);
348 double z = zeta_[ip];
349 double e = eta_[ip];
350
351 // Seed / recycle: unseeded particles spread evenly; particles
352 // that rode out the back are new water entering at the front.
353 if (z < 0.0) {
354 z = vol * (static_cast<double>(p) + 0.5) / static_cast<double>(npart_);
355 e = rwpt_uniform(seed, static_cast<std::uint64_t>(l), stepc,
356 static_cast<std::uint64_t>(p), 7);
357 } else {
358 z += v_in; // ride the bulk advection (volume coordinates)
359 if (z >= vol) {
360 z = std::min(v_in, vol * rwpt_uniform(seed, static_cast<std::uint64_t>(l),
361 stepc, static_cast<std::uint64_t>(p), 8));
362 e = rwpt_uniform(seed, static_cast<std::uint64_t>(l), stepc,
363 static_cast<std::uint64_t>(p), 9);
364 }
365 }
366
367 const int j_from = locate(z, nseg);
368
369 // Shear displacement in VOLUME units through the ratio form:
370 // dζ = u_dev·dt·A = (u_dev/ū)·(ū·A·dt) = (u_dev/ū)·(Q·dt) —
371 // exact and unit-safe; Q·dt is the substep's through-volume,
372 // supplied by the caller (NOT v_in, which differs from Q·dt
373 // whenever the link's volume is changing).
374 const double dz = (rwpt_u_dev(e, ubar, ustar, turbulent) / ubar) * q_dt;
375
376 // Vertical walk with Itô drift, reflected into (0,1). The
377 // kernels already return η-units (D_t/h² and its η-gradient).
378 const double d_eta = rwpt_d_eta(e, h, ustar, turbulent);
379 const double drift = rwpt_d_eta_grad(e, h, ustar, turbulent);
380 e += drift * dt + rwpt_normal(seed, static_cast<std::uint64_t>(l), stepc,
381 static_cast<std::uint64_t>(p), 3) *
382 std::sqrt(std::max(2.0 * d_eta * dt, 0.0));
383 while (e < 0.0 || e > 1.0) {
384 if (e < 0.0)
385 e = -e;
386 if (e > 1.0)
387 e = 2.0 - e;
388 }
389
390 // Longitudinal reflection at the link ends (no inter-link
391 // particle transfer — recorded).
392 z += dz;
393 while (z < 0.0 || z > vol) {
394 if (z < 0.0)
395 z = -z;
396 if (z > vol)
397 z = 2.0 * vol - z;
398 }
399
400 const int j_to = locate(z, nseg);
401
402 // Upwind-carried exchange: each crossing moves the ORIGIN
403 // segment's concentration × (V/N) of water-equivalent mass,
404 // one boundary at a time so multi-segment jumps stay upwind.
405 if (j_to != j_from) {
406 const double vshare = vol / static_cast<double>(npart_);
407 int j = j_from;
408 const int stepdir = (j_to > j_from) ? 1 : -1;
409 while (j != j_to) {
410 const int jn = j + stepdir;
411 // Quantum = upwind conc x the PENETRATION past this
412 // boundary (capped at the particle's own water, V/N).
413 // A fixed V/N quantum let random-walk RE-crossings each
414 // move a full share while the limiter zeroed the
415 // up-gradient legs -- a rectified pump measured at
416 // ~10x Elder on the 6000 ft probe (X3b round). The
417 // penetration scale restores flux ~ displacement, so
418 // the exchange magnitude is dt-robust.
419 const double bnd = bounds_[static_cast<std::size_t>(std::max(j, jn))];
420 const double pen = std::min(std::abs(z - bnd), vshare);
421 for (int s = 0; s < ns; ++s)
422 delta[static_cast<std::size_t>(s)] = segs.seg_conc(l, j, s) * pen;
423 exchange(segs, l, j, jn, delta.data(), ns);
424 j = jn;
425 }
426 }
427
428 zeta_[ip] = z;
429 eta_[ip] = e;
430 seg_idx_[ip] = j_to;
431 }
432 }
433
434private:
447 int locate(double z, int nseg) const {
448 int j = 0;
449 while (j + 1 < nseg && z >= bounds_[static_cast<std::size_t>(j) + 1])
450 ++j;
451 return j;
452 }
453
476 void exchange(SegmentStore& segs, int l, int j, int jn, const double* delta, int ns) {
477 for (int s = 0; s < ns; ++s) {
478 const double vj = segs.seg_volume(l, j);
479 const double vn = segs.seg_volume(l, jn);
480 if (vj <= 0.0 || vn <= 0.0)
481 continue;
482 const double cj = segs.seg_conc(l, j, s);
483 const double cn = segs.seg_conc(l, jn, s);
484 const double take = std::min({delta[s], cj * vj, std::max(0.0, (cj - cn)) * vn});
485 if (take <= 0.0)
486 continue;
487 segs.set_seg_conc(l, j, s, (cj * vj - take) / vj);
488 segs.set_seg_conc(l, jn, s, (cn * vn + take) / vn);
489 }
490 }
491
492 int nl_ = 0, npart_ = 0;
493 std::vector<double> zeta_, eta_;
494 std::vector<int> seg_idx_;
495 std::vector<double> bounds_;
496};
497
498} // namespace lard
499} // namespace openswmm
500
501#endif // OPENSWMM_QUALITY_LARD_RWPT_DISPERSION_HPP
Versioned archives of completed source-boundary state.
LARD segment slabs — per-link ring buffers of plug-flow segments.
The central, reentrant simulation context for the new engine.
Definition CompletedSourceArchive.hpp:25
Persistent particle field + the per-substep exchange operator.
Definition RwptDispersion.hpp:242
void checkpoint(detail::CompletedSourceArchive &a)
Serialize/restore fixed particle state then validate position/ordinate values.
Definition RwptDispersion.hpp:255
void disperse(SimulationContext &ctx, SegmentStore &segs, int l, double ubar, double h, double rh, double n_manning, double v_in, double q_dt, double dt, std::uint64_t stepc, std::uint64_t seed, std::vector< double > &delta)
One substep of dispersive exchange on link l's segments.
Definition RwptDispersion.hpp:321
void resize(int n_links, int n_per_link=kRwptParticlesPerLink)
Reset deterministic particle histories for new owner dimensions.
Definition RwptDispersion.hpp:273
Definition SegmentStore.hpp:67
double seg_conc(int l, int i, int s) const
Read one species concentration in a live segment.
Definition SegmentStore.hpp:162
int count(int l) const
Read the number of live segments in one ring.
Definition SegmentStore.hpp:122
double total_volume(int l) const
Sum live segment water volumes for one link.
Definition SegmentStore.hpp:140
double seg_volume(int l, int i) const
Read a live segment's stored water volume.
Definition SegmentStore.hpp:153
int species() const
Read initialized concentration row count.
Definition SegmentStore.hpp:127
Definition LagrangianSolver.hpp:125
constexpr double kKappa
von Kármán
Definition RwptDispersion.hpp:93
constexpr int kRwptParticlesPerLink
Definition RwptDispersion.hpp:92
double rwpt_d_eta_grad(double eta, double h, double ustar, bool turbulent)
Evaluate coordinate diffusivity gradient used as stochastic drift.
Definition RwptDispersion.hpp:207
std::uint64_t rwpt_hash(std::uint64_t z)
Mix one unsigned64-bit deterministic random key.
Definition RwptDispersion.hpp:107
double rwpt_hyd_radius(double area, double depth, double diam, bool circular)
Hydraulic radius: exact for CIRCULAR from flow area, ≈ depth otherwise (wide-channel v1 approximation...
Definition RwptDispersion.hpp:226
constexpr double kReTurb
Definition RwptDispersion.hpp:97
constexpr double kDm
molecular diffusivity, ft²/s
Definition RwptDispersion.hpp:95
double rwpt_normal(std::uint64_t seed, std::uint64_t link, std::uint64_t stepc, std::uint64_t particle, std::uint64_t draw)
Standard normal (Box–Muller on two keyed uniforms).
Definition RwptDispersion.hpp:148
double rwpt_d_eta(double eta, double h, double ustar, bool turbulent)
Vertical diffusivity in η-units (D_t/h²), and its η-gradient (the Itô drift). Turbulent: Rouse parabo...
Definition RwptDispersion.hpp:191
constexpr double kEtaMin
log-law floor
Definition RwptDispersion.hpp:96
double rwpt_uniform(std::uint64_t seed, std::uint64_t link, std::uint64_t stepc, std::uint64_t particle, std::uint64_t draw)
Uniform in (0,1), keyed. draw distinguishes multiple draws per particle.
Definition RwptDispersion.hpp:127
constexpr double kNuWater
kinematic viscosity, ft²/s
Definition RwptDispersion.hpp:94
double rwpt_u_dev(double eta, double ubar, double ustar, bool turbulent)
Velocity DEVIATION u(η) − ū. Turbulent: log-law; laminar: parabola.
Definition RwptDispersion.hpp:170
Definition ExchangeLaw.hpp:60
Central, reentrant simulation context.
Definition SimulationContext.hpp:390