cubical-transport-hott-lean4/Topolei/EML/Path.lean
Maximus Gorog c2e3ecb3e3
Some checks are pending
Lean Action CI / build (push) Waiting to run
Initial commit: topolei — cubical-transport HoTT in Lean 4 + Rust FFI
Implements the cells-spec vision: a computation space that preserves
auditability, correctness, interactivity. Phase 1 (Lean kernel +
naga-IR Rust backend) is closed; foundation hypothesis stack
(Selection H1+H2, Subobject H3, Trace H5, Obs.Ctx C2, Cubical.Trace)
landed.

Highlights:
- Cubical-HoTT syntax + value/eval/readback in Lean
- naga-IR pipeline (no GLSL string crosses FFI; 17/17 probes pass)
- Honesty audit: every non-transport (sealed cells, vertex shader,
  Y-flip, presentation conventions) is documented as such
- Polymorphic Trace α as free monoid; Cubical.Trace gives
  CTerm → Trace CTerm by structural fold (homomorphism = definition)
- Selection as Huet zipper; Subobject as Boolean algebra over WCell
- All theorems proven; the proof IS the implementation

See STATUS.md for the resume guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:40:45 -06:00

243 lines
11 KiB
Text

/-
Topolei.EML.Path
================
Connection between EML expressions and the cubical interval.
An EML expression with a free "dimension variable" (a named string)
that is evaluated at 0.0 or 1.0 is a path in the Float domain.
This is the bridge between:
· the cubical interval I (Bool endpoints: false = 0, true = 1)
· EML evaluation (Float-valued)
Key definitions:
· EMLExpr.evalWithEnv — generalised evaluator with custom var resolver
· EMLExpr.varAbsent — syntactic check that a variable does not appear
· EMLPath — an EML expression with a distinguished dim var
· EMLPath.atBool/at0/at1 — evaluation at Bool endpoints
· EMLPath.const_endpoints — absent dim var → at0 = at1
The GPU-layer connection (linking baseEnv to shaderVar) lives in
GPU/Spec.lean rather than here, to avoid a circular import.
-/
import Topolei.EML
import Topolei.Cubical.DimLine
-- ── Bool → Float endpoint map ─────────────────────────────────────────────────
/-- Map Bool to its Float interval endpoint: false ↦ 0.0, true ↦ 1.0 -/
def boolToFloat : Bool → Float
| false => 0.0
| true => 1.0
@[simp] theorem boolToFloat_false : boolToFloat false = 0.0 := rfl
@[simp] theorem boolToFloat_true : boolToFloat true = 1.0 := rfl
-- ── Generalised EML evaluator ─────────────────────────────────────────────────
/-- Evaluate an EML expression using a custom variable resolver.
This generalises EMLExpr.evalAt (which uses shaderVar) so we can
override the dimension variable without touching shader-layer types. -/
def EMLExpr.evalWithEnv (env : String → Float) : EMLExpr → Float
| .one => 1.0
| .var name => env name
| .eml l r =>
let lv := l.evalWithEnv env
let rv := r.evalWithEnv env
Float.exp lv - Float.log (max rv 1e-9)
-- ── Variable absence predicate ────────────────────────────────────────────────
/-- Syntactic check: named variable does not appear in the expression. -/
def EMLExpr.varAbsent (name : String) : EMLExpr → Bool
| .one => true
| .var n => n != name
| .eml l r => l.varAbsent name && r.varAbsent name
/-- Two environments that agree on all names except `name` give the same
evaluation on expressions that don't mention `name`. -/
theorem EMLExpr.evalWithEnv_congr
(e : EMLExpr) (name : String)
(habs : e.varAbsent name = true)
(env1 env2 : String → Float)
(henv : ∀ n, n ≠ name → env1 n = env2 n) :
e.evalWithEnv env1 = e.evalWithEnv env2 := by
induction e with
| one => rfl
| var n =>
simp only [varAbsent, bne_iff_ne] at habs
simp [evalWithEnv, henv n habs]
| eml l r ihl ihr =>
simp only [varAbsent, Bool.and_eq_true] at habs
simp [evalWithEnv, ihl habs.1, ihr habs.2]
-- ── EMLPath ───────────────────────────────────────────────────────────────────
/-- An EML path: an expression parametric in a named dimension variable.
The `dimName` variable ranges over {0.0, 1.0} ≅ Bool. -/
structure EMLPath where
dimName : String -- dimension variable (e.g. "t")
body : EMLExpr -- parametric expression
/-- Evaluate an EMLPath at a Bool endpoint, given a base variable resolver for
all variables other than dimName. -/
def EMLPath.atBool (path : EMLPath) (b : Bool) (baseEnv : String → Float) : Float :=
path.body.evalWithEnv (fun name =>
if name = path.dimName then boolToFloat b else baseEnv name)
def EMLPath.at0 (path : EMLPath) (baseEnv : String → Float) : Float :=
path.atBool false baseEnv
def EMLPath.at1 (path : EMLPath) (baseEnv : String → Float) : Float :=
path.atBool true baseEnv
-- ── Endpoint reduction lemmas ─────────────────────────────────────────────────
@[simp] theorem EMLPath.at0_def (path : EMLPath) (baseEnv : String → Float) :
path.at0 baseEnv =
path.body.evalWithEnv (fun name =>
if name = path.dimName then 0.0 else baseEnv name) := by
simp [at0, atBool, boolToFloat]
@[simp] theorem EMLPath.at1_def (path : EMLPath) (baseEnv : String → Float) :
path.at1 baseEnv =
path.body.evalWithEnv (fun name =>
if name = path.dimName then 1.0 else baseEnv name) := by
simp [at1, atBool, boolToFloat]
-- ── Constant path ─────────────────────────────────────────────────────────────
/-- When the body does not mention dimName, at0 = at1 for any baseEnv. -/
theorem EMLPath.const_endpoints (path : EMLPath)
(habs : path.body.varAbsent path.dimName = true)
(baseEnv : String → Float) :
path.at0 baseEnv = path.at1 baseEnv := by
simp only [at0_def, at1_def]
apply EMLExpr.evalWithEnv_congr _ _ habs
intro n hn
simp [if_neg hn]
-- ── Example: exp path ────────────────────────────────────────────────────────
/-- The exponential path: f(t) = exp(t) for t ∈ {0, 1}. -/
def expPath : EMLPath :=
{ dimName := "t"
body := EMLExpr.expOf (.var "t") }
/-- At t = 0, expPath evaluates to exp(0) - log(max 1.0 1e-9). -/
theorem expPath_at0 (baseEnv : String → Float) :
expPath.at0 baseEnv =
Float.exp 0.0 - Float.log (max 1.0 1e-9) := by
simp [expPath, EMLExpr.expOf, EMLExpr.evalWithEnv]
/-- At t = 1, expPath evaluates to exp(1) - log(max 1.0 1e-9). -/
theorem expPath_at1 (baseEnv : String → Float) :
expPath.at1 baseEnv =
Float.exp 1.0 - Float.log (max 1.0 1e-9) := by
simp [expPath, EMLExpr.expOf, EMLExpr.evalWithEnv]
-- ── Connection to DimLine ─────────────────────────────────────────────────────
/-
Structural parallel:
DimLine (cubical, CType-valued) ↔ EMLPath (rendering, Float-valued)
───────────────────────────────────────────────────────────────────────
DimLine.binder : DimVar ↔ EMLPath.dimName : String
DimLine.at0 : CType ↔ EMLPath.at0 : Float
DimLine.at1 : CType ↔ EMLPath.at1 : Float
transp_const_id (T2) ↔ EMLPath.const_endpoints
Transport along a constant DimLine is the identity (T2).
EML analogue: evaluation of a constant EMLPath (dimName absent) gives
the same Float value at both endpoints.
The rendering correctness claim:
"If the GPU evaluates shader at the 1-end of a DimLine,
the Float result equals EMLPath.at1 baseEnv."
This is the bridge axiom in GPU/Spec.lean (to be added there).
-/
theorem EMLPath_const_mirrors_T2
(path : EMLPath)
(habs : path.body.varAbsent path.dimName = true)
(baseEnv : String → Float) :
path.at0 baseEnv = path.at1 baseEnv :=
EMLPath.const_endpoints path habs baseEnv
-- ── PlotConfig → EMLPath ──────────────────────────────────────────────────────
/-
A `PlotConfig` carries an `EMLExpr` and a distinguished `dimName` which is
the path dimension. Viewing a plot as an `EMLPath` is a direct projection:
drop the display metadata, keep the expression and the dimension.
When the plot's expression does not mention `dimName`, the resulting path
is constant (T2 analogue); when it does, the plot is genuinely time-varying
and `at0`, `at1` differ.
-/
/-- Project a `PlotConfig` to its `EMLPath` view. -/
def PlotConfig.toEMLPath (cfg : PlotConfig) : EMLPath :=
{ dimName := cfg.dimName
body := cfg.expr }
@[simp] theorem PlotConfig.toEMLPath_dimName (cfg : PlotConfig) :
cfg.toEMLPath.dimName = cfg.dimName := rfl
@[simp] theorem PlotConfig.toEMLPath_body (cfg : PlotConfig) :
cfg.toEMLPath.body = cfg.expr := rfl
/-- A plot is a *constant path* exactly when its expression does not mention
the plot's declared dimension variable. -/
theorem PlotConfig.const_path_of_varAbsent
(cfg : PlotConfig)
(habs : cfg.expr.varAbsent cfg.dimName = true)
(baseEnv : String → Float) :
cfg.toEMLPath.at0 baseEnv = cfg.toEMLPath.at1 baseEnv :=
EMLPath.const_endpoints cfg.toEMLPath habs baseEnv
-- ── Demo expressions as paths ─────────────────────────────────────────────────
/-
`plotExp` and `plotLn` (in `EML.lean`) use `px` as their free variable,
not the dimension variable `t`. They are therefore *constant paths* — the
same value at both endpoints — under the default `dimName := "t"`.
-/
theorem plotExp_body_varAbsent : plotExp.expr.varAbsent "t" = true := by decide
theorem plotExp_is_const_path (baseEnv : String → Float) :
plotExp.toEMLPath.at0 baseEnv = plotExp.toEMLPath.at1 baseEnv :=
plotExp.const_path_of_varAbsent plotExp_body_varAbsent baseEnv
theorem plotLn_body_varAbsent : plotLn.expr.varAbsent "t" = true := by decide
theorem plotLn_is_const_path (baseEnv : String → Float) :
plotLn.toEMLPath.at0 baseEnv = plotLn.toEMLPath.at1 baseEnv :=
plotLn.const_path_of_varAbsent plotLn_body_varAbsent baseEnv
-- ── Parametric example (a genuinely non-constant path) ───────────────────────
/-- A parametric plot that actually uses the dimension variable `t`:
`eml(t, 1) = exp(t)` — a path from `exp(0)` at `t=0` to `exp(1)` at `t=1`. -/
def plotExpT : PlotConfig :=
{ expr := EMLExpr.expOf (.var "t")
dimName := "t" }
theorem plotExpT_at0 (baseEnv : String → Float) :
plotExpT.toEMLPath.at0 baseEnv =
Float.exp 0.0 - Float.log (max 1.0 1e-9) := by
simp [plotExpT, PlotConfig.toEMLPath, EMLPath.at0, EMLPath.atBool,
EMLExpr.expOf, EMLExpr.evalWithEnv, boolToFloat]
theorem plotExpT_at1 (baseEnv : String → Float) :
plotExpT.toEMLPath.at1 baseEnv =
Float.exp 1.0 - Float.log (max 1.0 1e-9) := by
simp [plotExpT, PlotConfig.toEMLPath, EMLPath.at1, EMLPath.atBool,
EMLExpr.expOf, EMLExpr.evalWithEnv, boolToFloat]
-- The shader whose semantic IS `EMLPath.toColor` is now built directly
-- as a `naga::Module` (no GLSL string intermediary) on the Rust side
-- by `native/canvas-rs/src/emit_naga.rs::build_probe_module`. See
-- `NAGA_IR_PLAN.md` for the construction; `Topolei.GPU.Spec`'s
-- `compileEMLPath_correct` axiom states the contract that builder
-- must satisfy.