Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions tests/test_lucid_engine.py → tests/test_lightfall_engine.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for LUCIDEngine."""
"""Tests for LightfallEngine."""

import tempfile
import threading
Expand All @@ -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


Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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()
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions tests/test_nats_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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"
Expand Down
10 changes: 5 additions & 5 deletions tests/test_nats_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ 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
assert cfg.connect_timeout == 5.0
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()
Expand All @@ -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"
12 changes: 6 additions & 6 deletions tests/test_nats_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down
10 changes: 5 additions & 5 deletions tsuchinoko/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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(
Expand Down
18 changes: 9 additions & 9 deletions tsuchinoko/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""LUCID execution engine — coordinates measurement via NATS + Tiled."""
"""Lightfall execution engine — coordinates measurement via NATS + Tiled."""

from __future__ import annotations

Expand All @@ -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
Expand All @@ -26,22 +26,22 @@ 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

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])
Expand All @@ -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:
Expand Down
24 changes: 12 additions & 12 deletions tsuchinoko/nats/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions tsuchinoko/nats/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading