Skip to content

Commit e2b1029

Browse files
authored
Merge pull request #248 from GalSim-developers/typing-inits-cleanup
fix: clean up type handling
2 parents 6ebfcb8 + 1aeb448 commit e2b1029

10 files changed

Lines changed: 269 additions & 339 deletions

File tree

docs/sharp-bits.rst

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ does not affect the original.
6161
# JAX-GalSim — real_part is a copy
6262
real_part = complex_image.real # independent array
6363
64+
Scalar Types, Array Types, and Casting
65+
--------------------------------------
66+
67+
With the use of JAX, there are now many possible types for numeric data. These include
68+
69+
- **Python scalars**: Objects with types that are ``float``, ``int``, or ``complex``.
70+
- **NumPy scalars**: Objects with types that are subclasses of ``np.floating``, ``np.integer``, etc.
71+
- **NumPy array scalars**: Objects with a type that is ``np.ndarray`` and have ``np.ndim(...) == 0``.
72+
- **NumPy arrays**: Objects with a type that is ``np.ndarray`` and have ``np.ndim(...) > 0``.
73+
- **JAX array scalars**: Objects with a type that is ``jax.numpy.ndarray`` and have ``jax.numpy.ndim(...) == 0``.
74+
- **JAX arrays**: Objects with a type that is ``jax.numpy.ndarray`` and have ``jax.numpy.ndim(...) > 0``.
75+
76+
**JAX does not have pure scalar types like NumPy. JAX uses array scalars for those instead.**
77+
78+
JAX-GalSim uses the following rules when handling data types and casting.
79+
80+
- If the item is a Python numeric type (i.e., ``int`` or ``float``) or a
81+
NumPy scalar type (i.e., ``isinstance(x, np.number)``, ``isinstance(x, np.integer)``, etc.),
82+
convert it to a Python type of the appropriate kind.
83+
- For all other array-like types, cast to the correct type via ``jax.numpy.astype(x, ...)``.
84+
- For putting data into FITS headers only, JAX-GalSim converts of NumPy/JAX arrays to Python
85+
numeric types as long as there is one element in the array (i.e., it is a NumPy scalar type,
86+
an array scalar, or a 1D array with one element).
87+
88+
These rules allow JAX-GalSim to transparently handle JAX's tracing operations, but can result in
89+
the code raising generic ``Exception`` instances instead of more specific ``GalSim`` exceptions in
90+
some cases.
91+
6492
Random Number Generation
6593
------------------------
6694

@@ -163,9 +191,6 @@ profile parameters passed into a ``jit``-compiled function):
163191
def good(sigma):
164192
return jax.lax.cond(sigma > 1.0, lambda s: s * 2, lambda s: s, sigma)
165193
166-
JAX-GalSim uses an internal ``has_tracers()`` utility to detect tracing and
167-
avoid problematic control flow in its own implementations.
168-
169194
Fixed output shapes
170195
^^^^^^^^^^^^^^^^^^^
171196

@@ -197,20 +222,9 @@ The ``__init__`` gotcha
197222

