OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
FvKernels.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
59
60#ifndef OPENSWMM_ENGINE_FV_KERNELS_HPP
61#define OPENSWMM_ENGINE_FV_KERNELS_HPP
62
63#include <algorithm>
64#include <cmath>
65
66#include "FvOptions.hpp"
67#include "NetworkMeshData.hpp"
68
69// Portable kernel-function marker — identical convention to
70// 2d/solver/InertialKernels.hpp:45. Host builds get plain `inline`; the GPU
71// plugin defines OPENSWMM_KERNEL_FN to KOKKOS_INLINE_FUNCTION *before*
72// including this header so the same scalar bodies compile for the device.
73#ifndef OPENSWMM_KERNEL_FN
74#define OPENSWMM_KERNEL_FN inline
75#endif
76
77namespace openswmm::fv::kernels {
78
81inline constexpr double kGravity = 32.2;
82
86inline constexpr double kDryDepth = 1.0e-7;
87
89inline constexpr double kDryArea = 1.0e-12;
90
94inline constexpr double kEtaDeadband = 1.0e-12;
95
96// ===========================================================================
97// Preissmann slot taper (plan §3.3.2)
98// ===========================================================================
99
108OPENSWMM_KERNEL_FN double slotRamp(double s) noexcept {
109 if (s <= 0.0)
110 return 0.0;
111 if (s >= 1.0)
112 return 1.0;
113 return s * s * (3.0 - 2.0 * s);
114}
115
124OPENSWMM_KERNEL_FN double slotRampIntegral(double s) noexcept {
125 if (s <= 0.0)
126 return 0.0;
127 if (s >= 1.0)
128 return s - 0.5; // 1 − ½ from the taper, then linear
129 const double s3 = s * s * s;
130 return s3 - 0.5 * s3 * s; // s³ − s⁴/2
131}
132
133// ===========================================================================
134// Closure — one continuous geometry from dry bed to full pressurization
135// ===========================================================================
136//
137// This is the memoryless SLOT closure — every cell, every state, no history.
138// TPA exception (issue #156): under FV_PRESSURE_CLOSURE TPA a cell whose
139// regime flag is set evaluates the tpa* kernels at the end of this file
140// instead. The flag is physical air-pathway history (cleared on cold start
141// and hotstart restore), not numerical relaxation; unflagged cells — and the
142// entire SLOT closure path — are untouched.
143
155OPENSWMM_KERNEL_FN double areaOfDepth(const FvGeometry& g, double h) noexcept {
156 return closureArea(g.closure_tbl, h);
157}
158
171OPENSWMM_KERNEL_FN double widthOfDepth(const FvGeometry& g, double h) noexcept {
172 return closureWidth(g.closure_tbl, h);
173}
174
186OPENSWMM_KERNEL_FN double hydRadOfDepth(const FvGeometry& g, double h) noexcept {
187 return closureHydRad(g.closure_tbl, h);
188}
189
204OPENSWMM_KERNEL_FN double i1OfDepth(const FvGeometry& g, double h, double area_at_h) noexcept {
205 (void)area_at_h;
206 return closureI1(g.closure_tbl, h);
207}
208
221OPENSWMM_KERNEL_FN double depthOfArea(const FvGeometry& g, double a) noexcept {
222 return closureDepthOfArea(g.closure_tbl, a);
223}
224
234OPENSWMM_KERNEL_FN double celerity(double a, double t) noexcept {
235 if (a <= kDryArea || t <= 0.0)
236 return 0.0;
237 return std::sqrt(kGravity * a / t);
238}
239
240// ===========================================================================
241// Riemann solver
242// ===========================================================================
243
245struct FaceState {
246 double a = 0.0;
247 double q = 0.0;
248 double u = 0.0;
249 double c = 0.0;
250 double i1 = 0.0;
255 uint8_t press = 0;
256};
257
259struct FaceFlux {
260 double mass = 0.0;
261 double mom = 0.0;
262 double sstar = 0.0;
263 double sl = 0.0;
264 double sr = 0.0;
265};
266
282OPENSWMM_KERNEL_FN void waveSpeeds(const FaceState& L, const FaceState& R, double& sl,
283 double& sr) noexcept {
284 const bool wetL = (L.a > kDryArea);
285 const bool wetR = (R.a > kDryArea);
286 if (wetL && wetR) {
287 sl = std::min(L.u - L.c, R.u - R.c);
288 sr = std::max(L.u + L.c, R.u + R.c);
289 // Slot/free-surface interface (one side in the slot, the other not).
290 // Davis's symmetric estimate carries the slot's acoustic celerity into
291 // BOTH waves, but no signal enters the free-surface side at the slot
292 // speed: the wave that crosses into it is the filling/emptying bore,
293 // whose speed is the Rankine–Hugoniot jump (ΔQ/ΔA), bounded below by
294 // that side's own u ± c and above by the Davis bound. With the slot
295 // celerity on both waves the HLL diffusion term at a pressurized
296 // entrance is O(c_slot·ΔA) — measured 52 cfs of spurious mass flux and
297 // a −245 ft⁴/s² momentum sink on a 3 ft culvert whose junction then
298 // flooded at 108 of 120 cfs however high its head rose.
299 // A vented junction's ghost (press == 2) is excluded: bounding the
300 // wave into a pressurized cell beside it cut the acoustic exchange at
301 // every lateral junction of a pressurized tunnel (the node solve then
302 // clamped at its rim and booked 63 000 m³ of flooding — Klaver
303 // Example_02, 2026-09-13), and bounding the wave into a free cell
304 // beside it drove a 2× over-conveying limit cycle at an overloaded
305 // entrance. The bound acts at the INTERIOR slot/free faces, which is
306 // where the entrance lock lived.
307 if (L.press != R.press && L.press < 2 && R.press < 2) {
308 if (L.press) {
309 const double da = L.a - R.a;
310 const double w = (da > 0.0) ? (L.q - R.q) / da : sr;
311 sr = std::max(R.u + R.c, std::min(sr, w));
312 } else {
313 const double da = R.a - L.a;
314 const double w = (da > 0.0) ? (R.q - L.q) / da : sl;
315 sl = std::min(L.u - L.c, std::max(sl, w));
316 }
317 }
318 } else if (wetR) { // dry left
319 sl = R.u - 2.0 * R.c;
320 sr = R.u + R.c;
321 } else if (wetL) { // dry right
322 sl = L.u - L.c;
323 sr = L.u + 2.0 * L.c;
324 } else {
325 sl = 0.0;
326 sr = 0.0;
327 }
328}
329
339OPENSWMM_KERNEL_FN void physicalFlux(const FaceState& S, double& fa, double& fq) noexcept {
340 fa = S.q;
341 fq = S.q * S.u + kGravity * S.i1;
342}
343
357OPENSWMM_KERNEL_FN double riemannMassFlux(const FaceState& L, const FaceState& R) noexcept {
358 if (L.a <= kDryArea && R.a <= kDryArea)
359 return 0.0;
360 double sl = 0.0, sr = 0.0;
361 waveSpeeds(L, R, sl, sr);
362 double fal = 0.0, fql = 0.0, far = 0.0, fqr = 0.0;
363 physicalFlux(L, fal, fql);
364 physicalFlux(R, far, fqr);
365 if (sl >= 0.0)
366 return fal;
367 if (sr <= 0.0)
368 return far;
369 return (sr * fal - sl * far + sl * sr * (R.a - L.a)) / (sr - sl);
370}
371
389 FaceFlux out;
390 if (L.a <= kDryArea && R.a <= kDryArea)
391 return out;
392
393 double sl = 0.0, sr = 0.0;
394 waveSpeeds(L, R, sl, sr);
395 out.sl = sl;
396 out.sr = sr;
397
398 double fal = 0.0, fql = 0.0, far = 0.0, fqr = 0.0;
399 physicalFlux(L, fal, fql);
400 physicalFlux(R, far, fqr);
401
402 if (sl >= 0.0) {
403 out.mass = fal;
404 out.mom = fql;
405 out.sstar = sl;
406 return out;
407 }
408 if (sr <= 0.0) {
409 out.mass = far;
410 out.mom = fqr;
411 out.sstar = sr;
412 return out;
413 }
414
415 const double dsr = sr - sl;
416 out.mass = (sr * fal - sl * far + sl * sr * (R.a - L.a)) / dsr;
417 out.mom = (sr * fql - sl * fqr + sl * sr * (R.q - L.q)) / dsr;
418
419 // Contact speed (plan §3.2). Falls back to the HLL-averaged velocity when
420 // the denominator degenerates, which happens only when both sides are
421 // vanishing — where the species flux is zero anyway.
422 const double den = R.a * (R.u - sr) - L.a * (L.u - sl);
423 if (std::fabs(den) > 1.0e-14) {
424 out.sstar = (sl * R.a * (R.u - sr) - sr * L.a * (L.u - sl)) / den;
425 } else {
426 const double a_hll = (sr * R.a - sl * L.a - (far - fal)) / dsr;
427 out.sstar = (a_hll > kDryArea) ? out.mass / a_hll : 0.0;
428 }
429 return out;
430}
431
450OPENSWMM_KERNEL_FN double speciesFlux(const FaceState& L, const FaceState& R, const FaceFlux& f,
451 double phi_l, double phi_r, bool hllc) noexcept {
452 if (hllc)
453 return f.mass * ((f.sstar >= 0.0) ? phi_l : phi_r);
454
455 // HLL on the A(phi) component, with the same supersonic branches as the
456 // hydrodynamic flux so the two can never disagree about which side is
457 // upstream of the whole fan.
458 if (f.sl >= 0.0)
459 return L.q * phi_l;
460 if (f.sr <= 0.0)
461 return R.q * phi_r;
462 const double dsr = f.sr - f.sl;
463 return (f.sr * L.q * phi_l - f.sl * R.q * phi_r + f.sl * f.sr * (R.a * phi_r - L.a * phi_l)) /
464 dsr;
465}
466
467// ===========================================================================
468// Source terms
469// ===========================================================================
470
485OPENSWMM_KERNEL_FN double frictionUpdate(double q, double u, double r, double dt,
486 double rough_factor) noexcept {
487 if (r <= 0.0 || rough_factor <= 0.0)
488 return q;
489 const double r43 = r * std::cbrt(r);
490 const double den = 1.0 + dt * rough_factor * std::fabs(u) / r43;
491 return q / den;
492}
493
504OPENSWMM_KERNEL_FN double localLossUpdate(double q, double u, double k, double dx,
505 double dt) noexcept {
506 if (k <= 0.0 || dx <= 0.0)
507 return q;
508 return q / (1.0 + dt * k * std::fabs(u) / (2.0 * dx));
509}
510
527OPENSWMM_KERNEL_FN double ufUpdate(double q, double a, double u_old, double k3, double grad_term,
528 double dt) noexcept {
529 if (k3 <= 0.0 || a <= 0.0)
530 return q;
531 // Dead-band (measured, issue #156): the implicit fold acts as added
532 // inertia — it resists velocity DECAY too, so applied to the ~mm/s
533 // numerical ripple of a storage-coupled pool it sustained noise that
534 // steady friction was correctly killing (0.004 → 0.011 cfs on the
535 // at-rest fixture). UF correlations are calibrated for real transients
536 // (paper velocities O(0.1–1 m/s)); below 0.01 ft/s the term is noise
537 // amplification, not physics. Both the old and candidate velocities must
538 // clear the floor, so a genuinely at-rest deck stays bit-identical.
539 constexpr double kUfVelFloor = 0.01; // ft/s, internal units
540 if (std::fabs(u_old) < kUfVelFloor && std::fabs(q / a) < kUfVelFloor)
541 return q;
542 double qn = (q + k3 * a * u_old) / (1.0 + k3);
543 qn -= dt * k3 * a * grad_term;
544 const double dq = qn - q;
545 const double cap = 0.5 * std::fabs(q);
546 if (std::fabs(dq) > cap)
547 qn = q + ((dq > 0.0) ? cap : -cap);
548 return qn;
549}
550
551// ---------------------------------------------------------------------------
552// TPA — two-component pressure approach (issue #156 Phase 4)
553// ---------------------------------------------------------------------------
554// Vasconcelos, Wright & Roe (2006). For a cell whose regime FLAG is set
555// (state.cell_tpa — physical air-pathway history, updated once per substep by
556// the solver, NOT evaluated here), the closure is the slot line extended to
557// BOTH signs of ΔA = A − a_crown:
558// h(A) = y_full + (A − a_crown)/t_slot (hs = h − y_full, signed)
559// A(h) = a_crown + t_slot·(h − y_full)
560// I₁(h) = i1_crown + a_crown·(h − y_full) (paper Eq. 12b: the slot's
561// ½·t_slot·d² numerical
562// storage pressure is dropped)
563// T = t_slot, R = r_full, c = √(g·A/t_slot) ≈ acoustic celerity a.
564// Free-surface (unflagged) cells use the table closure above UNCHANGED,
565// including the crown taper. The pair is continuous at A = a_crown, h = y_full.
566
578OPENSWMM_KERNEL_FN double tpaDepthOfArea(const FvGeometry& g, double a) noexcept {
579 return g.y_full + (a - g.a_crown) / g.t_slot;
580}
581
590OPENSWMM_KERNEL_FN double tpaAreaOfDepth(const FvGeometry& g, double h) noexcept {
591 return g.a_crown + g.t_slot * (h - g.y_full);
592}
593
605OPENSWMM_KERNEL_FN double tpaI1OfDepth(const FvGeometry& g, double h) noexcept {
606 return g.i1_crown + g.a_crown * (h - g.y_full);
607}
608
618OPENSWMM_KERNEL_FN double faceCflDt(double cfl, double dx, double u, double c) noexcept {
619 const double s = std::fabs(u) + c;
620 return (s > 1.0e-12) ? cfl * dx / s : 1.0e30;
621}
622
635OPENSWMM_KERNEL_FN double positivityScale(double vol, double outflow, double dt) noexcept {
636 if (outflow <= 0.0 || dt <= 0.0)
637 return 1.0;
638 const double take = outflow * dt;
639 const double avail = (vol > 0.0) ? vol : 0.0;
640 return (take <= avail) ? 1.0 : avail / take;
641}
642
643} // namespace openswmm::fv::kernels
644
645#endif // OPENSWMM_ENGINE_FV_KERNELS_HPP
#define OPENSWMM_KERNEL_FN
Definition ExplicitKokkosSurfaceSolver.cpp:22
Option struct for the explicit finite-volume 1D network solver.
SoA storage for the 1D finite-volume network mesh and its state.
Definition FvClosureKernels.hpp:115
OPENSWMM_KERNEL_FN double closureArea(const FvClosure &c, double h) noexcept
Evaluate the closure flow area.
Definition FvClosureKernels.hpp:289
OPENSWMM_KERNEL_FN FaceFlux riemannFlux(const FaceState &L, const FaceState &R) noexcept
Evaluate HLL hydraulic flux and the scalar contact-wave speed.
Definition FvKernels.hpp:388
OPENSWMM_KERNEL_FN double frictionUpdate(double q, double u, double r, double dt, double rough_factor) noexcept
Apply a semi-implicit Manning discharge sink.
Definition FvKernels.hpp:485
OPENSWMM_KERNEL_FN double closureWidth(const FvClosure &c, double h) noexcept
Evaluate the closure area derivative used as top width.
Definition FvClosureKernels.hpp:317
OPENSWMM_KERNEL_FN double closureI1(const FvClosure &c, double h) noexcept
Evaluate the closure hydrostatic first moment.
Definition FvClosureKernels.hpp:345
OPENSWMM_KERNEL_FN void physicalFlux(const FaceState &S, double &fa, double &fq) noexcept
Evaluate the conservative Saint-Venant physical flux of one state.
Definition FvKernels.hpp:339
OPENSWMM_KERNEL_FN double ufUpdate(double q, double a, double u_old, double k3, double grad_term, double dt) noexcept
Apply the bounded unsteady-friction acceleration correction.
Definition FvKernels.hpp:527
OPENSWMM_KERNEL_FN double closureDepthOfArea(const FvClosure &c, double a) noexcept
Invert aggregate closure area using the same forward geometry.
Definition FvClosureKernels.hpp:394
OPENSWMM_KERNEL_FN double widthOfDepth(const FvGeometry &g, double h) noexcept
Evaluate memoryless slot top width at a depth.
Definition FvKernels.hpp:171
OPENSWMM_KERNEL_FN double faceCflDt(double cfl, double dx, double u, double c) noexcept
Compute a local advective/acoustic Courant timestep bound.
Definition FvKernels.hpp:618
OPENSWMM_KERNEL_FN double localLossUpdate(double q, double u, double k, double dx, double dt) noexcept
Apply a semi-implicit entrance/exit momentum loss over an end cell.
Definition FvKernels.hpp:504
OPENSWMM_KERNEL_FN double riemannMassFlux(const FaceState &L, const FaceState &R) noexcept
Evaluate only the HLL volumetric flux for an algebraic node residual.
Definition FvKernels.hpp:357
OPENSWMM_KERNEL_FN void waveSpeeds(const FaceState &L, const FaceState &R, double &sl, double &sr) noexcept
Estimate left and right signal speeds with dry-front and pressure-bore bounds.
Definition FvKernels.hpp:282
OPENSWMM_KERNEL_FN double positivityScale(double vol, double outflow, double dt) noexcept
Limit exported substep volume to the available donor water.
Definition FvKernels.hpp:635
OPENSWMM_KERNEL_FN double hydRadOfDepth(const FvGeometry &g, double h) noexcept
Evaluate memoryless closure friction radius.
Definition FvKernels.hpp:186
OPENSWMM_KERNEL_FN double slotRamp(double s) noexcept
Evaluate the smooth slot-mouth opening fraction.
Definition FvKernels.hpp:108
constexpr double kDryArea
Area floor paired with kDryDepth for the u = Q/A division.
Definition FvKernels.hpp:89
constexpr double kEtaDeadband
Definition FvKernels.hpp:94
OPENSWMM_KERNEL_FN double tpaDepthOfArea(const FvGeometry &g, double a) noexcept
Evaluate the signed-pressure TPA area-to-head extension.
Definition FvKernels.hpp:578
OPENSWMM_KERNEL_FN double areaOfDepth(const FvGeometry &g, double h) noexcept
Evaluate memoryless slot area at a depth.
Definition FvKernels.hpp:155
OPENSWMM_KERNEL_FN double closureHydRad(const FvClosure &c, double h) noexcept
Evaluate the friction hydraulic radius.
Definition FvClosureKernels.hpp:363
OPENSWMM_KERNEL_FN double i1OfDepth(const FvGeometry &g, double h, double area_at_h) noexcept
Evaluate the memoryless closure hydrostatic area integral.
Definition FvKernels.hpp:204
OPENSWMM_KERNEL_FN double speciesFlux(const FaceState &L, const FaceState &R, const FaceFlux &f, double phi_l, double phi_r, bool hllc) noexcept
Evaluate a conservative advected-scalar flux using the hydraulic fan.
Definition FvKernels.hpp:450
OPENSWMM_KERNEL_FN double slotRampIntegral(double s) noexcept
Integrate the normalized slot-mouth opening from zero to s.
Definition FvKernels.hpp:124
OPENSWMM_KERNEL_FN double tpaI1OfDepth(const FvGeometry &g, double h) noexcept
Evaluate the TPA pressure integral without numerical slot-storage pressure.
Definition FvKernels.hpp:605
OPENSWMM_KERNEL_FN double depthOfArea(const FvGeometry &g, double a) noexcept
Invert aggregate area on the memoryless closure.
Definition FvKernels.hpp:221
constexpr double kDryDepth
Definition FvKernels.hpp:86
OPENSWMM_KERNEL_FN double tpaAreaOfDepth(const FvGeometry &g, double h) noexcept
Evaluate aggregate TPA area on the slot line in either pressure direction.
Definition FvKernels.hpp:590
constexpr double kGravity
Definition FvKernels.hpp:81
OPENSWMM_KERNEL_FN double celerity(double a, double t) noexcept
Compute the gravity-wave speed from aggregate area and top width.
Definition FvKernels.hpp:234
Cross-section closure for one conduit's cell chain.
Definition NetworkMeshData.hpp:71
Result of a face flux evaluation.
Definition FvKernels.hpp:259
double mom
F[1] — momentum flux (ft⁴/s²)
Definition FvKernels.hpp:261
double sstar
contact-wave speed; sign selects the species upwind
Definition FvKernels.hpp:262
double mass
F[0] — volumetric flux (cfs)
Definition FvKernels.hpp:260
double sr
right signal speed
Definition FvKernels.hpp:264
double sl
left signal speed (kept for the species flux)
Definition FvKernels.hpp:263
One side of a face after hydrostatic reconstruction.
Definition FvKernels.hpp:245
double c
celerity (ft/s)
Definition FvKernels.hpp:249
double i1
Definition FvKernels.hpp:250
uint8_t press
Definition FvKernels.hpp:255
double a
reconstructed flow area (ft²)
Definition FvKernels.hpp:246
double q
reconstructed discharge (cfs) = a·u
Definition FvKernels.hpp:247
double u
velocity (ft/s)
Definition FvKernels.hpp:248