diff --git a/0-Core-Formalism/lean/Semantics/Semantics/RouteCost.lean b/0-Core-Formalism/lean/Semantics/Semantics/RouteCost.lean index a8c191ce..6ad3cd73 100644 --- a/0-Core-Formalism/lean/Semantics/Semantics/RouteCost.lean +++ b/0-Core-Formalism/lean/Semantics/Semantics/RouteCost.lean @@ -210,12 +210,18 @@ def fammMemoryBonus (a b : RouteNode) : Nat := - near (1): same cluster, 1-10ms → qEighth - far (2): cross-network, 10-100ms → qQuarter - derp (3): DERP relay, >100ms → qHalf + - offline (4): Tailscale down/unreachable → qOne (maximum cost) + + When Tailscale doesn't exist or is down, the chain must not fail. + Offline nodes get latencyClass=4, which routes computation to local-only. + The cost is still computable — offline is just another latency class. The latency class determines the FPGA voltage mode: - local → σ₀ (1.2V, exact) - near → σ₁ (1.0V, normal) - far → σ₂ (0.8V, approximate) - derp → σ₃ (0.6V, coarse) + - offline → σ₃ (0.6V, coarse, local-only fallback) This is the "coursing agent" — latency shapes the computation. -/ def networkLatencyCost (a b : RouteNode) : Nat := @@ -227,6 +233,7 @@ def networkLatencyCost (a b : RouteNode) : Nat := | 1 => qEighth -- near: 1-10ms | 2 => qQuarter -- far: 10-100ms | 3 => qHalf -- derp: >100ms (DERP relay) + | 4 => qOne -- offline: Tailscale down, maximum cost | _ => qOne -- unknown: maximum cost def weighted (weight component : Nat) : Nat := diff --git a/4-Infrastructure/shim/scale_space_solver.py b/4-Infrastructure/shim/scale_space_solver.py index 33206b0c..fbde75b0 100644 --- a/4-Infrastructure/shim/scale_space_solver.py +++ b/4-Infrastructure/shim/scale_space_solver.py @@ -15,9 +15,105 @@ smoothing. At each scale σ, nodes whose pairwise cost is below σ·max_cost are clustered together. The reduced problem is solved, then expanded back. """ +import json import math +import subprocess from typing import Optional +# ── Tailscale Detection (graceful degradation) ────────────────────────── + +_LATENCY_CLASSES = { + 0: {'name': 'local', 'ms_max': 1, 'voltage': 1200, 'sigma': 0.0}, + 1: {'name': 'near', 'ms_max': 10, 'voltage': 1000, 'sigma': 0.25}, + 2: {'name': 'far', 'ms_max': 100, 'voltage': 800, 'sigma': 0.50}, + 3: {'name': 'derp', 'ms_max': 1000, 'voltage': 600, 'sigma': 1.0}, + 4: {'name': 'offline', 'ms_max': None, 'voltage': 600, 'sigma': 1.0}, +} + + +def detect_tailscale() -> dict: + """Detect Tailscale status. Returns dict with 'available', 'peers', 'latency_map'. + + If Tailscale is not installed or not running, returns available=False + with empty peers and latency_map. The chain never fails. + """ + result = { + 'available': False, + 'peers': {}, + 'latency_map': {}, + 'derp_region': None, + } + + # Check if tailscale binary exists + try: + proc = subprocess.run( + ['tailscale', 'status', '--json'], + capture_output=True, text=True, timeout=5 + ) + if proc.returncode != 0: + return result # tailscale not running + except (FileNotFoundError, subprocess.TimeoutExpired): + return result # tailscale not installed + + try: + status = json.loads(proc.stdout) + result['available'] = True + result['derp_region'] = status.get('CurrentTailnet', {}).get('Name') + + for peer_id, peer in status.get('Peer', {}).items(): + hostname = peer.get('HostName', peer_id) + tailscale_ip = peer.get('TailscaleIPs', [None])[0] + relay = peer.get('Relay', '') + latency = peer.get('CurAddr', '') + + # Classify latency + if not peer.get('Online', False): + latency_class = 4 # offline + elif relay: # DERP relay + latency_class = 3 # derp + elif tailscale_ip: + latency_class = 1 # near (same tailnet) + else: + latency_class = 2 # far + + result['peers'][hostname] = { + 'ip': tailscale_ip, + 'latency_class': latency_class, + 'relay': relay, + 'online': peer.get('Online', False), + } + if tailscale_ip: + result['latency_map'][tailscale_ip] = latency_class + + except (json.JSONDecodeError, KeyError, TypeError): + pass # malformed status, return what we have + + return result + + +def get_latency_class(node_ip: str, ts_status: Optional[dict] = None) -> int: + """Get latency class for a node. Returns 4 (offline) if Tailscale unavailable. + + The chain never fails — offline is just another latency class. + """ + if ts_status is None: + ts_status = detect_tailscale() + + if not ts_status['available']: + return 4 # offline — Tailscale not running + + return ts_status['latency_map'].get(node_ip, 4) # default to offline + + +def latency_to_voltage(latency_class: int) -> int: + """Map latency class to FPGA voltage in millivolts.""" + return _LATENCY_CLASSES.get(latency_class, _LATENCY_CLASSES[4])['voltage'] + + +def latency_to_sigma(latency_class: int) -> float: + """Map latency class to scale space sigma.""" + return _LATENCY_CLASSES.get(latency_class, _LATENCY_CLASSES[4])['sigma'] + try: import numpy as np HAS_NUMPY = True