1010
1111from autoarray import exc
1212
13+ from autoarray .operators import nufft_cpu
1314from autoarray .operators .transformer import _load_nufftax , nufftax_exception
1415
1516try :
@@ -474,25 +475,31 @@ def nufft_precision_operator_from(
474475
475476 - `"nufft"` (default) -- `nufft_precision_operator_via_nufft_from`, the type-1
476477 (adjoint) NUFFT. `O(K*nspread^2 + M log M)` for `M = 4*Ny*Nx`, i.e. seconds
477- where the brute-force builders take minutes to hours. Needs `nufftax`.
478+ where the brute-force builders take minutes to hours. Runs `finufft` (CPU, no
479+ JAX) by default and `nufftax` (JAX) with `use_jax=True`.
478480 - `"numpy"` -- `nufft_precision_operator_via_np_from`, the brute-force
479481 `O(N_pix*K)` reference builder. Kept as the reference the NUFFT builder is
480482 pinned against, and used as the fallback below.
481483 - `"jax"` -- `nufft_precision_operator_via_jax_from`, the same brute force on
482484 JAX. `method="jax"` is the explicit way to ask for it.
483485
484- `use_jax` is only honoured when a brute-force method is selected: it upgrades
485- `method="numpy"` to `method="jax"` and is otherwise ignored. Under the default
486- `method="nufft"` it does nothing, because the NUFFT already runs on JAX -- an
487- existing `use_jax=True` caller therefore keeps the fast path rather than being
488- demoted to the `O(N_pix*K)` brute force.
486+ `use_jax` picks the backend of the chosen method: under the default
487+ `method="nufft"`, `use_jax=False` runs the type-1 NUFFT on `finufft` (CPU, never
488+ imports JAX, so a NumPy fit does not start XLA before a multiprocessing search
489+ forks its workers) and `use_jax=True` runs it on `nufftax` (JAX); under
490+ `method="numpy"`, `use_jax=True` upgrades to `method="jax"`. Either way an
491+ existing `use_jax=True` caller keeps a NUFFT fast path rather than being demoted
492+ to the `O(N_pix*K)` brute force.
489493
490494 Two fallbacks to `"numpy"` are taken, both logged loudly (never silently),
491495 because they cost `O(N_pix*K)` where the NUFFT is `O(K*nspread^2 + M log M)`:
492496
493- 1. `disable_jax()` is true (`PYAUTO_DISABLE_JAX=1`, the test-mode kill switch).
494- Both `"nufft"` and `"jax"` run on JAX, so both are demoted.
495- 2. `nufftax` is not importable, so `"nufft"` cannot run.
497+ 1. `disable_jax()` is true (`PYAUTO_DISABLE_JAX=1`, the test-mode kill switch)
498+ and the method would run on JAX: `"jax"`, or `"nufft"` with `use_jax=True`
499+ (or with `finufft` missing, where the NumPy NUFFT falls back to `nufftax`).
500+ `"nufft"` on `finufft` is not JAX, so it is not demoted.
501+ 2. No NUFFT backend for the requested route is importable: `nufftax` for
502+ `use_jax=True`, or neither `finufft` nor `nufftax` for `use_jax=False`.
496503
497504 Any other `method` raises `ValueError`.
498505
@@ -519,9 +526,9 @@ def nufft_precision_operator_from(
519526 chunk_k
520527 The visibility chunk size of the two brute-force builders.
521528 use_jax
522- Only honoured when a brute-force method is selected: `method="numpy"` with
523- `use_jax= True` runs the JAX brute force (equivalent to `method="jax"`). It is
524- ignored under the default `method="nufft"`, which already runs on JAX .
529+ Under `method="nufft"`, `False` (default) runs the NUFFT on `finufft` and
530+ `True` on `nufftax` ( JAX). Under `method="numpy"`, `True` runs the JAX brute
531+ force (equivalent to `method="jax"`) .
525532 """
526533 if method == "numpy" and use_jax :
527534 method = "jax"
@@ -533,23 +540,32 @@ def nufft_precision_operator_from(
533540 "brute-force reference builders)."
534541 )
535542
536- if method in ("nufft" , "jax" ) and disable_jax ():
543+ # The NumPy NUFFT runs on JAX only when finufft is missing and it falls back to nufftax.
544+ nufft_on_jax = use_jax or nufft_cpu ._load_finufft () is None
545+
546+ if (method == "jax" or (method == "nufft" and nufft_on_jax )) and disable_jax ():
537547 logger .warning (
538548 f"INTERFEROMETER - `PYAUTO_DISABLE_JAX=1` is set, so the NUFFT precision "
539- f"operator cannot be built with method={ method !r} (both the NUFFT and the "
540- f"JAX brute force run on JAX ). Falling back to the NumPy brute force, which "
549+ f"operator cannot be built with method={ method !r} on JAX (use_jax= { use_jax } , "
550+ f"or `finufft` is not installed ). Falling back to the NumPy brute force, which "
541551 f"is O(N_pix * K) rather than O(K * nspread^2 + M log M) and can take "
542552 f"minutes to hours on a real dataset."
543553 )
544554 method = "numpy"
545555
546- if method == "nufft" and _load_nufftax () is None :
556+ # nufftax is only looked for on the JAX route; the NumPy route checks for a backend
557+ # without importing nufftax (which would start JAX).
558+ if method == "nufft" and (
559+ (use_jax and _load_nufftax () is None )
560+ or (not use_jax and not nufft_cpu .backend_available ())
561+ ):
547562 logger .warning (
548- "INTERFEROMETER - `nufftax` is not installed, so the NUFFT precision "
549- "operator cannot be built with the type-1 NUFFT. Falling back to the NumPy "
550- "brute force, which is O(N_pix * K) rather than O(K * nspread^2 + M log M) "
551- "and can take minutes to hours on a real dataset. Install it via "
552- "`pip install nufftax`."
563+ f"INTERFEROMETER - No NUFFT backend is installed for use_jax={ use_jax } "
564+ f"(`nufftax` for use_jax=True, `finufft` or `nufftax` for use_jax=False), so "
565+ f"the NUFFT precision operator cannot be built with the type-1 NUFFT. Falling "
566+ f"back to the NumPy brute force, which is O(N_pix * K) rather than "
567+ f"O(K * nspread^2 + M log M) and can take minutes to hours on a real dataset. "
568+ f"Install it via `pip install finufft` (or `pip install nufftax` for JAX)."
553569 )
554570 method = "numpy"
555571
@@ -561,6 +577,7 @@ def nufft_precision_operator_from(
561577 grid_radians_2d = grid_radians_2d ,
562578 eps = eps ,
563579 chunk_size = chunk_size ,
580+ use_jax = use_jax ,
564581 )
565582
566583 if method == "jax" :
@@ -666,6 +683,7 @@ def _type1_real_grid_from(
666683 centred : bool = False ,
667684 zero_nyquist : bool = False ,
668685 out : Optional [np .ndarray ] = None ,
686+ use_jax : bool = False ,
669687) -> np .ndarray :
670688 """
671689 The real part of a **type-1 (adjoint) NUFFT** of per-visibility `values` onto a regular
@@ -713,18 +731,23 @@ def _type1_real_grid_from(
713731 which is returned; nothing of the grid's size is allocated beyond `nufft2d1`'s own output
714732 and its real part per chunk. This is how `sparse_terms_from_chunks` accumulates the
715733 fine grids across a stream. If `None` a new array is returned.
734+ use_jax
735+ If `False` (default) the transform runs on `finufft` via `nufft_cpu` (CPU, never imports
736+ JAX); if `True` it runs on `nufftax` (JAX). The two agree to ~1e-14 relative and return
737+ the same NumPy array.
716738
717739 Returns
718740 -------
719741 np.ndarray
720742 `[N_y, N_x]` float64.
721743 """
722- nufftax = _load_nufftax ()
744+ if use_jax :
745+ nufftax = _load_nufftax ()
723746
724- if nufftax is None :
725- nufftax_exception ()
747+ if nufftax is None :
748+ nufftax_exception ()
726749
727- import jax .numpy as jnp
750+ import jax .numpy as jnp
728751
729752 uv_wavelengths = np .asarray (uv_wavelengths , dtype = np .float64 )
730753
@@ -761,14 +784,24 @@ def _type1_real_grid_from(
761784 for k0 in range (0 , total_visibilities , chunk_size ):
762785 k1 = min (total_visibilities , k0 + chunk_size )
763786
764- f = nufftax .nufft2d1 (
765- jnp .asarray (- x [k0 :k1 ]),
766- jnp .asarray (y [k0 :k1 ]),
767- jnp .asarray (values [k0 :k1 ], dtype = jnp .complex128 ),
768- n_modes ,
769- eps ,
770- 1 ,
771- )
787+ if use_jax :
788+ f = nufftax .nufft2d1 (
789+ jnp .asarray (- x [k0 :k1 ]),
790+ jnp .asarray (y [k0 :k1 ]),
791+ jnp .asarray (values [k0 :k1 ], dtype = jnp .complex128 ),
792+ n_modes ,
793+ eps ,
794+ 1 ,
795+ )
796+ else :
797+ f = nufft_cpu .nufft2d1 (
798+ - x [k0 :k1 ],
799+ y [k0 :k1 ],
800+ np .asarray (values [k0 :k1 ], dtype = np .complex128 ),
801+ n_modes ,
802+ eps ,
803+ 1 ,
804+ )
772805
773806 if real_modes is not None :
774807 real_modes += np .asarray (np .real (f ), dtype = np .float64 )
@@ -803,6 +836,7 @@ def nufft_precision_operator_via_nufft_from(
803836 * ,
804837 eps : float = 1.0e-12 ,
805838 chunk_size : Optional [int ] = None ,
839+ use_jax : bool = False ,
806840) -> np .ndarray :
807841 """
808842 Returns the `W~` precision operator built as the real part of a **type-1 (adjoint)
@@ -833,7 +867,8 @@ def nufft_precision_operator_via_nufft_from(
833867 P[i, j] = Re sum_k w_k exp(i(-j * x_k + i * y_k))
834868
835869 i.e. the real part of a type-1 NUFFT of the weights onto the `(2Ny, 2Nx)` mode grid.
836- `nufftax.nufft2d1(x, y, c, n_modes=(N1, N2), eps, isign)` returns
870+ `nufftax.nufft2d1(x, y, c, n_modes=(N1, N2), eps, isign)` (and `nufft_cpu.nufft2d1`, the
871+ `finufft` wrapper that mirrors it) returns
837872 `f[m2, m1] = sum_k c_k exp(isign * i(m1 * x_k + m2 * y_k))` on the **centred** mode
838873 grid, shape `(N2, N1)`, so the mapping is
839874
@@ -902,6 +937,9 @@ def nufft_precision_operator_via_nufft_from(
902937 The requested NUFFT precision.
903938 chunk_size
904939 Cap on the visibilities passed to `nufft2d1` in one call, or `None` for one shot.
940+ use_jax
941+ If `False` (default) the NUFFT runs on `finufft` (CPU, never imports JAX); if `True`
942+ on `nufftax` (JAX). Both return the same NumPy array to ~1e-14 relative.
905943
906944 Returns
907945 -------
@@ -927,6 +965,7 @@ def nufft_precision_operator_via_nufft_from(
927965 chunk_size = chunk_size ,
928966 centred = False ,
929967 zero_nyquist = True ,
968+ use_jax = use_jax ,
930969 )
931970
932971
@@ -2543,7 +2582,7 @@ def fine_grid_shapes_from(
25432582def _log_fine_grid_memory (shape_operator_fine , shape_dirty_image_fine , oversample ):
25442583 """
25452584 Log one line estimating the memory the `oversample` fine grids need: the two float64 grids
2546- held for the whole stream, plus the largest per-call `nufftax. nufft2d1` working set (its 2x
2585+ held for the whole stream, plus the largest per-call `nufft2d1` working set (its 2x
25472586 upsampled complex128 grid, its complex128 output and that output's float64 real part).
25482587 """
25492588 held = 8 * (
@@ -2665,7 +2704,7 @@ def sparse_terms_from_chunks(
26652704 grids need the NUFFT: `oversample` with a transformer other than `TransformerNUFFT` raises.
26662705
26672706 Memory and cost. At a 400 x 400 native grid and `q = 8`, `precision_operator_fine` is 8000^2
2668- float64 (512 MB) and `dirty_image_fine` 6400^2 (328 MB), and `nufftax` 's 2x upsampled complex
2707+ float64 (512 MB) and `dirty_image_fine` 6400^2 (328 MB), and the NUFFT 's 2x upsampled complex
26692708 work grid adds ~4 GB during each call: plan for 5-6 GB peak (`q = 4` is ~1.5 GB; ~112 pixels
26702709 at `q = 8` is under 0.5 GB). One info line logs the estimate for the actual shapes. The FFT
26712710 part of each fine NUFFT costs the same whatever the chunk length (seconds per chunk at
@@ -2709,6 +2748,8 @@ def sparse_terms_from_chunks(
27092748 method, eps, chunk_size, chunk_k, use_jax, show_progress
27102749 Passed to `nufft_precision_operator_from` for each chunk (`chunk_size` is the NUFFT
27112750 builder's own inner chunk, a memory ceiling within a chunk). `eps=None` is `1e-12`.
2751+ `use_jax` also picks the backend of the `oversample` fine grids (`finufft` for `False`,
2752+ `nufftax` for `True`); the dirty image and beam always use the transformer's NumPy path.
27122753 phase_centre
27132754 The `(y, x)` phase-centre shift in arcseconds applied to every chunk's visibilities
27142755 before forming the dirty image (see "Phase-centre shift" above). `None` applies no
@@ -2889,6 +2930,7 @@ def sparse_terms_from_chunks(
28892930 centred = False ,
28902931 zero_nyquist = False ,
28912932 out = precision_operator_fine ,
2933+ use_jax = use_jax ,
28922934 )
28932935
28942936 _type1_real_grid_from (
@@ -2904,6 +2946,7 @@ def sparse_terms_from_chunks(
29042946 centred = True ,
29052947 zero_nyquist = False ,
29062948 out = dirty_image_fine ,
2949+ use_jax = use_jax ,
29072950 )
29082951
29092952 dirty_beam_native = np .asarray (
0 commit comments