Skip to content

Commit 777411e

Browse files
committed
Prove no periodic orbit for weakly reversible deficiency-zero networks
1 parent 7e342b9 commit 777411e

4 files changed

Lines changed: 224 additions & 0 deletions

File tree

CRNT.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ import CRNT.Theorems.DeficiencyZero.Stability
294294
import CRNT.Theorems.DeficiencyZero.Confinement
295295
import CRNT.Theorems.DeficiencyZero.Lyapunov
296296
import CRNT.Theorems.DeficiencyZero.AsymptoticStability
297+
import CRNT.Theorems.DeficiencyZero.NoPeriodicOrbit
297298
import CRNT.Theorems.DeficiencyOne.Statement
298299
import CRNT.Theorems.DeficiencyOne.LogRatioUniqueness
299300
import CRNT.Theorems.DeficiencyOne.ToricReduction
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/-
2+
No non-constant positive periodic orbit for weakly reversible, deficiency-zero
3+
mass-action networks.
4+
5+
The relative entropy `relEntropy xstar` is a strict Lyapunov function: it is nonincreasing
6+
along every positive trajectory and its dissipation vanishes only at complex-balanced points.
7+
A positive periodic orbit returns the relative entropy to its starting value each period, so
8+
along the orbit the relative entropy is both nonincreasing and periodic, hence constant; its
9+
derivative, the dissipation, is therefore identically zero, so every point of the orbit is
10+
complex-balanced. Deficiency-zero per-class uniqueness then forces the orbit to be a single
11+
point. Consequently a weakly reversible deficiency-zero network sustains no oscillation, for
12+
every choice of rate constants.
13+
-/
14+
import CRNT.Theorems.DeficiencyZero.AsymptoticStability
15+
import CRNT.Theorems.DeficiencyZero.Existence
16+
17+
open scoped BigOperators
18+
19+
namespace CRNT
20+
21+
/-- A real function that is antitone and periodic with a positive period is constant. -/
22+
theorem eq_of_antitone_periodic {g : ℝ → ℝ} {T : ℝ} (hT : 0 < T)
23+
(hanti : Antitone g) (hper : Function.Periodic g T) : ∀ x y, g x = g y := by
24+
have hiter : ∀ (n : ℕ) (x : ℝ), g (x + n * T) = g x := by
25+
intro n
26+
induction n with
27+
| zero => intro x; simp
28+
| succ k ih =>
29+
intro x
30+
have hstep : g (x + ((k : ℝ) + 1) * T) = g x := by
31+
have harg : x + ((k : ℝ) + 1) * T = (x + (k : ℝ) * T) + T := by ring
32+
rw [harg, hper, ih x]
33+
simpa [Nat.cast_succ] using hstep
34+
have key : ∀ x y : ℝ, g x ≤ g y := by
35+
intro x y
36+
obtain ⟨n, hn⟩ := Archimedean.arch (y - x) hT
37+
have hnn : y - x ≤ (n : ℝ) * T := by simpa [nsmul_eq_mul] using hn
38+
have hle : y ≤ x + (n : ℝ) * T := by linarith
39+
calc g x = g (x + (n : ℝ) * T) := (hiter n x).symm
40+
_ ≤ g y := hanti hle
41+
intro x y
42+
exact le_antisymm (key x y) (key y x)
43+
44+
namespace Network
45+
46+
variable {S : Type} [DecidableEq S] [Fintype S]
47+
48+
/-- **No non-constant positive periodic orbit.** For a weakly reversible, deficiency-zero
49+
mass-action network, every positive periodic solution of the mass-action ODE is constant, for
50+
every choice of rate constants. This is the exclusion of sustained oscillation from the
51+
deficiency-zero class. -/
52+
theorem eq_of_periodic_solution
53+
(N : Network S) (hwr : N.WeaklyReversible) (hδ : N.DeficiencyZero) (κ : N.RateConstants)
54+
{γ : ℝ → Concentration S} {T : ℝ} (hT : 0 < T)
55+
(hpos : ∀ t, (γ t).Positive)
56+
(hsol : ∀ t s, HasDerivAt (fun τ => γ τ s) (N.massActionVectorField κ (γ t) s) t)
57+
(hper : Function.Periodic γ T) :
58+
∀ t, γ t = γ 0 := by
59+
-- A positive complex-balanced reference exists from weak reversibility and δ = 0.
60+
obtain ⟨xstar, hxs, hcb⟩ := N.exists_isComplexBalanced hwr hδ κ
61+
-- The relative entropy along the orbit is antitone (Lyapunov descent) and periodic, hence
62+
-- constant.
63+
have hanti : Antitone (fun t => relEntropy xstar (γ t)) :=
64+
N.relEntropy_antitone_along_solution κ hxs hcb hpos hsol
65+
have hgper : Function.Periodic (fun t => relEntropy xstar (γ t)) T := by
66+
intro t
67+
show relEntropy xstar (γ (t + T)) = relEntropy xstar (γ t)
68+
rw [hper t]
69+
have hgconst : ∀ a b, relEntropy xstar (γ a) = relEntropy xstar (γ b) :=
70+
eq_of_antitone_periodic hT hanti hgper
71+
-- Whole-vector form of the solution, for the compatibility-class lemma.
72+
have hsolwv : ∀ τ, HasDerivAt γ (N.massActionVectorField κ (γ τ)) τ :=
73+
fun τ => hasDerivAt_pi.mpr (fun s => hsol τ s)
74+
-- Every point of the orbit is complex-balanced: the relative entropy is locally constant, so
75+
-- its derivative (the dissipation) vanishes, and vanishing dissipation is complex balance.
76+
have hcbt : ∀ t, N.IsComplexBalanced κ (γ t) := by
77+
intro t₀
78+
have hchain := relEntropy_hasDerivAt hxs (hpos t₀) (fun s => hsol t₀ s)
79+
have hd0 : HasDerivAt (fun τ => relEntropy xstar (γ τ)) 0 t₀ := by
80+
have heq : (fun τ => relEntropy xstar (γ τ)) = fun _ => relEntropy xstar (γ t₀) := by
81+
funext u; exact hgconst u t₀
82+
rw [heq]; exact hasDerivAt_const t₀ _
83+
have hdiss : (∑ s, (Real.log (γ t₀ s) - Real.log (xstar s))
84+
* N.massActionVectorField κ (γ t₀) s) = 0 := hchain.unique hd0
85+
exact N.complexBalanced_of_dissipation_eq_zero κ (hpos t₀) hxs hcb hdiss
86+
-- Every point of the orbit lies in γ 0's positive compatibility class.
87+
have hmem : ∀ t, γ t ∈ N.positiveCompatibilityClass (γ 0) := by
88+
intro t
89+
refine ⟨?_, hpos t⟩
90+
show γ t - γ 0 ∈ N.stoichSubspace
91+
rcases le_total 0 t with h | h
92+
· have hsol0t : ∀ τ ∈ Set.Icc (0 : ℝ) t,
93+
HasDerivAt γ (N.massActionVectorField κ (γ τ)) τ := fun τ _ => hsolwv τ
94+
exact N.sub_mem_stoichSubspace_of_solution κ h hsol0t
95+
· have hsolt0 : ∀ τ ∈ Set.Icc t (0 : ℝ),
96+
HasDerivAt γ (N.massActionVectorField κ (γ τ)) τ := fun τ _ => hsolwv τ
97+
have h2 := N.sub_mem_stoichSubspace_of_solution κ h hsolt0
98+
simpa [neg_sub] using N.stoichSubspace.neg_mem h2
99+
-- Two complex-balanced points of one positive class are equal (deficiency-zero uniqueness).
100+
intro t
101+
exact N.isComplexBalanced_unique_in_positiveClass hwr κ (hmem t) (hmem 0) (hcbt t) (hcbt 0)
102+
103+
end Network
104+
end CRNT

