Skip to main content

generalized-maxflow

Maximum flow through a network whose edges change the amount that passes through them.

Every edge carries a capacity and a multiplier. Pushing x units into an edge consumes x units of its capacity and delivers multiplier · x units at the far end, so flow can leak away (multiplier below 1), be conserved (exactly 1) or be amplified (above 1). This is the generalized maximum flow problem, and it is what you want for power transmission with line losses, currency conversion, evaporation and spoilage in logistics, or any pipeline whose stages have a yield.

The interface is a JSON adjacency list, the same for every binding:

{
  "source": "s",
  "sink": "t",
  "adjacency": {
    "s": [{"to": "a", "capacity": 10, "multiplier": 1.0}],
    "a": [{"to": "t", "capacity": 5,  "multiplier": 2.0}]
  }
}
{
  "status": "optimal",
  "max_flow": 10,
  "source_outflow": 5,
  "paths": [
    {
      "kind": "path",
      "flow_in": 5, "flow_out": 10, "gain": 2,
      "nodes": ["s", "a", "t"],
      "steps": [
        {"edge": 0, "from": "s", "to": "a", "direction": "forward", "multiplier": 1, "flow_in": 5, "flow_out": 5},
        {"edge": 1, "from": "a", "to": "t", "direction": "forward", "multiplier": 2, "flow_in": 5, "flow_out": 10}
      ],
      "bottleneck": {"edge": 1, "from": "a", "to": "t", "direction": "forward", "step": 1}
    }
  ],
  "certificate": {"no_augmenting_path": true, "no_generating_cycle_reaching_sink": true}
}

Note what the answer is not: 5. The constriction is a -> t, which admits only 5 units, but those 5 units are doubled on the way through. A generalized flow solver has to account for gain downstream of the constriction.

Try it

→ wesboyt.github.io/generalized-maxflow

The solver compiled to WebAssembly, running in the page: the worked examples below plus a box for your own graphs. Nothing is uploaded — the whole solver is 148 KB of WebAssembly and every graph is solved locally.

To run it yourself:

bindings/wasm/build.sh                       # needs the Emscripten SDK
cp bindings/wasm/pkg/{index.mjs,gmaxflow.js,gmaxflow.wasm} docs/demo/
python -m http.server -d docs/demo 8000      # then open localhost:8000

Every push also builds it in CI, downloadable as the demo-site artifact of the demo workflow; running that workflow by hand republishes the hosted copy.

Install

pip install generalized-maxflow

The wheels are tagged py3-none-<platform>: the solver is a plain C library loaded through ctypes, never linked against libpython, so one wheel per OS and CPU serves every CPython from 3.8 up, the free-threaded builds, and PyPy. There are wheels for Linux (x86-64 and aarch64, glibc and musl), macOS (x86-64 and arm64) and Windows (x86-64 and ARM64); anywhere else pip builds the sdist, which needs CMake and a C++17 compiler. The solve releases the GIL.

npm install generalized-maxflow   # WebAssembly build, browser and Node

From source you need CMake 3.18 and a C++17 compiler:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
./build/gmaxflow graph.json

Use it

Python:

import gmaxflow

answer = gmaxflow.solve(graph)          # graph is a dict or JSON text
print(answer["max_flow"])

for path in answer["paths"]:
    print(path["nodes"], path["flow_in"], "->", path["flow_out"],
          "constricted at", path["bottleneck"]["edge"])

JavaScript / WebAssembly:

import createSolver from "generalized-maxflow";

const solver = await createSolver();
const answer = solver.solve(graph);     // takes and returns plain objects
console.log(answer.max_flow);

Command line (reads a file or stdin, writes JSON):

gmaxflow graph.json
cat graph.json | gmaxflow --compact

C++:

#include "gmf/gmf.hpp"

gmf::Problem problem;
problem.num_nodes = 3;
problem.source = 0;
problem.sink = 2;
problem.edges = {{0, 1, 10.0, 1.0}, {1, 2, 5.0, 2.0}};

const gmf::Result result = gmf::solve(problem);
// result.max_flow == 10, result.augmentations holds the filled paths

Input

