thread calulations and ui difretn so hughe numbers dont break ui flow

This commit is contained in:
Dominik Krenn 2025-11-05 10:42:24 +01:00
parent 02f398cf3a
commit 185962b4af
3 changed files with 179 additions and 17 deletions

View File

@ -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."""

View File

@ -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))
self.graph.add_delta(delta_sign)
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()

View File

@ -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)