Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
89 changes: 83 additions & 6 deletions src/ess/livedata/config/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
from __future__ import annotations

from collections import UserDict
from collections.abc import Callable, Sequence
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
import sciline

from ess.livedata.handlers.detector_view_specs import SpectrumViewSpec
from ess.livedata.handlers.log_context import LogContextBinding
from ess.livedata.handlers.stream_processor_workflow import ValueLog

import pydantic
import scipp as sc
Expand Down Expand Up @@ -90,6 +94,7 @@ class Instrument:
monitors: list[str] = field(default_factory=list)
workflow_factory: WorkflowFactory = field(default_factory=WorkflowFactory)
f144_attribute_registry: dict[str, dict[str, Any]] = field(default_factory=dict)
log_context_bindings: list[LogContextBinding] = field(default_factory=list)
source_metadata: dict[str, SourceMetadata] = field(default_factory=dict)
_detector_numbers: dict[str, sc.Variable] = field(default_factory=dict)
_nexus_file: str | None = None
Expand All @@ -107,6 +112,15 @@ def __post_init__(self) -> None:
register_timeseries_workflow_specs,
)

# Auto-derive f144_attribute_registry entries from bindings that
# declare units. Explicit registry entries win on conflict.
for binding in self.log_context_bindings:
if binding.units is None:
continue
self.f144_attribute_registry.setdefault(
binding.stream_name, {'units': binding.units}
)

