diff --git a/doc/version_history.rst b/doc/version_history.rst index e3b5df1e..8a8b63df 100644 --- a/doc/version_history.rst +++ b/doc/version_history.rst @@ -9,6 +9,7 @@ Version History v1.25.0 ------- +* Add new ``auxtel/calibrations/power_off_atcalsys.py`` script, unit test and executable to turn off the ATCalSys white light. * Add new ``auxtel/calibrations/power_on_atcalsys.py`` script, unit test and executable to turn on and set up the ATCalSys (ATWhiteLight and ATMonochromator) to take flats. v1.24.2 diff --git a/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py b/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py index d088112a..c502c5d4 100644 --- a/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py +++ b/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py @@ -19,4 +19,5 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .power_off_atcalsys import * from .power_on_atcalsys import * diff --git a/python/lsst/ts/standardscripts/auxtel/calibrations/power_off_atcalsys.py b/python/lsst/ts/standardscripts/auxtel/calibrations/power_off_atcalsys.py new file mode 100644 index 00000000..8c139c99 --- /dev/null +++ b/python/lsst/ts/standardscripts/auxtel/calibrations/power_off_atcalsys.py @@ -0,0 +1,151 @@ +# 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__ = ["PowerOffATCalSys"] + +import asyncio + +from lsst.ts import salobj +from lsst.ts.idl.enums import ATWhiteLight + + +class PowerOffATCalSys(salobj.BaseScript): + """Powers off the ATCalSys dome flat illuminator + turning white lamp off, closing the shutter and + stopping the chiller. + + Parameters + ---------- + index : `int` + Index of Script SAL component. + + """ + + def __init__(self, index, add_remotes: bool = True): + super().__init__( + index=index, + descr="Power OFF AT Calibration System ", + ) + + self.white_light_source = None + + # White lamp config + self.timeout_lamp_cool_down = 60 * 16 + self.cmd_timeout = 30 + + # Shutter + self.timeout_close_shutter = 60 * 2 + + @classmethod + def get_schema(cls): + return None + + async def configure(self, config): + # This script does not require any configuration + + self.log.info("Configure started") + + if self.white_light_source is None: + self.white_light_source = salobj.Remote( + domain=self.domain, + name="ATWhiteLight", + ) + + await self.white_light_source.start_task + + self.log.info("Configure completed") + + def set_metadata(self, metadata): + """Compute estimated duration.""" + + metadata.duration = self.timeout_lamp_cool_down + + async def run(self): + """Run script.""" + await self.assert_components_enabled() + + await self.checkpoint("Turning lamp off") + await self.switch_lamp_off() + + await self.checkpoint("Closing the shutter") + await self.white_light_source.cmd_closeShutter.start( + timeout=self.timeout_close_shutter + ) + + await self.checkpoint("Waiting for lamp to cool down") + await self.wait_for_lamp_to_cool_down() + + await self.checkpoint("Stopping chiller") + await self.white_light_source.cmd_stopChiller.start(timeout=self.cmd_timeout) + + async def switch_lamp_off(self): + """Switches white light source lamp off""" + self.white_light_source.evt_lampState.flush() + + await self.white_light_source.cmd_turnLampOff.start( + timeout=self.timeout_lamp_cool_down + ) + + async def wait_for_lamp_to_cool_down(self): + """Confirm the white lamp has switched OFF and has cooled down + to proceed with stopping the chiller. + + Raises + ------ + TimeOutError: + If the lamp doesn't cool down and fails to turn off + in self.timeout_lamp_cool_down. + """ + lamp_state = await self.white_light_source.evt_lampState.aget( + timeout=self.timeout_lamp_cool_down + ) + self.log.info( + f"Lamp state: {ATWhiteLight.LampBasicState(lamp_state.basicState)!r}." + ) + while lamp_state.basicState != ATWhiteLight.LampBasicState.OFF: + try: + lamp_state = await self.white_light_source.evt_lampState.next( + flush=False, timeout=self.timeout_lamp_cool_down + ) + cool_down_wait_time = ( + lamp_state.cooldownEndTime - lamp_state.private_sndStamp + ) + self.log.info( + f"Lamp state: {ATWhiteLight.LampBasicState(lamp_state.basicState)!r}. " + f"Waiting {cool_down_wait_time/60:0.1f} min" + ) + except asyncio.TimeoutError: + raise RuntimeError( + f"White Light Lamp failed to turn off after {self.timeout_lamp_warm_up} s." + ) + + async def assert_components_enabled(self): + """Check if ATWhiteLight is ENABLED + + Raises + ------ + RunTimeError: + If ATWhiteLight is not ENABLED""" + summary_state = await self.white_light_source.evt_summaryState.aget( + timeout=self.cmd_timeout + ) + if summary_state.summaryState != salobj.State.ENABLED: + raise RuntimeError("ATWhiteLight is not ENABLED") diff --git a/python/lsst/ts/standardscripts/data/scripts/auxtel/calibrations/power_off_atcalsys.py b/python/lsst/ts/standardscripts/data/scripts/auxtel/calibrations/power_off_atcalsys.py new file mode 100755 index 00000000..61e040bb --- /dev/null +++ b/python/lsst/ts/standardscripts/data/scripts/auxtel/calibrations/power_off_atcalsys.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.auxtel.calibrations import PowerOffATCalSys + +asyncio.run(PowerOffATCalSys.amain()) diff --git a/tests/test_auxtel_power_off_atcalsys.py b/tests/test_auxtel_power_off_atcalsys.py new file mode 100644 index 00000000..67950c46 --- /dev/null +++ b/tests/test_auxtel_power_off_atcalsys.py @@ -0,0 +1,138 @@ +# 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 +import logging +import random +import types +import unittest + +from lsst.ts import salobj, standardscripts, utils +from lsst.ts.idl.enums import ATWhiteLight +from lsst.ts.standardscripts.auxtel.calibrations import PowerOffATCalSys + +random.seed(47) # for set_random_lsst_dds_partition_prefix + +logging.basicConfig() + + +class TestPowerOffATCalSys( + standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase +): + async def basic_make_script(self, index): + self.script = PowerOffATCalSys(index=index, add_remotes=False) + + self.lamp_state = types.SimpleNamespace( + basicState=ATWhiteLight.LampBasicState.ON + ) + self.shutter_status = types.SimpleNamespace( + shutterState=ATWhiteLight.ShutterState.OPEN + ) + + await self.configure_mocks() + + return [ + self.script, + ] + + async def configure_mocks(self): + self.script.white_light_source = unittest.mock.AsyncMock() + self.script.white_light_source.start_task = utils.make_done_future() + + # Configure mocks + + self.script.white_light_source.configure_mock( + **{ + "evt_summaryState.aget.side_effect": self.mock_get_whitelightsource_summary_state, + "cmd_turnLampOff.start.side_effect": self.mock_get_lamp_status, + "cmd_closeShutter.start.side_effect": self.mock_close_shutter, + } + ) + + # Mock check methods + self.script.wait_for_lamp_to_cool_down = unittest.mock.AsyncMock( + side_effect=self.mock_lamp_temp + ) + + # Summary State + + async def mock_get_whitelightsource_summary_state(self, **kwargs): + return types.SimpleNamespace(summaryState=salobj.State.ENABLED) + + # Lamp + + async def mock_get_lamp_status(self, **kwargs): + await asyncio.sleep(0.5) + return self.lamp_state + + async def mock_lamp_temp(self, **kwargs): + self.lamp_state.basicState = ATWhiteLight.LampBasicState.TURNING_OFF + await asyncio.sleep(15.0) + self.lamp_state.basicState = ATWhiteLight.LampBasicState.OFF + + # Shutter + async def mock_close_shutter(self, **kwargs): + types.SimpleNamespace(shutterState=ATWhiteLight.ShutterState.OPEN) + await asyncio.sleep(3) + self.shutter_status = types.SimpleNamespace( + shutterState=ATWhiteLight.ShutterState.CLOSED + ) + + async def test_run_without_without_failures(self): + async with self.make_script(): + await self.configure_script() + + await self.run_script() + + # Summary State + self.script.white_light_source.evt_summaryState.aget.assert_awaited_once_with( + timeout=self.script.cmd_timeout + ) + + # White lamp + self.script.white_light_source.cmd_turnLampOff.start.assert_awaited_with( + timeout=self.script.timeout_lamp_cool_down, + ) + + self.script.wait_for_lamp_to_cool_down.assert_awaited_once() + + # Shutter + self.script.white_light_source.cmd_closeShutter.start.assert_awaited_with( + timeout=self.script.timeout_close_shutter, + ) + + # Chiller + self.script.white_light_source.cmd_stopChiller.start.assert_awaited_once_with( + timeout=self.script.cmd_timeout + ) + + # Check status + assert self.lamp_state.basicState == ATWhiteLight.LampBasicState.OFF + assert self.shutter_status.shutterState == ATWhiteLight.ShutterState.CLOSED + + async def test_executable(self): + scripts_dir = standardscripts.get_scripts_dir() + script_path = scripts_dir / "auxtel" / "calibrations" / "power_off_atcalsys.py" + await self.check_executable(script_path) + + +if __name__ == "__main__": + unittest.main()