Create a repo-local structure for deterministic computational math
experiments with provenance manifests, result hashing, and clear
limitations on what computation can prove vs. suggest.
Additions:
- experiments/README.md: Philosophy, directory structure, manifest schema,
graduation criteria (experiment→proof), and Sage compatibility notes
- experiments/MANIFEST_SCHEMA.md: JSON schema for experiment result manifests
(fields: code_hash, output_hash, assumptions, timestamp, runtime, etc.)
- experiments/template.py: Complete starter template with:
* deterministic seed handling
* self-hashing (code_hash, output_hash via SHA256)
* manifest generation boilerplate
* markdown report writer
* CLI args placeholder for parameter sweeps
- experiments/riemann_pii.py: Working toy experiment — Riemann sum π approx
Demonstrates full pattern:
* n=10000 rectangles → π ≈ 3.141391 (error ~2e-4)
* produces manifest + markdown report
* classifies as "suggests" (numerical approximation, not proof)
- experiments/fibonacci_cassini.py: Second toy experiment — Cassini identity
Verifies F_{n-1}F_{n+1} - F_n² = (-1)^n for n=1..1000
* integer arithmetic — exact result
* still "suggests" (finite check, not inductive proof)
* high confidence due to exact arithmetic
- experiments/.gitignore: ignores manifests/, results/, and *.manifest.json
so generated artifacts are never committed
Verification:
$ python experiments/riemann_pii.py
→ writes experiments/manifests/riemann_pii.manifest.json + report.md
$ python experiments/fibonacci_cassini.py
→ writes experiments/manifests/fibonacci_cassini.manifest.json + report.md
Both runs are reproducible: code_hash and output_hash match across runs.
Design rationale:
- Minimal dependency: pure Python 3.10+ (no Sage yet; optional future)
- Self-contained: each experiment is a single script
- Deterministic: no randomness unless fixed seed set
- Hash-based integrity: code_hash verifies exact source; output_hash verifies result
- Manifest captures all required metadata per acceptance criteria
- README explicitly explains computational limits: numerical cannot replace
analytical proof; bounded by assumptions and precision
Closes #879
165 lines
4.7 KiB
Python
165 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
[EXPERIMENT NAME HERE]
|
|
|
|
Brief description of the mathematical problem and its source.
|
|
|
|
Problem source: [URL or citation]
|
|
|
|
Assumptions:
|
|
- [Assumption 1]
|
|
- [Assumption 2]
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
# ============================================================
|
|
# CONFIGURATION
|
|
# ============================================================
|
|
EXPERIMENT_NAME = "experiment_name" # matches filename
|
|
PROBLEM_SOURCE = "https://example.com/problem" # where this problem originates
|
|
ASSUMPTIONS = [
|
|
"Assumption 1",
|
|
"Assumption 2",
|
|
]
|
|
SUGGESTS_OR_PROVES = "suggests" # or "proves" — see README limitations
|
|
CONFIDENCE = "medium" # high | medium | low
|
|
|
|
# Output directories
|
|
OUTPUT_DIR = Path(__file__).parent
|
|
MANIFEST_DIR = OUTPUT_DIR / "manifests"
|
|
RESULT_DIR = OUTPUT_DIR / "results"
|
|
MANIFEST_DIR.mkdir(parents=True, exist_ok=True)
|
|
RESULT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# ============================================================
|
|
# COMPUTATION
|
|
# ============================================================
|
|
def compute() -> dict[str, Any]:
|
|
"""Run the core computation.
|
|
|
|
Returns:
|
|
dict: result data (must be JSON-serializable)
|
|
"""
|
|
# Set deterministic seed if using randomness
|
|
# import random; random.seed(42)
|
|
|
|
# Your computation here
|
|
result_value = None
|
|
raise NotImplementedError("Fill in compute()")
|
|
|
|
return {
|
|
"value": result_value,
|
|
# add other structured fields as needed
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# HASHING
|
|
# ============================================================
|
|
def hash_file(path: Path) -> str:
|
|
"""Return sha256:<hex> of file contents."""
|
|
data = path.read_bytes()
|
|
return "sha256:" + hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def hash_json_serializable(obj: Any) -> str:
|
|
"""Return sha256:<hex> of JSON-serialized object (sorted keys)."""
|
|
serialized = json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
return "sha256:" + hashlib.sha256(serialized).hexdigest()
|
|
|
|
|
|
# ============================================================
|
|
# REPORT WRITING
|
|
# ============================================================
|
|
def write_markdown_report(result: dict[str, Any], manifest: dict[str, Any]) -> None:
|
|
"""Write human-readable computation report to results/."""
|
|
report_path = RESULT_DIR / f"{EXPERIMENT_NAME}.report.md"
|
|
lines = [
|
|
f"# Experiment: {EXPERIMENT_NAME}",
|
|
"",
|
|
"## Problem Source",
|
|
f"- {PROBLEM_SOURCE}",
|
|
"",
|
|
"## Assumptions",
|
|
]
|
|
for a in ASSUMPTIONS:
|
|
lines.append(f"- {a}")
|
|
lines.extend([
|
|
"",
|
|
"## Computation",
|
|
f"- **Regime:** {manifest['suggests_or_proves']}",
|
|
f"- **Confidence:** {manifest['confidence']}",
|
|
f"- **Runtime:** {manifest['runtime_seconds']:.4f} s",
|
|
"",
|
|
"## Result",
|
|
])
|
|
# Pretty-print result
|
|
lines.append("```json")
|
|
lines.append(json.dumps(result, indent=2))
|
|
lines.append("```")
|
|
lines.extend([
|
|
"",
|
|
"## Provenance",
|
|
f"- Code hash: `{manifest['code_hash']}`",
|
|
f"- Output hash: `{manifest['output_hash']}`",
|
|
f"- Timestamp (UTC): `{manifest['timestamp_utc']}`",
|
|
])
|
|
report_path.write_text("\n".join(lines) + "\n")
|
|
print(f"Report: {report_path}")
|
|
|
|
|
|
# ============================================================
|
|
# MAIN
|
|
# ============================================================
|
|
def main() -> None:
|
|
start = time.perf_counter()
|
|
|
|
# Run computation
|
|
result = compute()
|
|
|
|
# Hashes
|
|
script_path = Path(__file__)
|
|
code_hash = hash_file(script_path)
|
|
output_hash = hash_json_serializable(result)
|
|
|
|
# Build manifest
|
|
runtime = time.perf_counter() - start
|
|
manifest = {
|
|
"experiment_name": EXPERIMENT_NAME,
|
|
"problem_source": PROBLEM_SOURCE,
|
|
"assumptions": ASSUMPTIONS,
|
|
"code_hash": code_hash,
|
|
"output_hash": output_hash,
|
|
"timestamp_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
"runtime_seconds": round(runtime, 6),
|
|
"result": result,
|
|
"suggests_or_proves": SUGGESTS_OR_PROVES,
|
|
"confidence": CONFIDENCE,
|
|
}
|
|
|
|
# Write manifest
|
|
manifest_path = MANIFEST_DIR / f"{EXPERIMENT_NAME}.manifest.json"
|
|
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n")
|
|
print(f"Manifest: {manifest_path}")
|
|
|
|
# Write report
|
|
write_markdown_report(result, manifest)
|
|
|
|
print("\nDone.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|