field meaning
source, sink node labels; any string or number. Aliases: src/start, dst/target/end
adjacency {tail: [edge, ...]}, or an array whose index is the tail
edges a flat list; each edge names its own from
nodes optional, fixes the node ordering
options see below

An edge is an object with to, capacity and multiplier (aliases: head/v, cap, gain/mult; from/tail/u when it is not implied by the adjacency key), or a positional array — [to, capacity, multiplier] inside an adjacency list, [from, to, capacity, multiplier] in an edges list. A missing or null capacity means uncapacitated. Capacities must be non-negative and multipliers finite and strictly positive.

option default meaning
epsilon 1e-12 relative tolerance, scaled by the largest capacity
max_iterations size-derived stop after this many augmentations
collect_paths true set false to drop the paths array and save memory
cancel_generating_cycles true set false to reject amplifying inputs instead of solving them
use_potentials true set false to force the slow reference search (for cross-checking)
fat_path true run the Tardos–Wayne Fat-Path scaling phases; false is plain highest-gain augmentation
gain_scaling true round gains to powers of $(1+\xi)^{1/n}$ inside those phases
pretty false indent the output

Output

status is one of optimal, unbounded, iteration_limit, suboptimal, generating_cycle_rejected or invalid_input, and optimal is a boolean.

max_flow is what arrives at the sink and source_outflow is what left the source; with lossy or amplifying edges these differ, and that is the point.

paths is the sequence of augmentations in the order they were filled, each with the nodes it visits, the steps it took, the flow entering and leaving, the gain, and the bottleneck — the constricting edge that decided how much this path could carry. A step's direction is backward when the augmentation pushed flow back over an edge to re-route earlier flow. A kind of flow_generating_cycle means the entry is a cycle whose multipliers multiply to more than one, together with the route carrying the flow it manufactured.

certificate is the proof the answer is maximum, and stats reports iterations, searches, the Fat-Path phases and augmentations, the cycles cancelled, a timing breakdown by phase and the solver's own memory footprint.


The algorithm

How a constriction is found and how the update propagates

1. The problem

Let $G=(V,E)$ be a directed graph with a source $s$ and a sink $t$. Each arc $e\in E$ has a capacity $u(e)\in[0,\infty]$ and a multiplier (or gain) $\gamma(e)>0$. A generalized flow is $f:E\to\mathbb{R}$ with

0 \le f(e) \le u(e) \quad \forall e \in E,
\sum_{e=(w,v)} \gamma(e)f(e) \;-\!\! \sum_{e=(v,w)} f(e) \;=\; 0
\qquad \forall v \in V\setminus\{s,t\}.

Pushing $f(e)$ units into $e=(v,w)$ at $v$ delivers $\gamma(e)f(e)$ at $w$. The objective is the net amount arriving at the sink,

|f| \;=\; \sum_{e=(w,t)} \gamma(e)f(e) \;-\!\! \sum_{e=(t,w)} f(e),

to be maximised. With $\gamma \equiv 1$ this is ordinary maximum flow. It is a linear program, which is how tools/lp_check.py verifies the solver independently.

For a path or cycle $P$ write $\gamma(P)=\prod_{e\in P}\gamma(e)$. A cycle $C$ with $\gamma(C)>1$ is flow-generating: traversing it returns more than was put in.

2. The residual network

Each arc $e=(v,w)$ contributes two residual arcs,

arc capacity multiplier
$e$, forward $u(e)-f(e)$ $\gamma(e)$
$e^{-1}=(w,v)$, backward $\gamma(e)f(e)$ $1/\gamma(e)$

The backward capacity is what the arc delivered, and its multiplier is the reciprocal, because retracting $y$ units at $w$ returns $y/\gamma(e)$ at $v$. Pushing $x$ into either direction is the single rule

r(a) \mathrel{-}= x, \qquad r(a^{-1}) \mathrel{+}= x\,\gamma(a).

Reverse arcs are required for correctness, not speed. Greedy highest-gain filling without them gets stuck exactly as plain Ford–Fulkerson does: the instance in examples/rerouting.json has optimum 3 and greedy reaches 2. Note also that the reverse of a lossy arc is a gain-amplifying one, so after the first augmentation no search may assume all multipliers are below 1.

3. The bottleneck obeys the chain rule