scripts/metrics.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
"""Regenerate the headline quantitative metrics reported in the crnt-lean paper.
3+
4+
Paths resolve relative to the repository root, so the module counts, line counts,
5+
audited-theorem count, and analyze-contract shape can be checked against the
6+
artifact rather than taken on trust:
7+
8+
python3 scripts/metrics.py
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import re
14+
import subprocess
15+
from pathlib import Path
16+
17+
REPO_ROOT = Path(__file__).resolve().parent.parent
18+
19+
20+
def lean_files(*dirs: str) -> list[Path]:
21+
"""All `.lean` files under the given repo-relative directories."""
22+
files: list[Path] = []
23+
for directory in dirs:
24+
files.extend(sorted((REPO_ROOT / directory).rglob("*.lean")))
25+
return files
26+
27+
28+
def line_count(paths: list[Path]) -> int:
29+
"""Total newline count across the files, matching `cat ... | wc -l`."""
30+
return sum(path.read_text().count("\n") for path in paths)
31+
32+
33+
def audited_theorem_count() -> int:
34+
"""Number of `#print axioms` invocations in the audit module.
35+
36+
Only lines that begin with the directive are counted, so a prose mention of
37+
the string in the module docstring is not miscounted.
38+
"""
39+
audit = (REPO_ROOT / "test" / "AxiomAudit.lean").read_text().splitlines()
40+
return sum(1 for line in audit if line.startswith("#print axioms "))
41+
42+
43+
def analyze_contract() -> tuple[int, int]:
44+
"""The `analysisVersion` and the field count of `structure Analysis`."""
45+
text = (REPO_ROOT / "CRNT" / "Interop" / "Analysis.lean").read_text()
46+
version_match = re.search(r"analysisVersion\s*:\s*Nat\s*:=\s*(\d+)", text)
47+
version = int(version_match.group(1)) if version_match else -1
48+
49+
fields = 0
50+
in_struct = False
51+
for line in text.splitlines():
52+
if line.startswith("structure Analysis where"):
53+
in_struct = True
54+
continue
55+
if in_struct:
56+
if "deriving" in line:
57+
break
58+
if re.match(r"^ [A-Za-z]+ :", line):
59+
fields += 1
60+
return version, fields
61+
62+
63+
def git_short_sha() -> str:
64+
"""Short HEAD commit hash, or a placeholder outside a git checkout."""
65+
try:
66+
result = subprocess.run(
67+
["git", "rev-parse", "--short", "HEAD"],
68+
cwd=REPO_ROOT,
69+
capture_output=True,
70+
text=True,
71+
check=True,
72+
)
73+
return result.stdout.strip()
74+
except (subprocess.CalledProcessError, FileNotFoundError):
75+
return "no-git"
76+
77+
78+
def main() -> None:
79+
lib = lean_files("CRNT")
80+
test = lean_files("test")
81+
82+
lib_modules = len(lib)
83+
# total = CRNT modules + test harness + CRNT.lean aggregator + Analyze.lean
84+
total_modules = lib_modules + len(test) + 2
85+
86+
lib_lines = line_count(lib)
87+
total_lines = line_count(
88+
lib + test + [REPO_ROOT / "CRNT.lean", REPO_ROOT / "Analyze.lean"]
89+
)
90+
91+
audited = audited_theorem_count()
92+
version, fields = analyze_contract()
93+
94+
print(f"crnt-lean headline metrics (regenerated {git_short_sha()})")
95+
print("-" * 65)
96+
print(f"library modules (CRNT/) : {lib_modules}")
97+
print(f"total modules (+ aggregator/exe/test): {total_modules}")
98+
print(f"library lines (CRNT/) : {lib_lines}")
99+
print(f"total lines (+ aggregator/exe/test) : {total_lines}")
100+
print(f"audited theorems (#print axioms) : {audited}")
101+
print(f"analyze contract version : {version}")
102+
print(f"analyze contract fields : {fields}")
103+
print()
104+
print("per-area module counts:")
105+
areas = [
106+
(area.name, len(list(area.rglob("*.lean"))))
107+
for area in (REPO_ROOT / "CRNT").iterdir()
108+
if area.is_dir()
109+
]
110+
for name, count in sorted(areas, key=lambda item: item[1], reverse=True):
111+
print(f" {name:<28} {count}")
112+
113+
114+
if __name__ == "__main__":
115+
main()

test/AxiomAudit.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ The `(whitespace := lax)` mode makes the comparison insensitive to how long name
388388
#guard_msgs (whitespace := lax) in
389389
#print axioms CRNT.Network.omegaLimit_eq_singleton_of_comparableGrowthDescent
390390

391+
/-- info: 'CRNT.Network.eq_of_periodic_solution' depends on axioms: [propext, Classical.choice, Quot.sound] -/
392+
#guard_msgs (whitespace := lax) in
393+
#print axioms CRNT.Network.eq_of_periodic_solution
394+
391395
/-- info: 'CRNT.Network.siphonFacet_floor_of_nearFacet_dissipation' depends on axioms: [propext, Classical.choice, Quot.sound] -/
392396
#guard_msgs (whitespace := lax) in
393397
#print axioms CRNT.Network.siphonFacet_floor_of_nearFacet_dissipation

0 commit comments

Comments
 (0)