diff --git a/tests/test_lucid_engine.py b/tests/test_lightfall_engine.py similarity index 77% rename from tests/test_lucid_engine.py rename to tests/test_lightfall_engine.py index 6f43491..a0b9d85 100644 --- a/tests/test_lucid_engine.py +++ b/tests/test_lightfall_engine.py @@ -1,4 +1,4 @@ -"""Tests for LUCIDEngine.""" +"""Tests for LightfallEngine.""" import tempfile import threading @@ -10,7 +10,7 @@ from tiled.client import Context, from_context from tiled.server.app import build_app -from tsuchinoko.execution.lucid import LUCIDEngine +from tsuchinoko.execution.lightfall import LightfallEngine from tsuchinoko.tiled.reader import TiledReader @@ -47,18 +47,18 @@ def mock_nats_client(): @pytest.fixture -def lucid_engine(tiled_client, populated_run, mock_nats_client): +def lightfall_engine(tiled_client, populated_run, mock_nats_client): reader = TiledReader(tiled_client, populated_run, motor_names=["x_motor", "y_motor"], detector_name="detector") - engine = LUCIDEngine(nats_client=mock_nats_client, lucid_prefix="test.lucid", + engine = LightfallEngine(nats_client=mock_nats_client, lightfall_prefix="test.lightfall", tiled_reader=reader) return engine -def test_update_targets(lucid_engine, mock_nats_client): +def test_update_targets(lightfall_engine, mock_nats_client): """Publishes to 'tsuchinoko.targets' with run_uid, targets list, iteration.""" targets = [(1.0, 2.0), (3.0, 4.0)] - lucid_engine.update_targets(targets) + lightfall_engine.update_targets(targets) mock_nats_client.publish_threadsafe.assert_called_once() subject, payload = mock_nats_client.publish_threadsafe.call_args[0] @@ -72,22 +72,22 @@ def test_get_position_default(mock_nats_client, tiled_client, populated_run): """Returns (0, 0) before any targets are published.""" reader = TiledReader(tiled_client, populated_run, motor_names=["x_motor", "y_motor"], detector_name="detector") - engine = LUCIDEngine(nats_client=mock_nats_client, lucid_prefix="test.lucid", + engine = LightfallEngine(nats_client=mock_nats_client, lightfall_prefix="test.lightfall", tiled_reader=reader) assert engine.get_position() == (0, 0) -def test_get_position_after_targets(lucid_engine): +def test_get_position_after_targets(lightfall_engine): """Returns the last target after update_targets is called.""" targets = [(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)] - lucid_engine.update_targets(targets) - assert lucid_engine.get_position() == (5.0, 6.0) + lightfall_engine.update_targets(targets) + assert lightfall_engine.get_position() == (5.0, 6.0) -def test_get_measurements_after_signal(lucid_engine): +def test_get_measurements_after_signal(lightfall_engine): """signal_measurements_ready() then get_measurements() returns 3 rows.""" - lucid_engine.signal_measurements_ready() - measurements = lucid_engine.get_measurements() + lightfall_engine.signal_measurements_ready() + measurements = lightfall_engine.get_measurements() assert len(measurements) == 3 positions = [m[0] for m in measurements] @@ -100,12 +100,12 @@ def test_get_measurements_after_signal(lucid_engine): assert abs(values[2] - 0.3) < 1e-6 -def test_get_measurements_blocks(lucid_engine): +def test_get_measurements_blocks(lightfall_engine): """Verify get_measurements blocks until signalled.""" results = [] def reader(): - results.append(lucid_engine.get_measurements()) + results.append(lightfall_engine.get_measurements()) t = threading.Thread(target=reader, daemon=True) t.start() @@ -115,7 +115,7 @@ def reader(): assert t.is_alive(), "get_measurements() returned too early — should be blocking" # Signal and verify it finishes - lucid_engine.signal_measurements_ready() + lightfall_engine.signal_measurements_ready() t.join(timeout=5.0) assert not t.is_alive(), "get_measurements() did not return after signal" assert len(results) == 1 diff --git a/tests/test_nats_client.py b/tests/test_nats_client.py index 17883cb..8eb13c3 100644 --- a/tests/test_nats_client.py +++ b/tests/test_nats_client.py @@ -22,7 +22,7 @@ def mock_nc(): @pytest.fixture def config(): - return NATSConfig(url="nats://localhost:4222", lucid_prefix="test.lucid") + return NATSConfig(url="nats://localhost:4222", lightfall_prefix="test.lightfall") class TestConnection: @@ -59,11 +59,11 @@ async def test_auth_approved(self, config, mock_nc): client = NATSClient() with patch("tsuchinoko.nats.client.nats.connect", return_value=mock_nc): await client.connect(config) - creds = await client.authenticate("test.lucid", "tsuchinoko", "1.0", 10.0) + creds = await client.authenticate("test.lightfall", "tsuchinoko", "1.0", 10.0) assert creds["tiled_token"] == "tok123" call_args = mock_nc.request.call_args - assert call_args[0][0] == "test.lucid.auth.request" + assert call_args[0][0] == "test.lightfall.auth.request" async def test_auth_denied(self, config, mock_nc): mock_msg = MagicMock() @@ -74,7 +74,7 @@ async def test_auth_denied(self, config, mock_nc): with patch("tsuchinoko.nats.client.nats.connect", return_value=mock_nc): await client.connect(config) with pytest.raises(PermissionError, match="not trusted"): - await client.authenticate("test.lucid", "tsuchinoko", "1.0", 10.0) + await client.authenticate("test.lightfall", "tsuchinoko", "1.0", 10.0) async def test_auth_cached(self, config, mock_nc): mock_msg = MagicMock() @@ -86,8 +86,8 @@ async def test_auth_cached(self, config, mock_nc): client = NATSClient() with patch("tsuchinoko.nats.client.nats.connect", return_value=mock_nc): await client.connect(config) - await client.authenticate("test.lucid", "tsuchinoko", "1.0", 10.0) - creds = await client.authenticate("test.lucid", "tsuchinoko", "1.0", 10.0) + await client.authenticate("test.lightfall", "tsuchinoko", "1.0", 10.0) + creds = await client.authenticate("test.lightfall", "tsuchinoko", "1.0", 10.0) assert mock_nc.request.await_count == 1 assert creds["tiled_token"] == "tok" diff --git a/tests/test_nats_config.py b/tests/test_nats_config.py index 6bb5ada..84319fe 100644 --- a/tests/test_nats_config.py +++ b/tests/test_nats_config.py @@ -9,7 +9,7 @@ class TestNATSConfig: def test_defaults(self): cfg = NATSConfig() assert cfg.url == "" - assert cfg.lucid_prefix == "als.7011" + assert cfg.lightfall_prefix == "als.7011" assert cfg.app_name == "tsuchinoko" assert cfg.app_version == "" assert cfg.auth_timeout == 70.0 @@ -17,9 +17,9 @@ def test_defaults(self): assert cfg.reconnect is True def test_custom_values(self): - cfg = NATSConfig(url="nats://broker:4222", lucid_prefix="als.1234") + cfg = NATSConfig(url="nats://broker:4222", lightfall_prefix="als.1234") assert cfg.url == "nats://broker:4222" - assert cfg.lucid_prefix == "als.1234" + assert cfg.lightfall_prefix == "als.1234" def test_nats_disabled_by_default(self): cfg = NATSConfig() @@ -32,6 +32,6 @@ def test_app_config_includes_nats(self): assert cfg.nats.url == "" def test_app_config_from_dict(self): - cfg = AppConfig(**{"nats": {"url": "nats://localhost:4222", "lucid_prefix": "test.prefix"}}) + cfg = AppConfig(**{"nats": {"url": "nats://localhost:4222", "lightfall_prefix": "test.prefix"}}) assert cfg.nats.url == "nats://localhost:4222" - assert cfg.nats.lucid_prefix == "test.prefix" + assert cfg.nats.lightfall_prefix == "test.prefix" diff --git a/tests/test_nats_integration.py b/tests/test_nats_integration.py index c0470b3..e15f6d7 100644 --- a/tests/test_nats_integration.py +++ b/tests/test_nats_integration.py @@ -36,9 +36,9 @@ async def nats_available(nats_url): @pytest_asyncio.fixture -async def mock_lucid(nats_url, nats_available): +async def mock_lightfall(nats_url, nats_available): nc = await nats_lib.connect(nats_url) - prefix = f"test.lucid.{uuid.uuid4().hex[:8]}" + prefix = f"test.lightfall.{uuid.uuid4().hex[:8]}" async def handle_auth(msg): reply = json.dumps({ @@ -71,7 +71,7 @@ def _make_core(nats_url, measure_func=_slow_measure, exit_at=None, pause_at=None """Helper to build a Core with NATS that stays alive for queries.""" engine = RandomInProcess(dimensionality=2, parameter_bounds=[(0, 100), (0, 100)]) execution = SimpleEngine(measure_func=measure_func) - config = NATSConfig(url=nats_url, lucid_prefix="") + config = NATSConfig(url=nats_url, lightfall_prefix="") core = Core(execution_engine=execution, adaptive_engine=engine, compute_metrics=False, nats_config=config) if exit_at is not None: core.exit_at = exit_at @@ -90,11 +90,11 @@ async def test_connect_and_close(self, nats_url, nats_available): await client.close() assert not client.is_connected - async def test_auth_handshake(self, nats_url, mock_lucid): + async def test_auth_handshake(self, nats_url, mock_lightfall): client = NATSClient() - config = NATSConfig(url=nats_url, lucid_prefix=mock_lucid["prefix"]) + config = NATSConfig(url=nats_url, lightfall_prefix=mock_lightfall["prefix"]) await client.connect(config) - creds = await client.authenticate(mock_lucid["prefix"], "tsuchinoko", "test", timeout=5.0) + creds = await client.authenticate(mock_lightfall["prefix"], "tsuchinoko", "test", timeout=5.0) assert creds["tiled_token"] == "test-token-123" await client.close() diff --git a/tsuchinoko/cli.py b/tsuchinoko/cli.py index 5c3fe14..7aea1ce 100644 --- a/tsuchinoko/cli.py +++ b/tsuchinoko/cli.py @@ -13,9 +13,9 @@ def main(): @main.command() @click.option("--nats-url", default="", help="NATS broker URL (e.g. nats://localhost:4222). Empty = no NATS.") -@click.option("--lucid-prefix", default="als.7011", help="LUCID instance topic prefix.") +@click.option("--lightfall-prefix", default="als.7011", help="Lightfall instance topic prefix.") @click.option("--config", "config_path", default=None, type=click.Path(exists=True), help="YAML config file.") -def run(nats_url, lucid_prefix, config_path): +def run(nats_url, lightfall_prefix, config_path): """Run the Tsuchinoko adaptive experiment service.""" from tsuchinoko.config import AppConfig, set_config from tsuchinoko.core import Core @@ -31,8 +31,8 @@ def run(nats_url, lucid_prefix, config_path): # CLI flags override config file if nats_url: config.nats.url = nats_url - if lucid_prefix != "als.7011": - config.nats.lucid_prefix = lucid_prefix + if lightfall_prefix != "als.7011": + config.nats.lightfall_prefix = lightfall_prefix set_config(config) @@ -58,7 +58,7 @@ def run(nats_url, lucid_prefix, config_path): logger.info(f"Tsuchinoko starting (engine={ac.engine_type}, dim={ac.dimensionality})") logger.info(f" NATS: {config.nats.url or 'disabled'}") if config.nats.url: - logger.info(f" LUCID prefix: {config.nats.lucid_prefix}") + logger.info(f" Lightfall prefix: {config.nats.lightfall_prefix}") logger.info(f" Tiled: {config.tiled.url or 'disabled'}") core = Core( diff --git a/tsuchinoko/core/__init__.py b/tsuchinoko/core/__init__.py index c713801..f204fce 100644 --- a/tsuchinoko/core/__init__.py +++ b/tsuchinoko/core/__init__.py @@ -224,25 +224,25 @@ async def _main(self) -> None: self._nats_client = NATSClient() try: await self._nats_client.connect(self._nats_config) - if self._nats_config.lucid_prefix: + if self._nats_config.lightfall_prefix: try: await self._nats_client.authenticate( - self._nats_config.lucid_prefix, + self._nats_config.lightfall_prefix, self._nats_config.app_name, self._nats_config.app_version, self._nats_config.auth_timeout, ) except Exception as e: - logger.warning(f"LUCID auth failed (continuing without): {e}") + logger.warning(f"Lightfall auth failed (continuing without): {e}") - # Auto-create LUCIDEngine when no execution engine was provided + # Auto-create LightfallEngine when no execution engine was provided if self.execution_engine is None: - from tsuchinoko.execution.lucid import LUCIDEngine - self.execution_engine = LUCIDEngine( + from tsuchinoko.execution.lightfall import LightfallEngine + self.execution_engine = LightfallEngine( nats_client=self._nats_client, - lucid_prefix=self._nats_config.lucid_prefix, + lightfall_prefix=self._nats_config.lightfall_prefix, ) - logger.info("Auto-created LUCIDEngine (awaiting bind_run)") + logger.info("Auto-created LightfallEngine (awaiting bind_run)") self._nats_service = NATSService(self, self._nats_client) await self._nats_service.start() @@ -360,7 +360,7 @@ def experiment_iteration(self) -> None: self._has_fresh_data = False elif self._last_targets is not None: # Re-publish last targets to avoid deadlock when - # measurements came back empty (e.g. LUCID hasn't + # measurements came back empty (e.g. Lightfall hasn't # finished measuring yet). with log_time('re-publishing targets', cumulative_key='updating targets'): self.execution_engine.update_targets(self._last_targets) diff --git a/tsuchinoko/execution/lucid.py b/tsuchinoko/execution/lightfall.py similarity index 77% rename from tsuchinoko/execution/lucid.py rename to tsuchinoko/execution/lightfall.py index 82e38dd..aac16b2 100644 --- a/tsuchinoko/execution/lucid.py +++ b/tsuchinoko/execution/lightfall.py @@ -1,4 +1,4 @@ -"""LUCID execution engine — coordinates measurement via NATS + Tiled.""" +"""Lightfall execution engine — coordinates measurement via NATS + Tiled.""" from __future__ import annotations @@ -14,10 +14,10 @@ from . import Engine -class LUCIDEngine(Engine): - """ExecutionEngine that coordinates with LUCID over NATS. +class LightfallEngine(Engine): + """ExecutionEngine that coordinates with Lightfall over NATS. - Publishes targets via NATS, waits for LUCID's measurement signal, + Publishes targets via NATS, waits for Lightfall's measurement signal, then reads results from Tiled. The engine can be created before the bluesky run exists. Call @@ -26,11 +26,11 @@ class LUCIDEngine(Engine): """ def __init__(self, nats_client: NATSClient, - lucid_prefix: str, + lightfall_prefix: str, tiled_reader: TiledReader | None = None) -> None: self._nats_client = nats_client self._tiled_reader = tiled_reader - self._lucid_prefix = lucid_prefix + self._lightfall_prefix = lightfall_prefix self._position: tuple = (0, 0) self._measured_event = threading.Event() self._iteration = 0 @@ -38,10 +38,10 @@ def __init__(self, nats_client: NATSClient, def bind_run(self, tiled_reader: TiledReader) -> None: """Bind a TiledReader for an active bluesky run.""" self._tiled_reader = tiled_reader - logger.info("LUCIDEngine bound to run") + logger.info("LightfallEngine bound to run") def update_targets(self, targets: List[Tuple]) -> None: - """Publish targets to NATS for LUCID to measure.""" + """Publish targets to NATS for Lightfall to measure.""" self._iteration += 1 if len(targets): self._position = tuple(targets[-1]) @@ -56,7 +56,7 @@ def get_position(self) -> Tuple: return self._position def get_measurements(self) -> List[Tuple]: - """Block until LUCID signals measurements ready, then read from Tiled.""" + """Block until Lightfall signals measurements ready, then read from Tiled.""" self._measured_event.wait(timeout=10) self._measured_event.clear() if self._tiled_reader is None: diff --git a/tsuchinoko/nats/client.py b/tsuchinoko/nats/client.py index fff9eef..976874e 100644 --- a/tsuchinoko/nats/client.py +++ b/tsuchinoko/nats/client.py @@ -43,31 +43,31 @@ async def close(self) -> None: self._nc = None logger.info("NATS connection closed") - async def authenticate(self, lucid_prefix: str, app_name: str, app_version: str, timeout: float) -> dict: - """Auth handshake with LUCID. Returns Tiled creds. Caches per prefix.""" - cached = self._auth_state.get(lucid_prefix) + async def authenticate(self, lightfall_prefix: str, app_name: str, app_version: str, timeout: float) -> dict: + """Auth handshake with Lightfall. Returns Tiled creds. Caches per prefix.""" + cached = self._auth_state.get(lightfall_prefix) if cached == "approved": - return self._tiled_credentials.get(lucid_prefix, {}) + return self._tiled_credentials.get(lightfall_prefix, {}) if cached == "denied": - raise PermissionError(f"LUCID at '{lucid_prefix}' previously denied access") + raise PermissionError(f"Lightfall at '{lightfall_prefix}' previously denied access") - subject = f"{lucid_prefix}.auth.request" + subject = f"{lightfall_prefix}.auth.request" payload = json.dumps({"app_name": app_name, "app_version": app_version}).encode() msg = await self._nc.request(subject, payload, timeout=timeout) data = json.loads(msg.data) if data.get("status") == "approved": - self._auth_state[lucid_prefix] = "approved" - self._tiled_credentials[lucid_prefix] = { + self._auth_state[lightfall_prefix] = "approved" + self._tiled_credentials[lightfall_prefix] = { "tiled_token": data.get("tiled_token"), "tiled_url": data.get("tiled_url"), } - logger.info(f"Authenticated with LUCID at '{lucid_prefix}'") - return self._tiled_credentials[lucid_prefix] + logger.info(f"Authenticated with Lightfall at '{lightfall_prefix}'") + return self._tiled_credentials[lightfall_prefix] else: reason = data.get("reason", "denied by operator") - self._auth_state[lucid_prefix] = "denied" - raise PermissionError(f"LUCID denied access: {reason}") + self._auth_state[lightfall_prefix] = "denied" + raise PermissionError(f"Lightfall denied access: {reason}") async def publish(self, subject: str, payload: dict) -> None: if self._nc and self.is_connected: diff --git a/tsuchinoko/nats/config.py b/tsuchinoko/nats/config.py index b52069d..9767f01 100644 --- a/tsuchinoko/nats/config.py +++ b/tsuchinoko/nats/config.py @@ -6,9 +6,9 @@ class NATSConfig(BaseModel): """Configuration for the NATS connection.""" url: str = "" - lucid_prefix: str = "als.7011" + lightfall_prefix: str = "als.7011" app_name: str = "tsuchinoko" app_version: str = "" - auth_timeout: float = Field(70.0, description="Auth handshake timeout (>60s for LUCID trust dialog)") + auth_timeout: float = Field(70.0, description="Auth handshake timeout (>60s for Lightfall trust dialog)") connect_timeout: float = Field(5.0, description="Initial connection timeout") reconnect: bool = True diff --git a/tsuchinoko/nats/service.py b/tsuchinoko/nats/service.py index d307cd9..0b16df4 100644 --- a/tsuchinoko/nats/service.py +++ b/tsuchinoko/nats/service.py @@ -106,8 +106,8 @@ async def _reply(self, msg, data: dict) -> None: async def _handle_bind_run(self, msg) -> None: """Bind a bluesky run: create TiledReader + TiledPublisher. - Expects payload from LUCID with Tiled credentials (URL + API key - from LUCID's session-key cache, sent as ``tiled_api_key``), so + Expects payload from Lightfall with Tiled credentials (URL + API key + from Lightfall's session-key cache, sent as ``tiled_api_key``), so Tsuchinoko never authenticates independently. """ try: @@ -127,7 +127,7 @@ async def _handle_bind_run(self, msg) -> None: await self._reply(msg, {"status": "error", "message": "No tiled_url"}) return - lucid_prefix = data.get("lucid_prefix", config.nats.lucid_prefix) + lightfall_prefix = data.get("lightfall_prefix", config.nats.lightfall_prefix) from tsuchinoko.tiled.connect import connect_tiled tiled_client = connect_tiled( @@ -136,9 +136,9 @@ async def _handle_bind_run(self, msg) -> None: proxy_url=proxy_url, ) - # Wire TiledReader into LUCIDEngine - from tsuchinoko.execution.lucid import LUCIDEngine - if isinstance(self._core.execution_engine, LUCIDEngine): + # Wire TiledReader into LightfallEngine + from tsuchinoko.execution.lightfall import LightfallEngine + if isinstance(self._core.execution_engine, LightfallEngine): from tsuchinoko.tiled.reader import TiledReader reader = TiledReader( tiled_client, run_uid, motor_names, detector_name, @@ -152,17 +152,17 @@ async def _handle_bind_run(self, msg) -> None: publisher.write_config(self._core.adaptive_engine) self._core._tiled_publisher = publisher - # Subscribe to {lucid_prefix}.adaptive.measured for unblocking - measured_subject = f"{lucid_prefix}.adaptive.measured" + # Subscribe to {lightfall_prefix}.adaptive.measured for unblocking + measured_subject = f"{lightfall_prefix}.adaptive.measured" async def on_measured(nats_msg): - if isinstance(self._core.execution_engine, LUCIDEngine): + if isinstance(self._core.execution_engine, LightfallEngine): self._core.execution_engine.signal_measurements_ready() sub = await self._client.subscribe(measured_subject, on_measured) self._subscriptions.append(sub) - # Auto-start: LUCID expects tsuchinoko to begin after bind_run + # Auto-start: Lightfall expects tsuchinoko to begin after bind_run from tsuchinoko.core import CoreState if self._core.state == CoreState.Inactive: self._core.state = CoreState.Starting @@ -244,7 +244,7 @@ async def _handle_configure(self, msg) -> None: # Optimizer-rebuild keys — the engine reads these only at # init_optimizer time, so we setattr and then trigger reset() # so the GP is rebuilt with the new choices. Safe because - # configure happens before bind_run in the LUCID flow (no + # configure happens before bind_run in the Lightfall flow (no # data to lose). rebuild_keys = ("kernel", "prior_mean", "noise_function", "noise_variances") needs_rebuild = False diff --git a/tsuchinoko/tiled/connect.py b/tsuchinoko/tiled/connect.py index ace6a3e..68bcbe8 100644 --- a/tsuchinoko/tiled/connect.py +++ b/tsuchinoko/tiled/connect.py @@ -1,9 +1,9 @@ """Tiled connection helper with API-key auth. -LUCID Auth v2 mints session-lifetime API keys at user login and forwards +Lightfall Auth v2 mints session-lifetime API keys at user login and forwards the secret to tsuchinoko via the NATS bind_run payload. The executor uses the secret directly (no refresh, no Keycloak dependency); if the -key expires mid-job the executor fails the job and LUCID re-mints on +key expires mid-job the executor fails the job and Lightfall re-mints on next login. """ @@ -39,7 +39,7 @@ def connect_tiled( Args: url: Tiled server URL. - api_key: Optional Tiled API key (from LUCID's session-key cache). + api_key: Optional Tiled API key (from Lightfall's session-key cache). proxy_url: Optional proxy URL (e.g. ``socks5://localhost:1080``). Returns: @@ -53,7 +53,7 @@ def connect_tiled( logger.info(f"Connecting to Tiled at {url}") return from_uri(url, **kwargs) - # Route through proxy (same monkey-patch pattern as LUCID exporter) + # Route through proxy (same monkey-patch pattern as Lightfall exporter) import tiled.client.context as context_mod from tiled.client.transport import Transport as OriginalTransport