Along a path $P=(a_1,\dots,a_k)$ the amount entering $a_i$ when $x$ units enter $a_1$ is $x\prod_{j<i}\gamma(a_j)$. The largest feasible $x$ is therefore

\delta(P) \;=\; \min_{1\le i\le k} \; \frac{r(a_i)}{\prod_{j<i}\gamma(a_j)},

and the arc attaining the minimum is the constricting edge. It is not the arc of smallest residual capacity: gain downstream of a constriction counts.

When a walk uses an arc in both directions — a cycle one way and the route carrying its excess the other — the two uses cancel rather than compete. Writing $\alpha$ for the total coefficient on the forward arc of an edge and $\beta$ for that on its backward arc, the net change to that edge's flow per unit $x$ is $\nu = \alpha - \beta/\gamma$, and the limit is

x \;\le\; \begin{cases}
 r(e)\,/\,\nu & \nu > 0,\\[2pt]
 r(e^{-1})\,/\,(-\nu\,\gamma) & \nu < 0,\\[2pt]
 \infty & \nu = 0.
\end{cases}

Charging the two directions separately stays feasible, which is why the error is easy to miss; it only understates what the edge allows. It also made one 40-node instance repeat a single cycle forever with $x$ shrinking by a tenth each time — 500 000 iterations moved the delivered flow by $3\times10^{-6}$.

4. Highest-gain augmentation

Maximising a product of multipliers is minimising a sum of logarithms. Set

c(a) \;=\; -\ln \gamma(a),

so a highest-gain path is a shortest path under $c$, and a flow-generating cycle is a negative-cost cycle. The solver works in this log space throughout, which also stops long lossy chains from underflowing: fifty halvings give $\gamma(P)=2^{-50}$, and a solver multiplying doubles loses such paths.

Onaga's loop: while a residual $s\to t$ path exists, augment along one of maximum gain. The choice of maximum is what makes it correct. It is the exact finish of this solver; most of the flow is first placed by the Fat-Path phases of section 8, which choose paths by capacity as well as gain.

Theorem (Onaga, 1966). If the residual network contains no flow-generating cycle, then augmenting along a maximum-gain path leaves it with none.

Augmenting along a lesser path can break that invariant, after which the stopping test below no longer certifies anything.

5. Node potentials

Maintain $\pi:V\to\mathbb{R}$ with non-negative reduced costs

c_\pi(a) \;=\; c(a) + \pi(v) - \pi(w) \;\ge\; 0 \qquad \forall a=(v,w)
\text{ residual}.

Two facts make this the centre of the implementation.

Such a $\pi$ exists if and only if there is no flow-generating cycle. Summing $c_\pi\ge 0$ around a cycle telescopes to $\sum c(a)\ge 0$, i.e. $\gamma(C)\le 1$. So a valid potential is a certificate that no amplifying cycle exists, and the search for one is the same computation as the search for the other.

With $\pi$ in hand each augmentation is one Dijkstra. Reduced costs are non-negative, so the highest-gain path comes from an early-exit Dijkstra over $c_\pi$ instead of a label-correcting sweep over every arc. Tardos and Wayne put it the same way: "an augmentation takes $\tilde O(m)$ time, using an algorithm based on Dijkstra's shortest path algorithm."

After a search that settles $S$ with distances $d$, stopping at $d_{\text{stop}}$:

\pi(v) \;\mathrel{+}=\; d(v) - d_{\text{stop}} \qquad \forall v \in S,

leaving every other potential alone. This is valid because adding a constant to all potentials changes no reduced cost, so the update is the "shift by $d_{\text{stop}}$" that costs nothing plus a correction only on settled nodes. For $a=(v,w)$ with $v$ settled and $w$ not, $d(w)\ge d_{\text{stop}}$ gives $c_\pi(a)+d(v)-d_{\text{stop}}\ge 0$; the other cases are immediate. Arcs on the chosen path end at $c_\pi=0$, hence so do their reverses, which is why the new residual arcs an augmentation creates keep $\pi$ valid.

