Skip to content

Make the RomanOptics chromatic=True option more effective and more efficient.#198

Merged
rmjarvis merged 19 commits intomainfrom
roman4
Apr 24, 2026
Merged

Make the RomanOptics chromatic=True option more effective and more efficient.#198
rmjarvis merged 19 commits intomainfrom
roman4

Conversation

@rmjarvis
Copy link
Copy Markdown
Owner

@rmjarvis rmjarvis commented Apr 5, 2026

This PR is focused on the implementation of the chromatic=True option for RomanOptics. It includes the following pieces:

  • Added the bandpass specification in input, rather than as a detail of the RomanOptics PSF.
  • If the bandpass is a galsim.roman version, then RomanOptics class can get the filter name from that. If not (e.g. if we want to specify a different version that is more accurate perhaps), then filter_name can be specified manually for use in the getPSF call.
  • The input SED is immediately multiplied by the bandpass, and that combination is thinned directly, rather than only thinning the SED and still using the unthinned Bandpass, which was still quite slow.
  • Since we've already multiplied by the filter throughput, the draw command needs to use a flat version of the bandpass with just the end points, and throughput = 1 in between. This is called flat_bandpass in the code.
  • I added a more aggressive kind of thinning, which specifies a maximum number of samples to use in the SED. E.g. sed_max_samples=3 keeps only 3 points, so piecewise linear with 2 segments. I think given how uncertain our SED knowledge will be in practice, this will be acceptable on real data. Either sed_tol or sed_max_samples or both are available for us to try out on the data.
  • Fixed a few bugs that turned up related to using ChromaticObjects in various places.
  • Fixed an error in both RomanOpticalModel and OpticalModel. They should both have _centered=True, not False. With _centered=False, they don't re-centroid properly with uncertain star positions. This hadn't turned up, since both of them are usually used in combination with other components (e.g. in a SumPSF), which allows for recentroiding. It turned up in one of my tests here, but the error is unrelated to the chromatic option.

The config file in devel/roman using chromatic=True is about 5-6 times slower than the achromatic version. This is with just a linear approximation to the SED (sed_max_samples=2) -- it's almost double that with 3 samples. This seems reasonable enough given the extra work intrinsic to drawing chromatically, but there might be further optimizations available to try. E.g. I haven't tried using photon shooting (for either one), which might be worth trying despite the fact that it results in noisy images (which makes the empirical derivatives tricky, but there might be ways around that). I also don't have a good feel for how much the different SED approximations matter in practice, since we haven't tested yet with realistic SEDs.

I'll also reiterate that even with chromatic=False, the final PSF model is chromatic. Just the fitting is done achromatically with the PSF being evaluated at just a single wavelength (the bandpass effective wavelength). I'm open to adjustments to the naming of this parameter, since it's potentially confusing as it currently stands.

Comment thread piff/galsim_patch.py Outdated
bp_wave_native = _bandpass_native_waves(newx[in_band])
tp_native[in_band] = bandpass._tp(bp_wave_native) / bandpass.wave_factor
nz = tp_native != 0. # Don't divide by 0.
assert np.all(newf[~nz] == 0.)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's an temporary compatibility patch, but for clarity an error message can be added something like "thinned sed has non-zero values where throughput is zero"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Comments on this file should probably be migrated to GalSim-developers/GalSim#1355.
(I no longer have this patch in the PR.) But also, I don't think it's possible for this assert to trigger. That's why it's an assert, not a normal error.

