Skip to content

DPC: the field map tracks the beam region, and the pass is cancellable - #146

Open
CSSFrancis wants to merge 2 commits into
mainfrom
fix/dpc-live-region
Open

DPC: the field map tracks the beam region, and the pass is cancellable#146
CSSFrancis wants to merge 2 commits into
mainfrom
fix/dpc-live-region

Conversation

@CSSFrancis

Copy link
Copy Markdown
Member

Dragging the beam region did nothing until the pass before it had run all the way through. Measured in the real app on a 48×48×64×64 lazy scan: zero map repaints across a 6.4 s drag.

Three separate causes. The first is the one that matters.

The map did not follow the region

_on_region_drag armed a debounce on pointer_up only, so the map was frozen for the whole gesture.

The region is now a real BaseSelector — the same CircleSelector/AnnularSelector a virtual image puts on the same plot — so it inherits the navigator's serial latest-wins dispatcher and its trailing settle. The re-measure hangs off index_hooks, the seam the vector overlays already use. This deletes more than it adds: the settle timer, the live timer, the pending flags and a paint gate all go.

The pass could not be stopped

It ran through signal.map, which has no interruption point anywhere inside it, so a superseded pass finished and only then had its result discarded.

It is now one dask graph and one client.compute future on _measure_future — the cancellation virtual_image.reduce already uses. Chunks paint into the display array as they land (compute_with_live_buffer, the virtual-image stream's own call), and the map blanks when a new pass starts, so superseding is visible rather than something to trust.

The pass was slow enough that all of this showed

The centre of mass ran a Python function per frame with a scipy.ndimage call inside it. At tens of thousands of frames that overhead is the runtime. It is now one contraction per block against stacked detector-sized weights:

before (hyperspy map + scipy) 1429.4 ms
after (one stacked einsum) 38.3 ms (37×)
the virtual image, for scale 15.5 ms

Stacking the three moments into one (3, sy, sx) weight array reads the block once instead of three times (73 → 38 ms). Bit-identical to the per-frame reference, which is what preserves pyxem equivalence — a test asserts array_equal frame by frame, not allclose.

Also fixed, found while testing the above

  • Opening the wizard fired two passes. Placing the selector writes its geometry, and the widget reports that write as a move — so an identical pass immediately superseded the opening one, discarding a whole scan's work and the progressive fill with it.
  • The brightness readout did a dask frame read on the event loop at pointer_up. It runs on a worker now and refreshes when a pass lands.
  • The beam region has no "off". It is what the centre of mass is taken over, so switching it off only meant taking the whole frame with no handle to grab.
  • The default radius went from a quarter of the short detector axis to half — the inscribed circle. With the region always on, the default is what an unattended scan is measured with, and the old one clipped the beam: on the synthetic disc it under-read true shifts of 1.5 / 3.0 / 4.5 px by 0.30 / 1.00 / 1.76 px. The inscribed circle reproduces the whole-frame answer exactly.

Verification

dpc_live_region.spec.ts pins the user-visible claim by sampling the map's own canvas pixels across a real drag: 0 changed frames on the old behaviour, 12–13 on this one.

Deliberately not a window screenshot — the "Calculating…" chip pulses on a CSS animation, and hashing that scored the broken build 22 of 24 and passed. If you touch that metric, read the comment on mapHash first.

test_dpc.py + test_dpc_action.py: 160 passed. The e2e specs have not been run against this branch.

Scope note

The units/commit_result_tree work that also touches these files is deliberately not here — it depends on commit.py and views.py changes that belong with the report-embed PR.

…llable

Dragging the beam region did nothing until the pass before it had run all
the way through. Measured in the real app on a 48x48x64x64 lazy scan: ZERO
map repaints across a 6.4 s drag.

Three separate causes, and the first is the one that matters.

THE MAP DID NOT FOLLOW THE REGION. `_on_region_drag` armed a debounce on
`pointer_up` only, so the map was frozen for the whole gesture. The region
is now a real BaseSelector — the same CircleSelector/AnnularSelector a
virtual image puts on the same plot — so it inherits the navigator's serial
latest-wins dispatcher and its trailing settle instead of a hand-rolled
debounce. The re-measure hangs off `index_hooks`, the seam the vector
overlays already use. That is what "it should be identical to the virtual
image" actually means, and it deletes more than it adds: the settle timer,
the live timer, the pending flags and the paint gate are all gone.

THE PASS COULD NOT BE STOPPED. It ran through `signal.map`, which has no
interruption point anywhere inside it, so a superseded pass finished and
only THEN had its result discarded. It is now one dask graph and one
`client.compute` future held on `_measure_future` — the cancellation
`virtual_image.reduce` already uses. Chunks paint into the display array as
they land (`compute_with_live_buffer`, the virtual-image stream's own call),
and the map BLANKS when a new pass starts, so superseding is something you
can see rather than something you have to trust.

THE PASS WAS SLOW ENOUGH THAT ALL OF THIS SHOWED. The centre of mass ran a
Python function per frame with a `scipy.ndimage` call inside it; at tens of
thousands of frames that overhead IS the runtime. It is now one contraction
per block against stacked detector-sized weights:

  64x64 scan of 64x64 frames    before   1429.4 ms
                                after      38.3 ms   (37x)
  the virtual image, for scale             15.5 ms

Stacking the three moments into one `(3, sy, sx)` weight array reads the
block ONCE instead of three times (73 ms -> 38 ms). Bit-identical to the
per-frame reference, which is what keeps pyxem equivalence: a test asserts
`array_equal` frame by frame rather than `allclose`.

Also fixed, found while testing the above:

  - Opening the wizard fired TWO passes. Placing the selector writes its
    geometry, and the widget reports that write as a move, so an identical
    pass immediately superseded the opening one — a whole scan's work and
    the progressive fill with it. `measure` now records the region it ran
    with and a move to that same geometry does not re-measure.
  - The brightness readout did a dask frame read on the event loop at
    `pointer_up`. It runs on a worker now and refreshes when a pass lands.
  - The beam region has no "off". It IS what the centre of mass is taken
    over, so switching it off only meant taking the whole frame with no
    handle to grab.
  - The default radius went from a quarter of the short detector axis to
    HALF — the inscribed circle. With the region always on, the default is
    what an unattended scan is measured with, and the old one CLIPPED the
    beam: on the synthetic disc it under-read true shifts of 1.5/3.0/4.5 px
    by 0.30/1.00/1.76 px. The inscribed circle reproduces the whole-frame
    answer exactly.

`dpc_live_region.spec.ts` pins the user-visible claim by sampling the map's
own CANVAS pixels across a real drag: 0 changed frames on the old
behaviour, 12-13 on this one. Deliberately not a window screenshot — the
"Calculating…" chip pulses on a CSS animation, and hashing that scored the
BROKEN build 22 of 24 and passed.
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