To re-establish $\pi$ from scratch, run a label-correcting sweep with every node of the sink-reaching subgraph starting at $\lambda(v)=0$ and relaxing $\lambda(w)\ge\lambda(v)+\ln\gamma(a)$; then $\pi=-\lambda$. Starting every node at zero, rather than only the source, is what lets it find a cycle anywhere — a cycle can manufacture flow with the source cut off entirely, as examples/currency_arbitrage.json does. Any $\lambda$ satisfying the constraints works, not just the minimal one, so after a push the sweep is re-seeded only from the nodes that push disturbed.

6. Finding cycles without paying for them

The textbook negative-cycle test waits for a node to be relaxed more than $|V|$ times, which means up to $|V|$ passes over every arc after the cycle has already formed. The parent forest closes within a few relaxations of the cycle appearing, so the solver scans it for a cycle every $|V|$ relaxations — an $O(V)$ walk with three-colour marking, so $O(1)$ amortised per relaxation. On a dense amplifying network this replaced 14 million relaxations per cycle found with a few thousand.

Two guards keep it honest. Relaxation requires improvement by at least $\varepsilon$, so a chain can close on a cycle whose gain is not actually above one; every hit is verified before being acted on. And parent pointers outlive the search that set them, so a cycle read off them may name arcs that have since been saturated — every arc is re-checked for usable residual, because acting on a stale cycle once caused the solver to retire a live arc and return a flow 26% below optimum labelled optimal.

7. Stopping, with a certificate

A flow-generating cycle together with a residual path from it to the sink is a generalized augmenting path (GAP): it manufactures flow and delivers it.

Theorem (Onaga; Truemper, 1977). A feasible generalized flow is maximum if and only if its residual network contains neither an $s\to t$ path nor a flow-generating cycle with a path to the sink.

This follows from LP duality. An improving direction is a flow in the residual network conserved off $\{s,t\}$ that raises the net at $t$; decomposing it gives source-to-sink paths, flow-generating cycles with their excess routed somewhere, flow-absorbing cycles, and bicycles — a generating cycle joined to an absorbing one. Only two kinds raise the net at $t$, so if any direction improves, one of those two is present.

The solver stops on this condition and reports it. Both checks are recomputed from scratch at the end, deliberately using the slow routines — plain reachability for the path, full Bellman–Ford rounds for the cycle — rather than the accelerated ones the main loop runs on, because a certificate sharing the loop's heuristics could only confirm the loop's own blind spots. status is optimal only when both hold, and the certificate also carries the largest conservation error and capacity violation.

tools/gap_probe.py performs the same two checks in Python from the reported edge flows, sharing no code with the solver.

8. Fat-Path with gain-scaling

Onaga's loop has no useful bound on the number of augmentations: highest-gain first will happily spend an augmentation on a path that carries $10^{-9}$. The solver therefore runs Tardos and Wayne's Rounded Fat-Path (IPCO 1998, §6) first, and leaves Onaga only the last scrap of the gap.

Fat paths. Write $\mu(v)$ for the inverse of the best gain from $v$ to the sink, the canonical label. An arc is $\delta$-fat when it can raise the sink's excess by $\delta$, i.e. $r(a)/\mu(v)\ge\delta$, and a path is fat when every arc is. With potentials, $\mu(v)=e^{\pi(t)-\pi(v)}$, so the test is one comparison per arc against a per-node threshold. The phases are:

\begin{aligned}
&\Delta \leftarrow \Delta_0 \ge \mathrm{OPT}-|f| \\
&\textbf{repeat}\\
&\quad \textsc{CancelCycles} \quad\text{(the relabelled network becomes lossy)}\\
&\quad \textsc{FatAugmentations}(\Delta/2m) \quad\text{(highest-gain fat paths until none)}\\
&\quad \Delta \leftarrow \Delta/2\\
&\textbf{until } \Delta \text{ is below the tolerance}
\end{aligned}

Each fat augmentation either delivers $\delta=\Delta/2m$ or empties its source, so a phase makes at most $n+2m$ of them, and the discrepancy halves per phase: $O(\log(n/\xi))$ phases for a $\xi$-optimal flow. $\Delta_0$ is the cheaper of two cuts, what the sink's residual in-arcs can deliver and the relabelled capacity out of the source; a phase in which nothing fat exists jumps $\Delta$ straight down to the fattest arc the search had to skip, since every path leaves the fat subnetwork through one.

