diff --git a/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py b/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py index cd918d7..9ddf38b 100644 --- a/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py +++ b/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py @@ -87,6 +87,21 @@ class Section7Metrics: max_modulus_coverage_pct: float +@dataclass(frozen=True) +class D16D17Metrics: + d16_candidates: int + d16_cover: int + d16_maxA_after: int + d17_pairs: int + noyau_post_d17_size: int + + +@dataclass(frozen=True) +class Fusion25Metrics: + t_to_valid_clauses: dict[int, int] + t_to_domain_size: dict[int, int] + + def parse_extend_metrics_from_logs(pipeline_extend_log: str, paliers_finale_log: str) -> ExtendMetrics: """ Extract metrics needed for the report. @@ -160,6 +175,73 @@ def parse_section7_metrics(seuil_global_md: str, coverage_md: str) -> Section7Me return Section7Metrics(nstar=nstar, max_modulus_power=max_power, max_modulus_coverage_pct=max_cov) +def _parse_int_after_colon(line: str) -> int | None: + m = re.search(r":\s*([0-9][0-9_]*)\b", line) + if not m: + return None + return int(m.group(1).replace("_", "")) + + +def parse_d16_d17_metrics(d16_impact_md: str, d17_impact_md: str, noyau_post_d17_json: str) -> D16D17Metrics: + d16_candidates = None + d16_cover = None + d16_maxA_after = None + for line in d16_impact_md.splitlines(): + if "candidats D16" in line: + d16_candidates = _parse_int_after_colon(line) + if "couverture (avec sœurs)" in line: + d16_cover = _parse_int_after_colon(line) + if "invariant max A16 après" in line: + d16_maxA_after = _parse_int_after_colon(line) + if d16_candidates is None or d16_cover is None or d16_maxA_after is None: + raise ValueError("Cannot parse D16 metrics from impact md") + + d17_pairs = None + for line in d17_impact_md.splitlines(): + if "paires candidates D17" in line: + d17_pairs = _parse_int_after_colon(line) + if d17_pairs is None: + raise ValueError("Cannot parse D17 metrics from impact md") + + data = read_text(Path(noyau_post_d17_json)) + j = re.search(r'"noyau"\s*:\s*\[', data) + if not j: + raise ValueError("Cannot locate noyau list in noyau_post_D17.json") + # The file is JSON with {"noyau":[...], "palier": 28}. To avoid loading huge data, + # count commas in the noyau array region with a bounded approach (file is ~30MB). + # We do a lightweight parse by counting numbers. + import json as _json + + obj = _json.loads(data) + noyau_post_d17_size = len(obj.get("noyau", [])) + + return D16D17Metrics( + d16_candidates=d16_candidates, + d16_cover=d16_cover, + d16_maxA_after=d16_maxA_after, + d17_pairs=d17_pairs, + noyau_post_d17_size=noyau_post_d17_size, + ) + + +def parse_fusion25_metrics(md_text_by_t: dict[int, str]) -> Fusion25Metrics: + t_to_valid: dict[int, int] = {} + t_to_domain: dict[int, int] = {} + for t, text in md_text_by_t.items(): + domain = None + valid = None + for line in text.splitlines(): + if "Taille du domaine analysé" in line: + domain = _parse_int_after_colon(line) + if "Clauses valides" in line: + valid = _parse_int_after_colon(line) + if domain is None or valid is None: + raise ValueError(f"Cannot parse fusion metrics from md for t={t}") + t_to_domain[t] = domain + t_to_valid[t] = valid + return Fusion25Metrics(t_to_valid_clauses=t_to_valid, t_to_domain_size=t_to_domain) + + def parse_running_command_from_paliers_finale_log(paliers_finale_log: str) -> str: """ Extract the last 'Running: ...' command from paliers_finale.log. @@ -307,6 +389,104 @@ def write_section7_run_report( output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") +def write_d16_d17_run_report( + *, + output_path: Path, + report_title: str, + command: str, + git_commit: str, + sha_entries: list[Sha256Entry], + metrics: D16D17Metrics, + out_dir: Path, +) -> None: + lines: list[str] = [] + lines.append("**Auteur** : Équipe 4NK") + lines.append("") + lines.append(f"# {report_title}") + lines.append("") + lines.append("## Contexte") + lines.append("") + lines.append("- **But du run** : exécuter le pipeline D16/D17 après fusion (sections “pipeline directe”).") + lines.append("- **Assertion ciblée** : produire les CSV D16/D17 et le noyau résiduel `noyau_post_D17.json`.") + lines.append("") + lines.append("## Code et reproductibilité") + lines.append("") + if git_commit: + lines.append(f"- **Commit Git** : `{git_commit}`") + if command: + lines.append("- **Commande** :") + lines.append("") + lines.append("```bash") + lines.append(command) + lines.append("```") + lines.append("") + lines.append("## Empreintes sha256 (scripts, artefacts)") + lines.append("") + lines.extend(format_sha256_list(sha_entries)) + lines.append("") + lines.append("## Compteurs et métriques") + lines.append("") + lines.append(f"- candidats D16 : {metrics.d16_candidates}") + lines.append(f"- couverture D16 (avec sœurs) : {metrics.d16_cover}") + lines.append(f"- max A16 après : {metrics.d16_maxA_after}") + lines.append(f"- paires candidates D17 : {metrics.d17_pairs}") + lines.append(f"- noyau post-D17 (résidus) : {metrics.noyau_post_d17_size}") + lines.append("") + lines.append("## Chemins d’artefacts") + lines.append("") + lines.append(f"- OUT : `{out_dir}`") + lines.append("") + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_fusion25_run_report( + *, + output_path: Path, + report_title: str, + command: str, + git_commit: str, + sha_entries: list[Sha256Entry], + metrics: Fusion25Metrics, + out_dir: Path, +) -> None: + lines: list[str] = [] + lines.append("**Auteur** : Équipe 4NK") + lines.append("") + lines.append(f"# {report_title}") + lines.append("") + lines.append("## Contexte") + lines.append("") + lines.append("- **But du run** : produire des clauses de fusion F(t) au palier \\(2^{25}\\) (t=11,12,14).") + lines.append("") + lines.append("## Code et reproductibilité") + lines.append("") + if git_commit: + lines.append(f"- **Commit Git** : `{git_commit}`") + if command: + lines.append("- **Commande** :") + lines.append("") + lines.append("```bash") + lines.append(command) + lines.append("```") + lines.append("") + lines.append("## Empreintes sha256 (scripts, artefacts)") + lines.append("") + lines.extend(format_sha256_list(sha_entries)) + lines.append("") + lines.append("## Compteurs et métriques") + lines.append("") + for t in sorted(metrics.t_to_valid_clauses.keys()): + lines.append(f"- t={t} : domaine={metrics.t_to_domain_size[t]}, clauses_valides={metrics.t_to_valid_clauses[t]}") + lines.append("") + lines.append("## Chemins d’artefacts") + lines.append("") + lines.append(f"- OUT : `{out_dir}`") + lines.append("") + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + def pick_report_date_from_mtime(paths: list[Path]) -> str: mtimes = [p.stat().st_mtime for p in paths if p.exists()] if not mtimes: @@ -323,7 +503,7 @@ def main() -> None: ap.add_argument( "--profile", default="extend_finale", - choices=["extend_finale", "validation_section7"], + choices=["extend_finale", "validation_section7", "pipeline_d16_d17", "fusion_palier2p25"], help="Report profile", ) ap.add_argument("--pipeline-extend-log", default=None, help="Path to pipeline_extend.log (defaults to OUT/pipeline_extend.log)") @@ -423,6 +603,79 @@ def main() -> None: print(f"Wrote: {output_path}") return + if args.profile == "pipeline_d16_d17": + command = args.command.strip() if args.command.strip() else "cd applications/collatz && ./scripts/02-run-pipeline.sh" + d16_impact_path = out_dir / "candidats_D16_apres_fusion_palier2p27_et_impact.md" + d17_impact_path = out_dir / "candidats_D17_apres_fusion_palier2p28_et_impact.md" + noyau_post_d17_path = out_dir / "noyaux" / "noyau_post_D17.json" + metrics = parse_d16_d17_metrics( + read_text(d16_impact_path), + read_text(d17_impact_path), + str(noyau_post_d17_path), + ) + date_str = pick_report_date_from_mtime([d16_impact_path, d17_impact_path, noyau_post_d17_path]) + sha_paths: list[Path] = [ + repo_root / "applications" / "collatz" / "scripts" / "02-run-pipeline.sh", + repo_root / "applications" / "collatz" / "collatz_k_scripts" / "reproduce_all_audits.py", + repo_root / "applications" / "collatz" / "collatz_k_scripts" / "collatz_k_pipeline.py", + d16_impact_path, + d17_impact_path, + out_dir / "candidats_D16_apres_fusion_palier2p27.csv", + out_dir / "candidats_D17_apres_fusion_palier2p28.csv", + noyau_post_d17_path, + ] + sha_entries = compute_sha256_entries(sha_paths) + output_path = docs_dir / f"collatz_run_report_{date_str}_{args.scope}.md" + write_d16_d17_run_report( + output_path=output_path, + report_title=f"Rapport d’exécution — {args.scope}", + command=command, + git_commit=commit_hash, + sha_entries=sha_entries, + metrics=metrics, + out_dir=out_dir, + ) + print(f"Wrote: {output_path}") + return + + if args.profile == "fusion_palier2p25": + command = args.command.strip() if args.command.strip() else "cd applications/collatz && ./scripts/02-run-pipeline.sh" + md_by_t = { + 11: read_text(out_dir / "fusion_t11_palier2p25.md"), + 12: read_text(out_dir / "fusion_t12_palier2p25.md"), + 14: read_text(out_dir / "fusion_t14_palier2p25.md"), + } + metrics = parse_fusion25_metrics(md_by_t) + paths_for_date = [ + out_dir / "fusion_t11_palier2p25.md", + out_dir / "fusion_t12_palier2p25.md", + out_dir / "fusion_t14_palier2p25.md", + ] + date_str = pick_report_date_from_mtime(paths_for_date) + sha_paths: list[Path] = [ + repo_root / "applications" / "collatz" / "scripts" / "02-run-pipeline.sh", + repo_root / "applications" / "collatz" / "collatz_k_scripts" / "collatz_k_pipeline.py", + out_dir / "fusion_t11_palier2p25.md", + out_dir / "fusion_t11_palier2p25.csv", + out_dir / "fusion_t12_palier2p25.md", + out_dir / "fusion_t12_palier2p25.csv", + out_dir / "fusion_t14_palier2p25.md", + out_dir / "fusion_t14_palier2p25.csv", + ] + sha_entries = compute_sha256_entries(sha_paths) + output_path = docs_dir / f"collatz_run_report_{date_str}_{args.scope}.md" + write_fusion25_run_report( + output_path=output_path, + report_title=f"Rapport d’exécution — {args.scope}", + command=command, + git_commit=commit_hash, + sha_entries=sha_entries, + metrics=metrics, + out_dir=out_dir, + ) + print(f"Wrote: {output_path}") + return + raise ValueError(f"Unknown profile: {args.profile}") diff --git a/docs/collatz_run_report_format.md b/docs/collatz_run_report_format.md index dead285..3bde6cb 100644 --- a/docs/collatz_run_report_format.md +++ b/docs/collatz_run_report_format.md @@ -37,6 +37,26 @@ python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py \ --docs-dir docs ``` +Pour le pipeline D16/D17 après fusion : + +```bash +python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py \ + --profile pipeline_d16_d17 \ + --scope pipeline_d16_d17 \ + --out-dir applications/collatz/out \ + --docs-dir docs +``` + +Pour les fusions au palier \(2^{25}\) (t=11,12,14) : + +```bash +python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py \ + --profile fusion_palier2p25 \ + --scope fusion_palier2p25 \ + --out-dir applications/collatz/out \ + --docs-dir docs +``` + ### Contexte - **But du run** : (énoncé court) diff --git a/docs/features/collatz_run_report_generator.md b/docs/features/collatz_run_report_generator.md index 6b798e0..6985e78 100644 --- a/docs/features/collatz_run_report_generator.md +++ b/docs/features/collatz_run_report_generator.md @@ -23,6 +23,7 @@ afin d’éviter toute insertion de transcript terminal dans les documents math - Ajout d’un format standard : `docs/collatz_run_report_format.md`. - Ajout d’un générateur : `applications/collatz/collatz_k_scripts/collatz_generate_run_report.py`. - Ajout d’un exemple réel de rapport : `docs/collatz_run_report_2026-03-04_extend_D18_D21_resume_from_D20.md`. + - Profils supportés : `extend_finale`, `validation_section7`, `pipeline_d16_d17`, `fusion_palier2p25`. ## Modalités d’analyse