diff --git a/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py b/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py index fdac5a154..37505c0cc 100644 --- a/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py +++ b/python/lsst/ts/standardscripts/auxtel/calibrations/__init__.py @@ -18,4 +18,4 @@ # # You should have received a copy of the GNU General Public License -from .power_on_atcalsys import * \ No newline at end of file +from .power_on_atcalsys import * diff --git a/python/lsst/ts/standardscripts/auxtel/calibrations/power_on_atcalsys.py b/python/lsst/ts/standardscripts/auxtel/calibrations/power_on_atcalsys.py index 9634bce8a..57cb652fd 100644 --- a/python/lsst/ts/standardscripts/auxtel/calibrations/power_on_atcalsys.py +++ b/python/lsst/ts/standardscripts/auxtel/calibrations/power_on_atcalsys.py @@ -21,20 +21,23 @@ __all__ = ["PowerOnATCalSys"] import asyncio -import collections +import time as time -import numpy as np import yaml -from lsst.ts.idl.enums.Script import ScriptState -from lsst.ts.idl.enums import ATMonochromator, ATWhiteLight +from lsst.ts.idl.enums import ATWhiteLight +from lsst.ts.observatory.control.remote_group import Usages + from lsst.ts import salobj +track_lamp_warmup = 60 + class PowerOnATCalSys(salobj.BaseScript): - """Powers on the ATCalSys dome flat illuminator (ATWhiteLight and ATMonochromator) - required to perform imaging white light calibrations. + """Powers on the ATCalSys dome flat illuminator + (ATWhiteLight and ATMonochromator) + required to perform image calibrations over white light. Parameters ---------- @@ -45,26 +48,43 @@ class PowerOnATCalSys(salobj.BaseScript): ----- """ - def __init__(self, index): + + def __init__(self, index, add_remotes: bool = True): super().__init__( index=index, descr="Power On AT Calibration System ", ) - self.whitelightsource = salobj.Remote( - domain=self.domain, name="ATWhiteLight", log=self.log - ) - self.monochromator = salobj.Remote( - domain=self.domain, name="ATMonochromator", log=self.log - ) - - # self.cmd_timeout = 10 - self.change_grating_time = 60 - self.open_shutter_time = 20 + self.white_light_source = None + self.monochromator = None + + # White lamp config + self.timeout_lamp_warm_up = 60 * 15 + self.cmd_timeout = 30 + + # Chiller config + self.timeout_chiller_cool_down = 60 * 10 self.chiller_temp_tolerance_relative = 0.1 - self.chiller_temp_time = 60 * 2 - self.track_lamp_warmup = 60 - self.lamp_warmup_time = 15 * 60 + + if self.white_light_source is None: + self.white_light_source = salobj.Remote( + domain=self.domain, name="ATWhiteLight", + intended_usage= None if add_remotes else Usages.DryTest, + log =self.log + ) + + if self.monochromator is None: + self.monochromator = salobj.Remote( + domain=self.domain, name="ATMonochromator", + intended_usage= None if add_remotes else Usages.DryTest, + log=self.log + ) + + asyncio.gather( + self.white_light_source.start_task, + self.monochromator.start_task, + ) + @classmethod def get_schema(cls): @@ -76,44 +96,42 @@ def get_schema(cls): Each attribute can be specified as a scalar or array. All arrays must have the same length (one item per image). type: object - properties: chiller_temperature: - description: + description: Set temperature for the chiller + type: number default: 20 - - type: number - minimum: 0 + minimum: 15 - whitelight_power: - description: White light power. + whitelight_power: + description: White light power. + type: number default: 910 - - type: number - minimum: 0 + minimum: 0 wavelength: description: Wavelength (nm). 0 nm is for white light. + type: number default: 0 - - type: number - minimum: 0 + minimum: 0 grating_type: description: Grating type for each image. The choices are 0=blue, 1=red, 2=mirror. + type: integer + enum: [0, 1, 2] default: 2 - anyOf: - - type: integer - enum: [0, 1, 2] entrance_slit_width: description: Width of the monochrometer entrance slit (mm) - default: 5 - - type: number - minimum: 0 + type: number + minimum: 0 + default: 7 exit_slit_width: description: Width of the monochromator entrance slit (mm) - default: 5 - - type: number - minimum: 0 + type: number + minimum: 0 + default: 7 additionalProperties: false """ @@ -134,17 +152,22 @@ async def configure(self, config): """ self.log.info("Configure started") - # self.chiller_temperature = config.chiller_temperature - - # self.whitelight_power = config.whitelight_power - - # self.wavelength = config.wavelength + self.chiller_temperature = config.chiller_temperature + self.whitelight_power = config.whitelight_power + self.wavelength = config.wavelength + self.grating_type = config.grating_type + self.entrance_slit_width = config.entrance_slit_width + self.exit_slit_width = config.exit_slit_width - # self.grating_type = config.grating_type + # if self.white_light_source is None: + # self.white_light_source = salobj.Remote( + # domain=self.domain, name="ATWhiteLight" + # ) - # self.entrance_slit_width = config.entrance_slit_width - - # self.exit_slit_width = config.exit_slit_width + # if self.monochromator is None: + # self.monochromator = salobj.Remote( + # domain=self.domain, name="ATMonochromator" + # ) self.config = config @@ -157,99 +180,133 @@ def set_metadata(self, metadata): ---------- metadata : SAPY_Script.Script_logevent_metadataC """ - metadata.duration = 60 * 15 + metadata.duration = self.timeout_chiller_cool_down + self.timeout_lamp_warm_up async def run(self): """Run script.""" + #await self.assert_components_enabled() - await self.checkpoint("Start the chiller") - - await whitelightsource.cmd_setChillerTemperature.set_start(temperature=self.chiller_temperature) - await whitelightsource.cmd_startChiller.set_start() - - # confirm the chiller is running at chiller_temperature within tolerance - try: - await asyncio.wait_for( - is_chiller_temp_within_tolerance(), self.chiller_temp_time - ) - except asyncio.TimeoutError: - self.log.info(f"Gave up waiting after {self.chiller_temp_time} for the chiller to chill to {self.chiller_temperature}") + await self.checkpoint("Starting chiller") + await self.start_chiller() - await self.checkpoint("Open the shutter") + await self.checkpoint("Waiting for chiller to cool to set temperature") + await self.wait_for_chiller_temp_within_tolerance() - await whitelightsource.cmd_openShutter.set_start() + await self.checkpoint("Opening the shutter") + await self.open_white_light_shutter() - # Confirm shutter is open or sent an error message? Should we include an error message if it fails rather than the RunTimeError - ## shutter_state = await whitelightsource.evt_shutterState.aget() - await self.checkpoint("Turning on lamp") - await whitelightsource.cmd_turnLampOn.set_start(power = self.whitelight_power) + await self.switch_lamp_on() - # Confirm lamp state turns on (It will go into a warm up period before it will turn on) - try: - await asyncio.wait_for( - has_lamp_finish_warm_up(), self.lamp_warm_up_time - ), - except asyncio.TimeoutError: - self.log.info(f"Lamp didn't turn on after {self.lamp_warm_up_time} s") + await self.checkpoint("Waiting for lamp to warm up") + await self.wait_for_lamp_to_warm_up() await self.checkpoint("Configuring ATMonochromator") + await self.configure_atmonochromator() - await self.checkpoint("Set the grating") - - await monochromator.cmd_selectGrating.set_start(gratingType=self.grating, timeout = 180) + async def start_chiller(self): + await self.white_light_source.cmd_setChillerTemperature.set_start( + temperature=self.chiller_temperature, timeout=self.cmd_timeout + ) + await self.white_light_source.cmd_startChiller.set(timeout=self.timeout_chiller_cool_down) - await self.checkpoint("Set wavelength") + async def wait_for_chiller_temp_within_tolerance(self): + start_time_chill_time = time.time() + while time.time() - start_time_chill_time < self.timeout_chiller_cool_down: + chiller_temps = await self.white_light_source.tel_chillerTemperatures.aget() + tel_chiller_temp = chiller_temps.supplyTemperature + if ( + chiller_temps.setTemperature + * (1.0 - self.chiller_temp_tolerance_relative) + < tel_chiller_temp + < chiller_temps.setTemperature + * (1.0 + self.chiller_temp_tolerance_relative) + ): + chill_time = time.time() - start_time_chill_time + self.log.info( + f"Chiller reached target temperature {tel_chiller_temp}" + f"within tolerance in {chill_time} s" + ) + break + else: + continue + else: + raise TimeoutError( + f"Gave up waiting after {self.timeout_chiller_cool_down}" + f"for the chiller to chill to {self.chiller_temperature}." + f"Stayed at {tel_chiller_temp}" + ) + + async def open_white_light_shutter(self): + await self.white_light_source.cmd_openShutter.set_start() + # Q? Should we include a check that the shutter is open? + + async def switch_lamp_on(self): + await self.white_light_source.cmd_turnLampOn.set_start( + power=self.whitelight_power, timeout=self.timeout_lamp_warm_up + ) - await monochromator.cmd_changeWavelength.set_start(wavelength=self.wavelength) + async def wait_for_lamp_to_warm_up(self): + self.white_light_source.evt_lampState.flush() + start_time_lamp_warm_up = time.time() + while time.time() - start_time_lamp_warm_up < self.timeout_lamp_warm_up: + lamp_state = await self.white_light_source.evt_lampState.next( + flush=False, timeout=self.timeout_lamp_warm_up + ) + if lamp_state.basicState == ATWhiteLight.LampBasicState.WARMUP: + warmup_time_left = ( + lamp_state.warmupEndTime - lamp_state.private_sndStamp + ) + self.log.info(f"Time Left for lamp warmup: {warmup_time_left} min.") + await asyncio.sleep(track_lamp_warmup) - await self.checkpoint("Set slits wide open") + elif lamp_state.basicState == ATWhiteLight.LampBasicState.ON: + self.log.info( + f"White Light Lamp is on after a warm up of {time.time() - start_time_lamp_warm_up} s" + ) + else: + raise TimeoutError( + f"White Light Lamp failed to turn on after {self.timeout_lamp_warm_up} s" + ) + + async def configure_atmonochromator(self): + await self.monochromator.cmd_selectGrating.set_start( + gratingType=self.grating, timeout=self.cmd_timeout + ) - await monochromator.cmd_changeSlitWidth.set_start(slit=1, slitWidth= self.entrance_slit_width) - await monochromator.cmd_changeSlitWidth.set_start(slit=2, slitWidth= self.exit_slit_width) + await self.monochromator.cmd_changeWavelength.set_start( + wavelength=self.wavelength, timeout=self.cmd_timeout + ) - params = await get_monochromator_parameters() + await self.monochromator.cmd_changeSlitWidth.set_start( + slit=1, slitWidth=self.entrance_slit_width, timeout=self.cmd_timeout + ) - script.log.info(f"ATMonochromator grating is {params[0]}, wavelength is {params[1]} nm with entry slit width {param[2]} and exit slit width {param[3]}") - - async def get_monochromator_parameters(): - tmp1 = await monochromator.evt_selectedGrating.aget() - tmp2 = await monochromator.evt_wavelength.aget() - tmp3 = await monochromator.evt_entrySlitWidth.aget() - tmp4 = await monochromator.evt_exitSlitWidth.aget() - return (tmp1.gratingType, tmp2.wavelength, tmp3.width, tmp4.width ) + await self.monochromator.cmd_changeSlitWidth.set_start( + slit=2, slitWidth=self.exit_slit_width, timeout=self.cmd_timeout + ) + params = await self.get_monochromator_parameters() - async def has_lamp_finish_warm_up(): - lamp_done_warm_up = False - start_time_lamp_warm_up = time.time() - while not lamp_done_warm_up: - lamp_state = await whitelightsource.evt_lampState.aget() - if lamp_state.basicState == ATWhiteLight.LampBasicState.WARMUP: - warmup_time_left = lamp_state.warmupEndTime - lamp_state.private_rcvStamp - script.log.info("Time Left for lamp warmup: {} min.".format(warmup_time_left/60.)) - await asyncio.sleep(self.track_lamp_warmup) - lamp_done_warm_up = False - elif lamp_state.basicState == ATWhiteLight.LampBasicState.ON: - lamp_done_warm_up = True - warm_up_elapsed_time = time.time() - start_time_lamp_warm_up - script.log.info(f"White Light Lamp is on after {warm_up_elapsed_time} s") - else: - continue + self.log.info( + f"ATMonochromator grating is {params[0]}, " + f"wavelength is {params[1]} nm " + f"with entry slit width {params[2]} " + f"and exit slit width {params[3]}" + ) - async def is_chiller_temp_within_tolerance(): - chiller_temp_within_tolerance = False - start_time_chill_time = time.time() - while not chiller_temp_within_tolerance: - chiller_temps = await whitelightsource.tel_chillerTemperatures.aget() - if ( - chiller_temps.setTemperature * (1.0 - self.chiller_temp_tolerance_relative) - < chiller_temps.supplyTemperature - < chiller_temps.setTemperature * (1.0 + self.chiller_temp_tolerance_relative) + async def get_monochromator_parameters(self): + tmp1 = await self.monochromator.evt_selectedGrating.aget() + tmp2 = await self.monochromator.evt_wavelength.aget() + tmp3 = await self.monochromator.evt_entrySlitWidth.aget() + tmp4 = await self.monochromator.evt_exitSlitWidth.aget() + return (tmp1.gratingType, tmp2.wavelength, tmp3.width, tmp4.width) + + async def assert_components_enabled(self): + """Check if ATWhiteLight and ATMonochromator are ENABLED""" + for comp in [self.white_light_source, self.monochromator]: + summary_state = await comp.evt_summaryState.aget() + if salobj.State(summary_state.summaryState) != salobj.State( + salobj.State.ENABLED ): - chiller_temp_within_tolerance = True - chill_time = time.time() - start_time_chill_time - script.log.info(f"Chiller reached target temperature within tolerance in {chill_time} s") - else: - continue - + raise Exception(f"{comp} is not ENABLED") diff --git a/tests/test_auxtel_power_on_atcalsys.py b/tests/test_auxtel_power_on_atcalsys.py index 36eec64cb..e4a3336ac 100644 --- a/tests/test_auxtel_power_on_atcalsys.py +++ b/tests/test_auxtel_power_on_atcalsys.py @@ -18,34 +18,193 @@ # # You should have received a copy of the GNU General Public License -import asyncio import logging import random import unittest +import types +import asyncio + +# import pytest +# from lsst.ts.idl.enums import ATMonochromator, Script +from lsst.ts import salobj -import pytest -from lsst.ts.idl.enums import ATMonochromator, Script +from lsst.ts import standardscripts +from lsst.ts.standardscripts.auxtel.calibrations import PowerOnATCalSys +from lsst.ts.idl.enums import ATWhiteLight, ATMonochromator -from lsst.ts import salobj, standardscripts -from lsst.ts.standardscripts.auxtel import PowerOnATCalSys random.seed(47) # for set_random_lsst_dds_partition_prefix logging.basicConfig() -class TestPowerOnATCalSys(standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase): + +class TestPowerOnATCalSys( + standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase +): async def basic_make_script(self, index): - self.script = PowerOnATCalSys(index=index) - #self.atcs_mock = ATCSMock() + self.script = PowerOnATCalSys(index=index, add_remotes=False) + + self.chiller_status = types.SimpleNamespace(chillerState="NOTREADY") + self.lamp_status = types.SimpleNamespace( + lampState=ATWhiteLight.LampBasicState.OFF + ) + self.shutter_status = types.SimpleNamespace( + shutterState=ATWhiteLight.ShutterState.CLOSED + ) + self.grating_status = types.SimpleNamespace( + gratingtype=ATMonochromator.Grating.BLUE + ) + + self.wavelength_status = types.SimpleNamespace(wavelength=200) + + self.slit_status = types.SimpleNamespace(width=0) + + await self.configure_mocks() + + return [ + self.script, + ] + + async def configure_mocks(self): + self.script.white_light_source = unittest.mock.AsyncMock() + self.script.monochromator = unittest.mock.AsyncMock() + + # Configure mocks + + self.script.white_light_source.configure_mock( + **{ + "evt_summaryState.aget": self.mock_get_whitelightsource_summary_state, + "cmd_setChillerTemperature.set_start": self.mock_start_chiller_temp, + "cmd_startChiller.set": self.mock_chiller_temp, + "tel_ChillerTemp.aget": self.mock_get_chiller_status, + "com_openShutter.set_start": self.mock_open_shutter, + "cmd_turnLampOn.set_start": self.mock_lamp_temp, + "evt_lampState.next": self.mock_get_lamp_status, + } + ) + self.script.monochromator.configure_mock( + **{ + "evt_summaryState.aget": self.mock_get_monochromator_sumary_state, + "cmd_selectGrating.set_start": self.mock_change_grating, + "cmd_changeWavelength.set_start": self.mock_change_wavelength, + "cmd_changeSlitWidth.set_start": self.mock_change_slit_width, + } + ) + + # Chiller + async def mock_get_whitelightsource_summary_state(self): + return types.Simplenamespace(summaryState=salobj.State.ENABLED) + + async def mock_start_chiller_temp(self): + self.start_chiller_temperature = 30 + + async def mock_chiller_temp(self): + self.chiller_status = types.SimpleNamespace(chillerState="NOTREADY") + await asyncio.sleep(5.0) + self.chiller_status = types.SimpleNamespace(chillerState="READY") + + async def mock_get_chiller_status(self): + await asyncio.sleep(0.5) + return self.chiller_status + + # Shutter + async def mock_open_shutter(self): + self.shutter_status = types.SimpleNamespace( + shutterState=ATWhiteLight.ShutterState.OPEN + ) + + # Lamp - return (self.script, self.atcs_mock) + async def mock_lamp_temp(self): + self.lamp_status.status = ATWhiteLight.LampBasicState.WARMUP + await asyncio.sleep(10.0) + self.lamp_status.status = ATWhiteLight.LampBasicState.ON - async def test_run(self): + async def mock_get_lamp_status(self): + await asyncio.sleep(0.5) + return self.lamp_status + + # Monochromator + async def mock_get_monochromator_summary_state(self): + return types.Simplenamespace(summaryState=salobj.State.ENABLED) + + async def mock_change_grating(self): + self.grating_status = types.SimpleNamespace( + gratingState=self.script.grating + ) + + async def mock_change_wavelength(self): + self.wavelength_status = types.SimpleNamespace( + wavelength=self.script.wavelength + ) + + async def mock_change_slit_width(self): + self.slit_status = types.SimpleNamespace( + width=self.script.entrance_slit_width + ) + + async def test_run_without_without_failures(self): async with self.make_script(): await self.configure_script() await self.run_script() + # Chiller + self.script.white_light_source.cmd_setChillerTemperature.set_start.assert_awaited_once_with( + temperature=self.script.chiller_temperature, + timeout=self.script.cmd_timeout, + ) + + self.script.white_light_source.cmd_startChiller.set.assert_awaited_once_with( + timeout=self.script.timeout_chiller_cool_down + ) + + self.script.white_light_source.tel_chillerTemperatures.aget.assert_awaited_once() + + # Shutter + self.script.white_light_source.cmd_openShutter.set_start.assert_awaited_once() + + # White lamp + self.white_light_source.cmd_turnLampOn.set_start.assert_awaited_with( + temperature=self.script.whitelight_power, + timeout=self.script.timeout_lamp_warm_up, + ) + self.white_light_source.evt_lampState.next.assert_awaited_once_with( + flush=False, timeout=self.script.timeout_lamp_warm_up + ) + + # Monochromator configuration + self.monochromator.cmd_selectGrating.set_start.assert_awaited_once_with( + gratingType=self.script.grating, timeout=self.script.cmd_timeout + ) + + self.monochromator.cmd_changeWavelength.set_start.assert_awaited_once_with( + wavelength=self.script.wavelength, timeout=self.script.cmd_timeout + ) + + self.monochromator.cmd_changeSlitWidth.set_start.assert_awaited_once_with( + slit=1, + slitWidth=self.script.entrance_slit_width, + timeout=self.script.cmd_timeout, + ) + + self.monochromator.cmd_changeSlitWidth.set_start.assert_awaited_once_with( + slit=2, + slitWidth=self.script.exit_slit_width, + timeout=self.script.cmd_timeout, + ) + + # Summary State + self.white_light_source.evt_summaryState.aget.assert_awaited_once_with() + + self.monochromator.evt_summaryState.aget.assert_awaited_once_with() + + assert self.chiller_status.chillerState == "READY" + assert self.lamp_status.lampState == ATWhiteLight.LampBasicState.ON + assert self.shutter_status.shutterState == ATWhiteLight.ShutterState.OPEN + assert self.grating_status.gratingType == ATMonochromator.Grating.MIRROR + # Assert wavelength and slitWidth? + async def test_executable(self): scripts_dir = standardscripts.get_scripts_dir() script_path = scripts_dir / "auxtel" / "calibrations" / "power_on_atcalsys.py" @@ -53,4 +212,4 @@ async def test_executable(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main()