Canonical labels. Every phase starts from exact labels: one Dijkstra backwards from the sink over the reduced costs gives $D(v)$, and $\pi(v)\leftarrow\pi(v)-D(v)$ makes $e^{\pi(v)-\pi(t)}$ the true best gain to the sink while keeping every reduced cost non-negative (it is the triangle inequality $D(v)\le c_\pi(v,w)+D(w)$). Reading fatness off stale potentials instead — which on a lossy network claim a gain of one everywhere — filled paths that delivered almost nothing and cost 99 000 cycle cancellations on a 1 000-node instance.

Gain-scaling. Rounding each gain down to a power of $b=(1+\xi)^{1/n}$ is rounding its cost up to a multiple of $q=\ln b$. In the relabelled lossy network those costs are the reduced costs, so the rounded network is a Dijkstra on the integer keys $\lceil c_\pi(a)/q\rceil$. A search on those keys leaves every reduced cost above $-q$ — never below, however many searches run — so the phases keep $q$-optimal potentials, and CancelCycles need only remove cycles whose total log-gain exceeds $q$: exactly the cycles a sweep that ignores improvements smaller than $q$ cannot settle around. $\xi$ shrinks with $\Delta$ as $\xi=\min(\tfrac12,\Delta/\Delta_0)$. Rounding costs at most a factor $b^{|P|}\le 1+\xi$ on any path, which is Tardos and Wayne's Theorem 4.

CancelCycles. After a phase the potentials are valid on every arc except the thin ones the phase skipped, and every generating cycle passes through one of those. For a violating arc $(v,w)$ the most generating cycle through it is the arc plus the highest-gain $w\to v$ path over the certified arcs — one early-exit Dijkstra, after whose potential update the arc's reduced cost is the cycle's log-loss. Cancelling through violating arcs first, and only then running the label-correcting sweep for whatever two violating arcs share, cut a 3 200-node amplifying instance from 5 878 cancellations and 15.9 s to 313 and 1.3 s. A cancelled cycle's excess is routed to the sink by the highest-gain path over the certified arcs, not the fewest hops: the excess belongs on the rest of the path it was rerouted from, and an arbitrary route creates new cycles as fast as they are removed.

Two departures from the paper. Both come from reporting a flow that conserves at every node. The paper keeps its flow in the rounded network and interprets it in the original at the end, which strands excess at interior nodes; here a path chosen in the rounded network is pushed with its true gains, so it delivers at least what the rounded network promised and nothing is stranded. And the paper's CancelCycles leaves each cycle's excess on the cycle for the next augmentations to collect; here it is carried to the sink with the cycle, as a generalized augmenting path. The paper's exact algorithm ends by rounding a $B^{-4m}$-optimal flow, which means nothing in floating point; instead the phases stop once $\Delta$ is below the tolerance and Onaga's loop finishes on the true gains, from a flow already within the last $\Delta$ of optimal. fat_path: false and gain_scaling: false turn each layer off, and the tests check all three variants against each other.

9. Complexity

Each search is $O(m\log n)$ with early exit. The Fat-Path phases bound the augmentations at $n+2m$ per phase over $O(\log(n/\xi))$ phases, which is what replaces Onaga's unbounded count — Onaga's algorithm alone "may run in exponential time (or may not even terminate for irrational inputs)". What the implementation does not inherit is the paper's bound on CancelCycles, which rests on Goldberg–Tarjan cancel-and-tighten over integer costs; the violating-arc Dijkstra and the label-correcting sweep here measure faster on every input tried but carry no such guarantee, and neither does the exact finish. There is a limit, 1000 + 20(n+m) iterations by default, and reaching it is reported as iteration_limit rather than dressed up as an answer.

bound note
Goldberg–Plotkin–Tardos 1991 first polynomial combinatorial cancels all generating cycles as a batch
Goldfarb–Jin–Orlin 1997 $\tilde O(m^3\log B)$ capacity scaling, highest-gain paths
Tardos–Wayne 1998 matches best known implemented here (section 8): Fat-Path with gain-scaling
Radzik 2004 $\tilde O(m^2 n\log B)$ best known weakly polynomial
Végh 2016 strongly polynomial first such result; continuous scaling
Olver–Végh 2017 $O((m+n\log n)\,mn\log(n^2/m))$ simpler and faster strongly polynomial

