thread calulations and ui difretn so hughe numbers dont break ui flow
This commit is contained in:
parent
02f398cf3a
commit
185962b4af
@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import curses
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from mathstream import StreamNumber, add, mul, div, is_even, clear_logs
|
||||
|
||||
@ -13,19 +16,39 @@ from .dashboard import CollatzDashboard
|
||||
LOG_DIR = Path("instance/log")
|
||||
|
||||
|
||||
def collatz_step(n: StreamNumber, three: StreamNumber, two: StreamNumber, one: StreamNumber) -> StreamNumber:
|
||||
def collatz_step(
|
||||
n: StreamNumber,
|
||||
three: StreamNumber,
|
||||
two: StreamNumber,
|
||||
one: StreamNumber,
|
||||
) -> StreamNumber:
|
||||
"""Compute the next Collatz value."""
|
||||
return div(n, two) if is_even(n) else add(mul(n, three), one)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SharedState:
|
||||
step: int = 0
|
||||
digits: str = "0"
|
||||
delta_sign: int = 0
|
||||
computing: bool = False
|
||||
calc_start_time: Optional[float] = None
|
||||
last_calc_duration: float = 0.0
|
||||
reached_one: bool = False
|
||||
|
||||
|
||||
class CollatzApp:
|
||||
"""Main application loop driving the Collatz visualization."""
|
||||
|
||||
def __init__(self, stdscr: "curses._CursesWindow") -> None:
|
||||
self.stdscr = stdscr
|
||||
self.dashboard = CollatzDashboard(stdscr, LOG_DIR)
|
||||
self.step = 0
|
||||
self.start_time = time.time()
|
||||
self.state = SharedState()
|
||||
self._state_lock = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
self._worker: Optional[threading.Thread] = None
|
||||
self._error: Optional[BaseException] = None
|
||||
|
||||
def run(self) -> None:
|
||||
start_file = Path("start.txt")
|
||||
@ -40,31 +63,134 @@ class CollatzApp:
|
||||
n = StreamNumber(start_file)
|
||||
one, two, three = (StreamNumber(literal=s) for s in ("1", "2", "3"))
|
||||
|
||||
initial_digits = "".join(n.stream()) or "0"
|
||||
with self._state_lock:
|
||||
self.state.digits = initial_digits
|
||||
self.state.delta_sign = 0
|
||||
self.state.step = 0
|
||||
|
||||
self.start_time = time.time()
|
||||
self._worker = threading.Thread(
|
||||
target=self._worker_loop,
|
||||
args=(n, one, two, three),
|
||||
daemon=True,
|
||||
)
|
||||
self._worker.start()
|
||||
|
||||
try:
|
||||
self._ui_loop()
|
||||
finally:
|
||||
self._stop_event.set()
|
||||
if self._worker:
|
||||
self._worker.join()
|
||||
|
||||
def _ui_loop(self) -> None:
|
||||
while True:
|
||||
even_step = is_even(n)
|
||||
delta_sign = -1 if even_step else 1
|
||||
|
||||
self.step += 1
|
||||
n = collatz_step(n, three, two, one)
|
||||
digits = "".join(n.stream()) or "0"
|
||||
|
||||
snapshot = self._snapshot_state()
|
||||
elapsed = time.time() - self.start_time
|
||||
avg_step = elapsed / self.step if self.step else 0.0
|
||||
avg_step = elapsed / snapshot.step if snapshot.step else 0.0
|
||||
if snapshot.computing and snapshot.calc_start_time is not None:
|
||||
current_calc = time.time() - snapshot.calc_start_time
|
||||
else:
|
||||
current_calc = 0.0
|
||||
last_calc = snapshot.last_calc_duration
|
||||
|
||||
self.dashboard.render(self.step, elapsed, avg_step, digits, delta_sign)
|
||||
digits = snapshot.digits or "0"
|
||||
delta = snapshot.delta_sign
|
||||
|
||||
self.dashboard.render(
|
||||
snapshot.step,
|
||||
elapsed,
|
||||
avg_step,
|
||||
digits,
|
||||
delta,
|
||||
current_calc,
|
||||
last_calc,
|
||||
snapshot.computing,
|
||||
)
|
||||
|
||||
if self._error:
|
||||
self.dashboard.show_message(
|
||||
f"Error: {self._error}", UIColors.DOWN
|
||||
)
|
||||
break
|
||||
|
||||
ch = self.stdscr.getch()
|
||||
if ch == ord("q"):
|
||||
self._stop_event.set()
|
||||
break
|
||||
if ch != -1:
|
||||
self.dashboard.handle_input(ch)
|
||||
|
||||
if digits == "1":
|
||||
if snapshot.reached_one:
|
||||
self.dashboard.show_message(
|
||||
"Reached 1 — press any key to exit.", UIColors.UP
|
||||
)
|
||||
break
|
||||
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
def _snapshot_state(self) -> SharedState:
|
||||
with self._state_lock:
|
||||
return SharedState(
|
||||
step=self.state.step,
|
||||
digits=self.state.digits,
|
||||
delta_sign=self.state.delta_sign,
|
||||
computing=self.state.computing,
|
||||
calc_start_time=self.state.calc_start_time,
|
||||
last_calc_duration=self.state.last_calc_duration,
|
||||
reached_one=self.state.reached_one,
|
||||
)
|
||||
|
||||
def _worker_loop(
|
||||
self,
|
||||
current: StreamNumber,
|
||||
one: StreamNumber,
|
||||
two: StreamNumber,
|
||||
three: StreamNumber,
|
||||
) -> None:
|
||||
while not self._stop_event.is_set():
|
||||
with self._state_lock:
|
||||
self.state.computing = True
|
||||
self.state.calc_start_time = time.time()
|
||||
|
||||
even_step = is_even(current)
|
||||
delta_sign = -1 if even_step else 1
|
||||
calc_started = time.time()
|
||||
|
||||
try:
|
||||
next_n = collatz_step(current, three, two, one)
|
||||
digits = "".join(next_n.stream()) or "0"
|
||||
except Exception as exc: # pragma: no cover - safeguard for worker errors
|
||||
self._error = exc
|
||||
with self._state_lock:
|
||||
self.state.computing = False
|
||||
self.state.calc_start_time = None
|
||||
self._stop_event.set()
|
||||
return
|
||||
|
||||
duration = time.time() - calc_started
|
||||
|
||||
with self._state_lock:
|
||||
self.state.step += 1
|
||||
self.state.digits = digits
|
||||
self.state.delta_sign = delta_sign
|
||||
self.state.last_calc_duration = duration
|
||||
self.state.computing = False
|
||||
self.state.calc_start_time = None
|
||||
if digits == "1":
|
||||
self.state.reached_one = True
|
||||
|
||||
current.free(delete_file=False)
|
||||
current = next_n
|
||||
|
||||
if digits == "1":
|
||||
self._stop_event.set()
|
||||
return
|
||||
|
||||
|
||||
def run_collatz(stdscr: "curses._CursesWindow") -> None:
|
||||
"""Entry point expected by curses.wrapper."""
|
||||
|
||||
@ -23,13 +23,14 @@ class CollatzDashboard:
|
||||
UIColors.setup()
|
||||
self.num_scroll = 0
|
||||
self.log_scroll = 0
|
||||
self._last_step = 0
|
||||
self._build_views()
|
||||
|
||||
def _build_views(self) -> None:
|
||||
lines = curses.LINES
|
||||
cols = curses.COLS
|
||||
|
||||
header_h = 5
|
||||
header_h = 6
|
||||
graph_h = 1
|
||||
available = max(2, lines - header_h - graph_h - 2)
|
||||
num_h = max(1, (available * 3) // 4)
|
||||
@ -40,9 +41,31 @@ class CollatzDashboard:
|
||||
self.number = NumberView(num_h, cols, header_h + graph_h + 1, 0)
|
||||
self.log = LogView(log_h, cols, header_h + graph_h + num_h + 2, 0, self.log_dir)
|
||||
|
||||
def render(self, step: int, elapsed: float, avg_step: float, digits: str, delta_sign: int) -> None:
|
||||
self.header.render(step, elapsed, avg_step, len(digits))
|
||||
def render(
|
||||
self,
|
||||
step: int,
|
||||
elapsed: float,
|
||||
avg_step: float,
|
||||
digits: str,
|
||||
delta_sign: int,
|
||||
current_calc: float,
|
||||
last_calc: float,
|
||||
computing: bool,
|
||||
) -> None:
|
||||
self.header.render(
|
||||
step,
|
||||
elapsed,
|
||||
avg_step,
|
||||
len(digits),
|
||||
current_calc,
|
||||
last_calc,
|
||||
computing,
|
||||
)
|
||||
if step > self._last_step:
|
||||
self.graph.add_delta(delta_sign)
|
||||
self._last_step = step
|
||||
else:
|
||||
self.graph.redraw()
|
||||
self.num_scroll = self.number.render(digits, self.num_scroll)
|
||||
self.log_scroll = self.log.render(self.log_scroll)
|
||||
curses.doupdate()
|
||||
|
||||
@ -13,7 +13,16 @@ class HeaderView:
|
||||
def __init__(self, height: int, width: int, y: int, x: int) -> None:
|
||||
self.win = curses.newwin(height, width, y, x)
|
||||
|
||||
def render(self, step: int, elapsed: float, avg_step: float, digits_len: int) -> None:
|
||||
def render(
|
||||
self,
|
||||
step: int,
|
||||
elapsed: float,
|
||||
avg_step: float,
|
||||
digits_len: int,
|
||||
current_calc: float,
|
||||
last_calc: float,
|
||||
computing: bool,
|
||||
) -> None:
|
||||
self.win.erase()
|
||||
_, cols = self.win.getmaxyx()
|
||||
cols = max(1, cols)
|
||||
@ -23,6 +32,7 @@ class HeaderView:
|
||||
" Collatz Stream Visualizer ",
|
||||
f" Step {step:,} • Digits {digits_len:,}",
|
||||
f" Elapsed {elapsed:8.2f}s • Avg {avg_step:8.5f}s",
|
||||
f" Current calc {current_calc:6.2f}s {'⏳' if computing else '✓'} • Last {last_calc:6.2f}s",
|
||||
" ▲ green = 3n+1 • ▼ red = /2 • ↑↓ number scroll • PgUp/PgDn logs • q quit",
|
||||
]
|
||||
|
||||
@ -56,6 +66,9 @@ class GraphView:
|
||||
self._trim()
|
||||
self._render()
|
||||
|
||||
def redraw(self) -> None:
|
||||
self._render()
|
||||
|
||||
def _trim(self) -> None:
|
||||
_, max_x = self.win.getmaxyx()
|
||||
visible = max(0, max_x - self.padding)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user