pytweezer.coordinators.rearrangement module

Atom-rearrangement coordinator.

Ports the standalone rearrangement_node_server.py (a ZMQ REP server that held its own camera and talked to the SLM over another socket) into a composite-device Coordinator. The camera and SLM are now sub-devices of the same process, so this coordinator drives them with direct in-process calls: the GPU-computed phase sequence goes straight to slm.update_mask with no frame ever serialized onto a socket.

Roles in the composite’s "devices" block:

  • slm — a pytweezer.drivers.slm.SLM (update_mask). Required.

  • camera — an ImagEM-X2-style camera (setup_acquisition, set_roi, enable_em_gain, acquire_n_frames, …). Optional: without it the coordinator constructs SLM-only and initialise() raises, but the phase sequence generation and upload path is fully usable. Benchmarks that only time frame delivery run this way.

Lifecycle over RPC (all synchronous; each stalls the server for its duration, per the coordinator contract):

  • initialise() — build the GPU phasemask generator, configure the camera, precompute the initial array. Takes the two trap parameter sets (data1/ data2, shape (4, N): w, phi, x, y).

  • arm_rearrangement() — grab an image, extract the occupancy mask, then generate the interpolated phase sequence (OptimisationBasedPhasemaskGeneratorGPU.iter_rearrangement_sequence) and upload it to the SLM concurrently, then grab a reset image. Returns the before/after images.

  • test() / status() / shutdown().

Generation and upload are pipelined: the GPU loop pushes each finished frame onto a bounded queue and a writer thread copies it to the host and DMAs it to the board, so synthesis of frame n+1 overlaps the upload of frame n. The queue depth (UPLOAD_QUEUE_DEPTH) bounds how far the GPU may run ahead.

cupy/lap and the heavy GPU math are imported lazily, so this module imports and status() works on any machine; initialise()/arm_rearrangement() raise a clear error where the GPU stack is absent.

The remaining timing optimisation — preloading frames into the SLM’s on-board memory and clocking them out with a hardware trigger (preload_sequence / start_auto_increment) instead of per-frame software writes — is not wired up: arm_rearrangement() still plays the sequence software-timed through run_sequence, and nothing yet arms the SLM’s external trigger or clocks the frames.

pytweezer.coordinators.rearrangement.DEFAULT_PHASEMASK = {'blaze_dx_dy_um': (48, -4), 'focal_length_mm': 17.3, 'fresnel_f_mm': 1072, 'input_beam_waist_mm': 16, 'slm_pitch_um': 17, 'slm_res': (1024, 1024), 'wavelength_um': 0.852, 'zernike_coeff_dict': {5: 1.195, 6: 0.725, 7: 0.97, 8: 0.478, 9: -1.091, 10: 0.303, 11: 0.021, 12: 0.072, 13: 0.049}}

Default phasemask-generator geometry (the lab’s Rb SLM); overridable via config.

pytweezer.coordinators.rearrangement.DEFAULT_ROI = [50, 70, 384, 384]

Default camera ROI (x0, y0, width, height) if initialise isn’t given one.

class pytweezer.coordinators.rearrangement.Rearrangement(targets, conf)[source]

Bases: Coordinator

Camera + SLM rearrangement loop, run entirely in one process.

arm_rearrangement()[source]

Run one rearrangement and return the (before, after) camera images.

Loads the initial array, grabs an occupancy image, then generates the interpolated phase sequence and uploads it to the SLM concurrently - each frame goes to the board as soon as the GPU produces it - and finally grabs a reset image. Timing breakdown is logged.

Generation and upload overlap, so they are timed together; splitting them would only measure where the pipeline happened to stall.

camera: ImagEMX2Camera | None
camera_role = 'camera'
get_slm_temperature()[source]
Return type:

float

initialise(data1, data2, array_shape1, array_shape2, d0, fps, threshold, grid_positions, roi=None, profile='minimum_jerk')[source]

Build the phasemask generator, configure the camera, precompute masks.

data1/data2 are (4, N) arrays of trap parameters (w, phi, x, y) for the initial and target arrays. Everything GPU-side is kept on the device between here and arm_rearrangement().

profile picks the transport trajectory used by every subsequent arm_rearrangement(): "minimum_jerk" (smoother on the atoms) or "linear", which needs 1.875x fewer frames for the same d0 and so completes the move in proportionally less time.

Parameters:
Return type:

None

last_first_frame_at

time.perf_counter() at which the most recent _play_sequence_pipelined() finished displaying its first frame, or None if it never did. Frame 0 must be synthesised, copied to the host and DMA’d before anything reaches the panel, so this splits a pipelined run into time-to-first-frame and the move proper.

shutdown()[source]

Release rearrangement state. The camera/SLM backends close themselves.

Return type:

None

slm: SLM
slm_role = 'slm'
status()[source]

Report readiness. Works with or without the GPU stack.

Return type:

dict

test(delay_s=0.0)[source]

Round-trip smoke test: return two random (before, after) images.

No GPU needed — exercises the RPC path and image marshalling only.

Parameters:

delay_s (float)

pytweezer.coordinators.rearrangement.UPLOAD_QUEUE_DEPTH = 5

How many generated frames may queue ahead of the SLM before the GPU loop blocks.

pytweezer.coordinators.rearrangement.USE_SUM_CPP = True

Set False to force the numpy occupancy path instead of the C++ extension.