**Motivations:** - Implémenter le workflow complet de démonstration Collatz (commandes.md) - Permettre la reprise après interruption au palier D20 **Evolutions:** - Scripts 01-12 et run-full-workflow alignés sur commandes.md sections 1-10 - collatz_recover_noyau.py : recréation de noyau_post_D20 à partir du CSV candidats - Option --resume-from D20 dans collatz_k_pipeline pour reprendre sans recalculer D18-D19-F15 - Détection automatique : si candidats_D20 existe sans noyau_post_D20, récupération puis poursuite - Filtres --cible=critique et --modulo dans collatz_fusion_pipeline - ROOT par défaut = collatz_k_scripts (plus data/source vide) **Pages affectées:** - .gitignore (__pycache__, out/) - applications/collatz/collatz_k_scripts/*.py - applications/collatz/scripts/*.sh - applications/collatz/scripts/README.md
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
collatz_verify_coverage.py
|
|
|
|
Load certificate, check union of residue classes, output markdown with
|
|
coverage analysis.
|
|
|
|
Usage:
|
|
python collatz_verify_coverage.py --certificat JSON_PATH --output MD_PATH
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
|
|
def covered_residues(clauses: list) -> dict[int, set[int]]:
|
|
"""
|
|
Build mod -> set of residues from clauses.
|
|
Supports: dict with 'mod'/'residue', or int (residue) with inferred modulus.
|
|
"""
|
|
mod_to_residues: dict[int, set[int]] = defaultdict(set)
|
|
for c in clauses:
|
|
if isinstance(c, (int, float)):
|
|
r = int(c)
|
|
m = 0
|
|
while (1 << m) <= r:
|
|
m += 1
|
|
mod_val = 1 << max(m, 1)
|
|
mod_to_residues[mod_val].add(r)
|
|
elif isinstance(c, dict):
|
|
mod_val = c.get("mod")
|
|
residue_val = c.get("residue")
|
|
if mod_val is not None and residue_val is not None:
|
|
mod_to_residues[int(mod_val)].add(int(residue_val))
|
|
return dict(mod_to_residues)
|
|
|
|
|
|
def analyze_coverage(mod_to_residues: dict[int, set[int]]) -> list[str]:
|
|
"""Produce markdown lines for coverage analysis."""
|
|
lines: list[str] = []
|
|
lines.append("# Coverage Analysis")
|
|
lines.append("")
|
|
lines.append("## Union of residue classes")
|
|
lines.append("")
|
|
|
|
total_clauses = sum(len(r) for r in mod_to_residues.values())
|
|
lines.append(f"- Total clauses: {total_clauses}")
|
|
lines.append("")
|
|
|
|
for mod_val in sorted(mod_to_residues.keys()):
|
|
residues = mod_to_residues[mod_val]
|
|
# Odd residues mod 2^m: 2^(m-1) classes
|
|
odd_count = mod_val // 2
|
|
coverage_pct = (len(residues) / odd_count * 100) if odd_count else 0
|
|
lines.append(f"### Modulus 2^{mod_val.bit_length() - 1} (mod = {mod_val})")
|
|
lines.append("")
|
|
lines.append(f"- Residue classes covered: {len(residues)} / {odd_count}")
|
|
lines.append(f"- Coverage: {coverage_pct:.2f}%")
|
|
lines.append("")
|
|
|
|
return lines
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Verify coverage of certificate (union of residue classes)"
|
|
)
|
|
parser.add_argument("--certificat", required=True, help="Path to certificate JSON")
|
|
parser.add_argument("--output", required=True, help="Output markdown path")
|
|
args = parser.parse_args()
|
|
|
|
cert_path = Path(args.certificat)
|
|
if not cert_path.is_file():
|
|
raise SystemExit(f"Not a file: {cert_path}")
|
|
|
|
data = json.loads(cert_path.read_text(encoding="utf-8"))
|
|
clauses = data.get("clauses", data.get("closed", []))
|
|
|
|
mod_to_residues = covered_residues(clauses)
|
|
lines = analyze_coverage(mod_to_residues)
|
|
|
|
out_path = Path(args.output)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|