Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Performance

Both engines are benchmarked against their predecessors: the drainage engine against SWMM 5.2.4 and the water distribution engine against EPANET, on published networks. This page carries the published figures, the method behind them, and the tools to reproduce or check them on your own machine.

Measured against SWMM 5.2.4

Release builds of both engines, the same machine (Apple M-series), best of three runs each, timed end to end: parse, solve, and write. SWMM runs in its own default surcharge closure. Where a model sets INERTIAL_DAMPING, SWMM is given the setting Hydra substitutes, so the comparison is between implementations and not between documented modelling deviations.

WorkloadHydra / SWMM runtime
Bellinge (published, 1,020 nodes), 48 h including its storm1.00
Bellinge, the storm hours alone (03:00 to 12:00)0.91
Bellinge, a dry-weather day (12 h)1.24
SWMM test corpus, models running 0.2 to 1 s1.08

The corpus is the predecessor’s own regression suite. Its many sub-0.2-second models are dominated by process start and parse and are excluded from the ratio, because a single aggregate over them measures process creation rather than either solver.

The stepping behind these numbers is error-controlled where SWMM’s is not: the routing step is steered by a per-step local error estimate (hydraulics specification, section 6.5), and on these runs under 2% of Hydra’s steps end unconverged against SWMM’s 36% across the same 48 hours (56% during the storm itself). The dry-weather ratio is the cost of that control on a network whose regulators cycle continuously in dry weather; routing continuity on that day reads 0.34% for Hydra against SWMM’s 0.54%.

Measured against SWMM 6

SWMM 6 here means openswmm.engine 6.0.0-alpha.3, the community continuation of EPA SWMM: a C++ rework of the 5.x engine with OpenMP threading over its routing iteration. It is an alpha, so these numbers are pinned to that version, measured August 2026 with the same method as above; expect them to move as it matures. Its results track SWMM 5.2.4 very closely (99.96% or more of all-period node depths within 0.1 on the network below), so the accuracy comparisons on this page carry over to it unchanged.

WorkloadHydraSWMM 6, 1 threadSWMM 6, 4 threads
Bellinge, 48 h50 s serial, 26 s at width 655 s26 s

Per core the engines are at parity: Hydra’s serial run edges SWMM 6’s single thread. Threaded, both engines run the whole routing iteration as one parallel region and land at the same mark, with Hydra using fewer processor seconds to get there. Both engines hold results bit-identical across thread counts.

Width, for integrators

The SDK’s threads cargo feature runs the routing iteration and the statistics pass across a persistent worker team, with the width taken from the model’s own THREADS option. Results are byte-identical at every width: the specification fixes every accumulation order, and a test holds serial and threaded runs, results and report both, to the same bytes. So measured, same method: Bellinge at width 6 runs 26 s, 0.52 of SWMM 5’s serial time on the same window. The official binaries enable the feature: a model that never asks for width runs the serial arm, bit for bit, at a compiled-in cost of about 2%, and a model whose THREADS asks for width takes the whole gain.

Accuracy rides the same runs. On Bellinge, node depths match SWMM’s own Preissmann-slot closure on 992 of 1,020 nodes. Every remaining difference across the corpus is either fixed or documented in the specification with its cause and a source citation; the specifications’ correspondence notes are the index of them.

Reproducibility

Hydra is deterministic on a given machine and build. The same model run twice produces a byte-identical results file and report. That holds at any THREADS width, because the specification fixes every accumulation order and a test compares serial and threaded runs byte for byte.

Across platforms it is weaker. A Windows run and a macOS run of the same drainage model can report slightly different totals. Two things combine to produce that.

The first is the system maths library. Addition, subtraction, multiplication, division and square root are exact to the last bit on every processor, because IEEE 754 requires it. Powers, logarithms and inverse trigonometric functions carry no such requirement, and each platform ships its own implementation of them. The engine calls them constantly: Manning’s equation takes a two-thirds power for every channel on every step, and circular sections take an arc cosine. Comparing the implementation the desktop builds call against the one the browser build links, the two disagree by one unit in the last place on about 10% of those power calls and 16% of the arc cosine calls. Desktop operating systems ship different implementations from each other too.

The second is the error-controlled step. Hydra picks its routing step by comparing an estimated local error against a tolerance, and a last-bit difference can move an estimate across that line. The two runs then take different step sequences and their trajectories separate. What starts as a difference in the last bit becomes a small but readable difference in reported volumes.

The scale is worth knowing, because it is easy to mistake for a defect. Changing one junction invert in the published Bellinge network by a single nanometre moves the reported outflow by 16 m³ out of 26,542, which is 0.06%. Routing continuity is unmoved at about 0.12% either way, so both runs conserve equally well. A difference between platforms is of that order.

This is a property of time-marching drainage solvers rather than something particular to Hydra. Put the same nanometre through SWMM 5 and its reported outflow moves by 2 m³. Hydra moves further because it measures the local error at every step and adapts to it, which is the same mechanism that holds its continuity to 0.12% on that run where SWMM 5 reads 0.35%. The sensitivity and the accuracy have the same source.

Distribution models do not behave this way. Each hydraulic step is an equilibrium solve rather than a march, so a perturbation that small does not survive the step, and the same test leaves Hydra and EPANET both reporting identical results down to the byte. Expect water distribution runs to agree across platforms, and check the report if they do not.

