CubicPathwidth.Alg.DiffMain
Source file CubicPathwidth/Alg/DiffMain.lean of the Lean project (how it is built).
This file is not part of the libraries that lake build builds.
import Lean.Data.Json
import CubicPathwidth.Alg.Port
/-!
# Differential test runner: Lean port vs `sweep.py`
Reads the JSON written by `ancillary/experiments/sweep-algorithms/lean_diff.py` and compares every
recorded case with the Lean port. Run from `lean/` (after `lake build CubicPathwidth.Alg.Port`):
```
lake env lean --run CubicPathwidth/Alg/DiffMain.lean \
../ancillary/experiments/sweep-algorithms/lean_diff/cases.json [fn|all] [maxCasesPerFn]
```
Comparison rules (bags are compared as SETS: each bag is sorted and deduplicated on both
sides; the list of bags is compared in order):
* `induced`, `components`, `reduce_record`, `connector_order`: exact lists (orders matter);
* `plain_layout`, `algo_T2a`: bags;
* `switched_layout`, `pd_tw2`, `undo`: if Python raised, the Lean flag must be `true`;
otherwise the flag must be `false` and the bags equal;
* `algo_T1`, `algo_mixed`: flag `false` and bags equal;
* `algo_T4`: fallback `false`, bags equal, `Dn` equal, and `final` equal (kinds and node
lists in order).
Moreover every Lean output of a decomposition is checked with `checkPD` (a mirror of
`check_pd`), also when Python raised and the Lean fallback produced it.
Exit code 0 iff every selected case passes.
-/
open Lean CubicPathwidth.Alg
namespace CubicPathwidth.Alg.Diff
/-! ## JSON decoding -/
def natList (j : Json) : Except String (List Nat) := do
(← j.getArr?).toList.mapM (·.getNat?)
def natListList (j : Json) : Except String (List (List Nat)) := do
(← j.getArr?).toList.mapM natList
def graphOf (j : Json) : Except String Graph := do
let nodes ← natList (← j.getObjVal? "nodes")
let adj ← natListList (← j.getObjVal? "adj")
return Graph.ofAdjLists nodes adj
/-- Labels `[[v, num, den], …]` as a function (missing vertices get `0`). -/
def labelsOf (j : Json) : Except String Labels := do
let rows ← (← j.getArr?).toList.mapM fun r => do
let v ← (← r.getArrVal? 0).getNat?
let num ← (← r.getArrVal? 1).getInt?
let den ← (← r.getArrVal? 2).getNat?
return (v, mkRat num den)
let tab : Std.HashMap Nat Rat := rows.foldl (fun t p => t.insert p.1 p.2) ∅
return fun v => tab.getD v 0
def opOf (j : Json) : Except String Op := do
let tag ← (← j.getArrVal? 0).getStr?
if tag == "red" then
return .red (← (← j.getArrVal? 1).getNat?) (← natList (← j.getArrVal? 2))
else if tag == "del" then
return .del (← (← j.getArrVal? 1).getNat?)
else throw s!"unknown op {tag}"
def opsOf (j : Json) : Except String (List Op) := do
(← j.getArr?).toList.mapM opOf
def kindOf (s : String) : Except String FinalKind :=
if s == "sweep" then pure .sweep else if s == "thin" then pure .thin
else throw s!"unknown kind {s}"
/-! ## Canonical forms -/
def dedupSorted : List Nat → List Nat
| a :: b :: t => if a == b then dedupSorted (b :: t) else a :: dedupSorted (b :: t)
| l => l
def canonBag (B : Bag) : List Nat := dedupSorted (B.mergeSort (· ≤ ·))
def canonBags (bags : List Bag) : List (List Nat) := bags.map canonBag
def showBags (bags : List (List Nat)) : String :=
let s := toString bags
if s.length > 300 then (s.take 300).toString ++ "…" else s
def cmpBags (lean py : List Bag) : Option String :=
let a := canonBags lean
let b := canonBags py
if a == b then none
else some s!"bags differ: lean {a.length} bags {showBags a} vs python {b.length} bags {showBags b}"
def opsStr (ops : List Op) : String :=
toString (ops.map fun | .red v nb => s!"red {v} {nb}" | .del w => s!"del {w}")
/-- Computable mirror of Python `check_pd` (sweep.py lines 19-30): every node in some bag,
the bags of each node contiguous, every edge in some bag. -/
def checkPD (G : Graph) (bags : List Bag) : Bool := Id.run do
let mut first : Std.HashMap Nat Nat := ∅
let mut last : Std.HashMap Nat Nat := ∅
let mut cnt : Std.HashMap Nat Nat := ∅
for (B, i) in bags.zipIdx do
for v in canonBag B do
if !first.contains v then first := first.insert v i
last := last.insert v i
cnt := cnt.insert v (cnt.getD v 0 + 1)
for v in G.nodes do
if !first.contains v then return false
if cnt.getD v 0 != last.getD v 0 - first.getD v 0 + 1 then return false
for v in G.nodes do
for u in G.nbrs v do
if max (first.getD u 0) (first.getD v 0) > min (last.getD u 0) (last.getD v 0) then
return false
return true
/-! ## One case -/
/-- If Python raised, the Lean flag must be set (and the Lean output must still be a valid
decomposition, which `k ()` does not check: see `withValid`); otherwise the flag must be
clear and `k ()` decides. -/
def checkFlag (flag : Bool) (raised : Bool) (k : Unit → Option String) : Option String :=
if raised then (if flag then none else some "python raised but the lean flag is false")
else if flag then some "lean fallback fired but python did not raise"
else k ()
/-- Also require that the Lean output is a valid decomposition of `G` (`checkPD`). -/
def withValid (G : Graph) (bags : List Bag) (r : Option String) : Option String :=
match r with
| some m => some m
| none => if checkPD G bags then none else some "lean output is not a valid decomposition"
def raisedOf (j : Json) : Bool :=
match j.getObjValAs? Bool "raised" with
| .ok b => b
| .error _ => false
/-- Returns `none` if the case passes, or a description of the mismatch. -/
def runCase (fn : String) (j : Json) : Except String (Option String) := do
match fn with
| "induced" =>
let G ← graphOf (← j.getObjVal? "graph")
let keep ← natList (← j.getObjVal? "keep")
let ks : Std.HashSet Nat := keep.foldl (fun s v => s.insert v) ∅
let H := G.induced ks.contains
let P ← graphOf (← j.getObjVal? "out_graph")
return if (H.nodes, H.adjLists) == (P.nodes, P.adjLists) then none
else some s!"lean {repr H} vs python {repr P}"
| "components" =>
let G ← graphOf (← j.getObjVal? "graph")
let out ← natListList (← j.getObjVal? "out")
return if G.components == out then none else some s!"lean {G.components} vs python {out}"
| "plain_layout" | "algo_T2a" =>
let G ← graphOf (← j.getObjVal? "graph")
let Y ← labelsOf (← j.getObjVal? "labels")
let out ← natListList (← j.getObjVal? "out")
let res := if fn == "plain_layout" then plainLayout G Y else algoT2a G Y
return withValid G res (cmpBags res out)
| "connector_order" =>
let Qs ← natList (← j.getObjVal? "Qs")
let items ← natListList (← j.getObjVal? "items")
let out ← natList (← j.getObjVal? "out")
let (q, flag) := connectorOrderF Qs items
return checkFlag flag false fun _ =>
if q == out then none else some s!"lean {q} vs python {out}"
| "switched_layout" | "algo_T1" =>
let G ← graphOf (← j.getObjVal? "graph")
let Y ← labelsOf (← j.getObjVal? "labels")
let out ← natListList (← j.getObjVal? "out")
let (res, flag) := if fn == "algo_T1" then algoT1F G Y else switchedLayoutF G Y
return withValid G res (checkFlag flag (raisedOf j) fun _ => cmpBags res out)
| "reduce_record" =>
let G ← graphOf (← j.getObjVal? "graph")
let P ← graphOf (← j.getObjVal? "out_graph")
let ops ← opsOf (← j.getObjVal? "ops")
let (H, ops', flag) := reduceRecordF G
return checkFlag flag false fun _ =>
if (H.nodes, H.adjLists) != (P.nodes, P.adjLists) then
some s!"graph: lean {repr H} vs python {repr P}"
else if ops' != ops then some s!"ops: lean {opsStr ops'} vs python {opsStr ops}"
else none
| "pd_tw2" =>
let H ← graphOf (← j.getObjVal? "graph")
let out ← natListList (← j.getObjVal? "out")
let (res, flag) := pdTw2F H
return withValid H res (checkFlag flag (raisedOf j) fun _ => cmpBags res out)
| "undo" =>
let F ← graphOf (← j.getObjVal? "F")
let R ← graphOf (← j.getObjVal? "R")
let ops ← opsOf (← j.getObjVal? "ops")
let bagsR ← natListList (← j.getObjVal? "bagsR")
let out ← natListList (← j.getObjVal? "out")
let (res, flag) := undoF F ops R bagsR
return withValid F res (checkFlag flag (raisedOf j) fun _ => cmpBags res out)
| "algo_mixed" =>
let G ← graphOf (← j.getObjVal? "graph")
let Y ← labelsOf (← j.getObjVal? "labels")
let out ← natListList (← j.getObjVal? "out")
let (res, flag) := algoMixedF G (fun _ => Y)
return withValid G res (checkFlag flag false fun _ => cmpBags res out)
| "algo_T4" =>
let G ← graphOf (← j.getObjVal? "graph")
let Y ← labelsOf (← j.getObjVal? "labels")
let out ← natListList (← j.getObjVal? "out")
let dn ← (← j.getObjVal? "Dn").getNat?
let final ← (← (← j.getObjVal? "final").getArr?).toList.mapM fun e => do
let k ← kindOf (← (← e.getArrVal? 0).getStr?)
let ns ← natList (← e.getArrVal? 1)
return (k, ns)
let r := algoT4Full G (fun _ => Y)
let lf := r.final.map fun e => (e.kind, e.graph.nodes)
return withValid G r.bags <| checkFlag r.fallback false fun _ =>
match cmpBags r.bags out with
| some m => some m
| none =>
if r.numDeleted != dn then some s!"Dn: lean {r.numDeleted} vs python {dn}"
else if lf != final then some s!"final: lean {repr lf} vs python {repr final}"
else none
| _ => throw s!"unknown fn {fn}"
end CubicPathwidth.Alg.Diff
open CubicPathwidth.Alg.Diff in
def main (args : List String) : IO UInt32 := do
let path := args.headD "../experiments/sweep-algorithms/lean_diff/cases.json"
let only := args.getD 1 "all"
let maxPer := (args.getD 2 "1000000000").toNat!
let txt ← IO.FS.readFile path
let j ← IO.ofExcept (Json.parse txt)
let cases ← IO.ofExcept ((← IO.ofExcept (j.getObjVal? "cases")).getArr?)
let mut stats : Std.HashMap String (Nat × Nat) := ∅
let mut shown : Std.HashMap String Nat := ∅
let mut order : Array String := #[]
for c in cases do
let fn ← IO.ofExcept ((← IO.ofExcept (c.getObjVal? "fn")).getStr?)
if only != "all" && only != fn then continue
let (pass, total) := stats.getD fn (0, 0)
if total ≥ maxPer then continue
if !stats.contains fn then order := order.push fn
match runCase fn c with
| .error e =>
stats := stats.insert fn (pass, total + 1)
IO.println s!"[{fn} #{total}] decode error: {e}"
| .ok none => stats := stats.insert fn (pass + 1, total + 1)
| .ok (some msg) =>
stats := stats.insert fn (pass, total + 1)
let k := shown.getD fn 0
if k < 3 then
IO.println s!"[{fn} #{total}] MISMATCH: {msg}"
shown := shown.insert fn (k + 1)
let mut allOk := true
for fn in order do
let (pass, total) := stats.getD fn (0, 0)
if pass != total then allOk := false
IO.println s!"{fn}: {pass}/{total} pass"
return if allOk then 0 else 1
Back to top