Skip to content

Convert an instrument

An instrument is a tree, and every target reads the same walk over it. This is what that looks like from the outside.

Say what the instrument is

An Instrument names its pieces and says what each hangs from.

from niess.bifrost import Primary, Tank
from niess.bifrost.parameters import primary_parameters, tank_parameters
from niess.instrument import Instrument, Mount

bifrost = Instrument(
    name='bifrost',
    origin='sample_origin',
    parts=(
        Mount(name='primary', content=Primary.from_calibration(primary_parameters())),
        # the tank is described in coordinates about the sample, so it hangs there
        Mount(name='tank', content=Tank.from_calibration(tank_parameters()),
              relative_to='sample_origin'),
    ),
)

BIFROST's primary is described in global coordinates, so it hangs from nothing. Its tank is described about the sample — the first analyzer sits at [1.189, 0, 0], not 162 m down the guide — so it hangs from sample_origin, which the primary provides.

origin names the component everything is measured against. Targets need it, and saying it once here is better than each of them guessing.

A piece can be turned at run time

Mount(..., rotation=(0, a4, 0)) turns a piece about the frame it hangs from, in degrees. The three angles may be numbers or InstrumentParameters, because the interesting ones are set per run: a BIFROST run turns the sample by sample_rotation (a3) and the detector tank by detector_tank_angle (a4). McStas emits an Arm turned by the named parameter; NeXus emits a transformation linking to that parameter's NXlog.

Convert it

from niess.mccode import to_mccode

instrument = to_mccode(bifrost)          # an mccode_antlr Instr
from niess.nexus import to_nexus_structure
from niess.nexus.bifrost import BIFROST_REGISTRY

structure = to_nexus_structure(bifrost, registry=BIFROST_REGISTRY)

Instrument-specific translators are opt-in per conversion. BIFROST_REGISTRY gives its analyzers and detectors their ICD pixel numbering; without it they fall back to the generic classification, which places them correctly but does not classify them.

from niess.tof import chopper_specs

specs = chopper_specs(bifrost, origin=0.05)

origin is where the tof.Source sits, which belongs to the model rather than the instrument: an ESS source in tof carries a 0.05 m facility offset while the niess moderator is at the instrument origin.

from niess.brep import save_step

save_step(bifrost, 'bifrost.step')

Ask the instrument about itself

The tree answers questions an emitted instrument cannot.

Where the beam branches. McCode describes an instrument as a list, so the only flow it can express is declaration order. BIFROST's tank branches:

graph = bifrost.to_graph()
branches = list(graph.successors('primary/sample_origin'))

Ten paths leave the sample — nine channels and the elastic monitor — and a neutron takes one. That is what NeXus states through each group's inputs and outputs datasets.

What a thing will be called, and what it is measured from.

from niess.walk import visits

where = {v.id: v for v in visits(bifrost)}
analyzer = where['tank/channels[2]/pairs[0]/analyzer']

analyzer.emit_name('monochromator') is channel_3_1_monochromator, and its frame is the arm's own analyzer_point. A translator asks rather than rebuilding the name from an f-string, which is how the same name used to get written in two places and drift.

Converting an instrument niess did not build

You cannot, and that is deliberate. Every target reads the tree, and a .instr file has none.

There used to be a second route per target, recovering what it could from an assembled instrument — placement resolved back out of the emitted text, run-time values recovered by constant-folding DECLARE blocks, a detector's arc and triplet read off a generated WHEN clause with a regular expression. It worked, and it meant every target carried two implementations of itself forever, one of which could only ever be a reconstruction.

Reading a .instr is still supported and still useful: niess.io.mccode.load_instr parses one so its placements can be inspected, which is what Translate a McStas .instr does to check a niess submodule against the hand-written file it replaces. That is the migration story, and it survives. Converting one is not.