diff --git a/python/lsst/ts/externalscripts/base_parameter_march.py b/python/lsst/ts/externalscripts/base_parameter_march.py index 3e3ec7046..290e4bf79 100644 --- a/python/lsst/ts/externalscripts/base_parameter_march.py +++ b/python/lsst/ts/externalscripts/base_parameter_march.py @@ -60,6 +60,7 @@ def __init__(self, index, descr="Perform a parameter march.") -> None: self.ocps = None self.config = None + self.dofs = np.zeros(50) self.total_offset = 0.0 self.iterations_started = False @@ -102,6 +103,16 @@ def get_schema(cls) -> dict: description: Configuration for BaseParameterMarch. type: object properties: + az: + description: Azimuth position to point to. + type: number + minimum: 0 + maximum: 360 + el: + description: Elevation position to point to. + type: number + minimum: 0 + maximum: 90 filter: description: Filter name or ID; if omitted the filter is not changed. anyOf: @@ -174,11 +185,11 @@ def get_schema(cls) -> dict: type: string oneOf: - required: - - dof + - dofs - range - n_steps - required: - - dof + - dofs - step_sequence additionalProperties: false """ @@ -224,10 +235,9 @@ async def configure(self, config: types.SimpleNamespace) -> None: config.range = config.step_sequence[-1] - config.step_sequence[0] config.n_steps = len(config.step_sequence) elif hasattr(config, "range"): - config.step_sequence = [ - -config.range + 2 * i * config.range / (config.n_steps - 1) - for i in range(config.n_steps) - ] + config.step_sequence = np.linspace( + -config.range, config.range, config.n_steps + ).tolist() if hasattr(config, "rotation_sequence"): if isinstance(config.rotation_sequence, (int, float)): @@ -242,6 +252,7 @@ async def configure(self, config: types.SimpleNamespace) -> None: raise TypeError("rotation_sequence must be either a number or a list.") self.config = config + self.dofs = np.array(config.dofs) await super().configure(config=config) @@ -324,8 +335,8 @@ async def parameter_march(self) -> None: start_position = self.config.step_sequence[0] - offset_values = start_position * self.config.dofs - cam_hex, m2_hex, m1m3_bend, m2_bend = self.format_values(offset_values) + offset_values = start_position * self.dofs + cam_hex, m2_hex, m1m3_bend, m2_bend = await self.format_values(offset_values) await self.checkpoint( f"Step 1/{self.config.n_steps} starting positions:\n" @@ -337,7 +348,7 @@ async def parameter_march(self) -> None: self.log.info("Offset dofs to starting position.") # Apply dof vector with offset - offset_dof_data = self.tcs.rem.mtaos.cmd_offsetDOF.DataType() + offset_dof_data = await self.tcs.rem.mtaos.cmd_offsetDOF.DataType() for i, dof_offset in enumerate(offset_values): offset_dof_data.value[i] = dof_offset await self.tcs.rem.mtaos.cmd_offsetDOF.start(data=offset_dof_data) @@ -361,8 +372,8 @@ async def parameter_march(self) -> None: ) # Apply dof vector with offset - offset_dof_data = self.tcs.rem.mtaos.cmd_offsetDOF.DataType() - for i, dof_offset in enumerate(self.config.dofs * offset): + offset_dof_data = await self.tcs.rem.mtaos.cmd_offsetDOF.DataType() + for i, dof_offset in enumerate(self.dofs * offset): offset_dof_data.value[i] = dof_offset await self.tcs.rem.mtaos.cmd_offsetDOF.start(data=offset_dof_data) @@ -421,11 +432,11 @@ async def cleanup(self): f"Returning telescope to original position by moving " f"{self.total_offset} back along dofs vector {self.config.dofs}." ) - offset_dof_data = self.tcs.rem.mtaos.cmd_offsetDOF.DataType() - for i, dof_offset in enumerate(self.config.dofs * -self.total_offset): + offset_dof_data = await self.tcs.rem.mtaos.cmd_offsetDOF.DataType() + for i, dof_offset in enumerate(self.dofs * -self.total_offset): offset_dof_data.value[i] = dof_offset await self.tcs.rem.mtaos.cmd_offsetDOF.start(data=offset_dof_data) except Exception: self.log.exception( - "Error while trying to return hexapod to its original position." + "Error while trying to return telescope to its original position." ) diff --git a/python/lsst/ts/externalscripts/maintel/parameter_march_comcam.py b/python/lsst/ts/externalscripts/maintel/parameter_march_comcam.py index e3f39d1d4..a5be97d1b 100644 --- a/python/lsst/ts/externalscripts/maintel/parameter_march_comcam.py +++ b/python/lsst/ts/externalscripts/maintel/parameter_march_comcam.py @@ -49,6 +49,8 @@ def __init__(self, index, descr="Perform a parameter march with ComCam.") -> Non self.mtcs = None self.comcam = None + self.dz = 1500 # microns, offset for out-of-focus images + self.instrument_name = "LSSTComCam" @property @@ -95,13 +97,14 @@ async def take_images( self.log.info("Taking intra-focal image") + print(self.config) intra_visit_id = await self.camera.take_cwfs( exptime=self.config.exp_time, n=1, group_id=supplemented_group_id, - filter=self.filter, + filter=self.config.filter, reason="INTRA" + ("" if self.reason is None else f"_{self.reason}"), - program=self.program, + program=self.config.program, ) self.log.debug("Moving to extra-focal position") @@ -117,9 +120,9 @@ async def take_images( exptime=self.config.exp_time, n=1, group_id=supplemented_group_id, - filter=self.filter, + filter=self.config.filter, reason="EXTRA" + ("" if self.reason is None else f"_{self.reason}"), - program=self.program, + program=self.config.program, ) self.log.info("Send processing request to RA OCPS.") @@ -144,9 +147,9 @@ async def take_images( exptime=self.config.exp_time, n=1, group_id=self.group_id, - filter=self.filter, + filter=self.config.filter, reason="INFOCUS" + ("" if self.reason is None else f"_{self.reason}"), - program=self.program, + program=self.config.program, ) try: diff --git a/tests/maintel/test_maintel_parameter_march_comcam.py b/tests/maintel/test_maintel_parameter_march_comcam.py index e371b99ab..2f8ea7a19 100644 --- a/tests/maintel/test_maintel_parameter_march_comcam.py +++ b/tests/maintel/test_maintel_parameter_march_comcam.py @@ -61,6 +61,14 @@ def mock_mtcs(self): } ) + self.script.format_values = unittest.mock.AsyncMock() + self.script.format_values.return_value = ( + [f"+{i*0.1:.2f} um" for i in range(5)], + [f"+{i*0.1:.2f} arcsec" for i in range(5, 10)], + [f"+{i*0.1:.2f} um" for i in range(10, 30)], + [f"+{i*0.1:.2f} um" for i in range(30, 50)], + ) + def mock_camera(self): """Mock camera instance and its methods.""" self.script.comcam = mock.AsyncMock() @@ -77,49 +85,48 @@ def mock_ocps(self): async def test_configure(self): config = { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, "dofs": [1] * 50, "rotation_sequence": 50, "range": 1, "n_steps": 11, + "program": "BLOCK-TXXX", } async with self.make_script(): await self.configure_script(**config) + assert self.script.config.az == 35 + assert self.script.config.el == 15 assert self.script.config.filter == "g" assert self.script.config.exp_time == 30.0 assert np.array_equal(self.script.config.dofs, [1] * 50) assert self.script.config.rotation_sequence == [50] * 11 - assert self.script.config.step_sequence == [ - -1, - -0.8, - -0.6, - -0.4, - -0.2, - 0, - 0.2, - 0.4, - 0.6, - 0.8, - 1, - ] + assert self.script.config.step_sequence == np.linspace(-1, 1, 11).tolist() assert self.script.config.range == 1 assert self.script.config.n_steps == 11 + assert self.script.config.program == "BLOCK-TXXX" async def test_configure_step_and_rotation_sequence(self): config = { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], + "program": "BLOCK-TXXX", } async with self.make_script(): await self.configure_script(**config) + assert self.script.config.az == 35 + assert self.script.config.el == 15 assert self.script.config.filter == "g" assert self.script.config.exp_time == 30.0 assert np.array_equal(self.script.config.dofs, [1] * 50) @@ -127,15 +134,19 @@ async def test_configure_step_and_rotation_sequence(self): assert self.script.config.n_steps == 5 assert self.script.config.step_sequence == [0, 100, 200, 300, 400] assert self.script.config.rotation_sequence == [10, 20, 30, 40, 50] + assert self.script.config.program == "BLOCK-TXXX" async def test_configure_ignore(self): config = { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], "ignore": ["mtm1m3", "mtrotator"], + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -145,6 +156,8 @@ async def test_configure_ignore(self): await self.configure_script(**config) + assert self.script.config.az == 35 + assert self.script.config.el == 15 assert self.script.config.filter == "g" assert self.script.config.exp_time == 30.0 assert np.array_equal(self.script.config.dofs, [1] * 50) @@ -152,6 +165,7 @@ async def test_configure_ignore(self): assert self.script.config.n_steps == 5 assert self.script.config.step_sequence == [0, 100, 200, 300, 400] assert self.script.config.rotation_sequence == [10, 20, 30, 40, 50] + assert self.script.config.program == "BLOCK-TXXX" # Verify that the ignored components are correctly set to False assert not self.script.mtcs.check.mtm1m3 @@ -160,14 +174,18 @@ async def test_configure_ignore(self): async def test_invalid_configuration(self): bad_configs = [ { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, - "dofs": np.ones(51), + "dofs": [1] * 51, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], "ignore": ["mtm1m3", "mtrotator"], }, { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, "dofs": [1] * 50, @@ -184,11 +202,14 @@ async def test_invalid_configuration(self): async def test_parameter_march(self): config = { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -198,23 +219,29 @@ async def test_parameter_march(self): # Check if the hexapod moved the expected number of times assert ( self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.call_count - == config["n_steps"] + 1 + == self.script.config.n_steps + 1 ) # Check if the camera took the expected number of images - assert self.script.comcam.take_acq.call_count == config["n_steps"] + assert self.script.comcam.take_acq.call_count == self.script.config.n_steps # Check if the OCPS command was called - self.script.ocps.cmd_execute.set_start.assert_called_once() + assert ( + self.script.ocps.cmd_execute.set_start.call_count + == self.script.config.n_steps + ) - async def test_focus_sweep_sim_mode(self): + async def test_parameter_march_sim_mode(self): config = { + "az": 35, + "el": 15, "filter": "g", "exp_time": 30.0, "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], "sim": True, + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -224,14 +251,17 @@ async def test_focus_sweep_sim_mode(self): # Check if the hexapod moved the expected number of times assert ( self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.call_count - == config["n_steps"] + 1 + == self.script.config.n_steps + 1 ) # Check if the camera took the expected number of images - assert self.script.comcam.take_acq.call_count == config["n_steps"] + assert self.script.comcam.take_acq.call_count == self.script.config.n_steps # Check if the OCPS command was called - self.script.ocps.cmd_execute.set_start.assert_called_once() + assert ( + self.script.ocps.cmd_execute.set_start.call_count + == self.script.config.n_steps + ) # Verify that simulation mode is set correctly assert self.script.comcam.simulation_mode @@ -243,12 +273,13 @@ async def test_cleanup(self): "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], + "program": "BLOCK-TXXX", } async with self.make_script(): await self.configure_script(**config) - # Simulate an error during the focus sweep to trigger cleanup + # Simulate an error during the parameter march to trigger cleanup self.script.iterations_started = True self.script.total_offset = 400 # Simulate some offset with mock.patch.object( @@ -260,13 +291,13 @@ async def test_cleanup(self): await self.script.cleanup() # Ensure the hexapod is returned to the original position - offset_dof_data = self.tcs.rem.mtaos.cmd_offsetDOF.DataType() + offset_dof_data = await self.script.mtcs.rem.mtaos.cmd_offsetDOF.DataType() for i, dof_offset in enumerate( - self.script.config.dofs * -self.script.total_offset + self.script.dofs * -self.script.total_offset ): offset_dof_data.value[i] = dof_offset self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.assert_any_call( - self.script.total_offset + data=offset_dof_data ) async def test_executable(self): diff --git a/tests/maintel/test_maintel_parameter_march_lsstcam.py b/tests/maintel/test_maintel_parameter_march_lsstcam.py index 6256dd12c..3f65a6ba9 100644 --- a/tests/maintel/test_maintel_parameter_march_lsstcam.py +++ b/tests/maintel/test_maintel_parameter_march_lsstcam.py @@ -63,6 +63,14 @@ def mock_mtcs(self): } ) + self.script.format_values = unittest.mock.AsyncMock() + self.script.format_values.return_value = ( + [f"+{i*0.1:.2f} um" for i in range(5)], + [f"+{i*0.1:.2f} arcsec" for i in range(5, 10)], + [f"+{i*0.1:.2f} um" for i in range(10, 30)], + [f"+{i*0.1:.2f} um" for i in range(30, 50)], + ) + def mock_camera(self): """Mock camera instance and its methods.""" self.script.lsstcam = mock.AsyncMock() @@ -84,6 +92,7 @@ async def test_configure(self): "rotation_sequence": 50, "range": 1, "n_steps": 11, + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -93,21 +102,10 @@ async def test_configure(self): assert self.script.config.exp_time == 30.0 assert np.array_equal(self.script.config.dofs, [1] * 50) assert self.script.config.rotation_sequence == [50] * 11 - assert self.script.config.step_sequence == [ - -1, - -0.8, - -0.6, - -0.4, - -0.2, - 0, - 0.2, - 0.4, - 0.6, - 0.8, - 1, - ] + assert self.script.config.step_sequence == np.linspace(-1, 1, 11).tolist() assert self.script.config.range == 1 assert self.script.config.n_steps == 11 + assert self.script.config.program == "BLOCK-TXXX" async def test_configure_step_and_rotation_sequence(self): config = { @@ -116,6 +114,7 @@ async def test_configure_step_and_rotation_sequence(self): "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -128,6 +127,7 @@ async def test_configure_step_and_rotation_sequence(self): assert self.script.config.n_steps == 5 assert self.script.config.step_sequence == [0, 100, 200, 300, 400] assert self.script.config.rotation_sequence == [10, 20, 30, 40, 50] + assert self.script.config.program == "BLOCK-TXXX" async def test_configure_ignore(self): config = { @@ -137,6 +137,7 @@ async def test_configure_ignore(self): "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], "ignore": ["mtm1m3", "mtrotator"], + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -153,6 +154,7 @@ async def test_configure_ignore(self): assert self.script.config.n_steps == 5 assert self.script.config.step_sequence == [0, 100, 200, 300, 400] assert self.script.config.rotation_sequence == [10, 20, 30, 40, 50] + assert self.script.config.program == "BLOCK-TXXX" # Verify that the ignored components are correctly set to False assert not self.script.mtcs.check.mtm1m3 @@ -173,7 +175,7 @@ async def test_invalid_configuration(self): "exp_time": 30.0, "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50, 55], - "step_sequence": [0, 100, 200, 300, 400], + "range": 1, "ignore": ["mtm1m3", "mtrotator"], }, ] @@ -190,6 +192,7 @@ async def test_parameter_march(self): "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -199,16 +202,16 @@ async def test_parameter_march(self): # Check if the hexapod moved the expected number of times assert ( self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.call_count - == config["n_steps"] + 1 + == self.script.config.n_steps + 1 ) # Check if the camera took the expected number of images - assert self.script.lsstcam.take_acq.call_count == config["n_steps"] + assert self.script.lsstcam.take_acq.call_count == self.script.config.n_steps # Check if the OCPS command was called - self.script.ocps.cmd_execute.set_start.assert_called_once() + assert self.script.ocps.cmd_execute.set_start.call_count == 0 - async def test_focus_sweep_sim_mode(self): + async def test_parameter_march_sim_mode(self): config = { "filter": "g", "exp_time": 30.0, @@ -216,6 +219,7 @@ async def test_focus_sweep_sim_mode(self): "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], "sim": True, + "program": "BLOCK-TXXX", } async with self.make_script(): @@ -225,14 +229,14 @@ async def test_focus_sweep_sim_mode(self): # Check if the hexapod moved the expected number of times assert ( self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.call_count - == config["n_steps"] + 1 + == self.script.config.n_steps + 1 ) # Check if the camera took the expected number of images - assert self.script.lsstcam.take_acq.call_count == config["n_steps"] + assert self.script.lsstcam.take_acq.call_count == self.script.config.n_steps # Check if the OCPS command was called - self.script.ocps.cmd_execute.set_start.assert_called_once() + assert self.script.ocps.cmd_execute.set_start.call_count == 0 # Verify that simulation mode is set correctly assert self.script.lsstcam.simulation_mode @@ -244,12 +248,13 @@ async def test_cleanup(self): "dofs": [1] * 50, "rotation_sequence": [10, 20, 30, 40, 50], "step_sequence": [0, 100, 200, 300, 400], + "program": "BLOCK-TXXX", } async with self.make_script(): await self.configure_script(**config) - # Simulate an error during the focus sweep to trigger cleanup + # Simulate an error during the parameter march to trigger cleanup self.script.iterations_started = True self.script.total_offset = 400 # Simulate some offset with mock.patch.object( @@ -261,13 +266,13 @@ async def test_cleanup(self): await self.script.cleanup() # Ensure the hexapod is returned to the original position - offset_dof_data = self.tcs.rem.mtaos.cmd_offsetDOF.DataType() + offset_dof_data = await self.script.mtcs.rem.mtaos.cmd_offsetDOF.DataType() for i, dof_offset in enumerate( self.script.config.dofs * -self.script.total_offset ): offset_dof_data.value[i] = dof_offset self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.assert_any_call( - self.script.total_offset + data=offset_dof_data ) async def test_executable(self):