timeseries_names = list(self.f144_attribute_registry.keys())
self._timeseries_workflow_handle = register_timeseries_workflow_specs(
instrument=self, source_names=timeseries_names
Expand Down Expand Up @@ -350,6 +364,64 @@ def add_logical_view(
)
return handle

def apply_dynamic_transforms(
self,
workflow: sciline.Pipeline,
components: Mapping[str, type],
) -> dict[str, type[ValueLog]]:
"""Patch ``workflow`` to drive matching NXlog placeholders from f144 streams.

For each ``(source_name, component_type)`` entry, selects every
:class:`DynamicTransformBinding` in :attr:`log_context_bindings`
whose ``dependent_sources`` set includes that source name. For each
component type with at least one match, replaces the
``NeXusTransformationChain[T, SampleRun]`` provider with a closure
that consumes the matched bindings' ``log_key`` parameters and
writes the latest sample into the chain. Non-transform bindings
on the instrument are ignored.

Parameters
----------
workflow:
The Sciline pipeline to patch in place.
components:
Mapping ``source_name -> component_type`` for the
essreduce-loaded NeXus components whose ``depends_on`` chain
might need patching (e.g. ``{'loki_detector_0': NXdetector,
aux_source_names['incident_monitor']: Incident, ...}``).
Callers own the alias resolution: source names are the
actual on-disk names, not aliases.

Returns
-------
:
Mapping ``stream_name -> log_key`` for every binding that
actually matched; pass to :class:`StreamProcessorWorkflow`
as ``context_keys`` so SPW's wrapping rule delivers each
f144 NXlog to the right Sciline parameter.
"""
from ess.livedata.handlers.dynamic_transforms import (
DynamicTransformBinding,
build_patched_chain_provider,
)

transform_bindings = [
b
for b in self.log_context_bindings
if isinstance(b, DynamicTransformBinding)
]
context_keys: dict[str, type[ValueLog]] = {}
for source_name, component_type in components.items():
matched = [
b for b in transform_bindings if source_name in b.dependent_sources
]
if not matched:
continue
workflow.insert(build_patched_chain_provider(component_type, matched))
for binding in matched:
context_keys[binding.stream_name] = binding.log_key
return context_keys

def register_spec(
self,
*,
Expand Down Expand Up @@ -410,6 +482,14 @@ def register_spec(
-------
Handle for attaching factory later.
"""
# Merge log-context aux sources scoped per spec source list, so
# factories whose bindings cover any of the spec's sources
# automatically have the routing layer deliver matching f144 streams.
if self.log_context_bindings and source_names:
from ess.livedata.handlers.log_context import compose_aux_sources

aux_sources = compose_aux_sources(self, list(source_names), aux_sources)

spec = WorkflowSpec(
instrument=self.name,
group=group,
Expand Down Expand Up @@ -450,10 +530,7 @@ def load_factories(self) -> None:
)

if self._logical_views:
from ess.livedata.handlers.detector_view import (
DetectorViewFactory,
InstrumentDetectorSource,
)
from ess.livedata.handlers.detector_view import DetectorViewFactory
from ess.livedata.handlers.detector_view import (
LogicalViewConfig as ScilineLogicalViewConfig,
)
Expand All @@ -467,8 +544,8 @@ def load_factories(self) -> None:
spectrum_view=config.spectrum_view,
)
factory = DetectorViewFactory(
data_source=InstrumentDetectorSource(self),
view_config=view_config,
instrument=self,
)
handle.attach_factory()(factory.make_workflow)

Expand Down
1 change: 1 addition & 0 deletions src/ess/livedata/config/instruments/bifrost/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def _monitor_workflow_factory(source_name: str, params: TOAOnlyMonitorDataParams
edges=params.get_active_edges(),
range_filter=params.get_active_range(),
coordinate_mode='toa',
instrument=instrument,
)

# Create base reduction workflow
Expand Down
2 changes: 2 additions & 0 deletions src/ess/livedata/config/instruments/dream/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def _resolve_lookup_table_filename(instrument_configuration):
flip_x=True,
),
},
instrument=instrument,
)

@specs.projection_handle.attach_factory()
Expand Down Expand Up @@ -133,6 +134,7 @@ def _monitor_workflow_factory(source_name: str, params: DreamMonitorDataParams):
coordinate_mode=mode,
lookup_table_filename=lookup_table_filename,
geometry_filename=geometry_filename,
instrument=instrument,
)

# Powder reduction workflow setup
Expand Down
4 changes: 2 additions & 2 deletions src/ess/livedata/config/instruments/dummy/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ def setup_factories(instrument: Instrument) -> None:
from ess.livedata.handlers.area_detector_view import AreaDetectorView
from ess.livedata.handlers.detector_view import (
DetectorViewFactory,
InstrumentDetectorSource,
LogicalViewConfig,
)
from ess.livedata.handlers.stream_processor_workflow import StreamProcessorWorkflow
Expand All @@ -44,8 +43,8 @@ def setup_factories(instrument: Instrument) -> None:

# Create detector view for event-mode panel_0 using Sciline-based factory
_panel_0_view = DetectorViewFactory(
data_source=InstrumentDetectorSource(instrument),
view_config=LogicalViewConfig(), # Identity transform
instrument=instrument,
)

specs.panel_0_view_handle.attach_factory()(_panel_0_view.make_workflow)
Expand Down Expand Up @@ -78,6 +77,7 @@ def _monitor_workflow_factory(source_name: str, params: TOAOnlyMonitorDataParams
edges=params.get_active_edges(),
range_filter=params.get_active_range(),
coordinate_mode='toa',
instrument=instrument,
)

# Total counts workflow
Expand Down
1 change: 1 addition & 0 deletions src/ess/livedata/config/instruments/estia/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def _monitor_workflow_factory(source_name: str, params: TOAOnlyMonitorDataParams
edges=params.get_active_edges(),
range_filter=params.get_active_range(),
coordinate_mode='toa',
instrument=instrument,
)

@specs.reflectometry_reduction_handle.attach_factory()
Expand Down
26 changes: 19 additions & 7 deletions src/ess/livedata/config/instruments/loki/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ def setup_factories(instrument: Instrument) -> None:
StreamProcessorWorkflow,
)

from .specs import LOKI_DYNAMIC_TRANSFORMS

_nexus_geometry_filename = get_nexus_geometry_filename('loki')

def _resolve_lookup_table_filename() -> str:
Expand Down Expand Up @@ -102,11 +100,11 @@ def _make_base_workflow() -> LokiWorkflow:
)
for name, res in _bank_resolutions.items()
},
# Drive the rear bank's NeXus 'detector_carriage' transformation
# from the live f144 carriage readback. The mapping is shared with
# loki/specs.py so the spec routes the stream only to the consuming
# source.
dynamic_transforms=LOKI_DYNAMIC_TRANSFORMS,
# Apply the instrument's dynamic-transform registry so the rear
# bank's carriage NXlog placeholder is driven by the live f144
# readback. Other banks have no matching binding, so the helper is
# a no-op for them.
instrument=instrument,
)

from ess.livedata.handlers.detector_view_specs import DetectorViewParams
Expand Down Expand Up @@ -157,6 +155,7 @@ def _monitor_workflow_factory(source_name: str, params: MonitorDataParams):
coordinate_mode=mode,
lookup_table_filename=lookup_table_filename,
geometry_filename=geometry_filename,
instrument=instrument,
)

# --- Providers for current_run transmission mode ---
Expand Down Expand Up @@ -209,6 +208,18 @@ def _i_of_q_factory(
wf[sans_types.WavelengthBins] = params.wavelength_edges.get_edges()
wf[BeamCenter] = params.beam_center.get_vector()

# Patch the workflow to drive any matching NXlog placeholder along
# the loaded components' depends_on chains from f144 streams.
# For LOKI today this covers the rear-bank carriage (issue #922).
context_keys = instrument.apply_dynamic_transforms(
wf,
{
source_name: NXdetector,
aux_source_names['incident_monitor']: Incident,
aux_source_names['transmission_monitor']: Transmission,
},
)

target_keys: dict[str, sciline.typing.Key] = {
'i_of_q': IntensityQ[SampleRun],
}
Expand All @@ -235,6 +246,7 @@ def _i_of_q_factory(
return StreamProcessorWorkflow(
wf,
dynamic_keys=_dynamic_keys(source_name),
context_keys=context_keys,
target_keys=target_keys,
accumulators=_accumulators,
)
40 changes: 24 additions & 16 deletions src/ess/livedata/config/instruments/loki/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,40 @@
AuxSources,
WorkflowOutputsBase,
)
from ess.livedata.handlers.detector_view.types import TransformValueStream
from ess.livedata.handlers.detector_view_specs import (
DetectorROIAuxSources,
register_detector_view_spec,
)
from ess.livedata.handlers.detector_view_specs import register_detector_view_spec
from ess.livedata.handlers.dynamic_transforms import DynamicTransformBinding
from ess.livedata.handlers.monitor_workflow_specs import (
MonitorDataParams,
register_monitor_workflow_specs,
)
from ess.livedata.handlers.stream_processor_workflow import ValueLog
from ess.livedata.handlers.wavelength_lut_workflow_specs import (
register_wavelength_lut_workflow_spec,
)

from .views import get_tube_view

#: Per-source bindings of NeXus transformation entries to live f144 streams.
#: Single source of truth shared between the spec (for routing via
#: ``DetectorROIAuxSources``) and the factory (for graph wiring via
#: ``DetectorViewFactory(dynamic_transforms=...)``). Only the rear bank has
#: a live carriage readback; other banks have no dynamic geometry.
LOKI_DYNAMIC_TRANSFORMS: dict[str, TransformValueStream] = {
'loki_detector_0': TransformValueStream(
transform_name='/entry/instrument/detector_carriage/value',
aux_stream='detector_carriage',

#: Sciline key for the rear-detector carriage f144 NXlog. Subclassing
#: ValueLog gives the binding its own grep-able Sciline node, distinct
#: from any future bindings on the same component type.
class DetectorCarriageLog(ValueLog):
"""Carriage f144 NXlog (drives ``loki_detector_0`` chain)."""


#: Dynamic-transform bindings for LOKI. Only the rear bank's carriage entry
#: is dynamic; other banks have no live position readback. ``beam_monitor_m4``
#: is also movable (on the carriage, probably), but we currently have no file
#: with a correct depends_on chain.
LOKI_DYNAMIC_TRANSFORMS = [
DynamicTransformBinding(
nxlog_path='/entry/instrument/detector_carriage/value',
stream_name='detector_carriage',
log_key=DetectorCarriageLog,
dependent_sources=frozenset({'loki_detector_0'}),
units='mm',
),
}
]


class TransmissionMode(StrEnum):
Expand Down Expand Up @@ -209,6 +217,7 @@ class SansWorkflowParams(pydantic.BaseModel):
f144_attribute_registry={
name: {'units': info['units']} for name, info in f144_log_streams.items()
},
log_context_bindings=LOKI_DYNAMIC_TRANSFORMS,
source_metadata={
'loki_detector_0': SourceMetadata(title='Rear'),
'loki_detector_1': SourceMetadata(title='Mid Top'),
Expand Down Expand Up @@ -275,7 +284,6 @@ class SansWorkflowParams(pydantic.BaseModel):
instrument=instrument,
projection='xy_plane',
source_names=detector_names,
aux_sources=DetectorROIAuxSources(dynamic_transforms=LOKI_DYNAMIC_TRANSFORMS),
)

# Register tube view for all detector banks
Expand Down
4 changes: 2 additions & 2 deletions src/ess/livedata/config/instruments/nmx/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def setup_factories(instrument: Instrument) -> None:
# Lazy imports
from ess.livedata.handlers.detector_view import (
DetectorViewFactory,
InstrumentDetectorSource,
LogicalViewConfig,
)

Expand All @@ -37,8 +36,8 @@ def setup_factories(instrument: Instrument) -> None:

# Create detector view using Sciline-based factory (identity transform)
_nmx_panels_view = DetectorViewFactory(
data_source=InstrumentDetectorSource(instrument),
view_config=LogicalViewConfig(), # Identity transform
instrument=instrument,
)

specs.panel_xy_view_handle.attach_factory()(_nmx_panels_view.make_workflow)
Expand All @@ -55,4 +54,5 @@ def _monitor_workflow_factory(source_name: str, params: TOAOnlyMonitorDataParams
edges=params.get_active_edges(),
range_filter=params.get_active_range(),
coordinate_mode='toa',
instrument=instrument,
)
1 change: 1 addition & 0 deletions src/ess/livedata/config/instruments/odin/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ def _monitor_workflow_factory(source_name: str, params: TOAOnlyMonitorDataParams
edges=params.get_active_edges(),
range_filter=params.get_active_range(),
coordinate_mode='toa',
instrument=instrument,
)
1 change: 1 addition & 0 deletions src/ess/livedata/config/instruments/tbl/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ def _monitor_workflow_factory(source_name: str, params: TOAOnlyMonitorDataParams
edges=params.get_active_edges(),
range_filter=params.get_active_range(),
coordinate_mode='toa',
instrument=instrument,
)
Loading
Loading