The strongly polynomial results are landmarks but not implementation guides: at $n=10^3$, $m=5\times10^3$ the Olver–Végh bound is on the order of $10^{11}$ operations, far above what augmenting paths cost in practice. The preflow-push variant in the Tardos–Wayne paper is the natural next step.

Two notes on floating point. epsilon is relative, scaled by the largest finite capacity, so "optimal" means optimal to that tolerance. An arc is retired from the search only when nothing at all can move through it.

10. On concurrency

The breadth-first sweep is the one phase that fans out naturally, so it was built multi-threaded, measured, and then removed. The measurements:

breadth-first sweep share of solve ceiling at 16 cores
lossy, 16k nodes 0.001 s 0.06 % 1.0006×
amplifying, 3.2k nodes 0.232 s 68 % 2.7× in theory

The dominant phase is Dijkstra, which is order-dependent: it settles nodes in non-decreasing distance, and that order is what lets each node be finalised once. Relaxed concurrent priority queues (skip-list or MultiQueue based) do give parallel shortest paths, but they settle nodes out of order and re-settle them, and here the exact maximum-gain path is what Onaga's invariant rests on — so the risk lands on correctness, not just speed.

Meanwhile the parallelisable phase has been optimised into insignificance. The sweep is 0.06 % of a lossy solve, so Amdahl caps the whole benefit at 1.0006× no matter how many cores are thrown at it. On amplifying inputs there is real headroom, but the absolute numbers are already a quarter of a second, each round of the frontier is tens of microseconds of work, and a barrier costs a comparable amount. Atomic doubles work — the removed implementation used compare-exchange on std::atomic<double> — but WebAssembly needs SharedArrayBuffer and cross-origin isolation for threads at all, so the browser build would stay sequential regardless.

The parallel sweep as built was also 780× slower than the sequential one, for two reasons worth recording: it spawned threads per frontier round rather than once, and it lacked the parent-forest cycle check, so it ran the full $|V|$-round bound while the sequential path aborted in a few thousand relaxations. Fixing both would have bought a fraction of a 1.5× on one input class. It was removed rather than kept.

Performance

AMD, single core, release build. layered is $\sqrt n$ layers of $\sqrt n$ nodes — deep graphs where shortest paths are hundreds of arcs long; uniform is a random graph.

instance nodes edges Fat-Path Onaga only solver memory
lossy, layered 40 002 159 600 9.2 s 15.2 s 18 MB
lossy, layered 15 878 63 252 1.03 s 1.59 s 7 MB
lossy, uniform 16 000 80 000 0.10 s 0.11 s 6 MB
lossy, uniform 100 000 500 000 0.78 s 0.49 s 40 MB
lossy, uniform 250 000 1 250 000 2.2 s 2.5 s 100 MB
amplifying, layered 3 194 12 656 1.33 s 1.15 s 1 MB
amplifying, uniform 3 200 16 000 0.35 s 0.36 s 1 MB

"Onaga only" is fat_path: false. Fat-Path pays off where paths are long and many: on the deep layered networks it fills fat paths with smaller searches, and the margin grows with size. On shallow random graphs a handful of highest-gain augmentations already finish the job and the phases are overhead — a backward Dijkstra and a potential repair each — and on amplifying inputs much of the work is the input's own cycles, which both variants cancel the same way. Against the same benchmark before the potentials work: the 16k layered case was 11.07 s and the 3 200-node amplifying case was 588 s. Run it yourself with build/gmf_bench, or gmf_bench layered 16000 4 0.95 onaga (or exact, for Fat-Path without the gain rounding) to compare.

The layout is built for the inner loop. Arcs are renumbered so those leaving a node are contiguous, and the relaxation reads head, residual and cost straight through memory with no indirection. Multiplier and capacity live once per edge rather than twice per arc — and are not copied from the caller's problem at all. Incoming arcs are derived as twins of outgoing ones instead of a second adjacency. Scratch arrays are stamped rather than cleared, so a search costs the subgraph it touches rather than $O(V)$. The steps of every augmentation share one array at 16 bytes each; collect_paths: false drops it entirely.

