mathy/find_my.py
2025-11-05 12:20:31 +01:00

141 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Pi explorer powered by mathstream.
Computes π using the Nilakantha series with streamed big-int arithmetic and
optionally renders the progress in a curses dashboard (`--ui`).
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Iterable, Optional
from mathstream import (
clear_logs,
manual_free_only_enabled,
set_manual_free_only,
)
from pi_finder import iterate_nilakantha
def run_cli_mode(
*,
digits: int,
iterations: int,
extra_precision: int,
show_steps: bool,
) -> tuple[str, int]:
previous_manual = manual_free_only_enabled()
set_manual_free_only(True)
clear_logs()
final_value: Optional[str] = None
last_iteration = 0
try:
for state in iterate_nilakantha(
iterations,
digits=digits,
extra_precision=extra_precision,
):
final_value = state.digits
last_iteration = state.iteration
if show_steps:
print(f"[iter {state.iteration:02d}] {state.digits}")
if final_value is None:
final_value = "0"
last_iteration = 0
if show_steps:
print(final_value)
finally:
set_manual_free_only(previous_manual)
return final_value, last_iteration
def run_ui_mode(max_digits: Optional[int]) -> None:
try:
from pi_finder.ui import launch_pi_ui
except ImportError as exc: # pragma: no cover - import guard
raise SystemExit(f"pi UI is unavailable: {exc}") from exc
launch_pi_ui(max_digits=max_digits)
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Nilakantha-based π explorer using mathstream streamed arithmetic."
)
parser.add_argument(
"--digits",
type=int,
default=25,
help="Digits of π to keep after the decimal point (default: 25).",
)
parser.add_argument(
"--iterations",
type=int,
default=10,
help="Nilakantha iterations to run (each adds roughly two decimal digits).",
)
parser.add_argument(
"--extra",
type=int,
default=4,
help="Extra guard digits maintained during calculation for stability (default: 4).",
)
parser.add_argument(
"--show-steps",
action="store_true",
help="Print every intermediate approximation instead of just the final value.",
)
parser.add_argument(
"--output",
type=Path,
default=Path("found.pi"),
help="Optional path to write the final approximation (default: found.pi).",
)
parser.add_argument(
"--ui",
action="store_true",
help="Launch the curses dashboard instead of printing digits.",
)
parser.add_argument(
"--max-digits",
type=int,
default=None,
help="Limit digits in UI mode (defaults to --digits).",
)
return parser.parse_args(list(argv) if argv is not None else None)
def main(argv: Optional[Iterable[str]] = None) -> None:
args = parse_args(argv)
if args.ui:
precision = args.max_digits if args.max_digits is not None else max(10, args.digits)
run_ui_mode(precision)
else:
digits = max(0, args.digits)
iterations = max(0, args.iterations)
extra = max(0, args.extra)
result, last_iter = run_cli_mode(
digits=digits,
iterations=iterations,
extra_precision=extra,
show_steps=args.show_steps,
)
output_path: Path = args.output
output_path.parent.mkdir(parents=True, exist_ok=True)
data = result if result.endswith("\n") else f"{result}\n"
output_path.write_text(data, encoding="utf-8")
digits_written = sum(ch.isdigit() for ch in result)
print(
f"π approximation (iteration {last_iter}, {digits_written} digits) written to {output_path}"
)
if __name__ == "__main__":
main()