diff --git a/python/lsst/obs/base/_instrument.py b/python/lsst/obs/base/_instrument.py index 88a02744..a66ac016 100644 --- a/python/lsst/obs/base/_instrument.py +++ b/python/lsst/obs/base/_instrument.py @@ -659,7 +659,7 @@ def makeExposureRecordFromObsInfo( icrs = obsInfo.tracking_radec.icrs ra = float(icrs.ra.degree) dec = float(icrs.dec.degree) - if obsInfo.boresight_rotation_coord == "sky": + if (coord := obsInfo.boresight_rotation_coord) is not None and coord.lower() == "sky": assert obsInfo.boresight_rotation_angle is not None sky_angle = float(obsInfo.boresight_rotation_angle.degree) if obsInfo.altaz_begin is not None: diff --git a/python/lsst/obs/base/formatters/fitsExposure.py b/python/lsst/obs/base/formatters/fitsExposure.py index c75ad5f3..5ce2d7e2 100644 --- a/python/lsst/obs/base/formatters/fitsExposure.py +++ b/python/lsst/obs/base/formatters/fitsExposure.py @@ -165,16 +165,13 @@ def storageClass_dtype(self) -> np.dtype | None: def read_from_local_file(self, path: str, component: str | None = None, expected_size: int = -1) -> Any: # Docstring inherited. - if future_type := _get_future_image_type(self.file_descriptor.readStorageClass.name, component): - return future_type.read_legacy( - path, - component=component, - preserve_quantization=self.checked_parameters.get("preserve_quantization", False), - ) - elif self.checked_parameters.get("preserve_quantization", False): - raise NotImplementedError( - "preserve_quantization=True only works when converting to VisitImage on read." - ) + future_type, read_kwargs = _get_future_image_type_and_kwargs( + self.file_descriptor.readStorageClass.name, + component=component, + parameters=self.checked_parameters, + ) + if future_type is not None: + return future_type.read_legacy(path, component=component, **read_kwargs) # The methods doing the reading all currently assume local file # and assume that the file descriptor refers to a local file. @@ -893,7 +890,12 @@ def _fixFilterLabels( return data_id_filter_label -def _get_future_image_type(storage_class_name: str, component: str | None) -> type[Any] | None: +def _get_future_image_type_and_kwargs( + storage_class_name: str, component: str | None, parameters: dict[str, Any] +) -> tuple[type[Any] | None, dict[str, Any]]: + future_type: type[Any] | None = None + needs_exposure_record: bool = False + read_kwargs: dict[str, Any] = {} match storage_class_name, component: case ( ("VisitImage", None) @@ -903,15 +905,17 @@ def _get_future_image_type(storage_class_name: str, component: str | None) -> ty ): from lsst.images import VisitImage - return VisitImage + future_type = VisitImage + needs_exposure_record = True case ("DifferenceImage", None): from lsst.images import DifferenceImage - return DifferenceImage + future_type = DifferenceImage + needs_exposure_record = True case ("ImageV2", "image" | "variance") | ("MaskV2", "mask"): from lsst.images import MaskedImage - return MaskedImage + future_type = MaskedImage # The components below can't be used unless we fix daf_butler # restrictions on component names (they're checked against the original # storage class component names). @@ -923,5 +927,35 @@ def _get_future_image_type(storage_class_name: str, component: str | None) -> ty ): from lsst.images import VisitImage - return VisitImage - return None + future_type = VisitImage + needs_exposure_record = True + + exposure_record = parameters.get("exposure_record") + if needs_exposure_record: + if exposure_record is None: + raise ValueError( + "Converting a legacy lsst.afw.image.Exposure to VisitImage or DifferenceImage " + "requires the 'exposure_record' parameter (lsst.daf.butler.DimensionRecord)." + ) + read_kwargs["exposure_record"] = exposure_record + elif exposure_record is not None: + raise ValueError( + "The 'exposure_record' parameter is only valid when converting from a " + "legacy lsst.afw.image.Exposure to a VisitImage or DifferenceImage." + ) + + if "preserve_quantization" in parameters: + if future_type is None: + raise NotImplementedError( + "preserve_quantization=True only works when converting to lsst.images types on read." + ) + read_kwargs["preserve_quantization"] = parameters["preserve_quantization"] + + if future_type is not None and ( + extra := parameters.keys() - {"preserve_quantization", "exposure_record"} + ): + raise NotImplementedError( + f"Parameter(s) {extra} are not supported when converting to lsst.images types on read." + ) + + return future_type, read_kwargs diff --git a/python/lsst/obs/base/ingest_tests.py b/python/lsst/obs/base/ingest_tests.py index fd302197..b46c01aa 100644 --- a/python/lsst/obs/base/ingest_tests.py +++ b/python/lsst/obs/base/ingest_tests.py @@ -556,7 +556,7 @@ def testDefineVisits(self) -> None: assert isinstance(foundVisit.region, lsst.sphgeom.Region) # Use bare assert for mypy. assert dataId.region is not None - self.assertTrue(foundVisit.region.contains(dataId.region)) # type: ignore[call-overload] + self.assertTrue(foundVisit.region.contains(dataId.region)) # Check obscore table again. self._check_obscore(butler.registry, has_visits=True) diff --git a/python/lsst/obs/base/makeRawVisitInfoViaObsInfo.py b/python/lsst/obs/base/makeRawVisitInfoViaObsInfo.py index 00bad26d..bacf5c2f 100755 --- a/python/lsst/obs/base/makeRawVisitInfoViaObsInfo.py +++ b/python/lsst/obs/base/makeRawVisitInfoViaObsInfo.py @@ -203,7 +203,7 @@ def observationInfo2visitInfo( if obsInfo.boresight_rotation_coord is not None: rotType = RotType.UNKNOWN - if obsInfo.boresight_rotation_coord == "sky": + if obsInfo.boresight_rotation_coord.lower() == "sky": rotType = RotType.SKY argDict["rotType"] = rotType diff --git a/tests/test_instrument.py b/tests/test_instrument.py index 023d2a79..ebfe9ef0 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -24,7 +24,13 @@ import datetime import unittest -from lsst.obs.base import Instrument +import astropy.units as u +from astro_metadata_translator import ObservationInfo +from astropy.coordinates import SkyCoord +from astropy.time import Time + +from lsst.daf.butler import DimensionUniverse +from lsst.obs.base import Instrument, makeExposureRecordFromObsInfo from lsst.obs.base.instrument_tests import DummyCam, InstrumentTestData, InstrumentTests @@ -69,6 +75,27 @@ def test_group_name(self): with self.assertRaises(ValueError): self.instrument.group_name_to_group_id("no_int") + def test_sky_angle_case_insensitive(self): + """Test that makeExposureRecordFromObsInfo transfers sky_angle + regardless of the boresight_rotation_coord capitalization. + """ + obs_info = ObservationInfo( + instrument="DummyCam", + exposure_id=1, + exposure_time=10.0 * u.s, + exposure_time_requested=10.0 * u.s, + datetime_begin=Time("2025-01-01T00:00:00", scale="utc"), + datetime_end=Time("2025-01-01T00:00:10", scale="utc"), + observing_day=20250101, + observation_type="science", + physical_filter="g", + tracking_radec=SkyCoord("00:00:00.0 +00:00:00.0", unit="deg"), + boresight_rotation_angle=45.0 * u.deg, + boresight_rotation_coord="SKY", + ) + record = makeExposureRecordFromObsInfo(obs_info, DimensionUniverse()) + self.assertEqual(record.sky_angle, 45.0) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_makeRawVisitInfoViaObsInfo.py b/tests/test_makeRawVisitInfoViaObsInfo.py index 9793eb4e..fd2a903b 100644 --- a/tests/test_makeRawVisitInfoViaObsInfo.py +++ b/tests/test_makeRawVisitInfoViaObsInfo.py @@ -250,6 +250,14 @@ def testObservationInfo2VisitInfo(self): case _: raise RuntimeError(f"Encountered unexpected type for property {prop}") + def test_sky_uppercase(self): + with self.assertWarns(UserWarning): + obs_info = ObservationInfo(self.header, translator_class=NewTranslator) + with obs_info.edit_copy() as obs_info: + obs_info.boresight_rotation_coord = "SKY" + visit_info = MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(obs_info) + self.assertEqual(visit_info.getRotType(), lsst.afw.image.RotType.SKY) + if __name__ == "__main__": unittest.main()