How it is checked

  • Networks whose multipliers are all 1 are compared against an independent Dinic maximum flow implementation; 300 random instances per run.
  • tools/lp_check.py states the problem as a linear program and solves it with scipy's HiGHS, which shares no code with the solver. Several thousand random instances across lossy and amplifying regimes, up to 80 nodes, agree to 1e-6. This is what caught every defect in tests/test_regression.cpp, including one that only appeared on a single 40-node graph after thousands had passed.
  • tools/gap_probe.py rebuilds the residual network from a reported answer in Python and independently looks for the two things the certificate claims are absent.
  • The Dijkstra engine is checked against the label-correcting reference (use_potentials: false) on random instances, and every answer is checked for capacity feasibility and flow conservation.
  • Fat-Path with gain-scaling, Fat-Path on exact gains and plain Onaga (fat_path: false) are checked against each other on 240 random instances across both regimes.
  • tests/test_regression.cpp holds the instances that once produced a wrong answer, each with the optimum from the linear program and a note on the defect it caught.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
./build/gmf_tests
python tools/lp_check.py --trials 400 --max-multiplier 2.0
python tools/lp_check.py --graph examples/power_grid.json
pytest tests/test_python.py

Examples

examples/ holds three graphs worth running, each annotated with what it shows:

file what it shows
power_grid.json transmission with line losses: 1012 units leave the plant, 918 reach the city
rerouting.json the optimum needs flow pushed back over an edge; greedy filling stops at 2 of 3
currency_arbitrage.json exchange rates as multipliers, so a rate cycle whose product exceeds 1 manufactures 69.78 USD from no starting capital
./build/gmaxflow examples/currency_arbitrage.json

References

  • K. Onaga, Dynamic programming of optimum flows in lossy communication nets, IEEE Trans. Circuit Theory, 1966.
  • K. Truemper, On max flows with gains and pure min-cost flows, SIAM J. Appl. Math., 1977.
  • A. V. Goldberg, S. A. Plotkin, É. Tardos, Combinatorial algorithms for the generalized circulation problem, Math. of OR 16(2), 1991.
  • D. Goldfarb, Z. Jin, J. B. Orlin, Polynomial-time highest-gain augmenting path algorithms for the generalized circulation problem, Math. of OR 22(4), 1997.
  • É. Tardos, K. D. Wayne, Simple generalized maximum flow algorithms, IPCO 1998. (pdf)
  • T. Radzik, Improving time bounds on maximum generalised flow computations, Theor. Comput. Sci. 312, 2004.
  • L. A. Végh, A strongly polynomial algorithm for generalized flow maximization, Math. of OR 42(2), 2017.
  • N. Olver, L. A. Végh, A simpler and faster strongly polynomial algorithm for generalized flow maximization, J. ACM 67(2), 2020.
  • R. K. Ahuja, T. L. Magnanti, J. B. Orlin, Network Flows, 1993, chapter 15.

License

MIT. See LICENSE.

Metadata

Release files for generalized-maxflow 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for generalized-maxflow 0.3.0
File Size Uploaded Provenance
generalized_maxflow-0.3.0.tar.gz 85.8 kB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for generalized-maxflow 0.3.0
File
generalized_maxflow-0.3.0-py3-none-win_arm64.whl Python 3 none Windows ARM64 Details
generalized_maxflow-0.3.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
generalized_maxflow-0.3.0-py3-none-musllinux_1_2_x86_64.whl Python 3 none Linux musl 1.2+ x86-64 Details
generalized_maxflow-0.3.0-py3-none-musllinux_1_2_aarch64.whl Python 3 none Linux musl 1.2+ ARM64 Details
generalized_maxflow-0.3.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.24+ x86-64, Linux glibc 2.28+ x86-64 Details
generalized_maxflow-0.3.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.24+ ARM64, Linux glibc 2.28+ ARM64 Details
generalized_maxflow-0.3.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
generalized_maxflow-0.3.0-py3-none-macosx_10_13_x86_64.whl Python 3 none macOS 10.13+ x86-64 Details

Total release size: 2.9 MB

Release files / generalized_maxflow-0.3.0.tar.gz

