From 6b4d40f90cf69b4e5da84630b22c6f3aa9f33cc9 Mon Sep 17 00:00:00 2001 From: Parker Fagrelius Date: Wed, 6 Nov 2024 08:24:47 -0700 Subject: [PATCH] dealing with yaml file --- doc/news/DM-46276.feature.1.rst | 1 + doc/news/DM-46276.feature.rst | 8 + .../ts/observatory/control/data/mtcalsys.yaml | 18 ++ .../control/data/mtcalsys_schema.yaml | 19 +- .../observatory/control/maintel/mtcalsys.py | 220 ++++++++++++++---- tests/maintel/test_mtcalsys.py | 30 +++ 6 files changed, 244 insertions(+), 52 deletions(-) create mode 100644 doc/news/DM-46276.feature.1.rst create mode 100644 doc/news/DM-46276.feature.rst diff --git a/doc/news/DM-46276.feature.1.rst b/doc/news/DM-46276.feature.1.rst new file mode 100644 index 00000000..8c3af82d --- /dev/null +++ b/doc/news/DM-46276.feature.1.rst @@ -0,0 +1 @@ +In ``mtcalsys.yaml``, Added laser configuration information to all tests, including laser mode and optical configuration. diff --git a/doc/news/DM-46276.feature.rst b/doc/news/DM-46276.feature.rst new file mode 100644 index 00000000..35fca13c --- /dev/null +++ b/doc/news/DM-46276.feature.rst @@ -0,0 +1,8 @@ +Add features to allow ``MTCalSys`` to better handle the laser +- In ``mtcalsys.py`` made the following changes: + - Added ``laser_start_propagate`` and ``laser_stop_propagate()`` + - Added ``get_laser_parameters()`` + - Improved ``setup_laser()`` to change the wavelength and the optical configuration + - Changed ``change_laser_wavelength()`` so it can be used for the laser or whitelight system +- In ``mtcalsys.yaml`` added a laser functional setup +- In ``mtcalsys_schema.yaml`` added laser mode and optical configuration diff --git a/python/lsst/ts/observatory/control/data/mtcalsys.yaml b/python/lsst/ts/observatory/control/data/mtcalsys.yaml index 97c95218..41c5a16a 100644 --- a/python/lsst/ts/observatory/control/data/mtcalsys.yaml +++ b/python/lsst/ts/observatory/control/data/mtcalsys.yaml @@ -64,8 +64,26 @@ whitelight_y: scan_r: calib_type: Mono + laser_mode: 1 + optical_configuration: F1_SCU + use_camera: true + mtcamera_filter: r + led_location: 174.91 + led_focus: 15.380 wavelength: 625.0 wavelength_width: 250 wavelength_resolution: 5.0 exposure_times: - 15.0 + +laser_functional: + calib_type: Mono + laser_mode: 1 + use_camera: false + optical_configuration: SCU + wavelength: 500.0 + use_electrometer: false + use_fiberspectrograph_red: false + use_fiberspectrograph_blue: false + exposure_times: + - 15.0 diff --git a/python/lsst/ts/observatory/control/data/mtcalsys_schema.yaml b/python/lsst/ts/observatory/control/data/mtcalsys_schema.yaml index 5db4116d..36d6da18 100644 --- a/python/lsst/ts/observatory/control/data/mtcalsys_schema.yaml +++ b/python/lsst/ts/observatory/control/data/mtcalsys_schema.yaml @@ -127,18 +127,23 @@ properties: type: array items: type: number + laser_mode: + type: integer + optical_configuration: + type: string + enum: + - SCU + - NO_SCU + - F1_SCU + - F2_SCU + - F2_NO_SCU + - F1_NO_SCU required: - calib_type -- use_camera -- mtcamera_filter - wavelength -- led_location -- led_focus +- use_camera - use_electrometer - use_fiberspectrograph_red - use_fiberspectrograph_blue -- electrometer_integration_time -- electrometer_mode -- electrometer_range - exposure_times additionalProperties: false diff --git a/python/lsst/ts/observatory/control/maintel/mtcalsys.py b/python/lsst/ts/observatory/control/maintel/mtcalsys.py index 5fda92c6..1046eaf1 100644 --- a/python/lsst/ts/observatory/control/maintel/mtcalsys.py +++ b/python/lsst/ts/observatory/control/maintel/mtcalsys.py @@ -27,6 +27,9 @@ import numpy as np from lsst.ts import salobj, utils + +# TODO: (DM-46168) Revert workaround for TunableLaser XML changes +from lsst.ts.observatory.control.utils.enums import LaserOpticalConfiguration from lsst.ts.xml.enums.TunableLaser import LaserDetailedState from ..base_calsys import BaseCalsys @@ -157,6 +160,7 @@ def __init__( self.led_rest_position = 120.0 # mm self.laser_enclosure_temp = 20.0 # C + self.laser_warmup = 20.0 # sec self.exptime_dict: dict[str, float] = dict( camera=0.0, @@ -164,45 +168,8 @@ def __init__( fiberspectrograph=0.0, ) - def calculate_laser_focus_location(self, wavelength: float) -> float: - """Calculates the location of the linear stage that provides the focus - for the laser projector. This location is dependent on the - wavelength of the laser. - - Parameters - ---------- - wavelength : `float` - wavelength of the laser projector in nm - - Returns - ------ - location of the linear stage for the laser projector focus in mm - - """ - # TODO (DM-44772): implement the actual function - return 10.0 - - async def change_laser_wavelength(self, wavelength: float) -> None: - """Change the TunableLaser wavelength setting - - Parameters - ---------- - wavelength : `float` - wavelength of the laser in nm - """ - - task_wavelength = self.rem.tunablelaser.cmd_changeWavelength.set_start( - wavelength=wavelength, timeout=self.long_long_timeout - ) - task_focus = self.linearstage_laser_focus.cmd_moveAbsolute.set_start( - distance=self.calculate_laser_focus_location(wavelength), - timeout=self.long_long_timeout, - ) - - await asyncio.gather(task_wavelength, task_focus) - async def is_ready_for_flats(self) -> bool: - """Designates if the calibraiton hardware is in a state + """Designates if the calibration hardware is in a state to take flats. """ # TODO (DM-44310): Implement method to check that the @@ -240,12 +207,94 @@ async def setup_calsys(self, sequence_name: str) -> None: await self.linearstage_projector_select.cmd_moveAbsolute.set_start( distance=self.ls_select_laser_location, timeout=self.long_timeout ) - await self.setup_laser(config_data["laser_mode"]) - await self.rem.tunablelaser.cmd_startPropagating.start( - timeout=self.long_long_timeout + await self.setup_laser( + config_data["laser_mode"], + config_data["wavelength"], + config_data["optical_configuration"], ) + await self.laser_start_propagate() + + def calculate_laser_focus_location(self, wavelength: float = 500.0) -> float: + """Calculates the location of the linear stage that provides the focus + for the laser projector. This location is dependent on the + wavelength of the laser. + + Parameters + ---------- + wavelength : `float` + wavelength of the laser projector in nm + Default 500.0 + + Returns + ------ + location of the linear stage for the laser projector focus in mm - async def setup_laser(self, mode: LaserDetailedState) -> None: + """ + # TODO (DM-44772): implement the actual function + return 10.0 + + async def change_laser_wavelength( + self, + wavelength: float, + use_projector: bool = True, + ) -> None: + """Change the TunableLaser wavelength setting + + Parameters + ---------- + wavelength : `float` + wavelength of the laser in nm + use_projector : `bool` + identifies if you are using the projector while + changing the wavelength. + Default True + """ + task_wavelength = self.rem.tunablelaser.cmd_changeWavelength.set_start( + wavelength=wavelength, timeout=self.long_long_timeout + ) + + if use_projector: + task_focus = self.linearstage_laser_focus.cmd_moveAbsolute.set_start( + distance=self.calculate_laser_focus_location(wavelength), + timeout=self.long_long_timeout, + ) + await asyncio.gather(task_wavelength, task_focus) + + else: + await task_wavelength + + async def change_laser_optical_configuration( + self, optical_configuration: LaserOpticalConfiguration + ) -> None: + """Change the output of the laser. + + Parameters + ---------- + optical_configuration : LaserOpticalConfiguration + """ + assert optical_configuration in list(LaserOpticalConfiguration) + + current_configuration = ( + await self.rem.tunablelaser.evt_opticalConfiguration.aget() + ) + if current_configuration.configuration != optical_configuration: + self.log.debug( + f"Changing optical configuration from {current_configuration} to {optical_configuration}" + ) + await self.rem.tunablelaser.cmd_setOpticalConfiguration.set_start( + configuration=optical_configuration, timeout=self.long_timeout + ) + + else: + self.log.debug("Laser Optical Configuration already in place.") + + async def setup_laser( + self, + mode: LaserDetailedState, + wavelength: float, + optical_configuration: LaserOpticalConfiguration = LaserOpticalConfiguration.SCU, + use_projector: bool = True, + ) -> None: """Perform all steps for preparing the laser for monochromatic flats. This includes confirming that the thermal system is turned on and set at the right temperature. It also checks @@ -255,8 +304,16 @@ async def setup_laser(self, mode: LaserDetailedState) -> None: ---------- mode : LaserDetailedState Mode of the TunableLaser - Options: CONTINUOUS, BURST, TRIGGER - + Options: CONTINUOUS, BURST + wavelength : `float` + Wavelength fo the laser in nm + optical_configuration : LaserOpticalConfiguration + Output of laser + Default LaserOpticalConfiguration.SCU + use_projector : `bool` + identifies if you are using the projector while + changing the wavelength + Default True """ # TO-DO: DM-45693 implement thermal system checks @@ -279,6 +336,77 @@ async def setup_laser(self, mode: LaserDetailedState) -> None: f"{mode} not an acceptable LaserDetailedState [CONTINOUS, BURST, TRIGGER]" ) + await self.change_laser_optical_configuration(optical_configuration) + await self.change_laser_wavelength(wavelength, use_projector) + + async def get_laser_parameters(self) -> tuple: + """Get laser configuration + + Returns + ------- + list : configuration details + + """ + + return await asyncio.gather( + self.rem.tunablelaser.evt_opticalConfiguration.aget( + timeout=self.long_timeout + ), + self.rem.tunablelaser.evt_wavelengthChanged.aget(timeout=self.long_timeout), + self.rem.tunablelaser.evt_interlockState.aget(timeout=self.long_timeout), + self.rem.tunablelaser.evt_burstModeSet.aget(timeout=self.long_timeout), + self.rem.tunablelaser.evt_continuousModeSet.aget(timeout=self.long_timeout), + ) + + async def laser_start_propagate(self) -> None: + """Start the propagation of the Tunable Laser""" + + laser_state = await self.rem.tunablelaser.evt_detailedState.next( + flush=True, timeout=self.long_timeout + ) + self.log.debug(f"HERE: {laser_state.DetailedState}") + + if laser_state.DetailedState not in { + LaserDetailedState.PROPAGATING_CONTINUOUS_MODE, + LaserDetailedState.PROPAGATING_BURST_MODE, + }: + try: + await self.rem.tunablelaser.cmd_startPropagateLaser.start( + timeout=self.laser_warmup + ) + laser_state = await self.rem.tunablelaser.evt_detailedState.next( + flush=True, timeout=self.long_timeout + ) + self.log.info(f"Laser state: {laser_state.DetailedState}") + except asyncio.TimeoutError: + raise RuntimeError( + "Tunable Laser did not start propagating when commanded" + ) + + async def laser_stop_propagate(self) -> None: + """Stop the propagation of the Tunable Laser""" + + laser_state = await self.rem.tunablelaser.evt_detailedState.next( + flush=True, timeout=self.long_timeout + ) + + if laser_state.DetailedState not in { + LaserDetailedState.NONPROPAGATING_CONTINUOUS_MODE, + LaserDetailedState.NONPROPAGATING_BURST_MODE, + }: + try: + await self.rem.tunablelaser.cmd_stopPropagateLaser.start( + timeout=self.laser_warmup + ) + laser_state = await self.rem.tunablelaser.evt_detailedState.next( + flush=True, timeout=self.long_timeout + ) + self.log.info(f"Laser state: {laser_state.DetailedState}") + except asyncio.TimeoutError: + raise RuntimeError( + "Tunable Laser did not stop propagating when commanded" + ) + async def prepare_for_flat(self, sequence_name: str) -> None: """Configure the ATMonochromator according to the flat parameters @@ -345,6 +473,7 @@ async def run_calibration_sequence( """Perform full calibration sequence, taking flats with the camera and all ancillary instruments. + Parameters ---------- sequence_name : `str` @@ -384,7 +513,7 @@ async def run_calibration_sequence( self.log.debug( f"Performing {calibration_type.name} calibration with {exposure.wavelength=}." ) - await self.change_laser_wavelength(wavelength=exposure.wavelength) + mtcamera_exposure_info: dict = dict() for exptime in config_data["exposure_times"]: @@ -400,6 +529,7 @@ async def run_calibration_sequence( mtcamera_exposure_info.update(exposure_info) if calibration_type == CalibrationType.Mono: + await self.change_laser_wavelength(wavelength=exposure.wavelength) self.log.debug( "Taking data sequence without filter for monochromatic set." ) diff --git a/tests/maintel/test_mtcalsys.py b/tests/maintel/test_mtcalsys.py index de73e0f3..fdabcd87 100644 --- a/tests/maintel/test_mtcalsys.py +++ b/tests/maintel/test_mtcalsys.py @@ -48,6 +48,7 @@ async def setup_mocks(self) -> None: "evt_largeFileObjectAvailable.next.side_effect": self.mock_fiberspectrograph_lfoa } ) + self.mtcalsys.rem.tunablelaser.configure_mock() async def setup_types(self) -> None: pass @@ -112,6 +113,35 @@ async def test_change_laser_wavelength(self) -> None: await self.mtcalsys.change_laser_wavelength(wavelength=500.0) + async def test_setup_laser(self) -> None: + config_data = self.mtcalsys.get_calibration_configuration("laser_functional") + + await self.mtcalsys.setup_laser( + mode=config_data["laser_mode"], + wavelength=config_data["wavelength"], + optical_configuration=config_data["optical_configuration"], + use_projector=False, + ) + + self.mtcalsys.rem.tunablelaser.cmd_setOpticalConfiguration.set_start.assert_awaited_with( + configuration=config_data["optical_configuration"], + timeout=self.mtcalsys.long_timeout, + ) + + async def test_laser_start_propagation(self) -> None: + await self.mtcalsys.laser_start_propagate() + + self.mtcalsys.rem.tunablelaser.cmd_startPropagateLaser.start.assert_awaited_with( + timeout=self.mtcalsys.laser_warmup + ) + + async def test_laser_stop_propagation(self) -> None: + await self.mtcalsys.laser_stop_propagate() + + self.mtcalsys.rem.tunablelaser.cmd_stopPropagateLaser.start.assert_awaited_with( + timeout=self.mtcalsys.laser_warmup + ) + async def test_prepare_for_whitelight_flat(self) -> None: mock_comcam = ComCam( "FakeDomain", log=self.log, intended_usage=ComCamUsages.DryTest