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
|
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."""
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user