-
Notifications
You must be signed in to change notification settings - Fork 14
Make the RomanOptics chromatic=True option more effective and more efficient. #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b30096f
b237700
82267c8
c387637
7fcf9eb
7bbdcca
fdb9f12
a1ec916
933e6ce
b60a3d6
990b44a
022545e
94f2299
56e2ddf
30815fb
766f158
312cb3e
921ba6f
fb6e1d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import numpy as np | ||
| import galsim | ||
| from astropy import units | ||
| from galsim import utilities | ||
| from galsim.table import LookupTable, _LookupTable | ||
| from galsim.sed import SED | ||
| from galsim.errors import GalSimError | ||
|
|
||
| # Temporary compatibility patch for GalSim < 2.9. | ||
| # Remove this module once Piff can depend on GalSim 2.9+, which includes thin(bandpass=...). | ||
|
|
||
| def _piff_sed_thin(self, rel_err=1.e-4, trim_zeros=True, preserve_range=True, fast_search=True, | ||
| bandpass=None): # pragma: no cover | ||
| def _bandpass_native_waves(waves): | ||
| # The thinning algorithm runs on the SED's native tabulation grid, but when a | ||
| # bandpass target is provided we need to evaluate the throughput at the same physical | ||
| # observed wavelengths. This helper converts SED-native wavelengths into the native | ||
| # wavelength units used by bandpass._tp. | ||
| if self.wave_factor: | ||
| observed_nm = waves / self.wave_factor | ||
| else: | ||
| observed_nm = (waves * self.wave_type).to(units.nm, units.spectral()).value | ||
| observed_nm *= (1.0 + self.redshift) | ||
| if bandpass.wave_factor: | ||
| return observed_nm * bandpass.wave_factor | ||
| else: | ||
| return (observed_nm * units.nm).to(bandpass.wave_type, units.spectral()).value | ||
|
|
||
| if bandpass is not None: | ||
| if self.blue_limit > bandpass.red_limit or self.red_limit < bandpass.blue_limit: | ||
| raise GalSimError("Bandpass does not overlap the SED wavelength range.") | ||
| wave_list, _, _ = utilities.combine_wave_list(self, bandpass) | ||
| if preserve_range and not trim_zeros: | ||
| # If we want to preserve the range, add back the limits to each end. | ||
| front = [self.blue_limit] if self.blue_limit < wave_list[0] else [] | ||
| back = [self.red_limit] if self.red_limit > wave_list[-1] else [] | ||
| if front or back: | ||
| wave_list = np.concatenate((front, wave_list, back)) | ||
| else: | ||
| wave_list = self.wave_list | ||
|
|
||
| if len(wave_list) > 0: | ||
| rest_wave_native = self._get_rest_native_waves(wave_list) | ||
| spec_native = self._spec(rest_wave_native) | ||
|
|
||
| if bandpass is not None: | ||
| # Identify the overlapping region in the SED's native wavelength units to | ||
| # determine which portion of the wave_list is within the bandpass limits. | ||
| band_native_limits = self._get_rest_native_waves( | ||
| np.array([bandpass.blue_limit, bandpass.red_limit]) | ||
| ) | ||
| native_blue_limit = np.min(band_native_limits) | ||
| native_red_limit = np.max(band_native_limits) | ||
| in_band = np.logical_and( | ||
| rest_wave_native >= native_blue_limit, | ||
| rest_wave_native <= native_red_limit, | ||
| ) | ||
|
|
||
| # Compute the product SED * bandpass, since that is the quantity whose integral we | ||
| # want to preserve for this observation. | ||
| tp_native = np.zeros_like(rest_wave_native, dtype=float) | ||
| bp_wave_native = _bandpass_native_waves(rest_wave_native[in_band]) | ||
| tp_native[in_band] = bandpass._tp(bp_wave_native) / bandpass.wave_factor | ||
| spec_native *= tp_native | ||
|
|
||
| # 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( | ||
| rest_wave_native, spec_native, rel_err=rel_err, | ||
| trim_zeros=trim_zeros, preserve_range=preserve_range, | ||
| fast_search=fast_search, interpolant=interpolant) | ||
|
|
||
| if bandpass is not None: | ||
| # Convert the thinned product back into an SED by dividing out the bandpass | ||
| # wherever the throughput is non-zero. | ||
| in_band = np.logical_and(newx >= native_blue_limit, newx <= native_red_limit) | ||
| tp_native = np.zeros_like(newx, dtype=float) | ||
| 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.) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| newf[nz] /= tp_native[nz] | ||
|
|
||
| newspec = _LookupTable(newx, newf, interpolant=interpolant) | ||
| return SED(newspec, self.wave_type, self.flux_type, redshift=self.redshift, | ||
| fast=self.fast) | ||
| else: | ||
| return self | ||
|
|
||
|
|
||
| galsim.SED.thin = _piff_sed_thin | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,9 +41,12 @@ class PSF(object): | |
| # Normally overridden by subclasses. But this gives a reasonable default for | ||
| # tests that bypass places where it would be potentially set in normal operations. | ||
| degenerate_points = False | ||
| wcs = None | ||
| pointing = None | ||
| bandpass = None | ||
|
|
||
| @classmethod | ||
| def process(cls, config_psf, logger=None): | ||
| def process(cls, config_psf, wcs=None, pointing=None, bandpass=None, logger=None): | ||
| """Process the config dict and return a PSF instance. | ||
|
|
||
| As the PSF class is an abstract base class, the returned type will in fact be some | ||
|
|
@@ -56,14 +59,24 @@ def process(cls, config_psf, logger=None): | |
| This function merely creates a "blank" PSF object. It does not actually do any | ||
| part of the solution yet. Typically this will be followed by fit: | ||
|
|
||
| >>> psf = piff.PSF.process(config['psf']) | ||
| >>> stars, wcs, pointing = piff.Input.process(config['input']) | ||
| >>> psf.fit(stars, wcs, pointing) | ||
| >>> stars, wcs, pointing, bandpass = piff.Input.process(config['input']) | ||
| >>> psf = piff.PSF.process(config['psf'], wcs, pointing, bandpass) | ||
| >>> psf.fit(stars) | ||
|
|
||
| at which point, the ``psf`` instance would have a solution to the PSF model. | ||
|
|
||
| .. note:: | ||
|
|
||
| The preferred pattern now is to provide wcs and pointing here, but these used to | ||
| be set when calling fit. The old pattern is still supported, but deprecated. | ||
|
|
||
| :param config_psf: A dict specifying what type of PSF to build along with the | ||
| appropriate kwargs for building it. | ||
| :param wcs: A dict of WCS solutions indexed by chipnum. | ||
| :param pointing: A galsim.CelestialCoord object giving the telescope pointing. | ||
| [Note: pointing should be None if the WCS is not a CelestialWCS] | ||
| :param bandpass: Optional galsim.Bandpass shared by the input data. | ||
| [default: None] | ||
| :param logger: A logger object for logging debug info. [default: None] | ||
|
|
||
| :returns: a PSF instance of the appropriate type. | ||
|
|
@@ -92,6 +105,9 @@ def process(cls, config_psf, logger=None): | |
| # At top level, the num is always None. | ||
| # Composite PSF types will turn this into a series of integer values for each component. | ||
| psf.set_num(None) | ||
| psf.wcs = wcs | ||
| psf.pointing = pointing | ||
| psf.bandpass = bandpass | ||
|
|
||
| return psf | ||
|
|
||
|
|
@@ -373,7 +389,8 @@ def remove_outliers(self, stars, iteration, logger): | |
| nremoved = 0 | ||
| return stars, nremoved | ||
|
|
||
| def fit(self, stars, wcs, pointing, logger=None, convert_funcs=None, draw_method=None): | ||
| def fit(self, stars, wcs=None, pointing=None, logger=None, | ||
| convert_funcs=None, draw_method=None): | ||
| """Fit interpolated PSF model to star data using standard sequence of operations. | ||
|
|
||
| :param stars: A list of Star instances. | ||
|
|
@@ -391,8 +408,10 @@ def fit(self, stars, wcs, pointing, logger=None, convert_funcs=None, draw_method | |
| from .config import LoggerWrapper | ||
| logger = LoggerWrapper(logger) | ||
|
|
||
| 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.") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's warning, shouldn't it be logger.warning?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| self.wcs = wcs | ||
| self.pointing = pointing | ||
|
|
||
| # Initialize stars as needed by the PSF modeling class. | ||
| stars = self.initialize_flux_center(stars, logger=logger) | ||
|
|
@@ -766,9 +785,12 @@ def _write(self, writer, name, logger): | |
| if hasattr(self, 'stars'): | ||
| Star.write(self.stars, w, 'stars') | ||
| logger.verbose("Wrote the PSF stars to name %s", w.get_full_name('stars')) | ||
| if hasattr(self, 'wcs'): | ||
| if self.wcs is not None: | ||
| w.write_wcs_map('wcs', self.wcs, self.pointing) | ||
| logger.verbose("Wrote the PSF WCS to name %s", w.get_full_name('wcs')) | ||
| if self.bandpass is not None: | ||
| w.write_bandpass('bandpass', self.bandpass) | ||
| logger.verbose("Wrote the PSF bandpass to name %s", w.get_full_name('bandpass')) | ||
| self._finish_write(w, logger=logger) | ||
|
|
||
| @classmethod | ||
|
|
@@ -822,15 +844,17 @@ def _read(cls, reader, name, logger): | |
|
|
||
| with reader.nested(name) as r: | ||
| # Read the stars, wcs, pointing values | ||
| wcs, pointing = r.read_wcs_map('wcs', logger=logger) | ||
| bandpass = r.read_bandpass('bandpass') | ||
| stars = Star.read(r, 'stars') | ||
| if stars is not None: | ||
| logger.debug("stars = %s", stars) | ||
| psf.stars = stars | ||
| wcs, pointing = r.read_wcs_map('wcs', logger=logger) | ||
| if wcs is not None: | ||
| logger.debug("wcs = %s, pointing = %s",wcs,pointing) | ||
| logger.debug("wcs = %s, pointing = %s, bandpass = %s", wcs, pointing, bandpass) | ||
| psf.wcs = wcs | ||
| psf.pointing = pointing | ||
| psf.bandpass = bandpass | ||
|
|
||
| # Just in case the class needs to do something else at the end. | ||
| psf._finish_read(r, logger) | ||
|
|
||
There was a problem hiding this comment.
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