From ec45bf37299640b4f65e0c34362c796c169bada5 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Thu, 25 Jun 2026 16:39:06 +0200 Subject: [PATCH] daq: send commanded dtz (not live readback) to JFJoch/DB + wait for arrival The detector_distance_mm sent to JFJoch and written to the DB came from the live detector readback (diffraction.dtz_mm) captured before the detector finished moving, so a scan requested at e.g. 100 mm could be recorded at the old position (e.g. 800 mm). - JFJoch + DB now read the validated request target (request.dtz) instead of the live diffraction.dtz_mm; diffraction.dtz_mm stays live for the local-contact readout. - jfjoch: raise if request.dtz is unset (setup_datacollection must validate it first) instead of silently falling back to the live readback. - raster + rotation services: wait for the detector to reach request.dtz (set_dtz wait=True) right before triggering the scan, since the sa2dc move is non-blocking. (Raster setup-before-measure reorder landed earlier in daq.py.) - tests: request.dtz now required in jfjoch/aaredb tests; xtal-snapshot exposure assertion updated to 1 ms. Co-Authored-By: Claude Opus 4.8 --- src/aare/daq/aaredb.py | 4 ++-- src/aare/daq/operations/raster/service.py | 5 +++++ src/aare/daq/operations/rotation/service.py | 9 ++++++++- src/aare/devices/jfjoch.py | 8 ++++++-- tests/unit/daq/test_aaredb.py | 10 +++++----- tests/unit/daq/test_workflows.py | 4 ++-- tests/unit/devices/test_jfjoch.py | 8 +++++--- 7 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/aare/daq/aaredb.py b/src/aare/daq/aaredb.py index ba515fbe..035aa750 100644 --- a/src/aare/daq/aaredb.py +++ b/src/aare/daq/aaredb.py @@ -202,7 +202,7 @@ class AareWrapper: model=d.diffraction.detector_description, type="photon-counting", serialNumber=d.diffraction.detector_serial_number, - detectorDistance_mm=d.diffraction.dtz_mm, + detectorDistance_mm=r.dtz, resolution_at_edge_Ang=d.diffraction.max_resolution_angstrom, beamCenterX_px=d.diffraction.beam_center_pxl[0], beamCenterY_px=d.diffraction.beam_center_pxl[1], @@ -268,7 +268,7 @@ class AareWrapper: model=d.diffraction.detector_description, type="photon-counting", serialNumber=d.diffraction.detector_serial_number, - detectorDistance_mm=round(d.diffraction.dtz_mm,3), + detectorDistance_mm=round(r.dtz,3), resolution_at_edge_Ang=round(d.diffraction.max_resolution_angstrom,3), beamCenterX_px=round(d.diffraction.beam_center_pxl[0],2), beamCenterY_px=round(d.diffraction.beam_center_pxl[1],2), diff --git a/src/aare/daq/operations/raster/service.py b/src/aare/daq/operations/raster/service.py index 3573f926..8b5804b3 100644 --- a/src/aare/daq/operations/raster/service.py +++ b/src/aare/daq/operations/raster/service.py @@ -317,6 +317,11 @@ class RasterService: self.logger.debug( f"Starting grid scan with {request.n_x}x{request.n_y} points, exp time {request.exp_time_s}s" ) + if not self.ctx.deps.cfg.simulated_detector: + # The detector move (sa2dc) is non-blocking; ensure it has reached + # the requested distance before triggering the scan, so data is + # collected at the dtz already reported to JFJoch/DB. + self.ctx.deps.devs.set_dtz(request.dtz, wait=True) self.ctx.deps.devs.aerotech.grid_scan( grid_elem_size_y_um=request.grid_size_mm.y * 1000, grid_elem_size_x_um=request.grid_size_mm.x * 1000, diff --git a/src/aare/daq/operations/rotation/service.py b/src/aare/daq/operations/rotation/service.py index 64f540ed..76d8590b 100644 --- a/src/aare/daq/operations/rotation/service.py +++ b/src/aare/daq/operations/rotation/service.py @@ -41,6 +41,10 @@ class RotationService: if not self.ctx.deps.cfg.simulated_detector: self.ctx.deps.jfjoch.wait_till_running(timeout=60.0) + # The detector move (sa2dc) is non-blocking; ensure it has reached the + # requested distance before triggering the scan, so data is collected + # at the dtz already reported to JFJoch/DB. + self.ctx.deps.devs.set_dtz(request.dtz, wait=True) if request.screening: self.ctx.deps.devs.aerotech.screening_scan( @@ -84,8 +88,11 @@ class RotationService: def run(self, request: RotationScanRequest) -> CompletedRotationScan: sample = self.ctx.sample - self._prepare_detector(request) + # Validate + set the requested dtz (cfg.dtz, requires request.dtz) BEFORE + # configuring JFJoch, so JFJoch receives the commanded target rather than + # the live, still-in-transit detector position. self.ctx.services.datacollection.prepare(request) + self._prepare_detector(request) if sample is not None and sample.db_id is not None and self.ctx.services.events is not None: self.ctx.services.events.send(sample.db_id, SampleEventType.COLLECTING) diff --git a/src/aare/devices/jfjoch.py b/src/aare/devices/jfjoch.py index 56873201..6faf332b 100644 --- a/src/aare/devices/jfjoch.py +++ b/src/aare/devices/jfjoch.py @@ -125,14 +125,18 @@ class JFJochWrapper: wedge = None images = r.steps trigger = 1 - + if r.dtz is None: + raise ValueError( + "Scan request has no detector distance (dtz); setup_datacollection " + "must validate and set request.dtz before JFJoch is configured" + ) #build common dataset settings dataset_settings = jfjoch_client.DatasetSettings( beam_x_pxl=s.diffraction.beam_center_pxl[0], beam_y_pxl=s.diffraction.beam_center_pxl[1], ntrigger= trigger, images_per_trigger=images, - detector_distance_mm=s.diffraction.dtz_mm, + detector_distance_mm=r.dtz, file_prefix=f"{data_folder}/{r.file_prefix}", incident_energy_keV=s.diffraction.energy_keV, sample_name=sample, diff --git a/tests/unit/daq/test_aaredb.py b/tests/unit/daq/test_aaredb.py index c659060c..39d69c4a 100644 --- a/tests/unit/daq/test_aaredb.py +++ b/tests/unit/daq/test_aaredb.py @@ -157,7 +157,7 @@ def test_upload_jpg(mock_post, mock_api, mock_bl): def test_create_rotation_run(mock_sample, mock_api, mock_bl, sample_info, daq_status): wrapper = AareWrapper(bl=mock_bl) # Standard rotation - req = RotationScanRequest(exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix") + req = RotationScanRequest(exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix", dtz=200.0) wrapper.create_rotation_run(sample_info, req, daq_status) mock_sample.return_value.create_experiment_parameters_for_sample.assert_called_once() @@ -170,7 +170,7 @@ def test_create_rotation_run(mock_sample, mock_api, mock_bl, sample_info, daq_st @patch("aareDB.SamplesRunnerApi") def test_create_rotation_run_screening(mock_sample, mock_api, mock_bl, sample_info, daq_status): wrapper = AareWrapper(bl=mock_bl) - req = RotationScanRequest(exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix", screening=True, wedge_omega_deg=5.0) + req = RotationScanRequest(exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix", screening=True, wedge_omega_deg=5.0, dtz=200.0) wrapper.create_rotation_run(sample_info, req, daq_status) mock_sample.return_value.create_experiment_parameters_for_sample.assert_called_once() @@ -178,7 +178,7 @@ def test_create_rotation_run_screening(mock_sample, mock_api, mock_bl, sample_in @patch("aareDB.SamplesRunnerApi") def test_create_rotation_run_error(mock_sample, mock_api, mock_bl, sample_info, daq_status, caplog): wrapper = AareWrapper(bl=mock_bl) - req = RotationScanRequest(exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix") + req = RotationScanRequest(exp_time_s=0.1, incr_omega_deg=0.1, steps=100, file_prefix="test_prefix", dtz=200.0) mock_sample.return_value.create_experiment_parameters_for_sample.side_effect = Exception("Rotation error") wrapper.create_rotation_run(sample_info, req, daq_status) assert "Rotation error" in caplog.text @@ -187,7 +187,7 @@ def test_create_rotation_run_error(mock_sample, mock_api, mock_bl, sample_info, @patch("aareDB.SamplesRunnerApi") def test_create_gridscan_run(mock_sample, mock_api, mock_bl, sample_info, daq_status): wrapper = AareWrapper(bl=mock_bl) - req = RasterGridRequest(exp_time_s=0.1, n_x=10, n_y=10, grid_size_mm=Coordinate(x=0.01, y=0.01), smargon_top_left=None, file_prefix="test_grid_prefix") + req = RasterGridRequest(exp_time_s=0.1, n_x=10, n_y=10, grid_size_mm=Coordinate(x=0.01, y=0.01), smargon_top_left=None, file_prefix="test_grid_prefix", dtz=200.0) wrapper.create_gridscan_run(sample_info, req, daq_status) mock_sample.return_value.create_experiment_parameters_for_sample.assert_called_once() @@ -200,7 +200,7 @@ def test_create_gridscan_run(mock_sample, mock_api, mock_bl, sample_info, daq_st @patch("aareDB.SamplesRunnerApi") def test_create_gridscan_run_error(mock_sample, mock_api, mock_bl, sample_info, daq_status, caplog): wrapper = AareWrapper(bl=mock_bl) - req = RasterGridRequest(exp_time_s=0.1, n_x=10, n_y=10, grid_size_mm=Coordinate(x=0.01, y=0.01), smargon_top_left=None, file_prefix="test_grid_prefix") + req = RasterGridRequest(exp_time_s=0.1, n_x=10, n_y=10, grid_size_mm=Coordinate(x=0.01, y=0.01), smargon_top_left=None, file_prefix="test_grid_prefix", dtz=200.0) mock_sample.return_value.create_experiment_parameters_for_sample.side_effect = Exception("Grid error") wrapper.create_gridscan_run(sample_info, req, daq_status) assert "Grid error" in caplog.text diff --git a/tests/unit/daq/test_workflows.py b/tests/unit/daq/test_workflows.py index 38f61f8d..7d0c5eb3 100644 --- a/tests/unit/daq/test_workflows.py +++ b/tests/unit/daq/test_workflows.py @@ -118,7 +118,7 @@ def test_sa2xtal_snapshot(mock_devs, mock_cfg): mock_devs.bec_worker.move_to = MagicMock() sa2xtal_snapshot(mock_devs, mock_cfg) - assert mock_devs.samcam_settings.exposure == 0.0005 + assert mock_devs.samcam_settings.exposure == 0.001 _assert_bec_moved(mock_devs, BeamlineState.XTAL_SNAPSHOT) @@ -128,7 +128,7 @@ def test_dc2xtal_snapshot(mock_devs, mock_cfg): mock_devs.bec_worker.move_to = MagicMock() dc2xtal_snapshot(mock_devs, mock_cfg) - assert mock_devs.samcam_settings.exposure == 0.0005 + assert mock_devs.samcam_settings.exposure == 0.001 _assert_bec_moved(mock_devs, BeamlineState.XTAL_SNAPSHOT) diff --git a/tests/unit/devices/test_jfjoch.py b/tests/unit/devices/test_jfjoch.py index 722cbcb0..c6d2a9e2 100644 --- a/tests/unit/devices/test_jfjoch.py +++ b/tests/unit/devices/test_jfjoch.py @@ -121,7 +121,8 @@ def test_measure_rotation(jfjoch_wrapper, mock_jfjoch_client): incr_omega_deg=0.1, steps=100, exp_time_s=0.01, - file_prefix="test_prefix" + file_prefix="test_prefix", + dtz=200.0, ) s = create_mock_daq_status() s.sample = SampleShortInfo(sample_name="S1", user="p12345", db_id=1, dewar_name="D1", puck_name="P1", run_number=1, pin=1) @@ -132,7 +133,7 @@ def test_measure_rotation(jfjoch_wrapper, mock_jfjoch_client): def test_measure_rotation_error(jfjoch_wrapper, mock_jfjoch_client): mock_jfjoch_client.start_post.side_effect = Exception("API Error") - r = RotationScanRequest(exp_time_s=0.01, incr_omega_deg=0.1, steps=10) + r = RotationScanRequest(exp_time_s=0.01, incr_omega_deg=0.1, steps=10, dtz=200.0) s = create_mock_daq_status() s.sample = SampleShortInfo(sample_name="S1", user="p12345", db_id=1, dewar_name="D1", puck_name="P1", run_number=1, pin=1) @@ -147,7 +148,8 @@ def test_measure_raster(jfjoch_wrapper, mock_jfjoch_client): grid_size_mm=Coordinate(x=0.01, y=0.01), exp_time_s=0.01, file_prefix="raster_prefix", - smargon_top_left=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0)) + smargon_top_left=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0)), + dtz=200.0, ) s = create_mock_daq_status() s.sample = SampleShortInfo(sample_name="S1", user="p12345", db_id=1, dewar_name="D1", puck_name="P1", run_number=1, pin=1)