Comment thread piff/galsim_patch.py Outdated
# Note that this is thinning in native units, not nm and photons/nm.
interpolant = (self.interpolant if not isinstance(self._spec, LookupTable)
else self._spec.interpolant)
newx, newf = utilities.thin_tabulated_values(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it can be renamed as thinned_lambda thinned_flux but if this module is going to be removed anyway please ignore this comment

Comment thread piff/psf.py Outdated
self.wcs = wcs
self.pointing = pointing
if self.wcs is None:
logger.error("WARNING: wcs and pointing should now be given in process, not fit.")
Copy link
Copy Markdown
Collaborator

@HyeongHan HyeongHan Apr 5, 2026

Choose a reason for hiding this comment

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

If it's warning, shouldn't it be logger.warning?
If it's an error, the message should be ERROR.
self.pointing is None condition also be added?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

All such messages in Piff are done through the logger mechanism, not python's warnings module. Given that, logger.error vs logger.warning dictates what verbosity level it shows up. logger.error's are always written, which I think makes sense for deprecation warnings. The user should fix it. Warnings that are merely about something going a little weird in the processing, but might be ok are logger.warning.

Comment thread piff/convolvepsf.py

def set_context(self, wcs, pointing, bandpass):
super().set_context(wcs, pointing, bandpass)
if isinstance(self.components, list):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if self.components is not None: can be more straight forward.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

None wouldn't work. It's either a list or an int. The latter is when it is still in the process of being read in from a file, but _finish_read hasn't been called yet. The other check for whether the read is complete uses this, so I'm inclined to keep it this way for consistency.

Copy link
Copy Markdown
Collaborator

@nihardalal nihardalal left a comment

Choose a reason for hiding this comment

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

Looks good from my end - I think I have a couple questions/comments here and there, feel free to answer as/if necessary

Comment thread piff/roman/roman_psf.py
return self._interpolate_samples(star, sample_profiles)

def _draw_model_image_from_samples(self, star, sample_profiles, convert_func=None):
def _draw_model_image_from_samples(self, star, sample_profiles, convert_func):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is convert_func a mandatory argument now?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I generally prefer my backend functions (I.e. not ones that users would use) to not use default arguments. That way when I change anything about them, I immediately catch any uses where I forgot to update to a new signature, rather than silently using a default value that isn't appropriate. So this should not have used a default =None in the first place.

Comment thread piff/roman/roman_psf.py
self.bandpass = bandpass
self.flat_bandpass = make_flat(bandpass)

def _require_bandpass(self):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe consider renaming this method to _get_bandpass? This is just a matter of preference, but require suggests a bool output to me, and we use this more like a get

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The only thing this does is validate that the PSF object has a bandpass set (which happens upstream of the Roman-specific processing). Returning that value is secondary. Maybe the better change is just to remove the return value.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it is called in a place where it does actually return the value, so feel free to leave as is.

Comment thread piff/roman/roman_psf.py
# Only call set_num if they are actually built.
if isinstance(self.model, Model):
self.model.set_num(num)
if isinstance(self.interp, Interp):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Isn't the interp instance also potentially not real during the read?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes, but they are real or not real in tandem. So I simplified this to a single check. If Model is real, so is interp, and they can both do their set_num now. Otherwise they are both fake and will be dealt with by the read function.

Comment thread piff/input.py
g = np.linalg.lstsq(Aw, fw, rcond=None)[0]

# g is now the vector that gives the best least squares approximation f'.
return g
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Makes sense to me!

Comment thread piff/input.py
resid[selected] = -1.0 # Don't reselect something we already have.
bisect.insort(selected, np.argmax(resid))

return candidate_wave[selected]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also looks good to me!

Comment thread piff/input.py
wave = np.array(sed.wave_list, dtype=float)
if len(wave) == 0:
# If the sed doesn't have a wave_list, we can't thin it. (This is very rare!)
sed = sed.withFlux(1.0, flat_bandpass)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this could potentially be a failure mode that merits some extra documentation? I can imagine us accidentally having SEDs saved in an incorrect format and then this being some weird behavior that shows up (but I might be overthinking)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

It's pretty rare, but I think it's not something that would merit an error. It requires both the SED and the bandpass to be lambda expressions, rather than lookup tables read from a file. This is technically allowed, although it would require a user doing some very specific special case work. There is a test that covers this branch (https://github.com/rmjarvis/Piff/pull/198/changes#diff-4650d01edc49b02fbf29d980f24442b85e599827c64340d1ec43820d757e511eR2246), but I'm not really worried about this happening by accident in realistic scenarios.

Comment thread tests/test_input.py
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tests look good to me here!

Comment thread tests/test_roman.py
assert 0 < np.abs(star.fit.center[0]) < 0.05 # centroid shift should be small.
assert 0 < np.abs(star.fit.center[1]) < 0.05


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tests looks good and comprehensive

@rmjarvis rmjarvis merged commit cc23aaf into main Apr 24, 2026
9 checks passed
@rmjarvis rmjarvis deleted the roman4 branch April 24, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants