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 from __future__ import annotations
import curses import curses
import threading
import time import time
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Optional
from mathstream import StreamNumber, add, mul, div, is_even, clear_logs from mathstream import StreamNumber, add, mul, div, is_even, clear_logs
@ -13,19 +16,39 @@ from .dashboard import CollatzDashboard
LOG_DIR = Path("instance/log") 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.""" """Compute the next Collatz value."""
return div(n, two) if is_even(n) else add(mul(n, three), one) 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: class CollatzApp:
"""Main application loop driving the Collatz visualization.""" """Main application loop driving the Collatz visualization."""
def __init__(self, stdscr: "curses._CursesWindow") -> None: def __init__(self, stdscr: "curses._CursesWindow") -> None:
self.stdscr = stdscr self.stdscr = stdscr
self.dashboard = CollatzDashboard(stdscr, LOG_DIR) self.dashboard = CollatzDashboard(stdscr, LOG_DIR)
self.step = 0
self.start_time = time.time() 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: def run(self) -> None:
start_file = Path("start.txt") start_file = Path("start.txt")
@ -40,31 +63,134 @@ class CollatzApp:
n = StreamNumber(start_file) n = StreamNumber(start_file)
one, two, three = (StreamNumber(literal=s) for s in ("1", "2", "3")) 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: while True:
even_step = is_even(n) snapshot = self._snapshot_state()
delta_sign = -1 if even_step else 1
self.step += 1
n = collatz_step(n, three, two, one)
digits = "".join(n.stream()) or "0"
elapsed = time.time() - self.start_time 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() ch = self.stdscr.getch()
if ch == ord("q"): if ch == ord("q"):
self._stop_event.set()
break break
if ch != -1: if ch != -1:
self.dashboard.handle_input(ch) self.dashboard.handle_input(ch)
if digits == "1": if snapshot.reached_one:
self.dashboard.show_message( self.dashboard.show_message(
"Reached 1 — press any key to exit.", UIColors.UP "Reached 1 — press any key to exit.", UIColors.UP
) )
break 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: def run_collatz(stdscr: "curses._CursesWindow") -> None:
"""Entry point expected by curses.wrapper.""" """Entry point expected by curses.wrapper."""

View File

@ -23,13 +23,14 @@ class CollatzDashboard:
UIColors.setup() UIColors.setup()
self.num_scroll = 0 self.num_scroll = 0
self.log_scroll = 0 self.log_scroll = 0
self._last_step = 0
self._build_views() self._build_views()
def _build_views(self) -> None: def _build_views(self) -> None:
lines = curses.LINES lines = curses.LINES
cols = curses.COLS cols = curses.COLS
header_h = 5 header_h = 6
graph_h = 1 graph_h = 1
available = max(2, lines - header_h - graph_h - 2) available = max(2, lines - header_h - graph_h - 2)
num_h = max(1, (available * 3) // 4) 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.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) 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: def render(
self.header.render(step, elapsed, avg_step, len(digits)) self,
self.graph.add_delta(delta_sign) 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.num_scroll = self.number.render(digits, self.num_scroll)
self.log_scroll = self.log.render(self.log_scroll) self.log_scroll = self.log.render(self.log_scroll)
curses.doupdate() curses.doupdate()

View File

@ -13,7 +13,16 @@ class HeaderView:
def __init__(self, height: int, width: int, y: int, x: int) -> None: def __init__(self, height: int, width: int, y: int, x: int) -> None:
self.win = curses.newwin(height, width, y, x) 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() self.win.erase()
_, cols = self.win.getmaxyx() _, cols = self.win.getmaxyx()
cols = max(1, cols) cols = max(1, cols)
@ -23,6 +32,7 @@ class HeaderView:
" Collatz Stream Visualizer ", " Collatz Stream Visualizer ",
f" Step {step:,} • Digits {digits_len:,}", f" Step {step:,} • Digits {digits_len:,}",
f" Elapsed {elapsed:8.2f}s • Avg {avg_step:8.5f}s", 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", " ▲ green = 3n+1 • ▼ red = /2 • ↑↓ number scroll • PgUp/PgDn logs • q quit",
] ]
@ -56,6 +66,9 @@ class GraphView:
self._trim() self._trim()
self._render() self._render()
def redraw(self) -> None:
self._render()
def _trim(self) -> None: def _trim(self) -> None:
_, max_x = self.win.getmaxyx() _, max_x = self.win.getmaxyx()
visible = max(0, max_x - self.padding) visible = max(0, max_x - self.padding)