#!/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()