From a72e06fa69a9590e98f409e42bcb877c922293fa Mon Sep 17 00:00:00 2001 From: Parker Fagrelius Date: Wed, 14 Aug 2024 15:31:51 -0700 Subject: [PATCH] sal script for powering on tunable laser, using mtcalsys.py --- doc/news/DM-45729.feature.rst | 1 + .../calibration/power_on_tunablelaser.py | 27 +++ .../maintel/calibration/__init__.py | 22 ++ .../calibration/power_on_tunablelaser.py | 154 +++++++++++++ tests/test_maintel_power_on_tunablelaser.py | 208 ++++++++++++++++++ 5 files changed, 412 insertions(+) create mode 100644 doc/news/DM-45729.feature.rst create mode 100755 python/lsst/ts/standardscripts/data/scripts/maintel/calibration/power_on_tunablelaser.py create mode 100644 python/lsst/ts/standardscripts/maintel/calibration/__init__.py create mode 100644 python/lsst/ts/standardscripts/maintel/calibration/power_on_tunablelaser.py create mode 100644 tests/test_maintel_power_on_tunablelaser.py diff --git a/doc/news/DM-45729.feature.rst b/doc/news/DM-45729.feature.rst new file mode 100644 index 000000000..2d7e33aeb --- /dev/null +++ b/doc/news/DM-45729.feature.rst @@ -0,0 +1 @@ +New SalScript for powering on the Tunable Laser standalone diff --git a/python/lsst/ts/standardscripts/data/scripts/maintel/calibration/power_on_tunablelaser.py b/python/lsst/ts/standardscripts/data/scripts/maintel/calibration/power_on_tunablelaser.py new file mode 100755 index 000000000..1ce19e9f8 --- /dev/null +++ b/python/lsst/ts/standardscripts/data/scripts/maintel/calibration/power_on_tunablelaser.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.calibration import PowerOnTunableLaser + +asyncio.run(PowerOnTunableLaser.amain()) diff --git a/python/lsst/ts/standardscripts/maintel/calibration/__init__.py b/python/lsst/ts/standardscripts/maintel/calibration/__init__.py new file mode 100644 index 000000000..5728d764b --- /dev/null +++ b/python/lsst/ts/standardscripts/maintel/calibration/__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 .power_on_tunablelaser import * diff --git a/python/lsst/ts/standardscripts/maintel/calibration/power_on_tunablelaser.py b/python/lsst/ts/standardscripts/maintel/calibration/power_on_tunablelaser.py new file mode 100644 index 000000000..06eafe8d4 --- /dev/null +++ b/python/lsst/ts/standardscripts/maintel/calibration/power_on_tunablelaser.py @@ -0,0 +1,154 @@ +# 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__ = ["PowerOnTunableLaser"] + + +import yaml +from lsst.ts import salobj +from lsst.ts.observatory.control.maintel.mtcalsys import MTCalsys + + +class PowerOnTunableLaser(salobj.BaseScript): + """Starts propagating the Tunable Laser for functional + testing. + + Parameters + ---------- + index : `int` + Index of Script SAL component. + + """ + + def __init__(self, index): + super().__init__( + index=index, + descr="Power On Tunable Laser", + ) + + self.laser = None + self.mtcalsys = None + + @classmethod + def get_schema(cls): + schema_yaml = """ + $schema: http://json-schema.org/draft-07/schema# + $id: https://github.com/lsst-ts/ts_standardscripts/maintel/calibrations/power_on_tunablelaser.yaml + title: PowerOnTunableLaser v1 + description: Configuration for PowerOnTunableLaser. + Each attribute can be specified as a scalar or array. + All arrays must have the same length (one item per image). + type: object + properties: + sequence_name: + description: Name of sequence in MTCalsys + type: string + default: laser_functional + + additionalProperties: false + """ + return yaml.safe_load(schema_yaml) + + def set_metadata(self, metadata): + metadata.duration = 30 + + async def configure(self, config): + """Configure the script. + + Parameters + ---------- + config : ``self.cmd_configure.DataType`` + + """ + self.log.info("Configure started") + if self.mtcalsys is None: + self.log.debug("Creating MTCalSys.") + self.mtcalsys = MTCalsys(domain=self.domain, log=self.log) + await self.mtcalsys.start_task + + self.sequence_name = config.sequence_name + self.mtcalsys.load_calibration_config_file() + self.mtcalsys.assert_valid_configuration_option(name=self.sequence_name) + + self.config_data = self.mtcalsys.get_calibration_configuration( + self.sequence_name + ) + + self.laser_mode = self.config_data["laser_mode"] + self.optical_configuration = self.config_data["optical_configuration"] + self.wavelength = self.config_data["wavelength"] + + if self.laser is None: + self.laser = salobj.Remote( + domain=self.domain, + name="TunableLaser", + ) + + self.laser.start_task + + self.log.info("Configure completed") + + async def run(self): + """Run script.""" + await self.assert_components_enabled() + + await self.checkpoint("Configuring TunableLaser") + await self.mtcalsys.setup_laser( + mode=self.laser_mode, + wavelength=self.wavelength, + optical_configuration=self.optical_configuration, + use_projector=False, + ) + + params = await self.mtcalsys.get_laser_parameters() + + self.log.info( + f"Laser Configuration is {params[0]}, \n" + f"wavelength is {params[1]}, \n" + f"Interlock is {params[2]}, \n" + f"Burst mode is {params[3]}, \n" + f"Cont. mode is {params[4]}" + ) + + await self.checkpoint("Starting laser propagation") + await self.start_propagation_on() + + async def start_propagation_on(self): + """Starts propagation of the laser""" + + await self.mtcalsys.laser_start_propagate() + + async def assert_components_enabled(self): + """Checks if TunableLaser is ENABLED + + Raises + ------ + RunTimeError: + If either component is not ENABLED""" + + comps = [self.laser] + + for comp in comps: + summary_state = await comp.evt_summaryState.aget() + if salobj.State(summary_state.summaryState) != salobj.State( + salobj.State.ENABLED + ): + raise RuntimeError(f"{comp} is not ENABLED") diff --git a/tests/test_maintel_power_on_tunablelaser.py b/tests/test_maintel_power_on_tunablelaser.py new file mode 100644 index 000000000..4eeadd691 --- /dev/null +++ b/tests/test_maintel_power_on_tunablelaser.py @@ -0,0 +1,208 @@ +# 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 logging +import os +import random +import types +import unittest +import warnings + +from lsst.ts import salobj, standardscripts, utils +from lsst.ts.standardscripts.maintel.calibration import PowerOnTunableLaser +from lsst.ts.xml.enums.TunableLaser import LaserDetailedState + +# TODO: (DM-46168) Revert workaround for TunableLaser XML changes +try: + from lsst.ts.xml.enums.TunableLaser import ( + OpticalConfiguration as LaserOpticalConfiguration, + ) +except ImportError: + warnings.warn( + "OpticalConfiguration enumeration not availble in ts-xml. Using local version." + ) + from lsst.ts.observatory.control.utils.enums import LaserOpticalConfiguration + +random.seed(47) # for set_random_lsst_dds_partition_prefix + +logging.basicConfig() + + +class TestPowerOnTunableLaser( + standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase +): + async def basic_make_script(self, index): + self.script = PowerOnTunableLaser(index=index) + + self.laser_state = types.SimpleNamespace( + detailedState=LaserDetailedState.NONPROPAGATING_CONTINUOUS_MODE + ) + self.optical_config_state = types.SimpleNamespace( + configuration=LaserOpticalConfiguration.NO_SCU + ) + + await self.configure_mocks() + + return [ + self.script, + ] + + async def mock_setup_laser( + self, mode, wavelength, optical_configuration, use_projector + ): + self.laser_state = types.SimpleNamespace(detailedState=mode) + self.optical_config_state = types.SimpleNamespace( + configuration=optical_configuration + ) + self.script.optical_configuration = optical_configuration + self.script.wavelength = wavelength + + async def mock_laser_start_propagate(self, *args, **kwargs): + self.laser_state = types.SimpleNamespace( + detailedState=LaserDetailedState.PROPAGATING_CONTINUOUS_MODE + ) + + async def configure_mocks(self): + self.script.laser = unittest.mock.AsyncMock() + self.script.laser.start_task = utils.make_done_future() + # Mock evt_summaryState.aget to return ENABLED state + self.script.laser.evt_summaryState = unittest.mock.MagicMock() + self.script.laser.evt_summaryState.aget = unittest.mock.AsyncMock( + return_value=types.SimpleNamespace(summaryState=salobj.State.ENABLED) + ) + + # Mock MTCalsys + self.script.mtcalsys = unittest.mock.MagicMock() + self.script.mtcalsys.start_task = utils.make_done_future() + self.script.mtcalsys.load_calibration_config_file = unittest.mock.MagicMock() + self.script.mtcalsys.assert_valid_configuration_option = ( + unittest.mock.MagicMock() + ) + self.script.mtcalsys.get_calibration_configuration = unittest.mock.MagicMock( + return_value={ + "laser_mode": LaserDetailedState.NONPROPAGATING_CONTINUOUS_MODE, + "optical_configuration": LaserOpticalConfiguration.SCU.name, + "wavelength": 500.0, + } + ) + self.script.mtcalsys.setup_laser = unittest.mock.AsyncMock( + side_effect=self.mock_setup_laser + ) + self.script.mtcalsys.get_laser_parameters = unittest.mock.AsyncMock( + return_value=[ + "optical_configuration", + 500.0, + "interlock", + "burst_mode", + "cont_mode", + ] + ) + self.script.mtcalsys.laser_start_propagate = unittest.mock.AsyncMock( + side_effect=self.mock_laser_start_propagate + ) + + # async def configure_mocks(self): + # self.script.laser = unittest.mock.AsyncMock() + # self.script.laser.start_task = utils.make_done_future() + + # # Configure mocks + + self.script.laser.configure_mock( + **{ + "evt_summaryState.aget.side_effect": self.mock_get_laser_summary_state, + "cmd_setOpticalConfiguration.set_start.side_effect": self.mock_set_optical_config, + "cmd_setContinuousMode.start.side_effect": self.mock_set_continuous_mode, + "cmd_startPropagateLaser.start.side_effect": self.mock_start_laser, + } + ) + + async def mock_get_laser_summary_state(self, **kwargs): + return types.SimpleNamespace(summaryState=salobj.State.ENABLED) + + async def mock_set_optical_config(self, **kwargs): + self.optical_config_state = types.SimpleNamespace(configuration="SCU") + + async def mock_set_continuous_mode(self, **kwargs): + self.laser_state = types.SimpleNamespace( + detailedState=LaserDetailedState.NONPROPAGATING_CONTINUOUS_MODE + ) + + async def mock_start_laser(self, **kwargs): + self.laser_state = types.SimpleNamespace( + detailedState=LaserDetailedState.PROPAGATING_CONTINUOUS_MODE + ) + + async def test_configure(self): + # Try to configure with only some of the optional parameters + async with self.make_script(): + mode = LaserDetailedState.NONPROPAGATING_CONTINUOUS_MODE + optical_configuration = LaserOpticalConfiguration.SCU.name + wavelength = 500.0 + + await self.configure_script() + + assert self.script.laser_mode == mode + assert self.script.optical_configuration == optical_configuration + assert self.script.wavelength == wavelength + + async def test_run_without_failures(self): + async with self.make_script(): + await self.configure_script() + + await self.run_script() + + # self.script.laser.cmd_changeWavelength.set_start.assert_awaited_once_with( + # wavelength=self.script.wavelength, + # ) + + # self.script.laser.cmd_setOpticalConfiguration.start.assert_awaited_once_with( + # configuration=self.script.optical_configuration, + # ) + + # self.script.laser.cmd_setContinuousMode.assert_awaited_once( + # ) + + # self.script.laser.cmd_startPropagateLaser.start.assert_awaited_with( + # ) + + # Summary State + self.script.laser.evt_summaryState.aget.assert_awaited_once_with() + + # Assert states are OK + assert ( + self.laser_state.detailedState + == LaserDetailedState.PROPAGATING_CONTINUOUS_MODE + ) + assert ( + self.script.optical_configuration == LaserOpticalConfiguration.SCU.name + ) + assert self.script.wavelength == 500.0 + + async def test_executable(self): + scripts_dir = standardscripts.get_scripts_dir() + script_path = os.path.join( + scripts_dir, "maintel", "calibration", "power_on_tunablelaser.py" + ) + await self.check_executable(script_path) + + +if __name__ == "__main__": + unittest.main()