DM-54556: Add VisitImage.to_legacy and supporting code.#44
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #44 +/- ##
==========================================
- Coverage 74.15% 73.30% -0.86%
==========================================
Files 90 89 -1
Lines 10556 10718 +162
==========================================
+ Hits 7828 7857 +29
- Misses 2728 2861 +133 ☔ View full report in Codecov by Sentry. |
d25761e to
46884ab
Compare
|
|
||
| @property | ||
| def is_constant(self) -> bool: | ||
| return (self._data == self._data[0, 0]).all() |
There was a problem hiding this comment.
Maybe add a note that exact floating point comparison here is deliberate.
| pass | ||
| else: | ||
| # Image already has calibrated pixel units; return a constant. | ||
| # TODO[DM-54556]: make sure this shouldn't be 1/factor. |
There was a problem hiding this comment.
I assume this ticket is blocking us using this code for DP2 and prompt so will be fixed soon?
There was a problem hiding this comment.
Aha, no! That is this very ticket, and I forgot to go back and deal with it before sending out for review.
It actually can't come up at all right now; we'll always have factor == 1.0 when converting from afw. So it's kind of annoying that the extra generality of lsst.images creates this hypothetical problem. But it's fixed and tested now, and that extra testing revealed another problem due to an afw limitation (in field_to_legacy_photo_calib) that I've now fixed (and then squashed; sorry).
There was a problem hiding this comment.
Oops. I didn't notice it was this ticket 😄
| # legacy metadata conponent, to make sure we have everything. | ||
| if isinstance(self._opaque_metadata, FitsOpaqueMetadata): | ||
| result_info.getMetadata().update(self._opaque_metadata.headers[ExtensionKey()]) | ||
| result_info.getMetadata().update(self.metadata) |
There was a problem hiding this comment.
In some sense it might be preferable to namespace the modern non-FITS metadata so that it can't overwrite a pre-existing FITS header.
So something like:
result_info.getMetadata().update({f"LSST IMAGE {k.upper()}": v for k, v in self.metadata.items()})and then from_legacy can reverse that.
There was a problem hiding this comment.
Good idea! I've taken it a step further and mapped each item it to a pair of LSST IMAGES KEY {n} and LSST IMAGES VALUE {n} cards, so we can preserve case, too.
Testing this reminded me that I hadn't been thinking of round-tripping VisitImage through the Exposure file format as a goal, because I don't think we necessarily need it for the Prompt or DP2 transition. That revealed a few new issues related to unit conversion (we have to fight Astropy to let us read and write non-standard BUNIT values) and data IDs (since rewriting should drop the LSST BUTLER DATAID keys, but we want those on read). Please take a look at the two new commits.
f5ee16e to
9df43a6
Compare
| pass | ||
| else: | ||
| # Image already has calibrated pixel units; return a constant. | ||
| # TODO[DM-54556]: make sure this shouldn't be 1/factor. |
There was a problem hiding this comment.
Aha, no! That is this very ticket, and I forgot to go back and deal with it before sending out for review.
It actually can't come up at all right now; we'll always have factor == 1.0 when converting from afw. So it's kind of annoying that the extra generality of lsst.images creates this hypothetical problem. But it's fixed and tested now, and that extra testing revealed another problem due to an afw limitation (in field_to_legacy_photo_calib) that I've now fixed (and then squashed; sorry).
| # legacy metadata conponent, to make sure we have everything. | ||
| if isinstance(self._opaque_metadata, FitsOpaqueMetadata): | ||
| result_info.getMetadata().update(self._opaque_metadata.headers[ExtensionKey()]) | ||
| result_info.getMetadata().update(self.metadata) |
There was a problem hiding this comment.
Good idea! I've taken it a step further and mapped each item it to a pair of LSST IMAGES KEY {n} and LSST IMAGES VALUE {n} cards, so we can preserve case, too.
Testing this reminded me that I hadn't been thinking of round-tripping VisitImage through the Exposure file format as a goal, because I don't think we necessarily need it for the Prompt or DP2 transition. That revealed a few new issues related to unit conversion (we have to fight Astropy to let us read and write non-standard BUNIT values) and data IDs (since rewriting should drop the LSST BUTLER DATAID keys, but we want those on read). Please take a look at the two new commits.
| visit = _extract_or_check_header("LSST BUTLER DATAID VISIT", visit, primary_header, None, int) | ||
| visit = _extract_or_check_header( | ||
| "LSST BUTLER DATAID VISIT", visit, primary_header, obs_info.visit_id, int | ||
| ) |
There was a problem hiding this comment.
@timj I had to change this to get the visit ID from the ObservationInfo when LSST BUTLER DATAID VISIT isn't (as is the case in the tests when we write a VisitImage as an lsst.afw.image.Exposure, at least when that uses a data ID that doesn't have "visit" in it. I wanted to make sure this is okay, since it seemed like you might have had a reason for not falling back to it before.
There was a problem hiding this comment.
Oh. Right. I think you have to use obs_info.exposure_id because the ObservationInfo visit_id is not what you actually want and now we know that visi ID and exposure ID are meant to be the same.
timj
left a comment
There was a problem hiding this comment.
The new metadata header writing looks good to me.
| strip_butler_cards(primary_header) | ||
| metadata: dict[str, Any] = {} | ||
| for n in itertools.count(): | ||
| if (key := header.pop(f"LSST IMAGES KEY {n + 1}", None)) is None: |
There was a problem hiding this comment.
Technically a Astropy FITS Header can have a value of None but if you've written an undefined value into the header card you probably have other issues.
There was a problem hiding this comment.
Well, it's not a such a crazy thing to do if you consider the fact that we're trying to round-trip a dict that may well have a None value. I'll switch to ... as the sentinal value.
9df43a6 to
c0224b8
Compare
Since these aren't typed upstream, this does very little for static analysis, but Sphinx uses these types, too, and it's just a better pattern to establish.
None of this test would run without afw.
Since the concrete field types that can support to_legacy already do, this just moves a type-analysis error into a runtime error (which is what we want). That in turn helps us add a PhotoCalib converter to the base class.
'band' is a new required attribute. 'ID' is just stuffed into the metadata for now - we'd like to retire it someday, but as long as we need to be able to roundtrip a legacy VisitImage we'll need to carry it around.
This provides testing for code in obs_base.
All code using this module was removed on DM-54976.
c0224b8 to
e35eccb
Compare
This will inevitably drop background information and photometric scalings attached to already-calibrated images (because there's nowhere to put that information on a legacy Exposure), but it preserves everything else.
e35eccb to
58cfa82
Compare
Checklist
doc/changes