Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/lsst/obs/base/_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
66 changes: 50 additions & 16 deletions python/lsst/obs/base/formatters/fitsExposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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).
Expand All @@ -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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shame we don't put more metadata in the visit record which the Formatter already has access to.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few places in pipelines (especially AP) where a visit.group would have been particularly useful. Not sure it's worth changing it now, though.

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
2 changes: 1 addition & 1 deletion python/lsst/obs/base/ingest_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion python/lsst/obs/base/makeRawVisitInfoViaObsInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are we getting upper case values? ObservationInfo always generates lower case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VisitInfoTranslator lower cases that when generating the ObservationInfo.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the LSSTCam translator is the outlier that doesn't do that lowercasing. So this is a case where I thought it mattered before (but it didn't, because that translator wasn't getting loaded in the converter task), and it doesn't matter now (because we're explicitly avoiding the per-instrument translators now). And the real fix probably belongs in obs_lsst, though this might still be worth keeping for extra defensiveness.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely not harming anything by forcing lower here.

rotType = RotType.SKY
argDict["rotType"] = rotType

Expand Down
29 changes: 28 additions & 1 deletion tests/test_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()
8 changes: 8 additions & 0 deletions tests/test_makeRawVisitInfoViaObsInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading