diff --git a/doc/news/DM-47363.feature.rst b/doc/news/DM-47363.feature.rst new file mode 100644 index 000000000..d6669a1ac --- /dev/null +++ b/doc/news/DM-47363.feature.rst @@ -0,0 +1 @@ +Add new `set_dof.py`` to set absolute DOF position \ No newline at end of file diff --git a/python/lsst/ts/standardscripts/data/scripts/maintel/set_dof.py b/python/lsst/ts/standardscripts/data/scripts/maintel/set_dof.py new file mode 100755 index 000000000..f3c48b07d --- /dev/null +++ b/python/lsst/ts/standardscripts/data/scripts/maintel/set_dof.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# This file is part of ts_standardscripts +# +# Developed for the LSST Telescope and Site Systems. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +import asyncio + +from lsst.ts.standardscripts.maintel import SetDOF + +asyncio.run(SetDOF.amain()) diff --git a/python/lsst/ts/standardscripts/maintel/__init__.py b/python/lsst/ts/standardscripts/maintel/__init__.py index 9017f90d7..27c01a834 100644 --- a/python/lsst/ts/standardscripts/maintel/__init__.py +++ b/python/lsst/ts/standardscripts/maintel/__init__.py @@ -39,6 +39,7 @@ from .offset_mtcs import * from .open_mirror_covers import * from .point_azel import * +from .set_dof import * from .setup_mtcs import * from .standby_comcam import * from .standby_mtcs import * diff --git a/python/lsst/ts/standardscripts/maintel/set_dof.py b/python/lsst/ts/standardscripts/maintel/set_dof.py new file mode 100644 index 000000000..d574dc055 --- /dev/null +++ b/python/lsst/ts/standardscripts/maintel/set_dof.py @@ -0,0 +1,461 @@ +# This file is part of ts_standardscripts +# +# Developed for the LSST Telescope and Site Systems. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +__all__ = ["SetDOF"] + +import typing + +import numpy as np +import yaml +from lsst.ts import salobj +from lsst.ts.observatory.control.maintel.mtcs import MTCS +from lsst.ts.observatory.control.utils.enums import DOFName + +STD_TIMEOUT = 30 + +class SetDOF(salobj.BaseScript): + """Set absolute positions DOF to the main telescope, either bending + mode or hexapod position. + + Parameters + ---------- + index : `int` + Index of Script SAL component. + + Notes + ----- + **Checkpoints** + "Setting DOF..." - The DOF absolute position is being applied. + + """ + + def __init__(self, index) -> None: + super().__init__( + index=index, + descr="Set absolute degrees of freedom position of the main telescope.", + ) + + # Create the MTCS object + self.mtcs = None + + # Create the DOF vector + self.dofs = np.zeros(len(DOFName)) + + async def configure_tcs(self) -> None: + """Handle creating MTCS object and waiting for remote to start.""" + if self.mtcs is None: + self.log.debug("Creating MTCS.") + self.mtcs = MTCS( + domain=self.domain, + log=self.log, + ) + await self.mtcs.start_task + else: + self.log.debug("MTCS already defined, skipping.") + + @classmethod + def get_schema(cls) -> typing.Dict[str, typing.Any]: + schema_yaml = """ + $schema: http://json-schema.org/draft-07/schema# + $id: https://github.com/lsst-ts/ts_standardscripts/maintel/SetDOF.yaml + title: SetDOF v1 + description: Configuration for SetDOF Script. + type: object + properties: + dofs: + type: array + description: >- + Defines a 50-dimensional vector for all DOFs, combining M2, + Camera, M1M3, and M2 bending modes. This overrides individual DOF inputs. + First 5 elements for M2, next 5 for Camera, next 20 for M1M3 bending modes, + last 20 for M2 bending modes. Units: microns or arcsec. + items: + type: number + minItems: 50 + maxItems: 50 + M2_dz: + type: number + description: >- + Defines the offset applied to the M2 hexapod in the z direction. + Units in um. + default: 0.0 + M2_dx: + type: number + description: >- + Defines the offset applied to the M2 hexapod in the x direction. + Units in um. + default: 0.0 + M2_dy: + type: number + description: >- + Defines the offset applied to the M2 hexapod in the y direction. + Units in um. + default: 0.0 + M2_rx: + type: number + description: >- + Defines the offset applied to the M2 hexapod in rx. + Units in arcsec. + default: 0.0 + M2_ry: + type: number + description: >- + Defines the offset applied to the M2 hexapod in ry. + Units in arcsec. + default: 0.0 + Cam_dz: + type: number + description: >- + Defines the offset applied to the Camera hexapod in + the z direction. Units in um. + default: 0.0 + Cam_dx: + type: number + description: >- + Defines the offset applied to the Camera hexapod + in the x direction. Units in um. + default: 0.0 + Cam_dy: + type: number + description: >- + Defines the offset applied to the Camera hexapod in + the y direction. Units in um. + default: 0.0 + Cam_rx: + type: number + description: >- + Defines the offset applied to the Camera hexapod in rx. + Units in arcsec. + default: 0.0 + Cam_ry: + type: number + description: >- + Defines the offset applied to the Camera hexapod in ry. + Units in arcsec. + default: 0.0 + M1M3_B1: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 1. + Units in um. + default: 0.0 + M1M3_B2: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 2. + Units in um. + default: 0.0 + M1M3_B3: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 3. + Units in um. + default: 0.0 + M1M3_B4: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 4. + Units in um. + default: 0.0 + M1M3_B5: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 5. + Units in um. + default: 0.0 + M1M3_B6: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 6. + Units in um. + default: 0.0 + M1M3_B7: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 7. + Units in um. + default: 0.0 + M1M3_B8: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 8. + Units in um. + default: 0.0 + M1M3_B9: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 9. + Units in um. + default: 0.0 + M1M3_B10: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 10. + Units in um. + default: 0.0 + M1M3_B11: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 11. + Units in um. + default: 0.0 + M1M3_B12: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 12. + Units in um. + default: 0.0 + M1M3_B13: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 13. + Units in um. + default: 0.0 + M1M3_B14: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 14. + Units in um. + default: 0.0 + M1M3_B15: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 15. + Units in um. + default: 0.0 + M1M3_B16: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 16. + Units in um. + default: 0.0 + M1M3_B17: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 17. + Units in um. + default: 0.0 + M1M3_B18: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 18. + Units in um. + default: 0.0 + M1M3_B19: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 19. + Units in um. + default: 0.0 + M1M3_B20: + type: number + description: >- + Defines the offset applied to the M1M3 bending mode 20. + Units in um. + default: 0.0 + M2_B1: + type: number + description: >- + Defines the offset applied to the M2 bending mode 1. + Units in um. + default: 0.0 + M2_B2: + type: number + description: >- + Defines the offset applied to the M2 bending mode 2. + Units in um. + default: 0.0 + M2_B3: + type: number + description: >- + Defines the offset applied to the M2 bending mode 3. + Units in um. + default: 0.0 + M2_B4: + type: number + description: >- + Defines the offset applied to the M2 bending mode 4. + Units in um. + default: 0.0 + M2_B5: + type: number + description: >- + Defines the offset applied to the M2 bending mode 5. + Units in um. + default: 0.0 + M2_B6: + type: number + description: >- + Defines the offset applied to the M2 bending mode 6. + Units in um. + default: 0.0 + M2_B7: + type: number + description: >- + Defines the offset applied to the M2 bending mode 7. + Units in um. + default: 0.0 + M2_B8: + type: number + description: >- + Defines the offset applied to the M2 bending mode 8. + Units in um. + default: 0.0 + M2_B9: + type: number + description: >- + Defines the offset applied to the M2 bending mode 9. + Units in um. + default: 0.0 + M2_B10: + type: number + description: >- + Defines the offset applied to the M2 bending mode 10. + Units in um. + default: 0.0 + M2_B11: + type: number + description: >- + Defines the offset applied to the M2 bending mode 11. + Units in um. + default: 0.0 + M2_B12: + type: number + description: >- + Defines the offset applied to the M2 bending mode 12. + Units in um. + default: 0.0 + M2_B13: + type: number + description: >- + Defines the offset applied to the M2 bending mode 13. + Units in um. + default: 0.0 + M2_B14: + type: number + description: >- + Defines the offset applied to the M2 bending mode 14. + Units in um. + default: 0.0 + M2_B15: + type: number + description: >- + Defines the offset applied to the M2 bending mode 15. + Units in um. + default: 0.0 + M2_B16: + type: number + description: >- + Defines the offset applied to the M2 bending mode 16. + Units in um. + default: 0.0 + M2_B17: + type: number + description: >- + Defines the offset applied to the M2 bending mode 17. + Units in um. + default: 0.0 + M2_B18: + type: number + description: >- + Defines the offset applied to the M2 bending mode 18. + Units in um. + default: 0.0 + M2_B19: + type: number + description: >- + Defines the offset applied to the M2 bending mode 19. + Units in um. + default: 0.0 + M2_B20: + type: number + description: >- + Defines the offset applied to the M2 bending mode 20. + Units in um. + default: 0.0 + ignore: + description: >- + CSCs from the group to ignore in status check. Name must + match those in self.group.components, e.g.; hexapod_1. + type: array + items: + type: string + additionalProperties: false + """ + return yaml.safe_load(schema_yaml) + + async def configure(self, config) -> None: + """Configure script. + + Parameters + ---------- + config : `types.SimpleNamespace` + Script configuration, as defined by `schema`. + """ + + # Configure tcs and camera + await self.configure_tcs() + + if hasattr(config, "dofs"): + self.dofs = config.dofs + else: + # Loop through properties and assign their values to the vector + for key, value in vars(config).items(): + self.dofs[getattr(DOFName, key)] = value + + for comp in getattr(config, "ignore", []): + if comp not in self.mtcs.components_attr: + self.log.warning( + f"Component {comp} not in CSC Group. " + f"Must be one of {self.mtcs.components_attr}. Ignoring." + ) + else: + self.log.debug(f"Ignoring component {comp}.") + setattr(self.mtcs.check, comp, False) + + def set_metadata(self, metadata) -> None: + """Set script metadata. + + Parameters + ---------- + metadata : `lsst.ts.salobj.base.ScriptMetadata` + Script metadata. + """ + metadata.duration = 10 + + async def assert_feasibility(self) -> None: + """Verify that the telescope is in a feasible state to + execute the script. + """ + + await self.mtcs.assert_all_enabled() + + async def run(self) -> None: + """Run script.""" + # Assert feasibility + await self.assert_feasibility() + + await self.checkpoint("Setting DOF...") + current_dof = await self.mtcs.rem.mtaos.evt_degreeOfFreedom.aget(timeout=STD_TIMEOUT) + dof_data = self.mtcs.rem.mtaos.cmd_offsetDOF.DataType() + for i, dof_absolute in enumerate(self.dofs): + dof_data.value[i] = dof_absolute - current_dof.aggregatedDoF[i] + await self.mtcs.rem.mtaos.cmd_offsetDOF.start(data=offset_dof_data) diff --git a/tests/test_maintel_set_dof.py b/tests/test_maintel_set_dof.py new file mode 100644 index 000000000..bac695c37 --- /dev/null +++ b/tests/test_maintel_set_dof.py @@ -0,0 +1,115 @@ +# This file is part of ts_standardscripts +# +# Developed for the LSST Telescope and Site Systems. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +import random +import types +import unittest + +import numpy as np +from lsst.ts import standardscripts +from lsst.ts.observatory.control.maintel.mtcs import MTCS, MTCSUsages +from lsst.ts.observatory.control.utils.enums import DOFName +from lsst.ts.standardscripts.maintel import SetDOF + +random.seed(47) # for set_random_lsst_dds_partition_prefix + + +class TestSetDOF( + standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase +): + async def basic_make_script(self, index): + self.script = SetDOF(index=index) + + # Mock the MTCS + self.script.mtcs = MTCS( + domain=self.script.domain, + intended_usage=MTCSUsages.DryTest, + log=self.script.log, + ) + self.script.mtcs.rem.mtaos = unittest.mock.AsyncMock() + self.script.mtcs.rem.mtaos.cmd_offsetDOF.attach_mock( + unittest.mock.Mock( + return_value=types.SimpleNamespace(value=np.zeros(len(DOFName))) + ), + "DataType", + ) + self.script.mtcs.rem.mtaos.configure_mock( + **{ + "evt_degreeOfFreedom.aget": self.get_current_dof, + } + ) + + self.script.mtcs.assert_all_enabled = unittest.mock.AsyncMock() + + return (self.script,) + + def get_current_dof(): + return np.zeros(len(DOFName)) + + async def test_configure(self) -> None: + # Try configure with minimum set of parameters declared + async with self.make_script(): + config_dofs = {"M2_dz": 0.2, "Cam_dy": 0.3, "M1M3_B1": 0.5, "M2_B14": 0.7} + + await self.configure_script(**config_dofs) + + dofs = np.zeros(len(DOFName)) + for key, value in config_dofs.items(): + dofs[getattr(DOFName, key)] = value + + assert all(self.script.dofs == dofs) + + async def test_configure_with_dofs_vector(self) -> None: + async with self.make_script(): + dofs = [0] * len(DOFName) + config_dofs = { + "M2_dz": 0.2, + "Cam_dy": 0.3, + "M1M3_B1": 0.5, + "M2_B14": 0.7, + "dofs": dofs, + } + + await self.configure_script(**config_dofs) + + assert self.script.dofs == dofs + + async def test_run(self) -> None: + # Start the test itself + async with self.make_script(): + config_dofs = {"M2_dz": 0.2, "Cam_dy": 0.3, "M1M3_B1": 0.5, "M2_B14": 0.7} + + await self.configure_script(**config_dofs) + + # Run the script + await self.run_script() + + self.script.mtcs.rem.mtaos.cmd_offsetDOF.DataType.assert_called() + self.script.mtcs.rem.mtaos.cmd_offsetDOF.start.assert_awaited_once() + + async def test_executable(self) -> None: + scripts_dir = standardscripts.get_scripts_dir() + script_path = scripts_dir / "maintel" / "apply_dof.py" + await self.check_executable(script_path) + + +if __name__ == "__main__": + unittest.main()