Skip to content

loki: load the detector once and read it by reference - #165

Merged
SimonHeybrock merged 2 commits into
mainfrom
24-detector-by-reference
Nov 9, 2026
Merged

SimonHeybrock merged 2 commits into
mainfrom
24-detector-by-reference

Conversation

@SimonHeybrock

Copy link
Copy Markdown
Member

Part of #24. Motivated by interactive masking tools such as scipp/ess#834, which raised the question of how a small output, such as a mask or a beam centre, is shown on the large detector it belongs to.

What changes

MASKS and BEAM_CENTRE each returned a full detector image so that a plot could draw them. Now a new spec loads the detector once, and the other specs read it by reference and return only their small outputs:

detector = client.compute(CALIBRATED_DETECTOR, params={'run': file(run=60339)})
masks = client.compute(MASKS, params={'detector': detector.ref('detector'), 'mask_files': [...]})
centre = client.compute(BEAM_CENTRE, params={'detector': detector.ref('detector'), 'masks': masks.ref('masks')})
angles = client.compute(CREATE_MASK, params={'detector': detector.ref('detector'), 'beam_centre': centre.ref('centre'), 'base': masks.ref('masks'), 'shapes': [{'start': 0.0, 'stop': 1.0}]})

CREATE_MASK is a new example of such a spec. It masks ranges of the angle to the beam and adds to other masks.

A configured plot is now a Plot, and inputs names the referenced values it draws on:

Plot(masks_on_image, inputs=('detector',))

Why

  • A record's request names the detector its outputs were computed from, and a record never changes. So nothing has to keep a mask and its detector in step, and no identifier needs adding to outputs.
  • The image is about 100 MB at 3.2M pixels. Inside each record, a hosted app would read it for every trial of a mask. By reference, it reads the small output for each trial and the detector once.
  • These specs take no file of the run, because a file could differ from the detector. So the detector carries detector numbers and the sample and source positions.
  • A mask from one run may still be applied to another run. Masks hold for an experiment and one centre can serve a sample changer, so nothing enforces that the fields of a request share a detector.

Alternatives not taken

  • An "interactive" workflow type. A person using a tool is a controller, like a notebook, and a workflow that waits for clicks would produce outputs that its request does not determine.
  • Having a tool write a mask file as the main route. The file source does not see a file saved mid-session, and a file's identity is its path, so saving again changes what older records point to. Files remain the way to reuse a mask across proposals.
  • An identifier on each small output, which would repeat what provenance already records and would have to be filled in by each workflow author.

Behaviour to review

  • If a detector is released, the view of a record that read it fails and says what to do. Computing the detector again makes a new record, so the record that read it has to be run again too.
  • find_beam_centre refuses a detector with no counts outside the masks. The esssans finder loops forever on that input, and one typed angle range can now mask every pixel.
  • Two mask files with the same file name are refused. One mask used to replace the other without a message.
  • MASKS and BEAM_CENTRE stay at version 1 although their params changed, since nothing depends on the old form.

Known gaps

  • A detector picker shows DetectorImage (09:08), without the run, and only a hex prefix within the same minute. A proper fix changes output labels for every spec, which What a UI needs from the client: records that fit a type, and short record labels #95 tracks in general.
  • apply and Rule work on file fields, so per-run masks or centres need a hand-written chain. Automatic reduction with a per-run centre would need to persist a detector per run.
  • find_beam_centre adds a length-1 dimension with sc.concat, because the esssans finder otherwise expects events. A small change in esssans would remove it.
  • The cross in centre_on_image is relative to the sample, so it is misplaced if the sample has a transverse offset. This is unchanged, and the Larmor test files have the sample at the origin.

Review

Three independent reviews ran before this PR: scientific correctness, design and lifetimes, and test strength by mutation. The new bindings give the same counts, masks and centres as the old ones on run 60339, and the angle matches esssans' two_theta with gravity off. The reviews found the finder loop and the lost mask above, and 14 mutations that no test caught. The most relevant test gaps are now closed, such as measuring the angle from the sample rather than the origin.

Test plan

  • essspec and essinstruments tests pass, the latter on real LoKI files. essapps passes except test_the_slider_reads_only_the_cut_it_draws, which fails the same way without this change in my environment.
  • The code of loki-reduction.ipynb ran to the end.
  • In the LoKI app, run detector, masks, beam centre and create mask in order, and check the Detector image, Masks on image and Centre on image views.
  • Remove the detector in the Outputs tab, and check that the message on Masks on image says what to do.

🤖 Generated with Claude Code

SimonHeybrock and others added 2 commits November 9, 2026 08:55
Masks and the beam centre each returned a full detector image, so that a
plot could draw them. On a hosted service that image would be read for
every trial of a mask, and every record held its own.

CALIBRATED_DETECTOR now loads the detector once. MASKS and BEAM_CENTRE
take it as Referenced[DetectorImage] and return only their small outputs.
The record's request names the detector its outputs were computed from,
so nothing else has to keep the two in step. These specs take no file of
the run, which could differ from the detector, so the detector carries
the detector numbers and the sample and source positions they need.

CREATE_MASK is a new example of such a spec: it masks ranges of the angle
to the beam, and adds to other masks.

A configured plot is now a Plot, whose inputs name the referenced values
it draws on. A plot whose input is no longer readable shows why as text;
the person reruns the detector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
esssans' finder raises its cutoff until pixels are left, so with every
pixel masked or no counts it never returns. One typed angle range can
now mask the whole detector, so the binding refuses this.

Two mask files with one name no longer lose a mask without a word. The
detector has its own view. A plot whose input has gone names the input
and says what to do: computing it again makes a new record, which the
plotted record does not reference. Reading a plot's inputs needs no
widget library, so it moves next to Plot. scippnexus is imported
directly, so it is declared.

Adds the tests that mutating the code showed were missing, such as the
angle being measured from the sample rather than the origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SimonHeybrock SimonHeybrock self-assigned this Nov 9, 2026
@SimonHeybrock
SimonHeybrock merged commit e1aa8a7 into main Nov 9, 2026
@SimonHeybrock
SimonHeybrock deleted the 24-detector-by-reference branch November 9, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant