-
Notifications
You must be signed in to change notification settings - Fork 9
FEAT - Add chromatic psf and the corresponding tests (Issue #251) #253
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
Open
MaxRonce
wants to merge
16
commits into
GalSim-developers:main
Choose a base branch
from
MaxRonce:chromatic_psf
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
b890474
FEAT - Add chromatic psf and the corresponding tests
MaxRonce 3a2a14f
Merge branch 'main' into chromatic_psf
beckermr 67dcbae
Merge branch 'main' into chromatic_psf
beckermr c0cd2de
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 539826b
Add test_chromatic_jax.py file
beckermr 19e960d
Merge branch 'main' into chromatic_psf
beckermr 0d0f51f
Merge branch 'main' into chromatic_psf
beckermr 86249e6
Align chromatic API docs with upstream
MaxRonce f1cdc92
Make Bandpass throughput tracing-safe
MaxRonce 8ffb1a5
Annotate SED chromatic API subset
MaxRonce 294d4ee
Fix chromatic API semantics
MaxRonce d071dd1
Add chromatic API and JAX regression tests
MaxRonce 60d6a6d
Fix chromatic JAX test import formatting
MaxRonce c56234e
Clean up chromatic imports and helper names
MaxRonce ed243f9
Move chromatic API notes into implements metadata
MaxRonce 4ffdfe6
Update pytest hooks for pytest 9
MaxRonce File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| Wavelength-dependent Profiles | ||
| ============================= | ||
|
|
||
| .. currentmodule:: jax_galsim | ||
|
|
||
| JAX-GalSim supports a JAX-native subset of GalSim chromatic rendering. The | ||
| core use case is a wavelength-dependent PSF convolved with a source whose SED | ||
| is a traced JAX array: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import jax | ||
| import jax.numpy as jnp | ||
| import jax_galsim as galsim | ||
|
|
||
| wave = jnp.linspace(400.0, 900.0, 256) | ||
| sed = galsim.SED(wave, jnp.ones_like(wave)) | ||
| bandpass = galsim.Bandpass.tophat(550.0, 750.0) | ||
|
|
||
| gal = galsim.Gaussian(half_light_radius=0.5) * sed | ||
| psf = galsim.ChromaticAtmosphere(fwhm_ref=0.7, lam_ref=700.0, alpha=-0.2) | ||
| final = galsim.ChromaticConvolution([gal, psf]) | ||
| image = final.drawImage(bandpass, nx=64, ny=64, scale=0.2, n_waves=32) | ||
|
|
||
| Separable chromatic objects, such as ``GSObject * SED``, are rendered by | ||
| integrating only the scalar spectral weight, then drawing one unit-flux spatial | ||
| profile. Non-separable chromatic objects, such as a chromatic PSF whose size | ||
| changes with wavelength, are rendered by integrating their Fourier-space values | ||
| over a fixed wavelength grid. | ||
|
|
||
| The wavelength grid size is static, while SED flux values are traced. This | ||
| keeps the rendering path compatible with ``jax.jit`` and ``jax.grad``. | ||
|
|
||
| Spectral objects | ||
| ---------------- | ||
|
|
||
| .. autoclass:: SED | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| .. autoclass:: Bandpass | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| Chromatic objects | ||
| ----------------- | ||
|
|
||
| .. autoclass:: ChromaticObject | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| .. autoclass:: SimpleChromaticTransformation | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| .. autoclass:: ChromaticAtmosphere | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| .. autoclass:: ChromaticConvolution | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| .. autoclass:: ChromaticSum | ||
| :members: | ||
| :show-inheritance: | ||
|
|
||
| Compatibility notes | ||
| ------------------- | ||
|
|
||
| ``galsim.Convolve`` dispatches to ``ChromaticConvolution`` when any input is | ||
| chromatic. Multiplication follows GalSim's common pattern: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| chromatic_gal = galsim.Gaussian(half_light_radius=0.5) * sed | ||
| same_object = sed * galsim.Gaussian(half_light_radius=0.5) | ||
|
|
||
| The current implementation focuses on differentiable array-backed SEDs, | ||
| array-backed bandpasses, separable chromatic sources, and non-separable | ||
| Gaussian/Moffat atmospheric PSFs. Full GalSim spectral I/O, lookup-table | ||
| metadata, Airy/OpticalPSF chromatic optics, and photon shooting are still | ||
| outside this JAX-native subset. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ API Reference | |
| weak-lensing | ||
| wcs | ||
| noise | ||
| chromatic | ||
| photon_shooting | ||
| interpolation | ||
| fits | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| import os | ||
|
|
||
| import galsim as _galsim | ||
| import jax.numpy as jnp | ||
| from jax.tree_util import register_pytree_node_class | ||
|
|
||
| from jax_galsim.core.interpolate import akima_interp, akima_interp_coeffs | ||
| from jax_galsim.core.utils import cast_to_float, ensure_hashable, implements | ||
|
|
||
|
|
||
| @implements( | ||
| _galsim.Bandpass, | ||
| lax_description="""\ | ||
| JAX-GalSim supports array-backed bandpasses only. The wavelength grid is | ||
| static and the throughput array may be traced. File input, string | ||
| expressions, zeropoints, units, and LookupTable metadata are not implemented. | ||
| """, | ||
| ) | ||
| @register_pytree_node_class | ||
| class Bandpass: | ||
| def __init__(self, wave, throughput, blue_limit=None, red_limit=None): | ||
| if isinstance(wave, (str, bytes, os.PathLike)) or isinstance( | ||
| throughput, (str, bytes, os.PathLike) | ||
| ): | ||
| raise NotImplementedError( | ||
| "JAX-GalSim Bandpass supports array-backed bandpasses only; " | ||
| "file input, string expressions, and unit strings are not implemented." | ||
| ) | ||
|
|
||
| self._wave = jnp.asarray(wave, dtype=float) | ||
| self._throughput = jnp.asarray(throughput) | ||
|
|
||
| if self._wave.ndim != 1 or len(self._wave) < 2: | ||
| raise ValueError("wave must be a 1-D array with at least 2 elements.") | ||
| if self._throughput.shape != self._wave.shape: | ||
| raise ValueError("throughput must have the same shape as wave.") | ||
|
|
||
| self._blue_limit = ( | ||
| cast_to_float(blue_limit) | ||
| if blue_limit is not None | ||
| else cast_to_float(self._wave[0]) | ||
| ) | ||
| self._red_limit = ( | ||
| cast_to_float(red_limit) | ||
| if red_limit is not None | ||
| else cast_to_float(self._wave[-1]) | ||
| ) | ||
|
|
||
| @property | ||
| @implements( | ||
| None, | ||
| lax_description="JAX-GalSim-specific static wavelength grid for array-backed bandpasses.", | ||
| ) | ||
| def wave(self): | ||
| return self._wave | ||
|
|
||
| @property | ||
| @implements( | ||
| None, | ||
| lax_description="JAX-GalSim-specific traced throughput array for array-backed bandpasses.", | ||
| ) | ||
| def throughput(self): | ||
| return self._throughput | ||
|
|
||
| @property | ||
| @implements( | ||
| None, | ||
| lax_description="JAX-GalSim-specific blue wavelength limit.", | ||
| ) | ||
| def blue_limit(self): | ||
| return self._blue_limit | ||
|
|
||
| @property | ||
| @implements( | ||
| None, | ||
| lax_description="JAX-GalSim-specific red wavelength limit.", | ||
| ) | ||
| def red_limit(self): | ||
| return self._red_limit | ||
|
|
||
| @property | ||
| @implements(_galsim.Bandpass.effective_wavelength) | ||
| def effective_wavelength(self): | ||
| return self.calculateEffectiveWavelength() | ||
|
|
||
| @implements( | ||
| _galsim.Bandpass.calculateEffectiveWavelength, | ||
| lax_description=( | ||
| "The JAX implementation returns a traced scalar when throughput is traced." | ||
| ), | ||
| ) | ||
| def calculateEffectiveWavelength(self, precise=False): | ||
| if precise: | ||
| raise NotImplementedError( | ||
| "JAX-GalSim Bandpass does not support precise=True." | ||
| ) | ||
| waves = jnp.linspace(self._blue_limit, self._red_limit, 512) | ||
| throughput = self(waves) | ||
| norm = jnp.trapezoid(throughput, waves) | ||
| midpoint = 0.5 * (self._blue_limit + self._red_limit) | ||
| safe_norm = jnp.where(norm > 0, norm, 1.0) | ||
| return jnp.where( | ||
| norm > 0, | ||
| jnp.trapezoid(waves * throughput, waves) / safe_norm, | ||
| midpoint, | ||
| ) | ||
|
|
||
| @implements(_galsim.Bandpass.__call__) | ||
| def __call__(self, wave): | ||
| wave = jnp.asarray(wave, dtype=float) | ||
| if len(self._wave) >= 5: | ||
| coeffs = akima_interp_coeffs(self._wave, self._throughput, use_jax=True) | ||
| throughput = akima_interp(wave, self._wave, self._throughput, coeffs) | ||
| else: | ||
| throughput = jnp.interp( | ||
| wave, self._wave, self._throughput, left=0.0, right=0.0 | ||
| ) | ||
| in_band = (wave >= self._blue_limit) & (wave <= self._red_limit) | ||
| return jnp.where(in_band, throughput, 0.0) | ||
|
|
||
| @implements(_galsim.Bandpass.__mul__) | ||
| def __mul__(self, other): | ||
| if isinstance(other, Bandpass): | ||
| wave = jnp.unique(jnp.concatenate([self._wave, other._wave])) | ||
| t = jnp.interp(wave, self._wave, self._throughput, left=0.0, right=0.0) | ||
| t2 = jnp.interp(wave, other._wave, other._throughput, left=0.0, right=0.0) | ||
| blue = max(self._blue_limit, other._blue_limit) | ||
| red = min(self._red_limit, other._red_limit) | ||
| return Bandpass(wave, t * t2, blue_limit=blue, red_limit=red) | ||
| return Bandpass( | ||
| self._wave, | ||
| self._throughput * other, | ||
| self._blue_limit, | ||
| self._red_limit, | ||
| ) | ||
|
|
||
| @implements(getattr(_galsim.Bandpass, "__rmul__", None)) | ||
| def __rmul__(self, other): | ||
| return self.__mul__(other) | ||
|
|
||
| @implements(_galsim.Bandpass.truncate) | ||
| def truncate(self, blue_limit=None, red_limit=None): | ||
| blue = blue_limit if blue_limit is not None else self._blue_limit | ||
| red = red_limit if red_limit is not None else self._red_limit | ||
| return Bandpass( | ||
| self._wave, | ||
| self._throughput, | ||
| blue_limit=blue, | ||
| red_limit=red, | ||
| ) | ||
|
|
||
| @classmethod | ||
| @implements( | ||
| None, | ||
| lax_description=( | ||
| "Construct a unit-throughput tophat bandpass on a static JAX wavelength grid." | ||
| ), | ||
| ) | ||
| def tophat(cls, blue_limit, red_limit, n_wave=100): | ||
| wave = jnp.linspace(blue_limit, red_limit, n_wave) | ||
| return cls(wave, jnp.ones(n_wave)) | ||
|
|
||
| def tree_flatten(self): | ||
| children = (self._throughput,) | ||
| aux_data = { | ||
| "wave": tuple(self._wave.tolist()), | ||
| "blue_limit": ensure_hashable(self._blue_limit), | ||
| "red_limit": ensure_hashable(self._red_limit), | ||
| } | ||
| return (children, aux_data) | ||
|
|
||
| @classmethod | ||
| def tree_unflatten(cls, aux_data, children): | ||
| return cls( | ||
| wave=jnp.asarray(aux_data["wave"], dtype=float), | ||
| throughput=children[0], | ||
| blue_limit=aux_data["blue_limit"], | ||
| red_limit=aux_data["red_limit"], | ||
| ) | ||
|
|
||
| def __repr__(self): | ||
| return ( | ||
| f"Bandpass(wave=[{self._blue_limit:.1f}, {self._red_limit:.1f}] nm, " | ||
| f"lam_eff={float(self.effective_wavelength):.1f} nm)" | ||
| ) | ||
|
|
||
| def __eq__(self, other): | ||
| if not isinstance(other, Bandpass): | ||
| return False | ||
| return ( | ||
| jnp.array_equal(self._wave, other._wave) | ||
| & jnp.array_equal(self._throughput, other._throughput) | ||
| & jnp.array_equal(self._blue_limit, other._blue_limit) | ||
| & jnp.array_equal(self._red_limit, other._red_limit) | ||
| ) | ||
|
|
||
| def __hash__(self): | ||
| return hash( | ||
| ( | ||
| "galsim.Bandpass", | ||
| ensure_hashable(self._wave), | ||
| ensure_hashable(self._throughput), | ||
| ensure_hashable(self._blue_limit), | ||
| ensure_hashable(self._red_limit), | ||
| ) | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Let's not repeat doc strings from upstream and instead use the
implementsdecorator, with any caveats put into thelax_descriptionkeyword.