When comparing two runs from different machines:

  • Read the routing continuity table in the report rather than a volume total on its own. That table is the engine’s own account of what it conserved, and it should agree closely.
  • Expect volume totals to agree to a few hundredths of a percent instead of exactly.
  • Treat a difference larger than the run’s own continuity error as worth investigating. Anything smaller sits inside the numerical method’s margin, where neither run is the more correct one.

Also check that both machines run the same version (hydra -V) and that neither binary was built with just release-native, which tunes code generation to the host processor.

If you need agreement to the last bit across different platforms, run on one platform, or open an issue. Pinning these functions to a single implementation is possible and costs some speed.

Water distribution, measured against EPANET

The same method: release builds of both engines, the same machine, best of three runs, timed end to end, over the published research networks bundled in tests/benchmarks/wds/.

NetworkHydra / EPANET runtimewith --tank-tolerance 0
Balerma, Exeter, Kentucky 8/9/10, NY Tunnels0.9 to 1.2(sub-10 ms; process start dominates)
BWSN-2 (12,527 nodes)1.291.16
L-Town1.230.83
D-Town1.300.96
Micropolis1.321.07
Richmond2.041.20 (tens of ms; start shares dominate)

At larger sizes

The published research networks stop at BWSN-2’s 12,527 nodes, which is smaller than many real systems and small enough to hide how the ratio moves with size. Two networks of about 46,000 junctions, one a real distribution system and one generated by scripts/make_wds_benchmark.py, compared at a matched convergence tolerance and with both engines using the same first-order tank scheme:

NetworkJunctionsHydra / EPANET
Generated m8,1001.00
Generated l46,2251.26
A real 46,000-junction system46,1711.26

So the engines are level to around ten thousand junctions and Hydra is about a quarter slower at forty-six thousand. Two independently built networks agreeing on 1.26 is the reason to believe it.

Comparing at a matched tolerance is the point. Run the same real network at the loose Accuracy 0.1 it ships with and the ratio reads 2.7, because EPANET stops on its flow-change criterion alone while Hydra also requires nodal mass balance (hydraulics specification, section 3.8, criterion 5) and so keeps iterating. That is the same kind of purchase as the tank integration below: more work for a stricter answer, not slower arithmetic. Per iteration the two engines are close.

just perf-check-wds gates these numbers, and the generated networks are written on demand rather than committed.

The first column’s ratios above 1 are a documented purchase, not a loss, and the second column is the proof. Hydra integrates tank levels with a second-order predictor-corrector carrying a per-step error estimate, where EPANET takes one uncontrolled first-order step; on a tanked network that costs a corrector solve per step plus the error control’s retries (on Richmond’s eighteen level-switched pumps, most of the run). Setting the tolerance to zero (--tank-tolerance 0, or level_err_tol = 0 in the session API) restores EPANET’s own scheme exactly, and the engines then step in lockstep: on Micropolis, 785 hydraulic solves totalling 6,698 iterations against EPANET’s 780 and 6,701, at parity or faster on every network large enough to measure. The default keeps the error bound: solver speed is not where the time goes, and the bound is worth what it costs.

Accuracy rides the same runs: 95.4% of 2.6 million node pressures agree within 0.1 (in each model’s own pressure unit) across the set, and every residual is classified in the specifications’ correspondence notes. The survivors are three kinds, none a solver disagreement: isolated pockets behind closed links, whose head is physically undefined and where EPANET’s own value wanders between hours; runs both engines halt identically under Unbalanced Stop, where only the abandoned final iterate differs; and threshold events, where centimetre-scale tank drift between two different integrators flips a level-switched rule by one time step and the trajectories re-converge.

Reproducing the comparison

The tracked baseline harness times both engines when SWMM is available:

HYDRA_SWMM=/path/to/runswmm just perf-check

Without HYDRA_SWMM it times Hydra alone against the recorded baseline. The water networks are timed by just bench-report; comparing against EPANET means building EPANET’s runepanet from its repository and timing the same models with both, best-of-N, as above. The baseline (tests/benchmarks/uds/baseline.json) is tracked in the repository and gates every change on both runtime (25% band) and peak memory (15% band), so the published figures cannot quietly rot: a change that slows a benchmark model or grows its memory fails the check.

Timing methodology worth copying: always best-of-N, never single-shot (single runs on the 24-hour event scattered across 57 to 60 seconds where best-of-three gives 50.4), and read the report for errors rather than trusting the exit code, because SWMM exits zero after refusing a model.

Checking for regressions

The bundled networks are worth timing against each other. Comparing one Hydra build with another over a fixed set answers the question that matters during development: did this change make the solver slower? The arbitrariness that makes these networks useless as a public claim costs nothing here, because both runs meet the same models.

just bench-report

This builds the release CLI and runs scripts/benchmark.py, which times each of the networks in tests/benchmarks/wds/ and prints a Markdown table. Pass --runs N to change the sample count, or --hydra PATH to time a specific binary, which is how two builds are compared.

Note that it reads only the bundled directory, so it measures Hydra against Hydra. It is not a way to time your own model.

Building for maximum speed

The release profile already enables fat LTO and a single codegen unit. For the best local performance, build with native CPU features:

just release-native

This tunes the binary for the machine it is built on (-C target-cpu=native); such binaries are not portable to older CPUs.

Solver micro-benchmarks

For work on the solver itself, the criterion suite times the hydraulic solve step (warm and cold) in isolation:

just bench