Translate a McStas .instr¶
You have a working McStas model and want it as a niess.{instrument} submodule.
niess-scaffold does the mechanical part; this guide is about the part it cannot do.
That reads the instrument, resolves every placement, maps the components it recognises
onto niess classes, wraps the rest in Opaque,
checks that everything still lands where the .instr puts it, and writes a package you
own and edit.
What you gain, and what it costs¶
You gain calibration: quantities keep the units they were measured in, positions are chained instead of repeated, and the same object emits McStas, NeXus and CAD. You also gain a place to record things McStas has no concept of, such as which Kafka topic a monitor publishes on.
The cost is real. The .instr file stops being the source of truth. If someone edits
it directly afterwards, the change is lost the next time the module is built. Decide
that up front.
What the tool does not do¶
It converts a small fraction of a real instrument. The mapping is deliberately
limited to cases where the McStas component type determines the niess class. For
ESS_IN5_reprate that is 8 components of 50; the other 42 become Opaque. This is the
honest state of niess's component library rather than a limitation of the converter, and
the conversion report is a prioritised list of what is missing.
It cannot recognise a composite. BIFROST's 45 analyzers are one
Monochromator_Rowland each in the emitted file, and nothing can reconstruct
Analyzer(blades=...) from that. Recognising that several components are one device is
the work you are here to do.
It cannot group. You get one flat Section, because which components belong together
is a statement about the instrument.
It cannot recover units or intent. Everything is metres and degrees, because that is
all a .instr carries.
What you get¶
src/niess/my_instrument/
__init__.py the conversion report, as the module docstring
structure.py a Section: every component, typed, in beam order
parameters.py the calibration, chained the way the .instr chained it
instrument.py instrument() -> Instrument
test_placement.py the check that everything is still where it was
my_instrument.instr the file it came from
Then do this, roughly in order¶
1. Replace Opaque components, most frequent first¶
Each Opaque is a component that emits McStas faithfully and tells no other target
anything. Replacing one with a real niess class is what makes NeXus and CAD able to say
something about it. Work down the report:
Look for a fit in the component reference. If nothing fits,
writing a component is three methods — see
Writing a component. If the type is one
a general tool should always map the same way, add a recipe to
niess/scaffold/recipes.py instead, and every future conversion gets it.
2. Group into sections, in beam order¶
Contiguous components that do one job become a Section, which emits as a %include
rather than more lines in the top-level TRACE. Section field order must be beam order,
because Section.from_calibration constructs positionally; the calibration dictionary is
looked up by name, so its key order is yours to choose.
3. Re-express the numbers in the units they were measured in¶
scalar(1.5, unit='m') becomes 1500 * mm if that is what the drawing says. Conversion
happens exactly once, at the __mccode__ boundary, so nothing downstream cares.
4. Decide what should be run-time again¶
The report names the instrument parameters that drove geometry:
Instrument parameters that drove geometry and are now constants.
The geometry no longer follows them:
GUI_start = 2
TT = 50
These are written into parameters.py as named constants, so the relationships the author
expressed survive as Python — a component placed AT (0, 0, GUI_start) still reads
vector([0.0, 0.0, GUI_start], unit='m'). What is lost is that they are knobs. A
detector tank angle really should be run-time: make it a Motor on a Mount
(see niess.bifrost's
sample_rotation/detector_tank_angle, a3 and a4). A guide start distance really is fixed geometry: leave it a constant.
Keep the test¶
test_placement.py asserts that every component in the original .instr is at the same
absolute position in what the module emits. It is the only thing that will tell you if
step 1, 2 or 3 moved something by a millimetre. It is not a text diff, and should not
become one:
Do not diff the generated text against the original — it will never match, and should not: different names, different ordering, added metadata.
Two traps worth knowing about, because the tool hit both
A component's own reference point. A niess DiscChopper's position is its
spindle; the emitted AT is where the beam crosses the disc. So the generated
parameters.py keeps two things apart: at[...] is where the .instr put each
component and is what the chain is built from, while a chopper's position is
at[...] plus the offset to its spindle. Chaining off the spindle instead would carry
that offset into every component downstream.
Instr.resolve_orientations() is not a safe reference. It loses the
degrees-to-radians conversion on a symbolic rotation angle, so an instrument placed
through ROTATED (0, TT, 0) resolves to sin(TT) with TT in degrees. niess.scaffold
measures both sides with its own placements for that reason, and
check_against_mccode refuses to use McStas as a reference where it cannot be trusted.
Doing it by hand¶
Nothing here requires the CLI. niess.scaffold is an ordinary module:
from niess.io.mccode import load_instr
from niess.scaffold import convert, summary, to_instrument, write
instr = load_instr('my_instrument.instr')
conversion = convert(instr, origin='sample')
print(summary(conversion))
instrument = to_instrument(conversion) # the Instrument, with no source generated
write(conversion, 'src/niess') # or the module
Reading an instrument without converting it at all is one call, which is how to take stock before deciding whether to convert at all:
from niess.io.mccode import load_instr
instrument = load_instr(INSTR)
for instance in instrument.components:
at, at_relative_to = instance.at_relative
print(f'{instance.name:16s} {instance.type.name:20s} '
f'AT {tuple(str(x) for x in at)} '
f'RELATIVE {at_relative_to.name if at_relative_to else "ABSOLUTE"}')
print('run-time parameters:', [p.name for p in instrument.parameters])
And a submodule you wrote by hand deserves the same check the generated
test_placement.py does, against the file it replaces:
from niess.instrument import Instrument, Mount
from niess.io.mccode import load_instr
from niess.mccode import to_mccode
from niess.teaching import Primary
# the hand-written file, read as it is -- there is no tree for it yet
original = load_instr(INSTR)
# and the submodule that is meant to replace it
translated = to_mccode(Instrument(
name='teaching', origin='sample_origin',
parts=(Mount(name='primary', content=Primary.from_calibration()),)))
# resolve_orientations() gives each component's absolute placement
before = original.resolve_orientations()
after = translated.resolve_orientations()
for name in (c.name for c in original.components):
assert name in after, f'{name} missing from the translation'
assert before[name].position() == after[name].position(), name
resolve_orientations() is used there rather than niess.scaffold's own placements
because the teaching instrument has no rotations at all. For anything that does, use
niess.scaffold.verify.compare, for the reason in the warning above.
Checklist¶
- Conversion report read, and the
Opaquelist understood as the work queue - Every
Opaqueeither replaced, or deliberately left - Components grouped into
Sections in beam order - Numbers re-expressed in the units of the drawing
- Frozen parameters either promoted back to run-time or accepted as fixed
-
test_placement.pystill passing, and committed - The original
.instrkept as a fixture