niess.nexus¶
ESS NeXus Structure JSON, read off the niess object tree. See Produce NeXus Structure JSON for the task-oriented guide and Write NeXus translators for extending it.
Converting an instrument niess did not build is not supported: every target reads the
tree, and a .instr file has none.
nexus
¶
ESS NeXus Structure JSON from a niess instrument.
from niess.instrument import Instrument, Mount
from niess.nexus import to_nexus_structure
structure = to_nexus_structure(instrument)
Reads the tree: a component's position and orientation are on the component, a declared
Frame is a node, and a depends_on chain is the chain of frames a thing hangs from.
Writing a translator needs the node constructors, which are re-exported here so that takes one import. They are shared with the other route, being format rather than route.
One thing this module deliberately does not import: niess.nexus.bifrost.
Instrument-specific translators live in registries of their own, and generic NeXus has no
business loading niess.bifrost to publish them.
Converting an instrument niess did not build, by reading emitted McStas back, was a second route of some 1500 lines. It is gone: niess converts niess instruments.
Modules:
-
bifrost–BIFROST's own NeXus translators, from the tree.
-
bindings–Where a driven axis's numbers come from: a simulation knob, or a real motor.
-
nodes–NeXus Structure JSON node constructors.
-
registry–Translator lookup for the NeXus target.
-
streams–Filewriter stream and link module directives.
-
structure–ESS NeXus Structure JSON, from the tree.
Classes:
-
AxisBinding–One driven axis, resolved: what to write in an f144 config for it.
-
Binding–One entry of a facility bindings file: which stream fills one log.
-
BoundStreams–Topics and sources read from a facility bindings file, one log at a time.
-
LogBinding–One log, resolved: the stream that fills it.
-
RealStreams–Sources are EPICS PV roots, declared on the components that own the axes.
-
SimulatedStreams–Sources are the simulation's own parameter names.
-
UndeclaredAxis–A real conversion reached an axis with no PV declared for it.
-
UnitMismatch–A knob declares a different unit from the one its binding publishes.
-
NiessNexusRegistry–Translator lookup for the NeXus target.
-
NexusContext–
Functions:
-
node_name–The name of a group, dataset, or named stream node.
-
stream–A filewriter module directive --
ev44,da00,f144,link, ... -
load_bindings–Read a facility bindings file, such as ECDC's
bindings.yaml. -
resolve_stream–The stream group for a component, or
Noneif it publishes nothing. -
component_body–What a translator returns: the class and contents of one component's group.
-
emit–Put one component's group into the instrument.
-
to_nexus_structure–Convert
instrumentto ESS NeXus Structure JSON. -
translator–Register a function returning a component body for one or more niess classes.
AxisBinding
dataclass
¶
AxisBinding(topic: str, units: str = '', dtype: str = 'double', default: Any = None, source: Optional[str] = None, pv_root: Optional[str] = None, parameter: Optional[str] = None, logs: Optional[tuple[LogBinding, ...]] = None)
One driven axis, resolved: what to write in an f144 config for it.
pv_root set means this axis is a real EPICS positioner and the three canonical
logs follow from it. source set instead means a simulation parameter, and there
is one log to write.
Methods:
-
sources–The f144 source for each canonical log name this axis has.
Attributes:
Binding
dataclass
¶
Binding(key: str, schema: str, source: str, topic: str, source_type: str = 'forwarder', dtype: Optional[str] = None, value_units: Optional[str] = None, extra: Mapping[str, Any] = dict())
One entry of a facility bindings file: which stream fills one log.
key is the log's NeXus path below NXinstrument, dot-separated:
pulse_shaping_chopper_1.rotation_speed or sample_jaws.left.value.
source_type says who produces the stream. A forwarder source is an EPICS PV,
and an efu source is a name chosen by an event formation unit.
Attributes:
BoundStreams
dataclass
¶
BoundStreams(bindings: Mapping[str, Binding], simulated: bool = True, prefix: str = SIMULATION_PREFIX, topic: str = DEFAULT_STREAM_TOPIC, chopper_topic: str = DEFAULT_CHOPPER_TOPIC, pulse_source: Optional[str] = None, strict: bool = True)
Topics and sources read from a facility bindings file, one log at a time.
RealStreams builds each log's source from a PV root and a fixed suffix. This binder
looks the whole answer up instead, keyed by where the log sits in the file. That
covers everything a template cannot, such as a per-disc TDC channel or a piezo
motor's PzMtr record.
Two kinds of file come from one bindings file:
simulated=True (the default) is a simulation made to look like the real
instrument. Only logs the simulation has a value for are written: a knob's
value, a disc's speed, delay and park angle, and the TDC times, which data
reduction needs. A setpoint, a done flag, or a delay the electronics add has no
counterpart in a simulation, so leaving it out keeps the file from claiming one.
Every source the simulation serves over EPICS (source_type: forwarder) is
prefixed with prefix. The file then names the real instrument's topics, but a
simulated PV can never be mistaken for the real one, even on the same network.
Sources an event formation unit chooses (source_type: efu) are unchanged,
because the EFU fixes them. Each simulated log carries a simulation_parameter
attribute naming the instrument parameter that feeds it.
simulated=False writes every bound log, with the sources exactly as bound: a file
for the real instrument.
A knob must be declared in the unit its binding publishes. If it is not, the
conversion raises UnitMismatch, so no conversion is ever hidden between the
parameter and the stream. A knob with no binding gets a simulated source,
prefix plus its own name, on topic (chopper_topic for a disc), whatever
topic the knob itself names: the binder, not the knob, decides where a facility's
streams go. For a real file an unbound knob raises UndeclaredAxis unless
strict=False.
pulse_source defaults to prefix + pulse.
Methods:
-
source–The source a log is written with, in this kind of file.
-
log–One bound log, checked against the knob that feeds it, if there is one.
-
chopper–A disc's logs, from the bindings for its group.
-
pulse–The per-pulse reference sample, if
source.pulse_chargeis bound. -
stream–A detector's or monitor's stream selection, or
Noneif it is not bound.
source
¶
The source a log is written with, in this kind of file.
log
¶
log(binding: Binding, knob=None, name: Optional[str] = None) -> LogBinding
One bound log, checked against the knob that feeds it, if there is one.
Source code in src/niess/nexus/bindings.py
chopper
¶
A disc's logs, from the bindings for its group.
Source code in src/niess/nexus/bindings.py
pulse
¶
pulse() -> Optional[LogBinding]
The per-pulse reference sample, if source.pulse_charge is bound.
No simulation parameter feeds it. Whatever serves the simulation's pulse clock publishes it, one sample per pulse.
Source code in src/niess/nexus/bindings.py
stream
¶
A detector's or monitor's stream selection, or None if it is not bound.
Source code in src/niess/nexus/bindings.py
LogBinding
dataclass
¶
LogBinding(name: str, module: str, source: str, topic: str, units: str = '', dtype: str = 'double', parameter: Optional[str] = None)
One log, resolved: the stream that fills it.
parameter names the instrument parameter a simulation publishes the log from.
It is written on the NXlog, so whatever serves the simulation's PVs can tell which
parameter each source carries. None means nothing in the simulation feeds it:
either the file is for a real run, or the value is computed elsewhere, as a
chopper's top-dead-centre times are.
RealStreams
dataclass
¶
RealStreams(topic: str = DEFAULT_STREAM_TOPIC, chopper_topic: str = DEFAULT_CHOPPER_TOPIC, pulse_source: str = 'pulse', strict: bool = True)
Sources are EPICS PV roots, declared on the components that own the axes.
strict is the default because the failure it prevents is silent: a mistyped
pv_roots key would otherwise produce a plausible-looking simulated axis inside a
file that says it is real, and nothing downstream would ever say so. An instrument
that really is only half wired up can pass strict=False and get the simulated
source for the axes nobody has connected yet.
Methods:
-
chopper–A disc written against its real controller, if it declares one.
chopper
¶
A disc written against its real controller, if it declares one.
Source code in src/niess/nexus/bindings.py
SimulatedStreams
dataclass
¶
SimulatedStreams(topic: str = DEFAULT_STREAM_TOPIC, chopper_topic: str = DEFAULT_CHOPPER_TOPIC, pulse_source: str = 'pulse')
Sources are the simulation's own parameter names.
Methods:
-
chopper–A disc written from what the simulation knows, whatever it declares.
chopper
¶
A disc written from what the simulation knows, whatever it declares.
A declared pv_root is ignored rather than honoured: a simulated file that
named real PVs would be claiming values nothing published.
Source code in src/niess/nexus/bindings.py
UndeclaredAxis
¶
Bases: ValueError
A real conversion reached an axis with no PV declared for it.
UnitMismatch
¶
Bases: ValueError
A knob declares a different unit from the one its binding publishes.
NiessNexusRegistry
¶
Bases: NiessRegistry
Translator lookup for the NeXus target.
Created with hooks='nexus', so a class carrying __nexus_leaf__ is its own
translator. Registering wins, which is what an instrument-specific conversion needs
-- BIFROST's detectors must not give another instrument's their pixel numbering
merely because a module was imported.
Methods:
-
resolve_for_object–As the base class, but a class's own hooks get their body emitted.
Source code in src/niess/nexus/registry.py
resolve_for_object
¶
As the base class, but a class's own hooks get their body emitted.
NexusContext
dataclass
¶
NexusContext(instrument: Any, registry: Any = None, nodes: dict = dict(), nxlog_root: str = DEFAULT_NXLOG_ROOT, instrument_group: dict = None, paths: dict = dict(), _flow: Any = None, pending: list = list(), emitted_names: dict = dict(), streams: Any = None, placed: set = set(), efu_monitors: bool = False)
Bases: Context
Methods:
-
source_contents–What the NXsource holds: which facility, and where the pulse reference is.
-
pulse_log–The per-pulse reference sample, as the binder says it is published.
-
neutron_production–The accelerator, as an NXsource: the facility and the pulse reference.
-
pulse_source_topic–Where the per-pulse reference sample is published, for an unbound binder.
-
linked_log–A value a run sets, as an NXlog whose datasets link to an existing one.
-
axis–Where one driven axis's numbers come from.
-
as_motor–A knob as a Motor, whichever spelling it arrived in.
-
place–Put a node directly under NXinstrument, and say where it went.
-
positioner_name–What to call the positioner a motorised frame hangs from.
-
motor_log–An NXlog for a named run-time value that is not a positioner axis.
-
flow–The particle flow through the instrument, built once and kept.
-
neighbours–What feeds this node and what it feeds, under their emitted names.
-
stream_group–One monitor's or detector's data stream, as the instrument chose it.
-
depends_on–What a thing in
framehangs from, as a NeXus path.
source_contents
¶
What the NXsource holds: which facility, and where the pulse reference is.
Without the reference a top-dead-centre time cannot be used: a TDC timestamp is only meaningful against the pulse it is measured from, and every other timestamp in the file -- detector events, motor readbacks, chopper crossings -- has to share the same reference or none of them can be compared.
So one sample per pulse, of the proton charge on target in that pulse, timestamped
with the reference time itself. It is an NXlog named pulse_charge, in uC, which
is what ECDC calls it and what a reduction normalizes by; a binder that has a
binding for source.pulse_charge supplies its stream.
Source code in src/niess/nexus/structure.py
pulse_log
¶
pulse_log() -> dict
The per-pulse reference sample, as the binder says it is published.
Source code in src/niess/nexus/structure.py
neutron_production
¶
neutron_production() -> dict
The accelerator, as an NXsource: the facility and the pulse reference.
Under NXinstrument, which is the only place NXsource is allowed; a group of
this class directly under NXentry does not validate. NXsource extends
NXcomponent, so it is placed like one -- at the origin, since the accelerator is
not somewhere the instrument's frames reach.
Source code in src/niess/nexus/structure.py
pulse_source_topic
¶
Where the per-pulse reference sample is published, for an unbound binder.
Source code in src/niess/nexus/structure.py
linked_log
¶
A value a run sets, as an NXlog whose datasets link to an existing one.
A chopper's speed is not a number an instrument has; it is a knob, and what the
file should say is where to read it. niess.nexus decides this by folding a
McCode expression and seeing whether an instrument parameter survives. Here the
component names the knob it declared.
This builds an NXlog group whose datasets are link modules pointing into
an NXlog published elsewhere in the file -- deep links to the value and time
of one that already exists, rather than a link to the group itself. That is what
lets the group carry attributes of its own, which it must when it is part of an
NXtransformations chain and needs a transformation_type and a vector
the original has no reason to have.
Not to be confused with the filewriter's link module, which is what the
datasets inside it are; :func:niess.nexus.streams.link_specifier writes those.
.. note::
This assumes there is an NXlog elsewhere to point at -- somewhere an
f144 module is publishing the parameter. Where that is not true the group has
to be written directly instead, carrying its own value and time rather than
links to someone else's. Nothing in niess needs that yet, and when something
does this is where the choice belongs.
Source code in src/niess/nexus/structure.py
axis
¶
axis(owner, key: str, knob)
Where one driven axis's numbers come from.
Every translator asks this and no translator answers it. A jaw's edge and the
tank's a4 are the same question, and they used to be answered in two places --
_transformations read Motor.source directly while the aperture went through
streams -- so a mode switch could only ever have covered one of them.
Source code in src/niess/nexus/structure.py
as_motor
¶
A knob as a Motor, whichever spelling it arrived in.
place
¶
Put a node directly under NXinstrument, and say where it went.
For the things that are neither components nor children of one: an
NXpositioner for a motorised frame has to sit here, because NXcomponent
does not accept one and the frame it drives is an NXcomponent. NXinstrument
does accept one, which is the whole reason this can be this simple.
Deliberately not emit: that registers a name in the flow graph, and a
positioner is not in the beam.
Source code in src/niess/nexus/structure.py
positioner_name
¶
What to call the positioner a motorised frame hangs from.
The knob's name -- detector_tank_angle -- because that is the name a facility
gives the positioner and binds its streams to. Two frames may be turned by one
knob, and they cannot share a positioner because the chain each hangs from
differs, so the second one is named for its frame as well:
tank_mounting_detector_tank_angle.
Source code in src/niess/nexus/structure.py
motor_log
¶
An NXlog for a named run-time value that is not a positioner axis.
Chopper speed and delay come through here. They are knobs, but not motor
records: ESS spells a chopper as eight logs off a {root}:Spd_R-style root,
which is a different canonical shape from a positioner's three and is not yet
written. So these keep the simulation's own names in both modes.
Source code in src/niess/nexus/structure.py
flow
¶
The particle flow through the instrument, built once and kept.
McCode has no way to say a beam branches, which is why the route this replaced had to be handed the real flow. A niess instrument states it -- BIFROST's tank declares ten paths leaving the sample -- so it is read off the tree here.
Source code in src/niess/nexus/structure.py
neighbours
¶
What feeds this node and what it feeds, under their emitted names.
The graph is keyed on tree paths and the file is written in emitted names, so a node the conversion did not write cannot be named. Stepping over it rather than dropping it is what keeps the chain intact: a composite that only contains things, or a component that is a simulation device rather than part of the instrument, sits in the flow without appearing in the file, and what it joins stays joined.
Source code in src/niess/nexus/structure.py
stream_group
¶
One monitor's or detector's data stream, as the instrument chose it.
Source code in src/niess/nexus/structure.py
depends_on
¶
depends_on(frame) -> str
What a thing in frame hangs from, as a NeXus path.
The chain is the chain of frames the tree declares. Nothing here reads an emitted instrument to work out what depends on what.
Source code in src/niess/nexus/structure.py
stream
¶
A filewriter module directive -- ev44, da00, f144, link, ...
Source code in src/niess/nexus/nodes.py
load_bindings
¶
Read a facility bindings file, such as ECDC's bindings.yaml.
This is the file ECDC compiles, together with an instrument.yaml, into the NeXus
template the real instrument's filewriter uses. Each entry says which topic and
source fill one log. Reading it here lets a niess file name the same streams
without keeping a second copy of the answers.
Source code in src/niess/nexus/bindings.py
resolve_stream
¶
The stream group for a component, or None if it publishes nothing.
The protocol is never chosen here. Some monitors belong on da00 histograms
and some on ev44 events; which one is a property of the instrument setup, so
it is read from the instrument in priority order:
- a
METADATA "nexus_structure_stream_data"block on the component -- the escape hatch for instruments authored outside niess, emitted verbatim; - a
nexus_streamentry in the component's niess provenanceextra, which is how a niess component records the choice made when the instrument was built; default-- the component type's established behaviour, used only when the instrument expressed no preference at all.
A component with no selection and no default gets no stream group rather than a guessed one.
Source code in src/niess/nexus/streams.py
component_body
¶
component_body(nx_class: str, children=None, attrs=None, name=None, position=None, rotation=None) -> dict
What a translator returns: the class and contents of one component's group.
position and rotation state where the thing sits in the frame it hangs from,
for the composites that have no placement of their own -- an analyzer is at its
frame's origin, turned by the Bragg angle; a detector is a distance along its.
Anything with a position and an orientation of its own is placed by those.
Source code in src/niess/nexus/structure.py
emit
¶
emit(visit: Visit, body: dict) -> None
Put one component's group into the instrument.
Refuses a name already used directly under NXinstrument. Two groups with one name
would be one group in the file, silently: the second replaces or merges into the
first. The accelerator's NXsource is source, so a component of that name would
collide with it.
Source code in src/niess/nexus/structure.py
to_nexus_structure
¶
to_nexus_structure(instrument, registry=None, nxlog_root: str | None = None, streams=None, efu_monitors: bool = False) -> dict
Convert instrument to ESS NeXus Structure JSON.
streams says where a driven axis's numbers come from: SIMULATED (the
default) writes the simulation's own parameter names, REAL writes the EPICS
positioners the components declare. One tree, two files; see
:mod:niess.nexus.bindings.
efu_monitors describes every monitor that has a cbm channel by the stream its
EFU publishes -- for a simulation whose monitor rays are collected and replayed to
that EFU. Otherwise a monitor describes the histogram McStas produces.
Source code in src/niess/nexus/structure.py
translator
¶
Register a function returning a component body for one or more niess classes.
Source code in src/niess/nexus/structure.py
bifrost
¶
BIFROST's own NeXus translators, from the tree.
Instrument-specific, and kept off the shared registry: Triplet and Analyzer are niess
classes another instrument could reuse, and importing the NeXus target must not give
another instrument's detectors BIFROST's pixel numbering. Ask for BIFROST_REGISTRY:
from niess.nexus import to_nexus_structure
from niess.nexus.bifrost import BIFROST_REGISTRY
structure = to_nexus_structure(instrument, registry=BIFROST_REGISTRY)
Importing this module is what registers them, which is why the package __init__ does
not do it for you.
Functions:
-
bifrost_detector_source–Which triplet an ev44 message came from, as the source string ICD 01 uses.
-
icd_pixel–Pixel id per ICD 01 v6;
positionruns from 0 toresolution - 1. -
arc_and_triplet–Which arc and which triplet, from where the thing sits in the instrument.
-
register_bifrost–BIFROST's analyzers and detectors, read off the objects.
icd_pixel
¶
Pixel id per ICD 01 v6; position runs from 0 to resolution - 1.
arc_and_triplet
¶
Which arc and which triplet, from where the thing sits in the instrument.
niess.nexus matches a regex against a generated WHEN clause for this, because
an emitted McStas instrument is all it has. The tree knows: a triplet is the arm it
belongs to, in the channel that arm belongs to.
Source code in src/niess/nexus/bifrost.py
register_bifrost
¶
BIFROST's analyzers and detectors, read off the objects.
Source code in src/niess/nexus/bifrost.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |