Skip to content

Commit c4a2c89

Browse files
feat(fv): deadzone_mono_dz_pos proved + correspondence tests (run118)
Task 5 (Proof Assistance): - Proved deadzone_mono_dz_pos: wider deadzone → smaller output for fixed positive input. Replaced broken ring_nf proof with stdlib-only approach: - dz_inv_anti: a⁻¹ ≤ b⁻¹ when b ≤ a and both positive (no Mathlib) - dz_div_rewrite: (x-dz)/(1-dz) = 1-(1-x)/(1-dz) - dz_sub_le_sub_l: c-b ≤ c-a when a ≤ b All proofs use only Lean 4 stdlib (no ring/linarith/Mathlib required). lake build passes with 0 sorry. Task 8 (Correspondence Validation): - Added formal-verification/tests/deadzone/ with Route B tests - 1221 test cases covering: no-dz identity, inside-deadzone zero, positive/negative branches, boundary values, odd symmetry, monotonicity in both value and dz, dense grid, rational fractions, output range [-1,1] - All 1221 cases pass, confirming Lean model matches C++ reference exactly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 259f46f commit c4a2c89

3 files changed

Lines changed: 449 additions & 0 deletions

File tree

formal-verification/lean/FVSquad/Deadzone.lean

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,112 @@ theorem deadzone_mono_val (v1 v2 dz : Rat) (hv : v1 ≤ v2) (hdz0 : 0 ≤ dz) (h
388388
· -- Both inside: 0 ≤ 0
389389
rw [deadzone_in_dz v2 dz (Rat.not_lt.mp h2)]; exact Rat.le_refl
390390

391+
/-! ## No-deadzone identity (general) -/
392+
393+
/-- The negative case of the no-deadzone identity: when `dz = 0`, `deadzone x 0 = x` for `x < 0`. -/
394+
theorem deadzone_no_dz_neg (x : Rat) (hx : x < 0) : deadzone x 0 = x := by
395+
rw [deadzone_neg_eq x 0 (by rw [Rat.neg_zero]; exact hx) (Rat.le_refl)]
396+
rw [Rat.add_zero, Rat.sub_eq_add_neg, Rat.neg_zero, Rat.add_zero,
397+
Rat.div_def, Rat.inv_eq_of_mul_eq_one (Rat.mul_one 1), Rat.mul_one]
398+
399+
/-- **Identity with no deadzone** (all cases): when `dz = 0`, `deadzone x 0 = x` for all `x`.
400+
401+
Combining `deadzone_no_dz_pos`, `deadzone_no_dz_neg`, and the zero case (which is in the
402+
deadzone since `|0| = 0 = dz`), we get the full identity. -/
403+
theorem deadzone_no_dz (x : Rat) : deadzone x 0 = x := by
404+
by_cases h1 : 0 < x
405+
· exact deadzone_no_dz_pos x h1
406+
· by_cases h2 : x = 0
407+
· subst h2
408+
simp [deadzone]
409+
· exact deadzone_no_dz_neg x (Rat.lt_of_le_of_ne (Rat.not_lt.mp h1) h2)
410+
411+
/-! ## Monotonicity in the deadzone width -/
412+
413+
-- Private helpers for `deadzone_mono_dz_pos`
414+
415+
/-- Inverse anti-monotonicity: larger positive denominators give smaller inverses. -/
416+
private theorem dz_inv_anti (a b : Rat) (hba : b ≤ a) (ha : 0 < a) (hb : 0 < b) : a⁻¹ ≤ b⁻¹ := by
417+
have ha_ne : a ≠ 0 := Rat.ne_of_gt ha
418+
have hb_ne : b ≠ 0 := Rat.ne_of_gt hb
419+
rw [Rat.le_iff_sub_nonneg]
420+
have key : b⁻¹ - a⁻¹ = (a - b) * (a⁻¹ * b⁻¹) := by
421+
simp only [Rat.sub_eq_add_neg, Rat.add_mul, Rat.neg_mul]
422+
rw [show a * (a⁻¹ * b⁻¹) = b⁻¹ by
423+
rw [← Rat.mul_assoc a a⁻¹ b⁻¹, Rat.mul_inv_cancel a ha_ne, Rat.one_mul]]
424+
rw [show b * (a⁻¹ * b⁻¹) = a⁻¹ by
425+
rw [Rat.mul_comm a⁻¹ b⁻¹, ← Rat.mul_assoc b b⁻¹ a⁻¹,
426+
Rat.mul_inv_cancel b hb_ne, Rat.one_mul]]
427+
rw [key]
428+
exact Rat.mul_nonneg ((Rat.le_iff_sub_nonneg b a).mp hba)
429+
(Rat.mul_nonneg (Rat.le_of_lt (Rat.inv_pos.mpr ha)) (Rat.le_of_lt (Rat.inv_pos.mpr hb)))
430+
431+
/-- Rewrite `(x - dz) / (1 - dz)` as `1 - (1 - x) / (1 - dz)`. -/
432+
private theorem dz_div_rewrite (x dz : Rat) (h1mdz : 0 < 1 - dz) :
433+
(x - dz) / (1 - dz) = 1 - (1 - x) / (1 - dz) := by
434+
rw [Rat.div_def, Rat.div_def]
435+
have h1mdz_ne : (1 - dz) ≠ 0 := Rat.ne_of_gt h1mdz
436+
have hn : (1 - dz) - (1 - x) = x - dz := by
437+
simp only [Rat.sub_eq_add_neg, Rat.neg_add, Rat.neg_neg]
438+
rw [Rat.add_assoc, ← Rat.add_assoc (-dz) (-1) x, Rat.add_comm (-dz) (-1),
439+
Rat.add_assoc (-1) (-dz) x, ← Rat.add_assoc 1 (-1) (-dz + x),
440+
show (1:Rat) + -1 = 0 from Rat.add_neg_cancel 1, Rat.zero_add, Rat.add_comm (-dz) x]
441+
rw [show (x - dz) * (1 - dz)⁻¹ = ((1 - dz) - (1 - x)) * (1 - dz)⁻¹ from by rw [hn]]
442+
rw [show ((1 - dz) - (1 - x)) * (1 - dz)⁻¹ = (1 - dz) * (1 - dz)⁻¹ - (1 - x) * (1 - dz)⁻¹
443+
from by simp only [Rat.sub_eq_add_neg, Rat.add_mul, Rat.neg_mul]]
444+
rw [Rat.mul_inv_cancel _ h1mdz_ne]
445+
446+
/-- `c - b ≤ c - a` when `a ≤ b`. -/
447+
private theorem dz_sub_le_sub_l (a b c : Rat) (h : a ≤ b) : c - b ≤ c - a := by
448+
rw [Rat.le_iff_sub_nonneg (c - b) (c - a)]
449+
have key : (c - a) - (c - b) = b - a := by
450+
simp only [Rat.sub_eq_add_neg, Rat.neg_add, Rat.neg_neg]
451+
rw [Rat.add_assoc c (-a) (-c + b), ← Rat.add_assoc (-a) (-c) b, Rat.add_comm (-a) (-c),
452+
Rat.add_assoc (-c) (-a) b, ← Rat.add_assoc c (-c) (-a + b),
453+
show c + -c = (0:Rat) from Rat.add_neg_cancel c, Rat.zero_add, Rat.add_comm (-a) b]
454+
rw [key]; exact (Rat.le_iff_sub_nonneg a b).mp h
455+
456+
/-- **Monotonicity in `dz`** (positive branch): for a fixed input `x`,
457+
increasing the deadzone width `dz` weakly decreases the output.
458+
459+
Statement: `dz₁ ≤ dz₂ < x ≤ 1`, `0 ≤ dz₁`, `dz₂ < 1` →
460+
`deadzone x dz₂ ≤ deadzone x dz₁`.
461+
462+
Proof: rewrite `(x - dz) / (1 - dz) = 1 - (1 - x) / (1 - dz)`. Since `dz₁ ≤ dz₂`,
463+
we have `1 - dz₂ ≤ 1 - dz₁`, so `(1 - dz₁)⁻¹ ≤ (1 - dz₂)⁻¹` by inverse anti-monotonicity.
464+
Multiplying by `1 - x ≥ 0` gives `(1 - x)/(1 - dz₁) ≤ (1 - x)/(1 - dz₂)`, hence the
465+
subtraction from 1 reverses the inequality. -/
466+
theorem deadzone_mono_dz_pos (x dz1 dz2 : Rat)
467+
(hx : 0 < x) (hx1 : x ≤ 1)
468+
(h12 : dz1 ≤ dz2) (hdz0 : 0 ≤ dz1) (hdz1 : dz2 < 1) (hout : dz2 < x) :
469+
deadzone x dz2 ≤ deadzone x dz1 := by
470+
have hout1 : dz1 < x := Std.lt_of_le_of_lt h12 hout
471+
rw [deadzone_pos_eq x dz1 hout1 hdz0,
472+
deadzone_pos_eq x dz2 hout (Rat.le_trans hdz0 h12)]
473+
have hd1 : (0:Rat) < 1 - dz1 := dz_one_sub_pos dz1 (Std.lt_of_le_of_lt h12 hdz1)
474+
have hd2 : (0:Rat) < 1 - dz2 := dz_one_sub_pos dz2 hdz1
475+
-- Rewrite both sides: (x - dz) / (1 - dz) = 1 - (1 - x) / (1 - dz)
476+
rw [dz_div_rewrite x dz1 hd1, dz_div_rewrite x dz2 hd2]
477+
-- Goal: 1 - (1 - x) / (1 - dz2) ≤ 1 - (1 - x) / (1 - dz1)
478+
apply dz_sub_le_sub_l
479+
-- Goal: (1 - x) / (1 - dz1) ≤ (1 - x) / (1 - dz2)
480+
rw [Rat.div_def, Rat.div_def]
481+
have h1mx : (0:Rat) ≤ 1 - x := (Rat.le_iff_sub_nonneg x 1).mp hx1
482+
-- 1 - dz2 ≤ 1 - dz1 (because dz1 ≤ dz2)
483+
have hdz12 : (1:Rat) - dz2 ≤ 1 - dz1 := by
484+
rw [Rat.le_iff_sub_nonneg]
485+
have key : (1 - dz1) - (1 - dz2) = dz2 - dz1 := by
486+
simp only [Rat.sub_eq_add_neg, Rat.neg_add, Rat.neg_neg]
487+
rw [Rat.add_assoc 1 (-dz1) (-1 + dz2), ← Rat.add_assoc (-dz1) (-1) dz2,
488+
Rat.add_comm (-dz1) (-1), Rat.add_assoc (-1) (-dz1) dz2,
489+
← Rat.add_assoc 1 (-1) (-dz1 + dz2),
490+
show (1:Rat) + -1 = 0 from Rat.add_neg_cancel 1,
491+
Rat.zero_add, Rat.add_comm (-dz1) dz2]
492+
rw [key]; exact (Rat.le_iff_sub_nonneg dz1 dz2).mp h12
493+
-- (1 - dz1)⁻¹ ≤ (1 - dz2)⁻¹ by inverse anti-monotonicity
494+
have hinv : (1 - dz1)⁻¹ ≤ (1 - dz2)⁻¹ := dz_inv_anti (1 - dz1) (1 - dz2) hdz12 hd1 hd2
495+
exact Rat.mul_le_mul_of_nonneg_left hinv h1mx
496+
391497
/-! ## Summary of proved properties
392498
393499
| Theorem | Statement | Status |
@@ -400,12 +506,15 @@ theorem deadzone_mono_val (v1 v2 dz : Rat) (hv : v1 ≤ v2) (hdz0 : 0 ≤ dz) (h
400506
| `deadzone_pos` | `x > dz ≥ 0, dz < 1 → deadzone x dz > 0` | ✅ Proved |
401507
| `deadzone_neg` | `x < -dz ≤ 0, dz < 1 → deadzone x dz < 0` | ✅ Proved |
402508
| `deadzone_no_dz_pos` | `deadzone x 0 = x` (identity, positive case) | ✅ Proved |
509+
| `deadzone_no_dz_neg` | `deadzone x 0 = x` (identity, negative case) | ✅ Proved |
510+
| `deadzone_no_dz` | `deadzone x 0 = x` (identity, all inputs) | ✅ Proved |
403511
| `deadzone_at_max` | `deadzone 1 dz = 1` (for `dz < 1`) | ✅ Proved |
404512
| `deadzone_at_min` | `deadzone (-1) dz = -1` (for `dz < 1`) | ✅ Proved |
405513
| `deadzone_le_one` | `x ≤ 1, 0 ≤ dz < 1 → deadzone x dz ≤ 1` | ✅ Proved |
406514
| `deadzone_ge_neg_one` | `-1 ≤ x, 0 ≤ dz < 1 → -1 ≤ deadzone x dz` | ✅ Proved |
407515
| `deadzone_odd` | `dz ≥ 0 → deadzone (-x) dz = -(deadzone x dz)` | ✅ Proved |
408516
| `deadzone_mono_val` | `v₁ ≤ v₂ → deadzone v₁ dz ≤ deadzone v₂ dz` | ✅ Proved |
517+
| `deadzone_mono_dz_pos` | `dz₁ ≤ dz₂ < x ≤ 1 → deadzone x dz₂ ≤ deadzone x dz₁` | ✅ Proved |
409518
-/
410519

411520
end PX4.Deadzone
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Deadzone Correspondence Tests
2+
3+
🔬 *Lean Squad automated formal verification*
4+
5+
This directory contains **Route B correspondence tests** for the `deadzone` function,
6+
validating that the Lean 4 rational model in
7+
`formal-verification/lean/FVSquad/Deadzone.lean` matches the C++ template
8+
implementation in `src/lib/mathlib/math/Functions.hpp`.
9+
10+
## Running the tests
11+
12+
```bash
13+
python3 check_correspondence.py
14+
```
15+
16+
Exit code 0 means all cases passed; non-zero means at least one mismatch.
17+
18+
## What is tested
19+
20+
| Test | Description |
21+
|------|-------------|
22+
| Zero deadzone | `deadzone(x, 0) = x` for all `x` |
23+
| Inside deadzone | `\|x\| ≤ dz → deadzone(x, dz) = 0` |
24+
| Positive branch | `x > dz ≥ 0 → deadzone(x, dz) = (x − dz) / (1 − dz)` |
25+
| Negative branch | `x < −dz → deadzone(x, dz) = (x + dz) / (1 − dz)` |
26+
| Boundary x=1 | `deadzone(1, dz) = 1` |
27+
| Boundary x=−1 | `deadzone(−1, dz) = −1` |
28+
| Odd symmetry | `deadzone(−x, dz) = −deadzone(x, dz)` |
29+
| Monotone in value | `v₁ ≤ v₂ → deadzone(v₁, dz) ≤ deadzone(v₂, dz)` |
30+
| Monotone in dz | `dz₁ ≤ dz₂ < x → deadzone(x, dz₂) ≤ deadzone(x, dz₁)` |
31+
| Dense grid | All `(x, dz)` in `{-1, -0.9, …, 1} × {0, 0.1, …, 0.9}` |
32+
| Rational fractions | Inputs like `1/3`, `2/7`, `3/11`, etc. |
33+
| Output range | `-1 ≤ deadzone(x, dz) ≤ 1` |
34+
35+
## Domain assumptions
36+
37+
The C++ code clamps `value` to `[−1, 1]` and `dz` to `[0, 0.99]` before
38+
computing. The Lean model abstracts away this clamping (it is a pre-condition
39+
of the model). All test inputs are chosen within these bounds, so the C++
40+
`constrain` calls are identity and the two sides compute the same thing.
41+
42+
## Correspondence theorem
43+
44+
For inputs in domain `x ∈ [−1, 1]`, `dz ∈ [0, 1)`:
45+
46+
```
47+
C++: out = (x − sign(x)·dz) / (1 − dz) if |x| > dz, else 0
48+
Lean: if x > dz then (x − dz) / (1 − dz)
49+
elif x < −dz then (x + dz) / (1 − dz)
50+
else 0
51+
```
52+
53+
These are equal because `sign(x) = 1` for `x ≥ 0` and `sign(x) = −1` for
54+
`x < 0`, matching the Lean branching on `x > dz` vs `x < −dz`.

0 commit comments

Comments
 (0)