Expand description
EPANET-RS: A fast, modern re-implementation of the EPANET2 hydraulic solver
§Background
The EPANET2 solver has been the industry standard for hydraulic network simulation for decades due to its robustness, numerical stability, and extensive validation. Its algorithms and results are widely trusted in both academic and industrial applications.
The core EPANET2 codebase, however, is several decades old and written in C, making it difficult to maintain, extend, and optimize using modern software engineering practices. In addition, the original implementation predates modern CPU architectures and therefore does not fully exploit multi-core processors or SIMD (Single Instruction, Multiple Data) capabilities.
Modern applications of the EPANET solver, such as monte-carlo simulations, leak detection algorithms and real-time digital twins of huge networks require a more modern implementation, with better performance and maintainability.
epanet-rs is a modern reimplementation of the EPANET2 hydraulic solver written in Rust,
designed to preserve the original algorithms and numerical behavior while enabling safer memory
management, improved maintainability, and performance optimizations through multi-threading and
SIMD acceleration.
epanet-rs runs about as fast as the original EPANET2_3 solver in sequential mode, and up to 5
times faster in parallel mode for extended period simulations for supported networks (no
tanks/controls).
§Design Goals
- Numerical Parity with EPANET2_3 solver (
hyd_2_3branch), ensuring identical or equivalent results for the same input - High Performance through multi-threading, SIMD acceleration and a modern, faer based sparse solver
- Parallelization of the solver loop by rewriting the solver to return vectors of heads and flows instead of in-place assignment
- Memory Safety through Rust’s ownership and borrowing system
- Modern API with a focus on ease of use and parallelization
- Backwards Compatibility with EPANET2_3 network API methods
§Crate layout
The public API is organised into a few top-level modules:
model— data structures describing a hydraulic network (nodes, links, patterns, curves, controls, options). Networks can be loaded from EPANET.inpfiles or constructed programmatically.io— parsers and serializers for EPANET.inpfiles and JSON/MessagePack snapshots of amodel::network::Network.solver— the low-level hydraulic solver.solver::hydraulicsolver::HydraulicSolverperforms a single-step pressure/flow solve on a pre-built sparsity pattern, andsolver::state::SolverStateholds the per-step mutable state (flows, heads, demands, link statuses, …).simulation— the high-levelsimulation::Simulationdriver that mirrors the EPANETEN_openH/EN_initH/EN_runH/EN_nextH/EN_solveHworkflow, including tanks, controls and reporting.ffi— a subset of the EPANET C API exposed asextern "C"functions, for use from other languages.error— error types returned by the parser (error::InputError) and solver (error::SolverError).
§Examples
§Loading and solving a network
Use the high-level simulation::Simulation API to run a full extended-period simulation and
collect flows, heads and demands at every report step:
use epanet_rs::simulation::Simulation;
let mut simulation = Simulation::from_file("tests/pump.inp")?;
// `parallel = false` runs the EPANET-style sequential loop, which supports
// tanks, controls, and quality. Pass `true` for parallel solving when the
// network has no tanks or pressure controls.
let results = simulation.solve_hydraulics(false)?;
// `results.flows[step][link_index]` / `results.heads[step][node_index]` /
// `results.demands[step][node_index]` are indexed by report step.
println!("solved {} report steps", results.heads.len());§Running a parallel simulation using the low-level API
The following example shows how to drive the solver directly. A leak-detection scenario is
simulated for every junction in parallel with rayon, starting
from a shared, pre-solved initial state. For each junction an extra demand of 5 is added to
mimic a leak, the network is re-solved, and the resulting head at node "1" is recorded.
This pattern generalises to monte-carlo and fire-flow simulations on networks without tanks or
pressure controls.
use epanet_rs::model::network::Network;
use epanet_rs::model::node::NodeType;
use epanet_rs::solver::hydraulicsolver::HydraulicSolver;
use epanet_rs::solver::state::SolverState;
use rayon::prelude::*;
let network = Network::from_file("tests/pump.inp")?;
let solver = HydraulicSolver::new(&network)?;
// Build the initial state and solve once. The resulting state is used as a
// warm start for every parallel scenario, which accelerates convergence.
let mut initial = SolverState::new_with_initial_values(&network);
initial.apply_patterns(&network, 0);
let initial = solver.solve(&network, &initial)?;
// Indices of all junctions (only junctions carry a demand that can leak).
let junctions: Vec<usize> = network.nodes.iter().enumerate()
.filter(|(_, n)| matches!(n.node_type, NodeType::Junction(_)))
.map(|(i, _)| i)
.collect();
// Resolve the monitoring node once. Pressure = head - elevation.
let monitor = network.node_map["1"];
let monitor_elevation = network.nodes[monitor].elevation;
// For each junction, add a leak demand of 5, re-solve in parallel, and
// collect the resulting pressure at node "1".
let pressures: Vec<f64> = junctions.par_iter()
.map(|&j| {
let mut state = initial.clone();
state.demands[j] += 5.0;
let solved = solver.solve(&network, &state)?;
Ok(solved.heads[monitor] - monitor_elevation)
})
.collect::<Result<_, epanet_rs::error::SolverError>>()?;
println!("simulated {} leak scenarios", pressures.len());§Modifying and constructing networks
Networks can also be built and mutated programmatically through the strongly-typed *Data /
*Update structs re-exported from model::network. Topology changes (adding or removing
nodes/links, changing a link’s endpoints) invalidate any cached solver; property-only changes
(e.g. pipe roughness, pump speed) are tracked incrementally and applied to the solver state when the network is solved.
use epanet_rs::simulation::Simulation;
use epanet_rs::model::network::{Network, JunctionData, PipeData, ReservoirData, PipeUpdate};
use epanet_rs::model::link::LinkStatus;
use epanet_rs::model::options::HeadlossFormula;
use epanet_rs::model::units::FlowUnits;
let mut network = Network::new(FlowUnits::LPS, HeadlossFormula::DarcyWeisbach);
network.add_reservoir("R1", &ReservoirData {
elevation: 100.0,
..Default::default()
})?;
network.add_junction("J1", &JunctionData {
elevation: 50.0,
demands: vec![epanet_rs::model::demand::Demand {
basedemand: 1.0,
pattern: None,
pattern_index: None,
name: None,
}],
..Default::default()
})?;
network.add_pipe("P1", &PipeData {
start_node: "R1".into(),
end_node: "J1".into(),
length: 1000.0,
diameter: 200.0,
roughness: 0.1,
minor_loss: 0.0,
check_valve: false,
initial_status: LinkStatus::Open,
vertices: None,
})?;
let mut simulation = Simulation::new(network);
// solve the network for a single time step
simulation.run_hydraulics()?;
// update the pipe roughness
simulation.network.update_pipe("P1", &PipeUpdate {
roughness: Some(0.2),
..Default::default()
})?;
// solve the network again
simulation.run_hydraulics()?;
Modules§
- constants
- Physical constants, unit-conversion factors and solver tolerances.
- error
- Error types returned by the input parser and the hydraulic solver.
- ffi
- C-compatible FFI layer implementing a subset of the EPANET 2.3 toolkit API.
- io
- Parsers and serializers for EPANET
.inp, JSON and MessagePack files. - model
- Data structures for representing and modifying a hydraulic network (nodes, links, patterns, curves, controls and options).
- simulation
- High-level extended-period simulation driver for sequential and parallel hydraulic solves. The simulation driver is responsible for managing the simulation workflow for sequential and parallel solving of hydraulic networks and collecting results at each report step.
- solver
- Low-level hydraulic solver and its per-step state, results and matrix assembly. The hydraulic solver works by iteratively solving the system of equations for nodal heads and link flows until the flow errors are within the specified tolerance.
- utils
- Utilities for parsing time strings, reading EPANET
.binoutput files and validating results.