198223
During ``jit`` tracing, JAX calls constructors with **tracer objects** rather
199224
than concrete Python numbers. Type checks like ``isinstance(sigma, float)`` will
200-
fail on tracers. JAX-GalSim handles this internally, but if you subclass any
201-
JAX-GalSim object, be aware that ``__init__`` may receive tracers:
202-
203-
.. code-block:: python
204-
205-
from jax_galsim.core.utils import has_tracers
206-
207-
class MyProfile(jax_galsim.GSObject):
208-
def __init__(self, sigma, gsparams=None):
209-
if not has_tracers(sigma):
210-
# Only validate with concrete values
211-
if sigma <= 0:
212-
raise ValueError("sigma must be positive")
213-
...
225+
return ``False`` on tracers, and you cannot check correctness of values (e.g.,
226+
``if sigma > 0: ...```). JAX-GalSim handles this internally, but if you subclass any
227+
JAX-GalSim object, be aware that ``__init__`` may receive tracers.
214228

215229
Profile Restrictions
216230
--------------------
@@ -221,12 +235,9 @@ Some GalSim features are not yet implemented in JAX-GalSim:
221235
- **ChromaticObject**: All chromatic functionality (wavelength-dependent
222236
profiles) is not available.
223237
- **InterpolatedKImage**: Not implemented.
224-
- **Airy, Kolmogorov, OpticalPSF, RealGalaxy**: See :doc:`api-coverage` for
238+
- **Airy, Kolmogorov, OpticalPSF, RealGalaxy, etc.**: See :doc:`api-coverage` for
225239
the full list.
226240

227-
The project currently implements **22.5 %** of the GalSim public API, focused
228-
on the most commonly used profiles and operations.
229-
230241
Numerical Precision
231242
-------------------
232243

@@ -249,11 +260,11 @@ These differences are typically at the level of floating-point round-off
249260
should not affect scientific conclusions.
250261

251262
⚠️ Additional Sharp Bits
252-
--------------------------
263+
------------------------
253264

254265
In the :doc:`api/index` you will find **🔪 JAX-GalSim - The Sharp Bits 🔪** blocks highlighting additional important caveats for specific classes and or methods. These could include things like:
255266

256-
- Many classes do not perform some of Galsim's test for correctness during initialization (e.g., :meth:`~jax_galsim.GSObject.drawImage`).
267+
- Some classes do not perform some of Galsim's test for correctness during initialization (e.g., :meth:`~jax_galsim.InterpolatedImage`).
257268
- Certain profiles might not be auto-differentiable with respect to some of their parameters (e.g., :class:`~jax_galsim.Spergel`, :class:`~jax_galsim.Moffat`)
258269
- Limitations regarding what types of inputes are handled (e.g., :meth:`~jax_galsim.Image.calculate_fft` does not accept complex dtypes.)
259270

jax_galsim/angle.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from jax_galsim.core.utils import (
2828
cast_to_float,
2929
ensure_hashable,
30-
has_tracers,
3130
implements,
3231
)
3332

@@ -55,7 +54,7 @@ class AngleUnit(object):
5554
def __init__(self, value):
5655
if isinstance(value, AngleUnit):
5756
raise TypeError("Cannot construct AngleUnit from another AngleUnit")
58-
self._value = cast_to_float(value)
57+
self._value = cast_to_float(value, accept_strings=True)
5958

6059
@property
6160
@implements(_galsim.AngleUnit.value)
@@ -199,7 +198,7 @@ def __sub__(self, other):
199198
return _Angle(self._rad - other._rad)
200199

201200
def __mul__(self, other):
202-
if not (has_tracers(other) or isinstance(other, NON_COMPLEX_TYPES)):
201+
if isinstance(other, (Angle, AngleUnit)):
203202
raise TypeError(
204203
"Cannot multiply Angle by %s of type %s" % (other, type(other))
205204
)
@@ -210,7 +209,7 @@ def __mul__(self, other):
210209
def __div__(self, other):
211210
if isinstance(other, AngleUnit):
212211
return self._rad / other.value
213-
elif has_tracers(other) or isinstance(other, NON_COMPLEX_TYPES):
212+
elif not isinstance(other, Angle):
214213
return _Angle(self._rad / other)
215214
else:
216215
raise TypeError(

jax_galsim/bounds.py

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import galsim as _galsim
22
import jax
33
import jax.numpy as jnp
4+
import numpy as np
45
from jax.tree_util import register_pytree_node_class
56

67
from jax_galsim.core.utils import (
7-
CONST_TYPES,
88
cast_to_float,
99
cast_to_int,
10-
cast_to_python_float,
1110
check_is_int_then_cast,
1211
ensure_hashable,
13-
has_tracers,
1412
implements,
1513
)
1614
from jax_galsim.position import Position, PositionD, PositionI
@@ -23,10 +21,7 @@
2321
Further, the JAX implementation adds a new method, ``isStatic`` to the
2422
``BoundsI`` class. If JAX-GalSim detects that the ``BoundsI`` instance
2523
has been instantiated with static, known values, ``isStatic()`` will
26-
return ``True``. You can indicate to JAX-GalSim that a ``BoundsI``
27-
instance should be static via initializing it with the ``static``
28-
keyword set to the ``True``. If the object detects that it is being
29-
initialized with non-static data, an error will be raised.
24+
return ``True``.
3025
3126
``BoundsI`` objects in JAX-Galsim support an additional initialization
3227
call ``BoundsI(xmin=..., deltax=..., ymin=..., deltay=...)``. In this case,
@@ -139,8 +134,8 @@ def _parse_args(self, *args, **kwargs):
139134
else:
140135
max_delta = 1
141136
if (
142-
isinstance(self.deltax, CONST_TYPES)
143-
and isinstance(self.deltay, CONST_TYPES)
137+
isinstance(self.deltax, (int, float, np.integer, np.floating))
138+
and isinstance(self.deltay, (int, float, np.integer, np.floating))
144139
and (self.deltax < max_delta or self.deltay < max_delta)
145140
):
146141
self._isdefined = False
@@ -364,10 +359,8 @@ def from_galsim(cls, galsim_bounds):
364359
"""Create a jax_galsim `BoundsD/I` from a `galsim.BoundsD/I` object."""
365360
if isinstance(galsim_bounds, _galsim.BoundsD):
366361
_cls = BoundsD
367-
kwargs = {}
368362
elif isinstance(galsim_bounds, _galsim.BoundsI):
369363
_cls = BoundsI
370-
kwargs = {"static": True}
371364
else:
372365
raise TypeError(
373366
"galsim_bounds must be either a %s or a %s"
@@ -379,7 +372,6 @@ def from_galsim(cls, galsim_bounds):
379372
galsim_bounds.xmax,
380373
galsim_bounds.ymin,
381374
galsim_bounds.ymax,
382-
**kwargs,
383375
)
384376
else:
385377
return _cls()
@@ -505,24 +497,25 @@ def __init__(self, *args, **kwargs):
505497
# initial setting to let stuff pass through freely
506498
self._isstatic = True
507499

508-
force_static = kwargs.pop("static", False)
509-
510500
self._parse_args(*args, **kwargs)
511501

512-
if has_tracers(self.deltax) or has_tracers(self.deltay):
513-
raise RuntimeError(
514-
"Jax-GalSim BoundsI instances must have a fixed width! "
515-
f"Got deltax,deltay = {self.deltax!r},{self.deltay!r}."
516-
)
517-
518-
self.deltax = cast_to_python_float(self.deltax)
519-
self.deltay = cast_to_python_float(self.deltay)
502+
self.deltax = cast_to_float(self.deltax)
503+
self.deltay = cast_to_float(self.deltay)
520504
if (self.deltax != int(self.deltax)) or (self.deltay != int(self.deltay)):
521505
raise TypeError("BoundsI must be initialized with integer values")
522-
self.deltax = int(cast_to_int(self.deltax))
523-
self.deltay = int(cast_to_int(self.deltay))
506+
self.deltax = cast_to_int(self.deltax)
507+
self.deltay = cast_to_int(self.deltay)
524508

525-
if has_tracers(self._xmin) or has_tracers(self._ymin):
509+
if not (
510+
isinstance(
511+
self._xmin,
512+
(int, float, np.floating, np.integer),
513+
)
514+
and isinstance(
515+
self._ymin,
516+
(int, float, np.floating, np.integer),
517+
)
518+
):
526519
self._isstatic = False
527520

528521
# validate inputs are ints
@@ -536,13 +529,6 @@ def __init__(self, *args, **kwargs):
536529
if self.deltax < 1 and self.deltay < 1:
537530
self._isdefined = False
538531

539-
if force_static and not self._isstatic:
540-
raise RuntimeError(
541-
"BoundsI initialized with non-static "
542-
f"data (xmin,ymin = {self._xmin},{self._yminb}) "
543-
"when static data was explicitly requested."
544-
)
545-
546532
def _check_scalar(self, x, name):
547533
try:
548534
if (

0 commit comments

Comments
 (0)