Download URL generalized_maxflow-0.3.0.tar.gz
Size 85.8 kB
Tags Source
SHA-256 checksum
How to use checksums
31691cb573c133796425f6d263e4c4e9f4bd1abaf5cd6e57e811296f57b049ef
BLAKE2b-256 checksum
How to use checksums
7f9b38d75725362e61c95fdccfca3f7a464ae6563047496a4b9b7dea7e7e0627
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-win_arm64.whl

Download URL generalized_maxflow-0.3.0-py3-none-win_arm64.whl
Size 147.6 kB
Tags Python 3 Windows ARM64
SHA-256 checksum
How to use checksums
17372ad074f5a68660207281d6f7b103fcb77c9a899625572cbd07d90e692b79
BLAKE2b-256 checksum
How to use checksums
1b12bdbc59bed9854c0a3a754245fb488d847c3bbcc9c5be6fcbe5efebcffd17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-win_amd64.whl

Download URL generalized_maxflow-0.3.0-py3-none-win_amd64.whl
Size 165.2 kB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
5ef1ba9c1119ad7859f2deb33dde5b011490a057d26fa45186a11aca32cf658f
BLAKE2b-256 checksum
How to use checksums
a1f241239bb4d9bc69e5ee17f22b65bb660834ddb03188c7aac0c2338ed9405e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-musllinux_1_2_x86_64.whl

Download URL generalized_maxflow-0.3.0-py3-none-musllinux_1_2_x86_64.whl
Size 1.1 MB
Tags Linux musl 1.2+ x86-64 Python 3
SHA-256 checksum
How to use checksums
a09c391b540b72ecab316746d8d3f763707d5e4fd36b2e28bc25a7734d1dab9b
BLAKE2b-256 checksum
How to use checksums
1fa59753cfd16a510a4e9491cbc6f2dfdc26083f9f7174aec449c04056796e6a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-musllinux_1_2_aarch64.whl

Download URL generalized_maxflow-0.3.0-py3-none-musllinux_1_2_aarch64.whl
Size 1.1 MB
Tags Linux musl 1.2+ ARM64 Python 3
SHA-256 checksum
How to use checksums
2ea51f9817513853c120a1da37fe08157154ba00a8a432011cdb86ebd65423df
BLAKE2b-256 checksum
How to use checksums
0bf0a3575cc02b69a3a24a921e358f054baf67379a9d3a5aecbad06ac458b042
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl

Download URL generalized_maxflow-0.3.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
Size 90.7 kB
Tags Linux glibc 2.24+ x86-64 Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
abbe5a41ee447e807794c376352437c9fa52ab5d01f7928098a6fdb1b130334b
BLAKE2b-256 checksum
How to use checksums
066f95dc2007ca8368c016026f6c202e4ba6ff4ebb267d7cba9b78c453592c7e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl

Download URL generalized_maxflow-0.3.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl
Size 86.5 kB
Tags Linux glibc 2.24+ ARM64 Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
2422ae39b75c0a0f8ed4bc29133b0312e1fd9a1329a3223637745d158ad84974
BLAKE2b-256 checksum
How to use checksums
e6e72de69a94016ecc4e7e0f700be84b013a2beaa2ff24a1b6ed084da077754a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-macosx_11_0_arm64.whl

Download URL generalized_maxflow-0.3.0-py3-none-macosx_11_0_arm64.whl
Size 79.0 kB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
5c1b1660db3711fc608b913722c296f1c6c60959cdb4f9265c57f295bc6996b2
BLAKE2b-256 checksum
How to use checksums
b73ad20941059b347ddbf201b4a554f3d00911d6f5c9317453cf3291e71dd2c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / generalized_maxflow-0.3.0-py3-none-macosx_10_13_x86_64.whl

Download URL generalized_maxflow-0.3.0-py3-none-macosx_10_13_x86_64.whl
Size 82.1 kB
Tags Python 3 macOS 10.13+ x86-64
SHA-256 checksum
How to use checksums
76d8a30a94cfcb05e187944c1b6a06cda306a7b7e0aebfec81c5ff5402c3a1b5
BLAKE2b-256 checksum
How to use checksums
1b116601d294430fb8addfa9e77197aa890d3ce841c44f5baffade49cb863e09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Security for generalized-maxflow 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 9 files in this release have been uploaded with provenance metadata.

Select from 9 files

Attestation for generalized_maxflow-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

9 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page