diff --git a/python/lsst/ts/observatory/control/auxtel/atcalsys.py b/python/lsst/ts/observatory/control/auxtel/atcalsys.py index 4fc8b93d..3577fc8c 100644 --- a/python/lsst/ts/observatory/control/auxtel/atcalsys.py +++ b/python/lsst/ts/observatory/control/auxtel/atcalsys.py @@ -1,5 +1,5 @@ from typing import List, Optional, NamedTuple -from ..base_calsys import BaseCalsys, HardcodeCalsysThroughput +from ..base_calsys import BaseCalsys, HardcodeCalsysThroughput, CalibrationSequenceStepBase from lsst.ts import salobj from lsst.ts.idl.enums import ATMonochromator from lsst.ts.idl.enums import ATWhiteLight @@ -7,13 +7,26 @@ import astropy.units as un from astropy.units import Quantity from datetime import datetime - +from collections.abc import Awaitable +from dataclasses import dataclass class ATSpectrographSlits(NamedTuple): FRONTENTRANCE: float FRONTEXIT: float +@dataclass +class ATCalibrationSequenceStep(CalibrationSequenceStepBase): + grating: ATMonochromator.Grating + latiss_filter: str + latiss_grating: str + entrance_slit_width: float + exit_slit_width: float + fs_exp_time: float + fs_n_exp: int + em_exp_time: float + em_n_exp: int + class ATCalsys(BaseCalsys, HardcodeCalsysThroughput): """class which specifically handles the calibration system for auxtel""" @@ -52,8 +65,8 @@ async def setup_for_wavelength( self.log.debug(f"calculated slit widthsare {slit_widths}") self.log.debug(f"calculated grating is {grating}") - monoch_fut = self._sal_cmd_helper( - "monochromator", + monoch_fut = self._sal_cmd( + self.ATMonoChromator, "updateMonochromatorSetup", gratingType=grating, frontExitSlitWidth=slit_widths.FRONTEXIT, @@ -61,18 +74,15 @@ async def setup_for_wavelength( wavelength=wavelen, ) - elect_fut = self._sal_cmd_helper("electrometer", "performZeroCalib") - elect_fut2 = self._sal_cmd_helper( - "electrometer", + elect_fut = self._sal_cmd("electrometer", "performZeroCalib") + elect_fut2 = self._sal_cmd( + self.Electrometer, "setDigitalFilter", activateFilter=False, activateAvgFilter=False, activateMedFilter=False, ) - # TODO: electrometer - # TODO: fibre spectrograph - asyncio.wait( [monoch_fut, elect_fut, elect_fut2], return_when=asyncio.ALL_COMPLETED ) @@ -110,7 +120,6 @@ async def verify_chiller_operation(self): f"Chiller ambient temperature: {chiller_temps.ambientTemperature:0.1f} C") - async def turn_on_light(self, lamp_power: Quantity["power"]) -> None: #check lamp state first lamp_state = await self._sal_readvalue_helper(self.ATWhiteLight, "lampState") @@ -127,12 +136,10 @@ async def turn_on_light(self, lamp_power: Quantity["power"]) -> None: #turn on lamp and let it warm up await self._sal_cmd(self.ATWhiteLight, "turnLampOn", power=power_watts) - - async def wait_ready(self) -> None: #in the case of auxtel, need to wait for lamp to have warmed up #check that lamp state - lamp_state = await self._sal_waitevent(self.ATWhiteLight, "lampState") + lamp_state = await self._sal_waitevent(self.ATWhiteLight, "lampState", run_immediate=False) if lamp_state == ATWhiteLight.LampBasicState.On: return @@ -140,8 +147,32 @@ async def wait_ready(self) -> None: raise RuntimeError("unexpected lamp state when waiting for readiness!") - async def _chiller_setup(self, turnon: bool=True): + async def _electrometer_expose(self, exp_time: float) -> Awaitable[str]: + await self._sal_cmd(self.Electrometer, "startScanDt", scanDuration=exp_time, + run_immediate=False) + lfa_obj = self._sal_waitevent(self.Electrometer, "largeFileObjectAvailable", + run_immediate=False) + return lfa_obj.url + async def _spectrograph_expose(self, exp_time: float, numExposures: int) -> Awaitable[str]: + await self._sal_cmd(self.ATSpectrograph, "expose", numExposures = numExposures) + lfa_obj = await self._sal_waitevent(self.ATSpectrograph, "largeFileObjectAvailable", + run_immediate=False) + return lfa_obj.url + + + def _chiller_temp_check(temps) -> bool: + self.log.debug(f"Chiller supply temperature: {temps.supplyTemperature:0.1f} C " + f"[set:{temps.setTemperature} deg].") + pct_dev: float = (temps.supplyTemperature - temps.setTemperature) / temps.setTemperature + + if pct_dev <= self.CHILLER_TEMP_REL_TOL: + self.log.info( + f"Chiller reached target temperature, {temps.supplyTemperature:0.1f} deg ") + return True + return False + + async def _chiller_setup(self, turnon: bool=True): chiller_setpoint_temp: float = self.CHILLER_SETPOINT_TEMP.to(un.s).value chiller_wait_timeout: float = self.CHILLER_COOLDOWN_TIMEOUT.to(un.s).value await self._sal_cmd(self.ATWhiteLight, "setChillerTemperature", @@ -152,19 +183,9 @@ async def _chiller_setup(self, turnon: bool=True): chiller_temp_gen = self._sal_telem_gen(self.ATWhiteLight, "chillerTemperatures") now = datetime.now() - def temp_checker(temps) -> bool: - self.log.debug(f"Chiller supply temperature: {temps.supplyTemperature:0.1f} C " - f"[set:{temps.setTemperature} deg].") - pct_dev: float = (temps.supplyTemperature - temps.setTemperature) / temps.setTemperature - - if pct_dev <= self.CHILLER_TEMP_REL_TOL: - self.log.info( - f"Chiller reached target temperature, {temps.supplyTemperature:0.1f} deg ") - return True - return False try: - await self._long_wait(chiller_temp_gen, chiller_wait_timeout, temp_checker) + await self._long_wait(chiller_temp_gen, chiller_wait_timeout, self._chiller_temp_check) except TimeoutError as err: nowfail = datetime.now() wait_time: float = (nowfail - now).total_seconds() diff --git a/python/lsst/ts/observatory/control/base_calsys.py b/python/lsst/ts/observatory/control/base_calsys.py index f2567783..2bab2dad 100644 --- a/python/lsst/ts/observatory/control/base_calsys.py +++ b/python/lsst/ts/observatory/control/base_calsys.py @@ -1,4 +1,5 @@ from abc import ABCMeta, abstractmethod +from dataclasses import dataclass from .remote_group import RemoteGroup from typing import Iterable, Optional, Tuple, List, Union from typing import Sequence, Mapping, TypeAlias @@ -16,11 +17,19 @@ from astropy.units import ampere, watt, nm, Quantity import astropy.units as un + Responsivity: TypeAlias = Quantity[ampere / watt] +@dataclass +class CalibrationSequenceStepBase: + wavelength: float + n_exp: int + exp_time: float + TaskOrCoro = Union[asyncio.Task, Coroutine] + class CalsysThroughputCalculationMixin: """mixin class to allow pluggable source for calculation of throughputs""" @@ -174,29 +183,26 @@ def __init__( self._cmd_timeout = cmd_timeout def _sal_cmd( - self, salobj: salobj.Remote, cmdname: str, run_immediate: bool = True, **setargs + self, salobj: salobj.Remote, cmdname: str, run_immediate: bool = True, **setargs ) -> TaskOrCoro: cmdfun: salobj.topics.RemoteCommand = getattr(salobj, f"cmd_{cmdname}") - cmdfun.set(**setargs) - pkgtask = lambda: cmdfun.start(timeout=self._cmd_timeout) + pkgtask = cmdfun.set_start(**setargs, timeout=self._cmd_timeout) if run_immediate: - return asyncio.createtask(pkgtask()) - return pkgtask() + return asyncio.createtask(pkgtask) + return pkgtask def _sal_waitevent(self, salobj: salobj.Remote, evtname: str, run_immediate: bool=True, flush: bool=True, **evtargs) -> TaskOrCoro: cmdfun: salobj.topics.RemoteEvent = getattr(salobj, f"evt_{evtname}") - pkgtask = lambda: cmdfun.next(timeout=self._cmd_timeout, flush=flush) + pkgtask = cmdfun.next(timeout=self._cmd_timeout, flush=flush) if run_immediate: - return asyncio.create_task(pkgtask()) - return pkgtask() + return asyncio.create_task(pkgtask) + return pkgtask def _lfa_event(self, salobj: salobj.Remote, telname: str, run_immediate: bool = True, flush: bool=True, **evtargs) -> TaskOrCoro: - return _sal_waitevent_helper(salobj, "largeFileObjectAvailable", run_immediate, flush, **evtargs) + return self._sal_waitevent(salobj, "largeFileObjectAvailable", run_immediate, flush, **evtargs) - def _sal_telem_gen(self, salobj, telname) -> AsyncGenerator: - if isinstance(salobj, str): - salobj = getattr(self, salobj) + def _sal_telem_gen(self, salobj: salobj.Remote, telname: str) -> AsyncGenerator: cmdfun: salobj.topics.RemoteTelemetry = getattr(salobj, f"tel_{telname}") async def gen(): @@ -256,6 +262,11 @@ def spectrograph_exposure_time_for_nelectrons(self, nelec: float) -> float: def pd_exposure_time_for_nelectrons(self, nelec: float) -> float: pass + + @abstractmethod + async def powerup_sequence_run(self, scriptobj, **kwargs) -> AsyncGenerator: + pass + @abstractmethod async def turn_on_light(self, **kwargs) -> None: """awaitable command which turns on the calibration light, having @@ -295,12 +306,15 @@ def wavelen(self) -> Quantity[nm]: """returns the currently configured wavelength""" @abstractmethod - async def take_data(self): + async def take_detector_data(self): """This will fire off all async tasks to take calibration data in sequence, and return locations a nd metadata about the files supplied etc""" + @abstractmethod + async def gen_calibration_auxiliaries(self): + pass + async def wait_ready(self): """ Method to wait for prepared state for taking data - e.g. lamps on, warmed up, laser warmed up etc""" -