Skip to content

api: /interdict grows a ladder mode with a server-computed diverges flag (closes #38) - #61

Merged
bgent19 merged 7 commits into
mainfrom
classdemos
Aug 14, 2026
Merged

api: /interdict grows a ladder mode with a server-computed diverges flag (closes #38)#61
bgent19 merged 7 commits into
mainfrom
classdemos

Conversation

@bgent19

@bgent19 bgent19 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Closes #38.

/interdict grows a ladder mode: rungs k=1..K, each carrying both methods' removed lane set, residual throughput and counts, plus a server-computed diverges flag. The existing single-k shape is unchanged, so the CLI is untouched.

The point of putting this on the server is that termination and the identical-rung collapse become facts a test can pin, rather than logic living in the render pass. The boot fetch also drops from 2 × K × datasets calls to one per dataset.

Acceptance criteria

Each is pinned by a named test in tests/test_interdiction_ladder.py:

  • Rungs k=1..K with both trackstest_rungs_start_at_one_and_step_by_one, test_every_rung_carries_both_tracks
  • Both tracks requested explicitly; auto never produces a ladderauto silently picks exhaustive on any classroom-sized dataset, so it would never compute the greedy result at all
  • Server-computed diverges flagtest_the_trap_diverges_from_k_two_on, test_the_agreeing_dataset_states_its_agreement
  • Termination is the first k where both tracks reach zerotest_ladder_stops_where_both_tracks_reach_zero, test_termination_waits_for_the_worse_method, derived from data rather than hard-coded
  • Identical removed sets collapse to one rungtest_the_collapse_test_is_the_removed_set_not_the_residual; the test is the removed set, not the residual, so two blockades reaching the same number by different lanes stay two pictures
  • Counts labelled searched and skippedtest_greedy_reports_the_work_greedy_did_not_the_space, test_searched_and_skipped_account_for_the_whole_space
  • k=1 always agreestest_k_one_never_diverges; greedy's first iteration is exactly the exhaustive search over singletons, so the earliest possible divergence is k=2

Notes for review

  • Termination waiting for the worse method is deliberate: stopping at exhaustive-zero was rejected because k=2 sets up Day 4's punchline and k=3 lands it — greedy reaches zero but spends three lanes to buy what the optimum bought with two. Accepted cost: ladder length is set by the worse method.
  • A bidirectional lane costs one unit of interdiction budget while removing two directed arcs. Known and accepted — a teaching artifact of Day 4, not a bug.

Branch state

This branch also carries two merges that bring it level with main (the frontend shell and day-4 trap) and join the origin/classdemos history. The second used -s ours: origin/classdemos held the original #57 trace implementation that main later superseded, and a plain merge would have reverted main's refactor of throughput.py, maxflow.py and cli.py. Both expose the same CLI surface (a single --trace flag), so nothing user-facing is lost. The net diff against main is the five files above.

Test plan

  • python -m pytest -q — 289 passed locally
  • CI green on 094a1aa

The whole Edmonds-Karp run ships as one array on one request -- every
augmentation, complete enough to draw, each a full snapshot rather than a
delta, so a display steps backwards by decrementing an index and no
server-side cursor is needed.

The server prepends a step 0 holding the graph before the first push: the
uniform render rule is residuals from step k-1 and path annotation from step
k, and iteration 1 otherwise has no "before" to draw against. Synthesising
one client-side would put residual-graph construction in the browser.

The data layer stops filtering and the CLI starts. Both filters existed only
to tidy terminal output and both damaged a drawn trace: stripping the
synthetic terminals made a route appear to begin mid-graph, and dropping the
unbounded terms de-aligned lane_residuals from path, so a min(...) term could
not be anchored to the lane it came from. Both now ship complete -- terminals
under the reserved __source__/__sink__ ids, unbounded residuals as null --
and `len(lane_residuals) == len(path) - 1` holds on every augmentation.

Each step also carries explicit per-edge flows covering every arc, saturated
ones included, which the residual lists cannot say: they drop an arc the
moment it saturates, and those are the lanes Day 2 most wants drawn.

The CLI's text output is unchanged to the byte, pinned by a golden transcript
that was written and passing against the old code before any of this moved.
… the trace

The contract test asserted `len(lane_residuals) == max(len(path) - 1, 0)`,
which is the one shape of that assertion that would also pass on a real
augmentation de-aligned all the way down to no terms -- the exact failure it
exists to catch. Step 0 is now asserted as what it is (no path, no terms) and
every augmentation carries the equality strictly.

The `iterations + 1` check compared the array's length to its own contents,
which holds just as well on a trace that lost every augmentation. The textbook
theater's three iterations are pinned as a count from outside it.

README gains the `/maxflow?trace=true` payload alongside the `/scenario` and
`/naive` ones, and the endpoint table stops omitting the new parameter.
api: /maxflow?trace=true ships the whole trace statelessly (closes #36)
…lag (closes #38)

Day 4 walks up k with both interdiction methods side by side. `interdiction_ladder`
runs the exhaustive optimum and the greedy heuristic at every rung -- both
requested explicitly, because `method="auto"` silently returns exhaustive on any
classroom-sized network and never computes greedy at all -- and stops at the
first k where *both* tracks reach zero. On `greedy_trap` that is k=3, one rung
past exhaustive's zero, which is where the lesson lands: greedy spends three
lanes to buy what the optimum bought with two.

Termination and the identical-rung collapse live here rather than in a render
pass because both are arithmetic over solver results, and arithmetic over solver
results is testable. The boot cost also drops from 2 x K x datasets calls to one
per dataset.

`diverges` compares the removed *sets*, not the residuals: at k=3 on the trap
both tracks read zero and the sets differ by a lane, and a residual comparison
would report that rung as agreement.

`BudgetInterdiction` gains `searched`/`skipped` alongside `subsets_considered`.
The latter is the size of the space -- the denominator both tracks share -- not
a claim about either method's work; reporting it as greedy's own count would
assert work greedy declined to do, and the declining is the point.

The single-k and min-cut response shapes are untouched, and pinned as such.
The `searched`/`skipped` pair reports work done; it does not rank the two
methods. The exhaustive search stops the moment it severs the network, so on a
small instance it can finish in fewer evaluations than greedy -- on
`textbook_maxflow` at k=2 the optimum searches 9 and greedy searches 13. Two
tests asserted the opposite as if it were a law, on the one dataset where it
happens to hold. Both now pin greedy's count against the *space*, which is the
honest claim, and the inversion is pinned outright so it cannot later read as a
regression.

Also: the `ladder` parameter's OpenAPI text said it ignored `method` while the
handler 400s on it; `_removed_lanes` is declared once instead of spelled twice;
/interdict's wire helpers move to the helper cluster with a note on why this
endpoint hand-builds its dicts; and the module's tolerance is one named `_EPS`
rather than seven literals and a second constant beside them.

`k_through == k` is now asserted rather than `>=`, which is the collapse guard
reporting itself idle instead of a tautology.
origin/classdemos carried the original #57 trace work, which main later
superseded via its own implementation. Both expose the same CLI surface
(a single --trace flag); main's is the version CI runs and ships, so this
merge joins the histories without reverting main's refactor of
throughput.py, maxflow.py and cli.py.
@bgent19
bgent19 merged commit 537d38b into main Aug 14, 2026
1 check passed
@bgent19
bgent19 deleted the classdemos branch August 16, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/interdict grows a ladder mode with a server-computed diverges flag

1 participant