CubicPathwidth.Alg.Port
Source file CubicPathwidth/Alg/Port.lean of the Lean project (how it is built).
import CubicPathwidth.Alg.Graph
/-!
# Port of `ancillary/experiments/sweep-algorithms/sweep.py` (SKELETON)
Every function of `sweep.py` that produces a decomposition is ported here with its
exact signature, a docstring (Python function, line range in `sweep.py`, semantics,
contract) and, where marked with a todo-port comment, a TEMPORARY placeholder body that is
trivially total. Placeholders are to be replaced one at a time;
each replacement must compile, contain no `sorry`, and match Python on the harness
(`lean_diff.py` + `DiffMain.lean`).
Conventions.
* Iteration model M (see `Graph.lean`): subgraph views iterate in parent
order; `connector_order` receives `Wp` and the items in the node order of `G`.
* Python sets are `List Nat` read as sets. Where Python iterates a set and the order
matters, the docstring says which order the port uses.
* Python list used as a stack (`append`/`extend` at the end, `pop()` from the end) is
ported as a Lean list whose HEAD is the Python END: `pop` = take the head,
`extend(xs)` = `xs.reverse ++ stack`.
* Labels are not ported. `Y : Nat → Rat` replaces the label dict; `lab : Labeler`
gives the labels used for each graph to which the plain sweep is applied.
* Python `assert`s (and crashes) become FALLBACKS that keep the output valid; each
function `fooF` returns `(result, flag)` where `flag = true` iff some fallback fired
(in this call or a nested one). `foo := (fooF ..).1`. Stage 2 must show the flags are
`false` on the relevant inputs (statements in `Spec.lean`, section "Stage 2").
* Loops and recursion are total through explicit fuel; running out of fuel is a
fallback (flagged) that keeps the output valid.
-/
-- Placeholders leave arguments unused; remove this option once every body is ported.
set_option linter.unusedVariables false
namespace CubicPathwidth.Alg
open Graph
/-! ## Data (final, not placeholders) -/
/-- A recorded operation: Python `("red", v, nbrs)` (vertex `v` removed by a reduction,
`nbrs` = its neighbour list at that time, in adjacency order) or `("del", w)`. -/
inductive Op where
| red (v : Nat) (nbrs : List Nat)
| del (w : Nat)
deriving BEq, Repr, Inhabited, DecidableEq
/-- Labels for one graph: Python `Y[v]` (floats exported exactly as rationals). -/
abbrev Labels := Nat → Rat
/-- A labeler: the labels used when the plain sweep is applied to a graph `J`
(Python `labels(J, q_mixed(J), h, rng, True)`; the rng is not ported). -/
abbrev Labeler := Graph → Nat → Rat
/-- Python `D = {o[1] for o in ops if o[0] == "del"}`, as a list in op order. -/
def deletedSet (ops : List Op) : List Nat :=
ops.filterMap fun | .del w => some w | .red _ _ => none
/-- The vertices removed by reductions, in op order. -/
def redVertices (ops : List Op) : List Nat :=
ops.filterMap fun | .red v _ => some v | .del _ => none
/-- Python `[frozenset((v,) + nb) for _, v, nb in ops]` (pd_tw2, line 155): the
elimination bags `v :: nb`, in op order (deletions are skipped; pd_tw2 has none). -/
def elimBags (ops : List Op) : List Bag :=
ops.filterMap fun | .red v nb => some (v :: nb) | .del _ => none
/-- Python `[G.subgraph(c).copy() for c in nx.connected_components(G)]` (lines 216, 230):
the induced graphs of the components, in component order. (Final.) -/
def componentGraphs (G : Graph) : List Graph :=
G.components.map fun c => G.induced c.contains
/-- Python `G.subgraph([v for v in G if G.degree(v) > 0]).copy()` (line 214). (Final.) -/
def nonIsolated (G : Graph) : Graph :=
G.induced fun v => G.degree v != 0
/-- Python `[{v} for v in G if G.degree(v) == 0]` (line 239). (Final.) -/
def isolatedBags (G : Graph) : List Bag :=
(G.nodes.filter fun v => G.degree v == 0).map fun v => [v]
/-! ## Plain sweep (`plain_layout`, sweep.py lines 57-65) -/
/-- Python `sorted(G, key=lambda v: (-Y[v], v))`: nodes by DECREASING `Y`, ties by
INCREASING vertex number. Implement with `List.mergeSort` and the comparator
`fun a b => decide (Y b < Y a) || (decide (Y a = Y b) && decide (a ≤ b))`.
Contract: a permutation of `G.nodes`. -/
def plainOrder (G : Graph) (Y : Labels) : List Nat :=
G.nodes.mergeSort fun a b => decide (Y b < Y a) || (decide (Y a = Y b) && decide (a ≤ b))
/-- Python `plain_layout(G, Y)` (lines 57-65). With `order := plainOrder G Y`,
`idx v` = position of `v` in `order` (via a `Std.HashMap Nat Nat` built from
`order.zipIdx`, default `0`), `last v := (G.nbrs v).foldl (fun m u => max m (idx u)) (idx v)`,
the output has `order.length` bags and bag `i` is the set `{v ∈ G | idx v ≤ i ≤ last v}`.
REQUIRED form (for the proofs; `O(n²)`, fast enough for the harness):
`(List.range order.length).map fun i => G.nodes.filter fun v => idx v ≤ i && i ≤ last v`.
Contract (`PlainLayoutValid`): for `G.WF` and every `Y`, a valid path decomposition of
`G` whose bags only contain nodes of `G`. -/
def plainLayout (G : Graph) (Y : Labels) : List Bag :=
let order := plainOrder G Y
let idxMap : Std.HashMap Nat Nat := order.zipIdx.foldl (fun m p => m.insert p.1 p.2) ∅
let idx : Nat → Nat := fun v => idxMap.getD v 0
let last : Nat → Nat := fun v => (G.nbrs v).foldl (fun m u => max m (idx u)) (idx v)
(List.range order.length).map fun i => G.nodes.filter fun v => idx v ≤ i && i ≤ last v
/-! ## Connector order (`connector_order`, sweep.py lines 69-99) -/
/-- Python `inc` (lines 71-74): `inc[x]` = the indices `k` (increasing) of the items
containing `x`, for every `x` occurring in an item (`getD x []` for others).
Python's `inc` has keys `Qs`; items are assumed to lie in `Qs`. -/
def connectorIncidence (Qs : List Nat) (items : List (List Nat)) :
Std.HashMap Nat (List Nat) :=
items.zipIdx.foldl (fun m p => p.1.foldl (fun m x => m.insert x (m.getD x [] ++ [p.2])) m) ∅
/-- Python lines 75-86: the components of the hypergraph on `Qs` with edges `items`,
each as its `members` list, in the order Python creates them (scan `x ∈ Qs`; a new
component starts at an unassigned `x`; DFS with `stack.pop()` from the end, scanning
`k ∈ inc[y]` and `z ∈ items[k]` in list order, appending newly assigned `z` to
`members` and to the stack). `members.head` is the start `x`.
Totality: a DFS with fuel `Qs.length + (items.map List.length).sum + 1`; on exhaustion
the not yet assigned elements of `Qs` form a last component (cannot happen when the
items lie in `Qs`). Contract: a partition of `Qs` (when `Qs.Nodup` and items ⊆ `Qs`). -/
def connectorComponents (Qs : List Nat) (items : List (List Nat))
(inc : Std.HashMap Nat (List Nat)) : List (List Nat) :=
connOuter Qs (Qs.length + (items.map List.length).sum + 1) Qs ∅ []
where
/-- DFS step: state `(stack, assigned, memRev)`, stack head = Python end. -/
connDfs (fuel : Nat) (stack : List Nat) (assigned : Std.HashSet Nat) (memRev : List Nat) :
List Nat × Std.HashSet Nat × List Nat :=
match fuel, stack with
| 0, _ => (stack, assigned, memRev)
| _ + 1, [] => ([], assigned, memRev)
| f + 1, y :: rest =>
let st := (inc.getD y []).foldl (fun (acc : List Nat × Std.HashSet Nat × List Nat) k =>
(items.getD k []).foldl (fun (acc : List Nat × Std.HashSet Nat × List Nat) z =>
if acc.2.1.contains z then acc
else (z :: acc.1, acc.2.1.insert z, z :: acc.2.2)) acc) (rest, assigned, memRev)
connDfs f st.1 st.2.1 st.2.2
/-- Outer scan over `Qs` (Python `for x in Qs`); `all` = `Qs` for the last component. -/
connOuter (all : List Nat) (fuel : Nat) : List Nat → Std.HashSet Nat → List (List Nat) →
List (List Nat)
| [], _, comps => comps.reverse
| x :: qs, assigned, comps =>
if assigned.contains x then connOuter all fuel qs assigned comps
else
let r := connDfs fuel [x] (assigned.insert x) [x]
let comps' := r.2.2.reverse :: comps
if r.1.isEmpty then connOuter all fuel qs r.2.1 comps'
else
let rest := all.filter (fun y => !r.2.1.contains y)
(if rest.isEmpty then comps' else rest :: comps').reverse
/-- Python line 87: `exc[i] = len(comps[i]) - #{k | comp[first(items[k])] = i}`, where
`first(e)` is any element of `e` (all lie in one component); use `e.head?` and skip
empty items. One entry per component, in component order. -/
def connectorExcess (items : List (List Nat)) (comps : List (List Nat)) : List Int :=
let idxOf : Std.HashMap Nat Nat :=
comps.zipIdx.foldl (fun m p => p.1.foldl (fun m x => m.insert x p.2) m) ∅
let cnt : Std.HashMap Nat Nat :=
(items.filterMap List.head?).foldl (fun m x =>
let i := idxOf.getD x 0
m.insert i (m.getD i 0 + 1)) ∅
comps.zipIdx.map fun p => (p.1.length : Int) - (cnt.getD p.2 0 : Int)
/-- Python lines 90-98 for one component `c` (`start = c[0]`): greedy order.
State: `present` (set), `order`, `frontier` (Python list; Lean stack with head = Python
end). Initially `present = {start}`, `order = [start]`, `frontier = inc[start]`.
Loop while `present.size < c.length`: pop `k` from the frontier; let
`miss = items[k].filter (∉ present)`; if `miss.length = 1 ∧ items[k].length = 2`, add
`z = miss[0]` to `present` and `order` and extend the frontier by `inc[z]`; otherwise
discard `k`.
Fuel: `(items.map List.length).sum + 1` pops.
When the loop stops, ALWAYS append the members of `c` not yet present (in `c` order).
If the loop stopped because `present.size ≥ c.length` this appends nothing (every
element added lies in `c`, by the closure of components), so it is Python's behaviour;
it makes "every member of `c` is output" trivial to prove.
FALLBACK (Python: `IndexError` on `frontier.pop()` from an empty list, which happens
only when some item has size `> 2`, i.e. maximum degree `≥ 4`): if the loop stopped on
an empty frontier or exhausted fuel while `present.size < c.length`, set the flag.
Returns `(order of c, flag)`. -/
def connectorGreedy (items : List (List Nat)) (inc : Std.HashMap Nat (List Nat))
(c : List Nat) : List Nat × Bool :=
match c with
| [] => ([], false)
| start :: _ =>
let mkRes : Std.HashSet Nat → List Nat → Bool → List Nat × Bool := fun present ordRev fl =>
(ordRev.reverse ++ c.filter (fun z => !present.contains z), fl)
let rec loop : Nat → List Nat → Std.HashSet Nat → List Nat → Bool → List Nat × Bool
| 0, _, present, ordRev, fl => mkRes present ordRev (fl || decide (present.size < c.length))
| _ + 1, [], present, ordRev, fl =>
mkRes present ordRev (fl || decide (present.size < c.length))
| f + 1, k :: stack, present, ordRev, fl =>
if c.length ≤ present.size then mkRes present ordRev fl
else
let its := items.getD k []
match its.filter (fun z => !present.contains z), its.length with
| [z], 2 => loop f ((inc.getD z []).reverse ++ stack) (present.insert z) (z :: ordRev) fl
| _, _ => loop f stack present ordRev fl
loop ((items.map List.length).sum + 1) ((inc.getD start []).reverse)
((∅ : Std.HashSet Nat).insert start) [start] false
/-- Python `connector_order(Q, items)` (lines 69-99) with `Qs = list(Q)` given as a
list (model M: the caller passes `Wp` in node order). Components are processed in
the order `sorted(range(len(comps)), key=exc)`: a STABLE sort by `exc` (use
`List.mergeSort` on `(index, exc)` pairs with `≤` on `exc`), and the greedy orders are
concatenated. The flag is the `||` of the greedy flags.
Contract (`ConnectorOrderMem`): if every item lies in `Qs`, the output has exactly the
elements of `Qs` (as a set). Stage 2 (`ConnectorNoFallback`): if moreover every item is
duplicate-free of length 1 or 2, the flag is `false`. -/
def connectorOrderF (Qs : List Nat) (items : List (List Nat)) : List Nat × Bool :=
let inc := connectorIncidence Qs items
let comps := connectorComponents Qs items inc
let exc := connectorExcess items comps
let ordered := ((List.range comps.length).zip exc).mergeSort fun a b => decide (a.2 ≤ b.2)
let parts := ordered.map fun p => connectorGreedy items inc (comps.getD p.1 [])
(parts.flatMap (·.1), parts.any (·.2))
/-- `connector_order` without the flag. -/
def connectorOrder (Qs : List Nat) (items : List (List Nat)) : List Nat :=
(connectorOrderF Qs items).1
/-! ## Median-switched layout (`switched_layout`, sweep.py lines 102-128) -/
/-- Python `low`: `Y[v] < 0`. (Final.) -/
def isLow (Y : Labels) (v : Nat) : Bool := decide (Y v < 0)
/-- Python `U` (line 106): low vertices with a neighbour that is not low. (Final.) -/
def inU (G : Graph) (Y : Labels) (u : Nat) : Bool :=
isLow Y u && (G.nbrs u).any (fun x => !isLow Y x)
/-- Python `Wset` (line 107): non-low vertices with a low neighbour. (Final.) -/
def inW (G : Graph) (Y : Labels) (w : Nat) : Bool :=
!isLow Y w && (G.nbrs w).any (fun u => isLow Y u)
/-- Python `Dset` (line 108): vertices of degree 3 all of whose neighbours lie on the
other side. (Final.) -/
def inD (G : Graph) (Y : Labels) (v : Nat) : Bool :=
G.degree v == 3 && (G.nbrs v).all (fun u => isLow Y u != isLow Y v)
/-- Python `sorted(G, key=lambda v: (Y[v], v))`: INCREASING `Y`, ties by increasing
vertex number (`List.mergeSort` with
`fun a b => decide (Y a < Y b) || (decide (Y a = Y b) && decide (a ≤ b))`). -/
def switchedOrder (G : Graph) (Y : Labels) : List Nat :=
G.nodes.mergeSort fun a b => decide (Y a < Y b) || (decide (Y a = Y b) && decide (a ≤ b))
/-- Python `switched_layout(G, Y)` (lines 102-128) under model M.
With `order := switchedOrder G Y`, `ell :=` number of low nodes, `Up := U \ D`,
`Wp := Wset \ D`:
* `P := G.nodes.filter (fun u => inU u && !inD u && (G.nbrs u).any Wp)` (model M:
node order; Python iterates the set `Up`);
* `e u := (G.nbrs u).filter Wp`;
* `(q, flag) := connectorOrderF (G.nodes.filter Wp) (P.map e)` (model M: node order;
Python iterates the set `Wp`);
* inner bags, `i < ell`, `v = order[i]`, `T = order.take i`:
`v :: T.filter (fun u => (G.nbrs u).any (· ∉ T))`;
* `C₋₁ := G.nodes.filter (fun v => inU v || (inD v && inW v))`;
* for `j = 0, …, q.length`: `C_j := G.nodes.filter inD ++
P.filter (fun u => !subsetB (e u) (q.take (j - 1))) ++ q.take j`
(Python `prev = q[:max(j-1, 0)]`; natural subtraction gives the same);
* outer bags, `ell ≤ i < order.length`, `v = order[i]`, `T = low ∪ order[ell..i]`
(inclusive): `v :: G.nodes.filter (fun x => x ∉ T && (G.nbrs x).any (· ∈ T))`.
Output: inner ++ [C₋₁] ++ [C₀..C_m] ++ outer, and the connector flag.
Contract (`SwitchedLayoutValid`): for every `G.WF` and `Y` (any maximum degree, thanks
to the connector fallback) a valid path decomposition with bags inside `G`.
Stage 2: for maximum degree `≤ 3` the flag is `false`. -/
def switchedLayoutF (G : Graph) (Y : Labels) : List Bag × Bool :=
let order := switchedOrder G Y
let lowL := G.nodes.filter (isLow Y)
let ell := lowL.length
let Wp : Nat → Bool := fun x => inW G Y x && !inD G Y x
let P := G.nodes.filter fun u => inU G Y u && !inD G Y u && (G.nbrs u).any Wp
let e : Nat → List Nat := fun u => (G.nbrs u).filter Wp
let qf := connectorOrderF (G.nodes.filter Wp) (P.map e)
let q := qf.1
let inner := (List.range ell).map fun i =>
let v := order.getD i 0
let T := order.take i
let TS : Std.HashSet Nat := T.foldl (fun s x => s.insert x) ∅
v :: T.filter (fun u => (G.nbrs u).any (fun x => !TS.contains x))
let cm1 := G.nodes.filter fun v => inU G Y v || (inD G Y v && inW G Y v)
let Dl := G.nodes.filter (inD G Y)
let cs := (List.range (q.length + 1)).map fun j =>
Dl ++ P.filter (fun u => !subsetB (e u) (q.take (j - 1))) ++ q.take j
let outer := (List.range (order.length - ell)).map fun k =>
let v := order.getD (ell + k) 0
let T := lowL ++ (order.drop ell).take (k + 1)
let TS : Std.HashSet Nat := T.foldl (fun s x => s.insert x) ∅
v :: G.nodes.filter (fun x => !TS.contains x && (G.nbrs x).any (fun u => TS.contains u))
(inner ++ [cm1] ++ cs ++ outer, qf.2)
/-- `switched_layout` without the flag. -/
def switchedLayout (G : Graph) (Y : Labels) : List Bag := (switchedLayoutF G Y).1
/-! ## Reductions (`reduce_record`, sweep.py lines 132-143) -/
/-- The loop of `reduce_record` (lines 135-143). `stack` is Python's `stack` (head =
Python end). One step with `fuel + 1`: pop `v`; if `!G.mem v || G.degree v > 2`, skip;
else `nb := G.nbrs v`, record `.red v nb`, `G := G.removeNode v`, if `nb.length = 2`
then `G := G.addEdge nb[0] nb[1]` (R3, or R4 if present), and push `nb`
(`stack := nb.reverse ++ stack`).
Returns `(final graph, ops in chronological order, flag)`; `opsRev` accumulates the
ops in REVERSE order. Fuel `0` with a nonempty stack: stop and set the flag (the
result is still a valid record of reductions, only not maximal).
Termination bound: at most `|initial stack| + 2·numNodes` pops. -/
def reduceLoop (fuel : Nat) (G : Graph) (stack : List Nat) (opsRev : List Op) :
Graph × List Op × Bool :=
match fuel, stack with
| _, [] => (G, opsRev.reverse, false)
| 0, _ :: _ => (G, opsRev.reverse, true)
| fuel + 1, v :: rest =>
if !G.mem v || G.degree v > 2 then reduceLoop fuel G rest opsRev
else
let nb := G.nbrs v
let G1 := G.removeNode v
let G2 := match nb with
| [a, b] => G1.addEdge a b
| _ => G1
reduceLoop fuel G2 (nb.reverse ++ rest) (Op.red v nb :: opsRev)
/-- Python `reduce_record(G, ops)` (lines 132-143): apply R1-R4 until none applies.
Python mutates `G` and appends to `ops`; the port returns the final graph and the NEW
ops. Initial stack: `[v for v in G if G.degree(v) <= 2]`, i.e.
`(G.nodes.filter (G.degree · ≤ 2)).reverse` as a Lean stack. Fuel
`3 * G.numNodes + 1`.
Contract (`ReduceRecordSpec`). Stage 2 (`ReduceRecordNoFallback`): flag `false`. -/
def reduceRecordF (G : Graph) : Graph × List Op × Bool :=
reduceLoop (3 * G.numNodes + 1) G ((G.nodes.filter fun v => G.degree v ≤ 2).reverse) []
/-- `reduce_record` without the flag. -/
def reduceRecord (G : Graph) : Graph × List Op :=
let r := reduceRecordF G
(r.1, r.2.1)
/-! ## Width-2 elimination with balanced bags (`pd_tw2`, sweep.py lines 146-164) -/
/-- Python `pd_tw2(H)` (lines 146-164), with fuel:
* `H.numNodes = 0`: `([], false)`; `H.numNodes ≤ 3`: `([H.nodes], false)`;
* fuel `0` (otherwise): FALLBACK `([H.nodes], true)`;
* fuel `f + 1`: `(G', ops, fl) := reduceRecordF H.copy`.
FALLBACK if `G'.numNodes ≠ 0` (Python: `assert ... "graph has a K4 minor"`):
`([H.nodes], true)`.
Otherwise let `N := H.numNodes` and, for a bag `X`,
`comps X := (H.induced (fun v => !X.contains v)).components`. Take the FIRST
`X ∈ elimBags ops` with `∀ c ∈ comps X, 2 * c.length ≤ N` (Python `len(c) <= N / 2`).
FALLBACK if none (Python: `raise AssertionError("no balanced bag found")`):
`([H.nodes], true)`.
Otherwise, for each `c ∈ comps X` in order, `(Pc, fc) := pdTw2Fuel f
(H.induced c.contains)` (Python `H.subgraph(c).copy()`), output the concatenation of
`Pc.map (bagUnion · X)`, or `[X]` if that is empty; flag `fl || any fc`.
Contract (`PdTw2Valid`): for every `H.WF`, a valid path decomposition of `H` with
bags inside `H` (fallbacks included). Stage 2: no flag when `H` has treewidth `≤ 2`
(reduces to the empty graph) and `fuel ≥ H.numNodes`. -/
def pdTw2Fuel (fuel : Nat) (H : Graph) : List Bag × Bool :=
if H.numNodes = 0 then ([], false)
else if H.numNodes ≤ 3 then ([H.nodes], false)
else match fuel with
| 0 => ([H.nodes], true)
| f + 1 =>
let r := reduceRecordF H.copy
if r.1.numNodes != 0 then ([H.nodes], true)
else
let N := H.numNodes
let compsOf : Bag → List (List Nat) := fun X =>
(H.induced fun v => !X.contains v).components
match (elimBags r.2.1).find? (fun X => (compsOf X).all fun c => decide (2 * c.length ≤ N)) with
| none => ([H.nodes], true)
| some X =>
let parts := (compsOf X).map fun c => pdTw2Fuel f (H.induced c.contains)
let out := parts.flatMap fun p => p.1.map fun B => bagUnion B X
(if out.isEmpty then [X] else out, r.2.2 || parts.any fun p => p.2)
/-- `pd_tw2` with fuel `H.numNodes` (recursive calls have at most `N/2 < N` nodes). -/
def pdTw2F (H : Graph) : List Bag × Bool := pdTw2Fuel H.numNodes H
/-- `pd_tw2` without the flag. -/
def pdTw2 (H : Graph) : List Bag := (pdTw2F H).1
/-! ## Undoing reductions and deletions (`undo`, sweep.py lines 167-187) -/
/-- Python line 175: `S = {x for c in C for x in F[c] if x not in C and x not in D}`,
as a duplicate-free list in first-occurrence order (scan `c ∈ C`, `x ∈ F.nbrs c`). -/
def undoAttach (F : Graph) (D : List Nat) (C : List Nat) : List Nat :=
C.foldl (fun acc c =>
(F.nbrs c).foldl (fun acc x =>
if C.contains x || D.contains x || acc.contains x then acc else acc ++ [x]) acc) []
/-- Python `undo(F, ops, R, bagsR)` (lines 167-187) under model M.
* `D := deletedSet ops`; `rest := F.induced (fun v => !D.contains v && !R.mem v)`.
* For each component `C` of `rest` (in `rest.components` order): `S := undoAttach F D C`.
FALLBACK (Python `assert ... "invariant (I2) fails"`) unless
`S.all R.mem && S.length ≤ 2 && (S.length < 2 || R.hasEdge S[0] S[1])`.
`(P, fP) := pdTw2F (F.induced C.contains)`.
If `S = []`, append `P` to `tail`. Otherwise `i :=` the FIRST index with
`subsetB S bagsR[i]` (FALLBACK if none; Python `next(...)` would raise
`StopIteration`) and append `P.map (bagUnion · bagsR[i])` to `inserts[i]`.
* `out := (for each i: bagsR[i] :: inserts[i]) ++ tail`; result
`out.map (bagUnion · D)`, or `[D]` if `out = []`.
* FALLBACK result: `([F.nodes], true)` (one bag with every vertex of `F`).
* The flag also includes every `fP`.
Contract (`UndoValid`): under the preconditions stated there (implied by the calls in
`algoMixed` and `algoT4`), a valid path decomposition of `F`. -/
def undoF (F : Graph) (ops : List Op) (R : Graph) (bagsR : List Bag) : List Bag × Bool :=
let D := deletedSet ops
let rest := F.induced fun v => !D.contains v && !R.mem v
-- one plan per component C of `rest`: (S, P, fP, attachment index if S ≠ [])
let plans : List (List Nat × List Bag × Bool × Option Nat) := rest.components.map fun C =>
let S := undoAttach F D C
let Pf := pdTw2F (F.induced C.contains)
(S, Pf.1, Pf.2, if S.isEmpty then none else bagsR.findIdx? (subsetB S ·))
let validS : List Nat → Bool := fun S => S.all R.mem && match S with
| [] => true
| [_] => true
| [a, b] => R.hasEdge a b
| _ => false
let ok := plans.all fun ⟨S, _, _, idx⟩ => validS S && (S.isEmpty || idx.isSome)
if !ok then ([F.nodes], true) else
let fl := plans.any fun ⟨_, _, f, _⟩ => f
let tail := plans.flatMap fun ⟨S, P, _, _⟩ => if S.isEmpty then P else []
let out := bagsR.zipIdx.flatMap fun ⟨B, i⟩ =>
B :: plans.flatMap fun ⟨_, P, _, idx⟩ => if idx == some i then P.map (bagUnion · B) else []
let res := (out ++ tail).map (bagUnion · D)
(if res.isEmpty then [D] else res, fl)
/-- `undo` without the flag. -/
def undo (F : Graph) (ops : List Op) (R : Graph) (bagsR : List Bag) : List Bag :=
(undoF F ops R bagsR).1
/-! ## The algorithms (sweep.py lines 191-241) -/
/-- Python `algo_T1(G, rng, h)` (lines 195-197) with the labels `Y` given:
`switched_layout(G, Y)`. (Final.) -/
def algoT1F (G : Graph) (Y : Labels) : List Bag × Bool := switchedLayoutF G Y
/-- `algo_T1` without the flag. -/
def algoT1 (G : Graph) (Y : Labels) : List Bag := (algoT1F G Y).1
/-- Python `algo_T2a(G, rng, h)` (lines 200-202) with the labels `Y` given:
`plain_layout(G, Y)`. (Final.) -/
def algoT2a (G : Graph) (Y : Labels) : List Bag := plainLayout G Y
/-- Python `algo_mixed(G, rng, h)` (lines 205-209):
`(H, ops, f1) := reduceRecordF G.copy`;
`bagsH := if H.numNodes = 0 then [] else plainLayout H (lab H)`;
`(bags, f2) := undoF G ops H bagsH`; result `(bags, f1 || f2)`.
Contract (`AlgoMixedValid`): valid for every `G.WF` and every labeler. -/
def algoMixedF (G : Graph) (lab : Labeler) : List Bag × Bool :=
let r := reduceRecordF G.copy
let H := r.1
let bagsH := if H.numNodes = 0 then [] else plainLayout H (lab H)
let u := undoF G r.2.1 H bagsH
(u.1, r.2.2 || u.2)
/-- `algo_mixed` without the flag. -/
def algoMixed (G : Graph) (lab : Labeler) : List Bag := (algoMixedF G lab).1
/-- The kind of a final component in `algo_T4`. -/
inductive FinalKind where
| sweep
| thin
deriving BEq, Repr, Inhabited, DecidableEq
/-- An entry `(J, kind, tops)` of Python's `final` (`tops = []` for Python `None`). -/
structure FinalEntry where
graph : Graph
kind : FinalKind
tops : List Op
deriving Inhabited
/-- The result of `algo_T4`: Python's `(bags, Dn, final)` plus the fallback flag. -/
structure T4Result where
bags : List Bag
numDeleted : Nat
final : List FinalEntry
fallback : Bool
deriving Inhabited
/-- Python lines 223-225: among the nodes of maximum degree `Dl := J.maxDegree`
(`cands`, node order), those with a neighbour of smaller degree (`good`); the result
is the numerically smallest element of `good` if nonempty, else of `cands`
(`min(good or cands)`), `0` if `J` is empty. -/
def t4ChooseW (J : Graph) : Nat :=
let Dl := J.maxDegree
let cands := J.nodes.filter fun v => J.degree v == Dl
let good := cands.filter fun v => (J.nbrs v).any fun u => J.degree u < Dl
let pool := if good.isEmpty then cands else good
(pool.min?).getD 0
/-- The `while work:` loop of `algo_T4` (lines 218-230). `stack` is Python's `work`
(head = Python end), `final` is Python's `final` (in Python order), `ops` the global
ops (chronological). One step with `fuel + 1`, `J :: stack`:
* if `J.maxDegree ≤ 5`: `final := final ++ [⟨J, .sweep, []⟩]`;
* else `w := t4ChooseW J`, `(Js, red, f) := reduceRecordF (J.copy.removeNode w)`,
`tops := .del w :: red`; if `Js.numNodes = 0` then `final := final ++ [⟨J, .thin, tops⟩]`,
else `ops := ops ++ tops` and push the components of `Js`
(Python `work += [Js.subgraph(c).copy() for c in nx.connected_components(Js)]`, so
`stack := (componentGraphs Js).reverse ++ stack`).
Empty stack: `(final, ops, false)`. Fuel `0` with a nonempty stack: FALLBACK, the
remaining graphs (in stack order) are appended to `final` as `.sweep` entries, flag
`true`. The flag also includes every `f`. Fuel bound: `2 * numNodes` rounds. -/
def t4Loop (fuel : Nat) (stack : List Graph) (final : List FinalEntry) (ops : List Op) :
List FinalEntry × List Op × Bool :=
match fuel, stack with
| _, [] => (final, ops, false)
| 0, _ :: _ => (final ++ stack.map fun J => FinalEntry.mk J FinalKind.sweep [], ops, true)
| fuel + 1, J :: rest =>
if J.maxDegree ≤ 5 then
t4Loop fuel rest (final ++ [FinalEntry.mk J FinalKind.sweep []]) ops
else
let w := t4ChooseW J
let red := reduceRecordF (J.copy.removeNode w)
let tops := Op.del w :: red.2.1
if red.1.numNodes = 0 then
let r := t4Loop fuel rest (final ++ [FinalEntry.mk J FinalKind.thin tops]) ops
(r.1, r.2.1, r.2.2 || red.2.2)
else
let r := t4Loop fuel ((componentGraphs red.1).reverse ++ rest) final (ops ++ tops)
(r.1, r.2.1, r.2.2 || red.2.2)
/-- The decomposition of a final component (lines 234-237): `.sweep` →
`(plainLayout e.graph (lab e.graph), false)`; `.thin` →
`undoF e.graph e.tops Graph.empty []`. -/
def t4Decompose (lab : Labeler) (e : FinalEntry) : List Bag × Bool :=
match e.kind with
| .sweep => (plainLayout e.graph (lab e.graph), false)
| .thin => undoF e.graph e.tops Graph.empty []
/-- Python `algo_T4(G, rng, h)` (lines 212-241) under model M:
* `G0 := nonIsolated G`; `(H, ops0, f0) := reduceRecordF G0.copy`;
* `work := componentGraphs H` (Python order);
* `(final, ops, f1) := t4Loop (2 * H.numNodes + 1) work.reverse [] ops0`;
* `R := unionAll (final.map (·.graph))`; `parts := final.map (t4Decompose lab)`;
`bagsR := parts.flatMap (·.1)`;
* `(bags, f2) := undoF G0 ops R bagsR`; result bags `bags ++ isolatedBags G`;
* `numDeleted := (deletedSet ops).length`; `fallback := f0 || f1 || any parts flag || f2`.
Contract (`AlgoT4Valid`): valid for every `G.WF` and every labeler. -/
def algoT4Full (G : Graph) (lab : Labeler) : T4Result :=
let G0 := nonIsolated G
let r0 := reduceRecordF G0.copy
let H := r0.1
let work := componentGraphs H
let lp := t4Loop (2 * H.numNodes + 1) work.reverse [] r0.2.1
let final := lp.1
let R := unionAll (final.map (·.graph))
let parts := final.map (t4Decompose lab)
let bagsR := parts.flatMap (·.1)
let u := undoF G0 lp.2.1 R bagsR
⟨u.1 ++ isolatedBags G, (deletedSet lp.2.1).length, final,
r0.2.2 || lp.2.2 || parts.any (·.2) || u.2⟩
/-- `algo_T4`'s bags. -/
def algoT4 (G : Graph) (lab : Labeler) : List Bag := (algoT4Full G lab).bags
end CubicPathwidth.Alg
Back to top