DM-54912: add photometric_scaling to VisitImage#39
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #39 +/- ##
==========================================
- Coverage 76.11% 75.01% -1.10%
==========================================
Files 91 91
Lines 10607 10805 +198
==========================================
+ Hits 8073 8105 +32
- Misses 2534 2700 +166 ☔ View full report in Codecov by Sentry. |
|
Note to self: if this lands before #37, update the docs there. If it lands after, update the docs here. |
natelust
left a comment
There was a problem hiding this comment.
There are lots of things to consider as quick design thoughts. I am pretty ok with whatever you come up with for an answer, but I wanted to bring things up before it gets baked in forever. I do think some of the nests of branches are a bit of a rats nest, but I am not sure if there is a create alternative.
| unit: astropy.units.UnitBase | None = None, | ||
| bounds: Bounds | None = None, | ||
| ) -> ChebyshevField: | ||
| """Convert from a legacy `lsst.afw.math.ChebyshevBoundedField`.""" |
There was a problem hiding this comment.
I think these need proper docstrings, someone running this would have no idea what the other things are without reading the code.
| bounds: Bounds | None = None, | ||
| ) -> Field: | ||
| """Convert a legacy `lsst.afw.math.BoundedField` subclass to a `BaseField` | ||
| object. |
| @@ -70,9 +77,9 @@ def field_from_legacy( | |||
|
|
|||
| match legacy_bounded_field: | |||
| case ChebyshevBoundedField(): | |||
There was a problem hiding this comment.
I'm 99% sure if you wanted to remove the (), you could just do afwMath.ChebyshevBoundedField , etc. Might make it clearer too, but the import is right above so not needed, just wanted to bring up the stylistic choice.
There was a problem hiding this comment.
That would actually turn this into an issubclass check instead of an isinstance check.
| def field_from_legacy_photo_calib( | ||
| legacy_photo_calib: LegacyPhotoCalib, | ||
| bounds: Bounds, | ||
| post_isr_unit: astropy.units.UnitBase = astropy.units.electron, |
There was a problem hiding this comment.
It seems a little weird to bake this directly into the parameter name
There was a problem hiding this comment.
I've been wondering about calling it instrumental_unit instead, and it sounds like I should do that (note that I can't just a call it "unit" because the full unit is something like nJy/electron; it's just the denominator I'm trying to name).
| bounds: Bounds, | ||
| post_isr_unit: astropy.units.UnitBase = astropy.units.electron, | ||
| ) -> Field | None: | ||
| """Convert a legacy `lsst.afw.image.PhotoCalib` into a `BaseField` object. |
There was a problem hiding this comment.
I strongly dislike the name BaseField to represent some kind of unit conversion. It makes it seem like it is the base class instance for all Fields. Or is it that, and you didn't specialize anything?
There was a problem hiding this comment.
This is indeed the base class for all fields. It's confusing because BaseField is the ABC, while Field is a type alias to a union of all the concrete field types; we need to use the former in docs (because Sphinx can't handle type aliases) but we want to use the latter in annotations to get better static analysis.
In afw, PhotoCalib is a nonpolymorphic class that has a BoundedField, but it doesn't really add anything on top of it except an expectation of the units. And here all the field types have explicit units, so I decided to just not have that extra level of indirection.
There was a problem hiding this comment.
but later if you need that concept in some other sense it gets ambiguous, and I can't really ask the interpreter (type) to understand what the thing is if I have one
There was a problem hiding this comment.
I'm just pretty confident we're not going to need that concept in the future. I don't think it's fundamentally different from using an Image to represent both [the image plane of] science data and a flat field.
There was a problem hiding this comment.
And you can't think of anytime there would be Field types that would not be appropriate for being a photoclaib?
There was a problem hiding this comment.
There might be some that wouldn't be very useful - e.g. since it's multiplicative, I don't really see a sum of fields ever being used. But I don't think there could ever be one that actually couldn't be used because it would be problematic. The whole point of the BaseField hierarchy is that you don't care what subclass you get, and we only have an enumerated closed hierarchy for serialization (i.e. because otherwise you need a plugin system to deserialize and that gets really messy).
| compare_aperture_corrections_to_legacy( | ||
| tc, aperture_corrections, legacy_exposure.info.getApCorrMap(), tiny_bbox | ||
| ) | ||
| if (photometric_scaling := alternates.get("photometic_scaling", ...)) is not ...: |
There was a problem hiding this comment.
Is this because None is a valid return type?
| legacy_exposure: Any | ||
| plane_map: dict[str, MaskPlane] | ||
| visit_image: VisitImage | ||
| unit: u.UnitBase |
There was a problem hiding this comment.
Is this actually a mixing for final data classes, so you specify these as class attribute here, or are they really class attributes?
There was a problem hiding this comment.
Or are you just setting these for typing reasons?
There was a problem hiding this comment.
Mostly just typing reasons (even though this file isn't formally type-checked, I like reducing editor squiggles where I can).
| Requires legacy code. | ||
| """ | ||
|
|
||
| @classmethod |
There was a problem hiding this comment.
This seems like an extremely strange pattern of class creation. Why does this need to be class level, is it to stop multiple reads or something? Why does this not become standard class instantiation or something?
There was a problem hiding this comment.
This is how you get unittest to do test-setup only once (or at least only once per process). It is indeed to reduce I/O in this case.
I don't really know why unittest doesn't use constructors for initialization; presumably they wanted to use them for something else. But that's the pattern.
| return self._photometric_scaling | ||
|
|
||
| @photometric_scaling.setter | ||
| def photometric_scaling(self, value: Field | None): |
There was a problem hiding this comment.
Is it really true all Field s are valid?
There was a problem hiding this comment.
Yes. This is a deliberate change from afw (all fields were valid there, too, but PhotoCalib added another level of indirection that I'm deliberately dropping).
| An image with the given units. Will always share most attributes | ||
| with self, with the `image` and `variance` copied only if they | ||
| need to be modified (i.e. they do not already have the right | ||
| units). |
There was a problem hiding this comment.
A sometimes copied method is worse than a never copied or always copied method in my opinion, but I get it might be an efficiency thing, but I can see this becoming no end a source of bugs for us in the future.
There was a problem hiding this comment.
I'd usually very much agree. I've done it this way here because I think it's going to be used almost all of the time in a
visit_image = visit_image.convert_unit(...)
sense where you blow away what you had before and are really wanting to simulate an in-place operation as much as possible for efficiency. But we can't do that directly because in-place changing the units of an array that might be shared is an even worse problem (IMO).
There was a problem hiding this comment.
I'm really not sure I agree on this one. I agree with the reasoning of what you said, but this is such a giant gaping hole in an otherwise really great api I think that something somewhere has been overlooked or there has to be a better pattern. I see what you said in your example, but I can also see someone doing:
converted_image = visit_image.conver_unit(...)
function_on_new(converted_image) # mutating
use_old_image(visit_image)
There was a problem hiding this comment.
I think what I'm missing is a better alternative. The only one I can think of is to always copy everything by default, with a kwarg to switch back to the current behavior (which I want to preserve as an option because I know it's the right one for the use case I know about). That would make the behavior a lot more explicit, but more verbose in the usual case.
There was a problem hiding this comment.
The only other thing I can think of is an abstraction with COW
There was a problem hiding this comment.
I tried a few things before things got to this stage, and I think I've convinced myself that COW is just impossible with Numpy.
Anyhow, I've switched to the kwarg control proposed above. I worry a bit that it's extra logic to maintain that won't get used (i.e. we'll always pass the options to make it behave like it was before), but even in that case it is easier to read the code and see what's going on.
0beed28 to
5a5aa4f
Compare
MyPy is having a hard time working out that
if x in (a, b):
if x == a:
return
implies x != a
9949daa to
ff38799
Compare
7e5d2d2 to
4a3c4db
Compare
Checklist
doc/changes