OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
ExplicitFvSolver.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
38
39#ifndef OPENSWMM_ENGINE_FV_EXPLICIT_FV_SOLVER_HPP
40#define OPENSWMM_ENGINE_FV_EXPLICIT_FV_SOLVER_HPP
41
42#include <array>
43#include <cstdint>
44#include <vector>
45
46#include "FvKernels.hpp"
47#include "FvOptions.hpp"
48#include "INetworkSolver.hpp"
49#include "NetworkMeshData.hpp"
51
53struct SpeciesKernelView; // transport/fvkernels/SpeciesTransportKernels.hpp
54}
55
56namespace openswmm::fv {
57
59public:
60 ExplicitFvSolver() = default;
61 ~ExplicitFvSolver() override = default;
62
65
67 const FvOptions& opts) override;
68 double advance(double t_current, double t_target,
69 const FvStepForcing& forcing) override;
70 void reinitialize(double t0) override;
71 void finalize() override;
72
73 long last_num_steps() const noexcept override { return last_nsteps_; }
74 double last_step_size() const noexcept override { return last_h_; }
75 double suggested_step() const noexcept override { return suggested_h_; }
76 RunStats run_stats() const noexcept override;
77 bool is_initialized() const noexcept override { return mesh_ != nullptr; }
78 bool divergence(Divergence& d) const noexcept override {
79 if (div_link_ < 0) return false;
80 d.link = div_link_;
81 d.value = div_value_;
82 d.what = div_what_;
83 return true;
84 }
85
89 const std::vector<double>& node_exchange() const noexcept { return node_exch_; }
90
97 const std::vector<double>& node_inflow_volume() const noexcept { return node_in_; }
98 const std::vector<double>& node_outflow_volume() const noexcept { return node_out_; }
99
102 const std::vector<uint8_t>& inlet_control() const noexcept {
103 return inlet_control_;
104 }
105
107 const std::vector<double>& node_flood_volume() const noexcept {
108 return flood_vol_;
109 }
110
115 const std::vector<double>& dummy_volume() const noexcept {
116 return dummy_vol_;
117 }
118
125 const std::vector<std::uint8_t>& node_passthrough() const noexcept {
126 return node_pass_;
127 }
128
140 const std::vector<std::uint8_t>& node_publish_stage() const noexcept {
141 return node_pub_;
142 }
143
148 const std::vector<double>& cell_flow_integral() const noexcept {
149 return cell_q_int_;
150 }
151
154 double nodeVolumeFromDepth(int node, double depth) const;
155
158 int lts_tiers() const noexcept { return lts_tiers_; }
159
162 const std::array<long, kMaxLtsTiers>& tier_occupancy() const noexcept {
163 return tier_occupancy_;
164 }
165
166private:
167 // -- substep pipeline ---------------------------------------------------
168 void refreshDepths();
169 void refreshNodeAreas();
170 void rebuildActiveLists();
177 double censusDt(bool press_edit = false) const;
178 void reconstructState();
179 void computeFaceFlux(int face);
183 void reconstructScalars(double dt);
184 transport::fvkernels::SpeciesKernelView speciesKernelView();
185 void computeFluxes();
186 void limitPositivity(double dt);
187
199 void relaxNodeFluxes(double dt, const FvStepForcing& forcing);
200
204 void relaxOneNode(int node, double dt, const FvStepForcing& forcing);
205
219 void solveAlgebraicNode(int node, double dt, const FvStepForcing& forcing);
220
225 void settleAlgebraicNode(int node, double carry);
226
229 std::vector<std::uint8_t> node_alg_;
232 std::vector<std::uint8_t> node_pass_static_;
238 std::vector<std::uint8_t> node_pass_;
245 std::vector<std::uint8_t> node_pub_static_;
246 std::vector<std::uint8_t> node_pub_;
250 std::vector<double> node_carry_;
259 std::vector<double> cell_qlat_;
260 std::vector<std::uint8_t> node_lat_div_;
261 double nodeLateral(const FvStepForcing& forcing,
262 std::size_t un) const noexcept {
263 if (!node_lat_div_.empty() && node_lat_div_[un]) return 0.0;
264 return forcing.node_lateral ? forcing.node_lateral[un] : 0.0;
265 }
286 std::vector<double> face_a_rim_;
287 std::vector<double> face_lat_spill_;
288 double injectDivertedLateral(std::size_t uc, double a_new, double dt,
289 double inv_dx);
290 void bookLateralSpills(const std::vector<int>* nodes);
292 std::vector<double> node_vfull_;
293
298 bool algebraicActive(int n) const noexcept {
299 const auto un = static_cast<std::size_t>(n);
300 if (!node_alg_[un]) return false;
301 if (mesh_->node_can_pond[un] &&
302 state_->node_volume[un] > node_vfull_[un]) return false;
303 return true;
304 }
305
311 bool incidentPressurized(std::size_t un) const noexcept {
312 for (int p = mesh_->node_face_ptr[un];
313 p < mesh_->node_face_ptr[un + 1]; ++p) {
314 const auto uf = static_cast<std::size_t>(
315 mesh_->node_face_idx[static_cast<std::size_t>(p)]);
316 const int c = (mesh_->face_cl[uf] >= 0) ? mesh_->face_cl[uf]
317 : mesh_->face_cr[uf];
318 if (c < 0) continue;
319 const auto uc = static_cast<std::size_t>(c);
320 if (tpaCell(uc)) return true;
321 const FvGeometry& g =
322 mesh_->geom[static_cast<std::size_t>(mesh_->cell_geom[uc])];
323 if (!g.is_open && state_->cell_h[uc] >= g.y_crown) return true;
324 }
325 return false;
326 }
327 void updateCells(double dt, const FvStepForcing& forcing);
328 void updateNodes(double dt, const FvStepForcing& forcing);
329 void dispersionSolve(double dt);
330
337 void refreshStructFlows(const FvStepForcing& forcing);
338 std::vector<double> node_qstruct_;
339
348 std::vector<double> node_qdummy_;
349
353 std::vector<double> dummy_vol_;
354
363 template <class ArrivedFn>
364 void refreshDummyFlows(double dt, const FvStepForcing& forcing,
365 ArrivedFn&& arrived);
366
370 double nodeQDummy(std::size_t un) const noexcept {
371 return node_qdummy_.empty() ? 0.0 : node_qdummy_[un];
372 }
373
374 double nodeDepthFromVolume(int node, double volume) const;
375
396 bool ltsEligible() const noexcept {
397 return opts_.lts &&
398 opts_.time_integration != TimeIntegration::RK2 &&
399 state_ && state_->n_species == 0;
400 }
401
402 bool faceIsLive(int f) const noexcept {
403 return all_faces_live_ ||
404 (!live_stamp_.empty() &&
405 live_stamp_[static_cast<std::size_t>(f)] == live_gen_);
406 }
407
410 std::vector<std::uint32_t> live_stamp_;
411 std::uint32_t live_gen_ = 0;
416 bool all_faces_live_ = true;
417
452 double algebraicNodeStableDt(int n) const noexcept;
453
454 double frictionFor(const FvGeometry& g, double q, double u, double h,
455 double dt) const;
456
466 void applyNodeCapacity(int node, double v_prev, double& vol, double& depth);
467
468 // -- local time stepping (plan §3.3) ------------------------------------
471 double cellStableDt(int cell) const;
472 double nodeStableDt(int node) const;
473
477 int assignTiers(double& dt0);
478
491 void fireFaces(const std::vector<int>& faces, double dt0);
492 void fireCells(const std::vector<int>& cells, double dt0,
493 const FvStepForcing& forcing);
494 void fireNodes(const std::vector<int>& nodes, double dt0,
495 const FvStepForcing& forcing);
496 void runMacroCycle(double dt0, int nsub, const FvStepForcing& forcing);
497
502 void settleAccumulators();
503
505 void saveState();
506 void restoreState();
507
510 void takeSubstep(double dt, const FvStepForcing& forcing);
511
512 // -- implicit pressurized head update (slot program R2a) ----------------
516 bool anyPressurizedCell() const;
520 bool nodePressFolded(int n) const;
521 PressurizedView pressView(const FvStepForcing& forcing);
522 PressurizedHeadSolver press_;
524 bool press_step_ = false;
525
530 void rkSave();
531 void rkAverage(const FvStepForcing& forcing);
532
539 void faceSide(int face, int cell, int node, double zstar, int dir,
540 double u_interior, kernels::FaceState& out,
541 double& i1_unreconstructed, double& z_side,
542 bool measure_only) const;
543
544 NetworkMeshData* mesh_ = nullptr;
545 NetworkStateData* state_ = nullptr;
546
550 const FvStepForcing* forcing_ = nullptr;
551 FvOptions opts_{};
552
553 // Face scratch. corr_l/corr_r are the Audusse well-balanced corrections
554 // g·(I₁(h_K) − I₁(h*_K)) — per-cell, not per-face, which is why they are
555 // stored separately from the shared flux.
556 std::vector<double> f_mass_, f_mom_, f_sstar_, f_corr_l_, f_corr_r_;
557
563 std::vector<double> f_phi_l_, f_phi_r_;
564
566 std::vector<double> f_phi_flux_;
567
570 std::vector<double> lo_flux_, anti_flux_, td_, anew_, rplus_, rminus_;
571
575 std::vector<kernels::FaceState> f_state_l_, f_state_r_;
576 std::vector<kernels::FaceFlux> f_flux_;
577 bool hllc_ = true;
578
580 std::vector<double> cell_slope_;
581
588 std::vector<double> cell_eta_slope_, cell_u_slope_;
589
595 std::vector<char> cell_ho2_;
596
597
598 // Cell scratch.
599 std::vector<double> cell_eta_;
600 std::vector<double> cell_u_;
601
606 std::vector<double> uf_grad_;
607
612 void computeUfGradients(const std::vector<int>* cells);
613
614 // -- TPA pressure closure (issue #156 Phase 4) ---------------------------
615 bool tpa_ = false;
616 std::vector<uint8_t> tpa_scratch_;
617 std::vector<uint8_t> save_tpa_;
618
621 void updateTpaFlags();
622
624 bool tpaCell(std::size_t uc) const noexcept {
625 return tpa_ && state_->cell_tpa[uc] != 0;
626 }
627 std::vector<double> cell_q_int_;
631
632 // Node scratch.
633 std::vector<double> node_exch_;
634 std::vector<double> node_in_, node_out_;
635 std::vector<double> flood_vol_;
636 std::vector<uint8_t> inlet_control_;
637
638 // Step-rejection snapshot. A cell can cross the crown INSIDE a substep,
639 // taking its celerity from the free-surface value to the slot value — a
640 // factor of ~20 on a 3 ft pipe — so a step sized on the pre-step state can
641 // violate CFL by more than an order of magnitude exactly when the model is
642 // doing something interesting. Re-censusing the POST-step state and rolling
643 // back when it disagrees is the principled fix; a heuristic fill-rate cap
644 // is not, because the stiffness ratio depends on FV_SLOT_CELERITY.
645 std::vector<double> save_cell_a_, save_cell_q_, save_cell_phi_;
646 std::vector<double> save_node_vol_, save_node_head_;
647 std::vector<double> save_exch_, save_in_, save_out_, save_flood_, save_qint_,
648 save_carry_;
649
650 // SSP-RK2 stage-1 snapshot. Separate from the rejection snapshot above:
651 // a rejected RK2 step has to roll back to Uⁿ, and Uⁿ must survive the
652 // stage-1 save that the rejection machinery performs.
653 std::vector<double> rk_cell_a_, rk_cell_q_, rk_cell_phi_;
654 std::vector<double> rk_node_vol_, rk_node_head_;
655 std::vector<double> rk_exch_, rk_in_, rk_out_, rk_flood_, rk_qint_,
656 rk_carry_;
657 std::vector<uint8_t> rk_tpa_;
658
661 static constexpr double kStepAcceptRatio = 0.5;
662 static constexpr int kMaxStepRetries = 8;
663
671 static constexpr double kMinSubstep = 1.0e-6;
672
673 // Divergence guard (issue #156 R3). The retry loop above is sound for CFL
674 // violations, which shrink away with dt — but a dt-INDEPENDENT
675 // amplification (measured: RK2 x TPA on the study's e3 siphon grows the
676 // crown head x20-40 per millisecond at FV_CFL 0.9, 0.45 and 0.25 alike)
677 // fails all 8 retries identically and the loop then ACCEPTS the diverged
678 // step silently. These bounds are physical absurdities, not tolerances: no
679 // water transient carries particle velocity past 1000 ft/s (celerity is
680 // not particle speed), and 10,000 ft of pressure head is 300 bar. A run
681 // that crosses either has left the physics and must say so instead of
682 // scoring OK (P6 finding F3). Checked once per routing step, O(cells).
683 static constexpr double kDivergedVelocity = 1000.0;
684 static constexpr double kDivergedDepth = 10000.0;
685 int div_link_ = -1;
686 double div_value_ = 0.0;
687 const char* div_what_ = nullptr;
688
689 // Work lists (plan §5.2.1). `halo_` is the compaction safety margin: a wet
690 // front advances at most CFL cells per substep, so a halo of `rebuild_
691 // interval_` cells keeps a stale list conservative between rebuilds.
692 std::vector<int> active_faces_;
693 std::vector<char> cell_active_;
694 std::vector<char> halo_prev_;
695 bool lists_valid_ = false;
696 int since_rebuild_ = 0;
697 static constexpr int kRebuildInterval = 8;
698
699 // Local time stepping (plan §3.3). Tiers are assigned only at a
700 // synchronisation point — never mid-cycle — because a cell re-tiered
701 // between firings would either skip a flux it owes or drain one twice.
702 std::vector<std::uint8_t> cell_tier_, face_tier_, node_tier_;
703 std::vector<std::vector<int>> cells_by_tier_, faces_by_tier_, nodes_by_tier_;
704
710 std::vector<std::vector<int>> due_f_upto_, due_c_upto_, due_n_upto_;
711
716 bool lts_valid_ = false;
717 int lts_countdown_ = 0;
718 double lts_dt0_ = 0.0;
719 static constexpr int kRetierEveryCycles = 8;
720
724 std::vector<double> acc_a_, acc_q_, acc_nvol_;
725
730 int lts_tiers_ = 1;
731 std::array<long, kMaxLtsTiers> tier_occupancy_{};
732
733 // Statistics.
734 long last_nsteps_ = 0;
735 double last_h_ = 0.0;
736 double suggested_h_ = 0.0;
737 long total_steps_ = 0;
738 long total_flux_ = 0;
739 double min_h_ = 0.0;
740 double sim_time_ = 0.0;
741
742 // dt-argmin attribution (slot program R0): who owned the binding CFL
743 // element, counted once per censusDt (global path) / assignTiers (LTS
744 // path). `mutable` because censusDt is const; these are telemetry.
745 mutable long dt_argmin_pressurized_ = 0;
746 mutable long dt_argmin_band_ = 0;
747 mutable long dt_argmin_free_ = 0;
748 mutable long dt_argmin_node_ = 0;
749 double active_sum_ = 0.0;
750 double active_min_ = -1.0;
751 double active_max_ = -1.0;
752 long active_n_ = 0;
753
757 double dt_cache_ = 0.0;
758
764 double dt_census_ = 0.0;
765
768 int census_count_ = 0;
769};
770
771} // namespace openswmm::fv
772
773#endif // OPENSWMM_ENGINE_FV_EXPLICIT_FV_SOLVER_HPP
Single-source scalar kernels for the explicit FV 1D network solver.
Option struct for the explicit finite-volume 1D network solver.
Backend-neutral interface for the explicit FV 1D network integrator.
SoA storage for the 1D finite-volume network mesh and its state.
Implicit acoustic/slot head update on the pressurized subset (slot program R2a, Strategy E).
ExplicitFvSolver(const ExplicitFvSolver &)=delete
const std::array< long, kMaxLtsTiers > & tier_occupancy() const noexcept
Definition ExplicitFvSolver.hpp:162
int lts_tiers() const noexcept
Definition ExplicitFvSolver.hpp:158
const std::vector< uint8_t > & inlet_control() const noexcept
Definition ExplicitFvSolver.hpp:102
bool is_initialized() const noexcept override
True once initialize() has completed and the solver is ready.
Definition ExplicitFvSolver.hpp:77
const std::vector< double > & node_flood_volume() const noexcept
Per-node flooding VOLUME (ft³) over the last advance().
Definition ExplicitFvSolver.hpp:107
long last_num_steps() const noexcept override
Substep count used by the last advance() call.
Definition ExplicitFvSolver.hpp:73
double last_step_size() const noexcept override
Last internal substep size (s).
Definition ExplicitFvSolver.hpp:74
void reinitialize(double t0) override
Reinitialize at t0 after external state edits (hot start, API writes).
Definition ExplicitFvSolver.cpp:240
double nodeVolumeFromDepth(int node, double depth) const
Definition ExplicitFvSolver.cpp:292
bool divergence(Divergence &d) const noexcept override
Definition ExplicitFvSolver.hpp:78
ExplicitFvSolver & operator=(const ExplicitFvSolver &)=delete
RunStats run_stats() const noexcept override
Read cumulative statistics. Default: zeros (backend has no counters).
Definition ExplicitFvSolver.cpp:256
const std::vector< double > & dummy_volume() const noexcept
Definition ExplicitFvSolver.hpp:115
void finalize() override
Release all backend resources.
Definition ExplicitFvSolver.cpp:251
double advance(double t_current, double t_target, const FvStepForcing &forcing) override
Definition ExplicitFvSolver.cpp:3694
const std::vector< std::uint8_t > & node_passthrough() const noexcept
Definition ExplicitFvSolver.hpp:125
double suggested_step() const noexcept override
Definition ExplicitFvSolver.hpp:75
const std::vector< double > & node_exchange() const noexcept
Definition ExplicitFvSolver.hpp:89
const std::vector< double > & node_outflow_volume() const noexcept
Definition ExplicitFvSolver.hpp:98
const std::vector< double > & node_inflow_volume() const noexcept
Definition ExplicitFvSolver.hpp:97
~ExplicitFvSolver() override=default
const std::vector< double > & cell_flow_integral() const noexcept
Definition ExplicitFvSolver.hpp:148
void initialize(NetworkMeshData &mesh, NetworkStateData &state, const FvOptions &opts) override
One-time setup. mesh, state and opts must outlive the solver.
Definition ExplicitFvSolver.cpp:58
const std::vector< std::uint8_t > & node_publish_stage() const noexcept
Definition ExplicitFvSolver.hpp:140
Abstract time integrator for the explicit FV 1D network.
Definition INetworkSolver.hpp:121
Definition ExplicitFvSolver.cpp:25
@ RK2
SSP-RK2 (Heun) — strong-stability-preserving.
Definition FvOptions.hpp:77
Definition Node.cpp:38
Definition ExplicitFvSolver.hpp:52
Knobs for FLOW_ROUTING FV.
Definition FvOptions.hpp:123
Definition INetworkSolver.hpp:53
Definition INetworkSolver.hpp:188
SoA mesh geometry and topology for the FV network solver.
Definition NetworkMeshData.hpp:176
std::vector< int > cell_geom
index into geom
Definition NetworkMeshData.hpp:182
std::vector< int > node_face_idx
face indices
Definition NetworkMeshData.hpp:377
std::vector< int > face_cl
left cell (-1 ⇒ node on the left)
Definition NetworkMeshData.hpp:214
std::vector< uint8_t > node_can_pond
Definition NetworkMeshData.hpp:397
std::vector< int > node_face_ptr
[n_nodes + 1] row pointers
Definition NetworkMeshData.hpp:376
std::vector< FvGeometry > geom
Definition NetworkMeshData.hpp:432
Mutable solver state — the conserved variables and the node volumes.
Definition NetworkMeshData.hpp:489
std::vector< double > cell_h
depth (ft)
Definition NetworkMeshData.hpp:497
std::vector< double > node_volume
stored volume (ft³)
Definition NetworkMeshData.hpp:502
Non-owning view over everything the species kernels read and write.
Definition SpeciesTransportKernels.hpp:96