diff --git a/doc/news/DM-41081.feature.rst b/doc/news/DM-41081.feature.rst new file mode 100644 index 00000000..01573dff --- /dev/null +++ b/doc/news/DM-41081.feature.rst @@ -0,0 +1 @@ +* Add new ``maintel/mtrotator/move_rotator.py`` SAL Script. \ No newline at end of file diff --git a/python/lsst/ts/standardscripts/data/scripts/maintel/mtrotator/move_rotator.py b/python/lsst/ts/standardscripts/data/scripts/maintel/mtrotator/move_rotator.py new file mode 100755 index 00000000..bbe388ee --- /dev/null +++ b/python/lsst/ts/standardscripts/data/scripts/maintel/mtrotator/move_rotator.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 . + +import asyncio + +from lsst.ts.standardscripts.maintel.mtrotator import MoveRotator + +asyncio.run(MoveRotator.amain()) diff --git a/python/lsst/ts/standardscripts/maintel/mtrotator/__init__.py b/python/lsst/ts/standardscripts/maintel/mtrotator/__init__.py new file mode 100644 index 00000000..b18a2f0b --- /dev/null +++ b/python/lsst/ts/standardscripts/maintel/mtrotator/__init__.py @@ -0,0 +1,22 @@ +# 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 . + +from .move_rotator import MoveRotator diff --git a/python/lsst/ts/standardscripts/maintel/mtrotator/move_rotator.py b/python/lsst/ts/standardscripts/maintel/mtrotator/move_rotator.py new file mode 100644 index 00000000..0c10b4ad --- /dev/null +++ b/python/lsst/ts/standardscripts/maintel/mtrotator/move_rotator.py @@ -0,0 +1,141 @@ +# 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 . + +__all__ = ["MoveRotator"] + +import yaml +from lsst.ts.observatory.control.maintel.mtcs import MTCS + +from ...base_block_script import BaseBlockScript + + +class MoveRotator(BaseBlockScript): + """Move the rotator to a given angle. It has the option of completing the + script before the rotator reaches the desired angle. + + Parameters + ---------- + index : `int` + Index of Script SAL component. + + Notes + ----- + **Checkpoints** + + - "Start moving rotator to {angle} degrees.": Start moving rotator. + - "Stop script and keep rotator moving.": Stop script. + - "Rotator reached {angle} degrees.": Rotator reached angle. + + """ + + def __init__(self, index: int) -> None: + super().__init__(index=index, descr="Move Rotator") + + self.mtcs = None + + self.rotator_velocity = 3.5 # degrees per second + self.short_timeout = 10 # seconds + self.long_timeout = 120 # seconds + + @classmethod + def get_schema(cls): + url = "https://github.com/lsst-ts/" + path = ( + "ts_standardscripts/blob/main/python/lsst/ts/standardscripts/" + "maintel/mtrotator/move_rotator.py" + ) + schema_yaml = f""" + $schema: http://json-schema.org/draft-07/schema# + $id: {url}{path} + title: MoveRotator v1 + description: Configuration for Maintel move rotator SAL Script. + type: object + properties: + angle: + description: final angle of the rotator. + type: number + minimum: -90 + maximum: 90 + wait_for_complete: + description: >- + whether wait for the rotator to reach the desired angle or + complete the script before the rotator reaches the desired + angle. + type: boolean + default: true + required: + - angle + additionalProperties: false + """ + schema_dict = yaml.safe_load(schema_yaml) + + base_schema_dict = super().get_schema() + + for properties in base_schema_dict["properties"]: + schema_dict["properties"][properties] = base_schema_dict["properties"][ + properties + ] + + return schema_dict + + async def configure(self, config): + """ + Configure the script. + + Parameters + ---------- + config : `dict` + Dictionary containing the configuration parameters. + """ + await self.configure_tcs() + + self.target_angle = config.angle + self.wait_for_complete = config.wait_for_complete + + await super().configure(config=config) + + 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.") + + def set_metadata(self, metadata): + """Set the metadata for the script.""" + metadata.duration = self.long_timeout + + async def run_block(self): + """Run the script.""" + await self.checkpoint(f"Start moving rotator to {self.target_angle} degrees.") + await self.mtcs.move_rotator( + angle=self.target_angle, wait_for_complete=self.wait_for_complete + ) + await self.checkpoint( + f"Move rotator returned. Wait for complete: {self.wait_for_complete}." + ) diff --git a/tests/test_maintel_mtrotator_move_rotator.py b/tests/test_maintel_mtrotator_move_rotator.py new file mode 100644 index 00000000..7c267695 --- /dev/null +++ b/tests/test_maintel_mtrotator_move_rotator.py @@ -0,0 +1,118 @@ +# 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 . + +import unittest + +from lsst.ts import standardscripts +from lsst.ts.observatory.control.maintel.mtcs import MTCS, MTCSUsages +from lsst.ts.standardscripts.maintel.mtrotator import MoveRotator + + +class TestMoveRotator( + standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase +): + async def basic_make_script(self, index): + self.script = MoveRotator(index=index) + + self.script.mtcs = MTCS( + domain=self.script.domain, + intended_usage=MTCSUsages.DryTest, + log=self.script.log, + ) + + self.start_angle = 0.0 # degrees + self.very_short_sleep = 0.1 # seconds + self.script.mtcs.move_rotator = unittest.mock.AsyncMock() + + return (self.script,) + + async def test_configure_default(self): + """Test the default configuration""" + + async with self.make_script(): + target_angle = 45.0 + + await self.configure_script(angle=target_angle) + + assert self.script.target_angle == target_angle + assert self.script.wait_for_complete is True + assert self.script.program is None + assert self.script.reason is None + assert self.script.checkpoint_message is None + + async def test_configure_dont_wait_for_complete(self): + """Test with the configuration where ``wait_for_complete`` is False""" + + async with self.make_script(): + target_angle = 45.0 + wait_for_complete = False + + await self.configure_script(angle=target_angle, wait_for_complete=False) + + assert self.script.target_angle == target_angle + assert self.script.wait_for_complete is wait_for_complete + assert self.script.program is None + assert self.script.reason is None + assert self.script.checkpoint_message is None + + async def test_configure_with_program_reason(self): + """Testing a valid configuration: with program and reason""" + + # Try configure with a list of valid actuators ids + async with self.make_script(): + self.script.get_obs_id = unittest.mock.AsyncMock( + side_effect=["202306060001"] + ) + await self.configure_script( + angle=10.0, + wait_for_complete=True, + program="BLOCK-123", + reason="SITCOM-321", + ) + + assert self.script.program == "BLOCK-123" + assert self.script.reason == "SITCOM-321" + assert ( + self.script.checkpoint_message + == "MoveRotator BLOCK-123 202306060001 SITCOM-321" + ) + + async def test_run_with_default_config(self): + async with self.make_script(): + target_angle = 45.0 + + await self.configure_script(angle=target_angle) + + await self.run_script() + + self.script.mtcs.move_rotator.assert_called_once_with( + angle=target_angle, wait_for_complete=True + ) + + async def test_executable(self): + scripts_dir = standardscripts.get_scripts_dir() + script_path = scripts_dir / "maintel" / "mtrotator" / "move_rotator.py" + print(script_path) + await self.check_executable(script_path) + + +if __name__ == "__main__": + unittest.main()