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
79
80#include <algorithm>
81#include <cmath>
82#include <cstdint>
83#include <vector>
84
86#include "SegmentStore.hpp"
87
88namespace openswmm {
89namespace lard {
90
91constexpr int kRwptParticlesPerLink = 2000;
92constexpr double kKappa = 0.41;
93constexpr double kNuWater = 1.05e-5;
94constexpr double kDm = 1.3454e-8;
95constexpr double kEtaMin = 1.0e-3;
96constexpr double kReTurb = 2000.0;
97
98// ---------------------------------------------------------------------------
99// D-L6 counter-based RNG: pure function of the key, no state anywhere.
100// ---------------------------------------------------------------------------
101inline std::uint64_t rwpt_hash(std::uint64_t z) {
102 z += 0x9e3779b97f4a7c15ULL;
103 z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL;
104 z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL;
105 return z ^ (z >> 31);
106}
107
108inline double rwpt_uniform(std::uint64_t seed, std::uint64_t link,
109 std::uint64_t stepc, std::uint64_t particle,
110 std::uint64_t draw) {
111 std::uint64_t k = rwpt_hash(seed ^ rwpt_hash(link));
112 k = rwpt_hash(k ^ rwpt_hash(stepc));
113 k = rwpt_hash(k ^ rwpt_hash(particle * 8ULL + draw));
114 // 53-bit mantissa, strictly inside (0,1).
115 return (static_cast<double>(k >> 11) + 0.5) * (1.0 / 9007199254740992.0);
116}
117
118inline double rwpt_normal(std::uint64_t seed, std::uint64_t link,
119 std::uint64_t stepc, std::uint64_t particle,
120 std::uint64_t draw) {
121 const double u1 = rwpt_uniform(seed, link, stepc, particle, draw);
122 const double u2 = rwpt_uniform(seed, link, stepc, particle, draw + 1);
123 return std::sqrt(-2.0 * std::log(u1)) *
124 std::cos(6.283185307179586 * u2);
125}
126
127// ---------------------------------------------------------------------------
128// Profile kernels — exposed free functions so the unit gates can pin the
129// math without an engine run.
130// ---------------------------------------------------------------------------
132inline double rwpt_u_dev(double eta, double ubar, double ustar,
133 bool turbulent) {
134 // The kEtaMin floor belongs to the LOG-LAW only (ln 0 = -inf); clamping
135 // the laminar parabola too biased its mean by ~1.5e-6*ubar — gate 4's
136 // mean-free quadrature caught it (X3b round).
137 if (turbulent)
138 return (ustar / kKappa) * (1.0 + std::log(std::max(eta, kEtaMin)));
139 return 1.5 * ubar * eta * (2.0 - eta) - ubar; // parabola, mean-free
140}
141
143inline double rwpt_d_eta(double eta, double h, double ustar, bool turbulent) {
144 if (!turbulent) return kDm / (h * h);
145 const double e = std::min(std::max(eta, 0.0), 1.0);
146 return kKappa * ustar * e * (1.0 - e) / h; // (κ u* h η(1-η))/h²
147}
148inline double rwpt_d_eta_grad(double eta, double h, double ustar,
149 bool turbulent) {
150 if (!turbulent) return 0.0;
151 return kKappa * ustar * (1.0 - 2.0 * eta) / h;
152}
153
156inline double rwpt_hyd_radius(double area, double depth, double diam,
157 bool circular) {
158 if (!circular || diam <= 0.0 || depth <= 0.0) return std::max(depth, 0.0);
159 const double ratio =
160 std::min(std::max(1.0 - 2.0 * depth / diam, -1.0), 1.0);
161 const double theta = 2.0 * std::acos(ratio);
162 if (theta <= 1.0e-6) return std::max(depth, 0.0);
163 const double a = diam * diam / 8.0 * (theta - std::sin(theta));
164 const double p = diam * theta / 2.0;
165 (void)area;
166 return (p > 0.0) ? a / p : std::max(depth, 0.0);
167}
168
173public:
174 void resize(int n_links, int n_per_link = kRwptParticlesPerLink) {
175 nl_ = n_links;
176 npart_ = n_per_link;
177 const std::size_t total =
178 static_cast<std::size_t>(nl_) * static_cast<std::size_t>(npart_);
179 zeta_.assign(total, -1.0); // <0 marks "not yet seeded"
180 eta_.assign(total, 0.5);
181 seg_idx_.assign(total, 0);
182 }
183
192 void disperse(SimulationContext& ctx, SegmentStore& segs, int l,
193 double ubar, double h, double rh, double n_manning,
194 double v_in, double q_dt, double dt, std::uint64_t stepc,
195 std::uint64_t seed, std::vector<double>& delta) {
196 (void)ctx;
197 const int nseg = segs.count(l);
198 if (nseg < 2 || h <= 1.0e-3 || ubar <= 0.0) return;
199 const double vol = segs.total_volume(l);
200 if (vol <= 0.0) return;
201 const int ns = segs.species();
202
203 // Friction: Manning S_f at the link's own n → u*. Regime by Re.
204 const double sf = (n_manning * ubar) * (n_manning * ubar) /
205 (2.208 * std::pow(std::max(rh, 1.0e-3), 4.0 / 3.0));
206 const double ustar =
207 std::sqrt(32.174 * std::max(rh, 1.0e-3) * sf);
208 const bool turbulent = (ubar * 4.0 * rh / kNuWater) >= kReTurb;
209
210 // Segment boundaries in volume coordinates, front → back.
211 bounds_.assign(static_cast<std::size_t>(nseg) + 1, 0.0);
212 for (int j = 0; j < nseg; ++j)
213 bounds_[static_cast<std::size_t>(j) + 1] =
214 bounds_[static_cast<std::size_t>(j)] + segs.seg_volume(l, j);
215
216 const std::size_t base = static_cast<std::size_t>(l) *
217 static_cast<std::size_t>(npart_);
218 for (int p = 0; p < npart_; ++p) {
219 const std::size_t ip = base + static_cast<std::size_t>(p);
220 double z = zeta_[ip];
221 double e = eta_[ip];
222
223 // Seed / recycle: unseeded particles spread evenly; particles
224 // that rode out the back are new water entering at the front.
225 if (z < 0.0) {
226 z = vol * (static_cast<double>(p) + 0.5) /
227 static_cast<double>(npart_);
228 e = rwpt_uniform(seed, static_cast<std::uint64_t>(l), stepc,
229 static_cast<std::uint64_t>(p), 7);
230 } else {
231 z += v_in; // ride the bulk advection (volume coordinates)
232 if (z >= vol) {
233 z = std::min(v_in,
234 vol * rwpt_uniform(
235 seed, static_cast<std::uint64_t>(l),
236 stepc,
237 static_cast<std::uint64_t>(p), 8));
238 e = rwpt_uniform(seed, static_cast<std::uint64_t>(l),
239 stepc, static_cast<std::uint64_t>(p), 9);
240 }
241 }
242
243 const int j_from = locate(z, nseg);
244
245 // Shear displacement in VOLUME units through the ratio form:
246 // dζ = u_dev·dt·A = (u_dev/ū)·(ū·A·dt) = (u_dev/ū)·(Q·dt) —
247 // exact and unit-safe; Q·dt is the substep's through-volume,
248 // supplied by the caller (NOT v_in, which differs from Q·dt
249 // whenever the link's volume is changing).
250 const double dz =
251 (rwpt_u_dev(e, ubar, ustar, turbulent) / ubar) * q_dt;
252
253 // Vertical walk with Itô drift, reflected into (0,1). The
254 // kernels already return η-units (D_t/h² and its η-gradient).
255 const double d_eta = rwpt_d_eta(e, h, ustar, turbulent);
256 const double drift = rwpt_d_eta_grad(e, h, ustar, turbulent);
257 e += drift * dt +
258 rwpt_normal(seed, static_cast<std::uint64_t>(l), stepc,
259 static_cast<std::uint64_t>(p), 3) *
260 std::sqrt(std::max(2.0 * d_eta * dt, 0.0));
261 while (e < 0.0 || e > 1.0) {
262 if (e < 0.0) e = -e;
263 if (e > 1.0) e = 2.0 - e;
264 }
265
266 // Longitudinal reflection at the link ends (no inter-link
267 // particle transfer — recorded).
268 z += dz;
269 while (z < 0.0 || z > vol) {
270 if (z < 0.0) z = -z;
271 if (z > vol) z = 2.0 * vol - z;
272 }
273
274 const int j_to = locate(z, nseg);
275
276 // Upwind-carried exchange: each crossing moves the ORIGIN
277 // segment's concentration × (V/N) of water-equivalent mass,
278 // one boundary at a time so multi-segment jumps stay upwind.
279 if (j_to != j_from) {
280 const double vshare = vol / static_cast<double>(npart_);
281 int j = j_from;
282 const int stepdir = (j_to > j_from) ? 1 : -1;
283 while (j != j_to) {
284 const int jn = j + stepdir;
285 // Quantum = upwind conc x the PENETRATION past this
286 // boundary (capped at the particle's own water, V/N).
287 // A fixed V/N quantum let random-walk RE-crossings each
288 // move a full share while the limiter zeroed the
289 // up-gradient legs -- a rectified pump measured at
290 // ~10x Elder on the 6000 ft probe (X3b round). The
291 // penetration scale restores flux ~ displacement, so
292 // the exchange magnitude is dt-robust.
293 const double bnd =
294 bounds_[static_cast<std::size_t>(std::max(j, jn))];
295 const double pen = std::min(std::abs(z - bnd), vshare);
296 for (int s = 0; s < ns; ++s)
297 delta[static_cast<std::size_t>(s)] =
298 segs.seg_conc(l, j, s) * pen;
299 exchange(segs, l, j, jn, delta.data(), ns);
300 j = jn;
301 }
302 }
303
304 zeta_[ip] = z;
305 eta_[ip] = e;
306 seg_idx_[ip] = j_to;
307 }
308 }
309
310private:
315 int locate(double z, int nseg) const {
316 int j = 0;
317 while (j + 1 < nseg &&
318 z >= bounds_[static_cast<std::size_t>(j) + 1])
319 ++j;
320 return j;
321 }
322
330 void exchange(SegmentStore& segs, int l, int j, int jn,
331 const double* delta, int ns) {
332 for (int s = 0; s < ns; ++s) {
333 const double vj = segs.seg_volume(l, j);
334 const double vn = segs.seg_volume(l, jn);
335 if (vj <= 0.0 || vn <= 0.0) continue;
336 const double cj = segs.seg_conc(l, j, s);
337 const double cn = segs.seg_conc(l, jn, s);
338 const double take =
339 std::min({delta[s], cj * vj,
340 std::max(0.0, (cj - cn)) * vn});
341 if (take <= 0.0) continue;
342 segs.set_seg_conc(l, j, s, (cj * vj - take) / vj);
343 segs.set_seg_conc(l, jn, s, (cn * vn + take) / vn);
344 }
345 }
346
347 int nl_ = 0, npart_ = 0;
348 std::vector<double> zeta_, eta_;
349 std::vector<int> seg_idx_;
350 std::vector<double> bounds_;
351};
352
353} // namespace lard
354} // namespace openswmm
355
356#endif // OPENSWMM_QUALITY_LARD_RWPT_DISPERSION_HPP
LARD segment slabs — per-link ring buffers of plug-flow segments.
The central, reentrant simulation context for the new engine.
Persistent particle field + the per-substep exchange operator.
Definition RwptDispersion.hpp:172
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:192
void resize(int n_links, int n_per_link=kRwptParticlesPerLink)
Definition RwptDispersion.hpp:174
Definition SegmentStore.hpp:66
double seg_conc(int l, int i, int s) const
Definition SegmentStore.hpp:91
int count(int l) const
Definition SegmentStore.hpp:80
double total_volume(int l) const
Definition SegmentStore.hpp:84
double seg_volume(int l, int i) const
Definition SegmentStore.hpp:90
int species() const
Definition SegmentStore.hpp:81
Definition LagrangianSolver.hpp:122
constexpr double kKappa
von Kármán
Definition RwptDispersion.hpp:92
constexpr int kRwptParticlesPerLink
Definition RwptDispersion.hpp:91
double rwpt_d_eta_grad(double eta, double h, double ustar, bool turbulent)
Definition RwptDispersion.hpp:148
std::uint64_t rwpt_hash(std::uint64_t z)
Definition RwptDispersion.hpp:101
double rwpt_hyd_radius(double area, double depth, double diam, bool circular)
Definition RwptDispersion.hpp:156
constexpr double kReTurb
Definition RwptDispersion.hpp:96
constexpr double kDm
molecular diffusivity, ft²/s
Definition RwptDispersion.hpp:94
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:118
double rwpt_d_eta(double eta, double h, double ustar, bool turbulent)
Definition RwptDispersion.hpp:143
constexpr double kEtaMin
log-law floor
Definition RwptDispersion.hpp:95
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:108
constexpr double kNuWater
kinematic viscosity, ft²/s
Definition RwptDispersion.hpp:93
double rwpt_u_dev(double eta, double ubar, double ustar, bool turbulent)
Velocity DEVIATION u(η) − ū. Turbulent: log-law; laminar: parabola.
Definition RwptDispersion.hpp:132
Definition NodeCoupling.cpp:16
Central, reentrant simulation context.
Definition SimulationContext.hpp:353