haliax 1.4.dev411__tar.gz → 1.4.dev412__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {haliax-1.4.dev411 → haliax-1.4.dev412}/.agents/projects/api_parity.md +13 -18
  2. {haliax-1.4.dev411 → haliax-1.4.dev412}/.playbooks/wrap-non-named.md +8 -0
  3. {haliax-1.4.dev411 → haliax-1.4.dev412}/PKG-INFO +1 -1
  4. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/api.md +43 -0
  5. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/__about__.py +1 -1
  6. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/__init__.py +22 -0
  7. haliax-1.4.dev412/src/haliax/fft.py +365 -0
  8. haliax-1.4.dev412/tests/test_fft.py +81 -0
  9. {haliax-1.4.dev411 → haliax-1.4.dev412}/.coveragerc +0 -0
  10. {haliax-1.4.dev411 → haliax-1.4.dev412}/.flake8 +0 -0
  11. {haliax-1.4.dev411 → haliax-1.4.dev412}/.github/workflows/publish_dev.yaml +0 -0
  12. {haliax-1.4.dev411 → haliax-1.4.dev412}/.github/workflows/run_pre_commit.yaml +0 -0
  13. {haliax-1.4.dev411 → haliax-1.4.dev412}/.github/workflows/run_quick_levanter_tests.yaml +0 -0
  14. {haliax-1.4.dev411 → haliax-1.4.dev412}/.github/workflows/run_tests.yaml +0 -0
  15. {haliax-1.4.dev411 → haliax-1.4.dev412}/.gitignore +0 -0
  16. {haliax-1.4.dev411 → haliax-1.4.dev412}/.playbooks/add-types.md +0 -0
  17. {haliax-1.4.dev411 → haliax-1.4.dev412}/.pre-commit-config.yaml +0 -0
  18. {haliax-1.4.dev411 → haliax-1.4.dev412}/.readthedocs.yaml +0 -0
  19. {haliax-1.4.dev411 → haliax-1.4.dev412}/AGENTS.md +0 -0
  20. {haliax-1.4.dev411 → haliax-1.4.dev412}/AUTHORS.md +0 -0
  21. {haliax-1.4.dev411 → haliax-1.4.dev412}/CONTRIBUTING.md +0 -0
  22. {haliax-1.4.dev411 → haliax-1.4.dev412}/CONTRIBUTORS.md +0 -0
  23. {haliax-1.4.dev411 → haliax-1.4.dev412}/LICENSE +0 -0
  24. {haliax-1.4.dev411 → haliax-1.4.dev412}/README.md +0 -0
  25. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/broadcasting.md +0 -0
  26. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/cheatsheet.md +0 -0
  27. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/css/material.css +0 -0
  28. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/css/mkdocstrings.css +0 -0
  29. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/faq.md +0 -0
  30. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/data_parallel_mesh.png +0 -0
  31. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  32. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_1d.png +0 -0
  33. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_1d_zero.png +0 -0
  34. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_2d.png +0 -0
  35. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  36. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  37. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  38. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  39. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/figures/device_mesh_2d_zero.png +0 -0
  40. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/fp8.md +0 -0
  41. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/index.md +0 -0
  42. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/indexing.md +0 -0
  43. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/matmul.md +0 -0
  44. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/nn.md +0 -0
  45. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/partitioning.md +0 -0
  46. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/primer.md +0 -0
  47. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/rearrange.ipynb +0 -0
  48. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/rearrange.md +0 -0
  49. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/requirements.txt +0 -0
  50. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/scan.md +0 -0
  51. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/state-dict.md +0 -0
  52. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/tutorial.md +0 -0
  53. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/typing.md +0 -0
  54. {haliax-1.4.dev411 → haliax-1.4.dev412}/docs/vmap.md +0 -0
  55. {haliax-1.4.dev411 → haliax-1.4.dev412}/etc/license_header.txt +0 -0
  56. {haliax-1.4.dev411 → haliax-1.4.dev412}/mkdocs.yml +0 -0
  57. {haliax-1.4.dev411 → haliax-1.4.dev412}/pyproject.toml +0 -0
  58. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/__init__.py +0 -0
  59. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/compile_utils.py +0 -0
  60. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/dot.py +0 -0
  61. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/einsum.py +0 -0
  62. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/fp8.py +0 -0
  63. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/parsing.py +0 -0
  64. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/rearrange.py +0 -0
  65. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/scan.py +0 -0
  66. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/state_dict.py +0 -0
  67. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/_src/util.py +0 -0
  68. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/axis.py +0 -0
  69. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/core.py +0 -0
  70. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/debug.py +0 -0
  71. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/field.py +0 -0
  72. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/haxtyping.py +0 -0
  73. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/hof.py +0 -0
  74. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/jax_utils.py +0 -0
  75. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/__init__.py +0 -0
  76. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/activations.py +0 -0
  77. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/attention.py +0 -0
  78. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/conv.py +0 -0
  79. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/dropout.py +0 -0
  80. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/embedding.py +0 -0
  81. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/linear.py +0 -0
  82. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/loss.py +0 -0
  83. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/mlp.py +0 -0
  84. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/normalization.py +0 -0
  85. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/pool.py +0 -0
  86. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/nn/scan.py +0 -0
  87. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/ops.py +0 -0
  88. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/partitioning.py +0 -0
  89. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/quantization.py +0 -0
  90. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/random.py +0 -0
  91. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/specialized_fns.py +0 -0
  92. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/state_dict.py +0 -0
  93. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/tree_util.py +0 -0
  94. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/types.py +0 -0
  95. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/util.py +0 -0
  96. {haliax-1.4.dev411 → haliax-1.4.dev412}/src/haliax/wrap.py +0 -0
  97. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/core_test.py +0 -0
  98. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_attention.py +0 -0
  99. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_axis.py +0 -0
  100. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_bitwise_ops.py +0 -0
  101. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_conv.py +0 -0
  102. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_debug.py +0 -0
  103. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_dot.py +0 -0
  104. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_dtype_typing.py +0 -0
  105. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_einsum.py +0 -0
  106. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_field.py +0 -0
  107. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_fp8.py +0 -0
  108. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_hof.py +0 -0
  109. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_int8.py +0 -0
  110. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_moe_linear.py +0 -0
  111. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_namedarray_typing.py +0 -0
  112. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_nan_reductions.py +0 -0
  113. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_nn.py +0 -0
  114. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_ops.py +0 -0
  115. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_parsing.py +0 -0
  116. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_partitioning.py +0 -0
  117. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_pool.py +0 -0
  118. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_random.py +0 -0
  119. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_rearrange.py +0 -0
  120. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_scan.py +0 -0
  121. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_scatter_gather.py +0 -0
  122. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_specialized_fns.py +0 -0
  123. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_state_dict.py +0 -0
  124. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_tree_util.py +0 -0
  125. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_utils.py +0 -0
  126. {haliax-1.4.dev411 → haliax-1.4.dev412}/tests/test_visualize_sharding.py +0 -0
  127. {haliax-1.4.dev411 → haliax-1.4.dev412}/uv.lock +0 -0
@@ -161,24 +161,19 @@ APIs that don't translate well to named tensors are intentionally omitted here.
161
161
  - [ ] `vstack`
162
162
 
163
163
  ## fft
164
- - [ ] `fft`
165
- - [ ] `fft2`
166
- - [ ] `fftfreq`
167
- - [ ] `fftn`
168
- - [ ] `fftshift`
169
- - [ ] `hfft`
170
- - [ ] `ifft`
171
- - [ ] `ifft2`
172
- - [ ] `ifftn`
173
- - [ ] `ifftshift`
174
- - [ ] `ihfft`
175
- - [ ] `irfft`
176
- - [ ] `irfft2`
177
- - [ ] `irfftn`
178
- - [ ] `rfft`
179
- - [ ] `rfft2`
180
- - [ ] `rfftfreq`
181
- - [ ] `rfftn`
164
+ Multi-dimensional transforms like `fftn` or `rfft2` are handled by passing a
165
+ mapping of axes to :func:`haliax.fft` and friends.
166
+
167
+ - [x] `fft`
168
+ - [x] `fftfreq`
169
+ - [x] `fftshift`
170
+ - [x] `hfft`
171
+ - [x] `ifft`
172
+ - [x] `ifftshift`
173
+ - [x] `ihfft`
174
+ - [x] `irfft`
175
+ - [x] `rfft`
176
+ - [x] `rfftfreq`
182
177
 
183
178
  ## linalg
184
179
  - [ ] `cholesky`
@@ -45,6 +45,14 @@ def sum(a, axis=None):
45
45
  ## Harder Cases
46
46
  Some functions need bespoke handling. For example `jnp.unique` returns several arrays and may change shape unpredictably. There is no generic helper, so you will need to manually map between `NamedArray` axes and the outputs. Use the lower level utilities in `haliax.wrap` for broadcasting and axis lookup.
47
47
 
48
+ ## Axis-aware functions
49
+ For JAX functions that accept an ``axis`` or ``axes`` argument, prefer a single
50
+ ``axis`` parameter of type :class:`haliax.AxisSelection` that also accepts an
51
+ ordered mapping of axes to sizes. A mapping dispatches to the corresponding
52
+ ``n``‑dimensional JAX primitive (e.g. :func:`jax.numpy.fft.fftn`) and allows
53
+ resizing the transformed axes by specifying integer lengths or ``Axis``
54
+ instances.
55
+
48
56
  ## Testing
49
57
  Add tests to ensure that named and unnamed calls produce the same results and that axis names are preserved or removed correctly.
50
58
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haliax
3
- Version: 1.4.dev411
3
+ Version: 1.4.dev412
4
4
  Summary: Named Tensors for Legible Deep Learning in JAX
5
5
  Project-URL: Homepage, https://github.com/stanford-crfm/haliax
6
6
  Project-URL: Bug Tracker, https://github.com/stanford-crfm/haliax/issues/
@@ -289,6 +289,49 @@ These are all more or less directly from JAX's NumPy API.
289
289
  ::: haliax.triu
290
290
  ::: haliax.where
291
291
 
292
+ ### FFT
293
+
294
+ All FFT helpers accept an ``axis`` argument which may be a single axis, its
295
+ name, or an ordered mapping from axes to output sizes. Passing a mapping
296
+ dispatches to the ``n``‑dimensional variants in :mod:`jax.numpy.fft`.
297
+
298
+ For example::
299
+
300
+ import jax.numpy as jnp
301
+ import haliax as hax
302
+
303
+ T = hax.Axis("time", 8)
304
+ signal = hax.arange(T, dtype=jnp.float32)
305
+
306
+ # operate along a single axis specified by name
307
+ hax.fft(signal, axis="time")
308
+
309
+ # resize by passing an Axis object
310
+ hax.fft(signal, axis=hax.Axis("time", 16))
311
+
312
+ X, Y = hax.make_axes(X=4, Y=6)
313
+ image = hax.arange((X, Y), dtype=jnp.float32)
314
+
315
+ # transform across several axes in order by passing a sequence
316
+ hax.fft(image, axis=("X", "Y"))
317
+
318
+ # selectively resize axes by providing a mapping
319
+ hax.fft(image, axis={"X": None, "Y": hax.Axis("Y", 10)})
320
+
321
+ # mappings can cover just a subset of axes when only partial resizing is needed
322
+ hax.fft(image, axis={"Y": 10})
323
+
324
+ ::: haliax.fft
325
+ ::: haliax.ifft
326
+ ::: haliax.hfft
327
+ ::: haliax.ihfft
328
+ ::: haliax.rfft
329
+ ::: haliax.irfft
330
+ ::: haliax.fftfreq
331
+ ::: haliax.rfftfreq
332
+ ::: haliax.fftshift
333
+ ::: haliax.ifftshift
334
+
292
335
 
293
336
 
294
337
  ## Named Array Reference
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
 
6
- __version__ = "1.4.dev411"
6
+ __version__ = "1.4.dev412"
@@ -96,6 +96,18 @@ from .ops import (
96
96
  bincount,
97
97
  where,
98
98
  )
99
+ from .fft import (
100
+ fft,
101
+ fftfreq,
102
+ fftshift,
103
+ hfft,
104
+ ifft,
105
+ ifftshift,
106
+ ihfft,
107
+ irfft,
108
+ rfft,
109
+ rfftfreq,
110
+ )
99
111
  from .partitioning import auto_sharded, axis_mapping, fsdp, named_jit, shard, shard_with_axis_mapping
100
112
  from .specialized_fns import top_k
101
113
  from .types import Scalar
@@ -1214,6 +1226,16 @@ __all__ = [
1214
1226
  "clip",
1215
1227
  "tril",
1216
1228
  "triu",
1229
+ "fft",
1230
+ "ifft",
1231
+ "hfft",
1232
+ "ihfft",
1233
+ "rfft",
1234
+ "irfft",
1235
+ "fftfreq",
1236
+ "rfftfreq",
1237
+ "fftshift",
1238
+ "ifftshift",
1217
1239
  "add",
1218
1240
  "arctan2",
1219
1241
  "bitwise_and",
@@ -0,0 +1,365 @@
1
+ """Named wrappers around :mod:`jax.numpy.fft`.
2
+
3
+ These functions mirror the behaviour of their :mod:`jax.numpy.fft` counterparts
4
+ while accepting named axes. Instead of separate ``fftn``/``fft2`` variants we
5
+ provide a single ``fft`` family of functions whose ``axis`` argument controls
6
+ which axes are transformed.
7
+
8
+ The ``axis`` parameter can be one of:
9
+
10
+ * ``None`` – operate on the last axis.
11
+ * ``str`` – name of an existing axis in the input.
12
+ * :class:`~haliax.Axis` – specifies both the axis to transform (by name) and the
13
+ desired FFT length. The output axis is replaced by the provided ``Axis``.
14
+ * ``dict`` – mapping from axis selectors (names or ``Axis`` objects) to optional
15
+ sizes. A value of ``None`` uses the existing axis length. The mapping order
16
+ determines the order of transforms and dispatches to the ``n``‑dimensional
17
+ variants in :mod:`jax.numpy.fft`.
18
+
19
+ Example
20
+ -------
21
+
22
+ ```python
23
+ X, Y = hax.make_axes(X=4, Y=6)
24
+ arr = hax.arange((X, Y))
25
+
26
+ # 1D transform along ``Y``
27
+ hax.fft(arr, axis="Y")
28
+
29
+ # 2D transform across both axes
30
+ hax.fft(arr, axis={"X": None, "Y": None})
31
+
32
+ # Resize the ``Y`` axis before transforming
33
+ hax.fft(arr, axis={"Y": Axis("Y", 8)})
34
+ ```
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from typing import Mapping, MutableSequence, Sequence
40
+
41
+ import jax.numpy.fft as jfft
42
+
43
+ from .axis import Axis, AxisSelector, AxisSelection
44
+ from .core import NamedArray
45
+
46
+
47
+ AxisSizeLike = int | Axis | None
48
+ AxisMapping = Mapping[AxisSelector, AxisSizeLike]
49
+
50
+
51
+ def _single_axis(a: NamedArray, axis: AxisSelector | None):
52
+ if axis is None:
53
+ idx = a.ndim - 1
54
+ ax = a.axes[idx]
55
+ n = None
56
+ elif isinstance(axis, Axis):
57
+ idx = a.axis_indices(axis.name)
58
+ if idx is None:
59
+ raise ValueError(f"Axis {axis} not found in {a.axes}")
60
+ ax = axis
61
+ n = axis.size
62
+ else:
63
+ idx = a.axis_indices(axis)
64
+ if idx is None:
65
+ raise ValueError(f"Axis {axis} not found in {a.axes}")
66
+ ax = a.axes[idx]
67
+ n = None
68
+ return idx, ax, n
69
+
70
+
71
+ def _multi_axis(a: NamedArray, axis: AxisMapping):
72
+ axes_idx: MutableSequence[int] = []
73
+ sizes: MutableSequence[int] = []
74
+ new_axes = list(a.axes)
75
+ for key, val in axis.items():
76
+ idx = a.axis_indices(key)
77
+ if idx is None:
78
+ raise ValueError(f"Axis {key} not found in {a.axes}")
79
+ ax = a.axes[idx]
80
+ if isinstance(val, Axis):
81
+ size = val.size
82
+ new_axes[idx] = val
83
+ elif val is None:
84
+ size = ax.size
85
+ else:
86
+ size = int(val)
87
+ new_axes[idx] = ax.resize(size)
88
+ axes_idx.append(idx)
89
+ sizes.append(size)
90
+ return list(axes_idx), list(sizes), new_axes
91
+
92
+
93
+ def fft(
94
+ a: NamedArray,
95
+ axis: AxisSelector | Sequence[AxisSelector] | AxisMapping | None = None,
96
+ norm: str | None = None,
97
+ ) -> NamedArray:
98
+ """Named version of :func:`jax.numpy.fft.fft`.
99
+
100
+ See module level documentation for the behaviour of the ``axis`` argument.
101
+ """
102
+
103
+ if isinstance(axis, Mapping):
104
+ axes_idx, sizes, new_axes = _multi_axis(a, axis)
105
+ out = jfft.fftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
106
+ return NamedArray(out, tuple(new_axes))
107
+ elif isinstance(axis, Sequence) and not isinstance(axis, (str, Axis)):
108
+ axes_idx, sizes, new_axes = _multi_axis(a, {ax: None for ax in axis})
109
+ out = jfft.fftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
110
+ return NamedArray(out, tuple(new_axes))
111
+ else:
112
+ idx, new_axis, n = _single_axis(a, axis) # type: ignore[arg-type]
113
+ out = jfft.fft(a.array, n=n, axis=idx, norm=norm)
114
+ axes = list(a.axes)
115
+ axes[idx] = new_axis
116
+ return NamedArray(out, tuple(axes))
117
+
118
+
119
+ def ifft(
120
+ a: NamedArray,
121
+ axis: AxisSelector | Sequence[AxisSelector] | AxisMapping | None = None,
122
+ norm: str | None = None,
123
+ ) -> NamedArray:
124
+ """Named version of :func:`jax.numpy.fft.ifft`."""
125
+
126
+ if isinstance(axis, Mapping):
127
+ axes_idx, sizes, new_axes = _multi_axis(a, axis)
128
+ out = jfft.ifftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
129
+ return NamedArray(out, tuple(new_axes))
130
+ elif isinstance(axis, Sequence) and not isinstance(axis, (str, Axis)):
131
+ axes_idx, sizes, new_axes = _multi_axis(a, {ax: None for ax in axis})
132
+ out = jfft.ifftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
133
+ return NamedArray(out, tuple(new_axes))
134
+ else:
135
+ idx, new_axis, n = _single_axis(a, axis) # type: ignore[arg-type]
136
+ out = jfft.ifft(a.array, n=n, axis=idx, norm=norm)
137
+ axes = list(a.axes)
138
+ axes[idx] = new_axis
139
+ return NamedArray(out, tuple(axes))
140
+
141
+
142
+ def rfft(
143
+ a: NamedArray,
144
+ axis: AxisSelector | Sequence[AxisSelector] | AxisMapping | None = None,
145
+ norm: str | None = None,
146
+ ) -> NamedArray:
147
+ """Named version of :func:`jax.numpy.fft.rfft`."""
148
+
149
+ if isinstance(axis, Mapping):
150
+ axes_idx, sizes, new_axes = _multi_axis(a, axis)
151
+ out = jfft.rfftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
152
+ last_idx = axes_idx[-1]
153
+ last_in = sizes[-1]
154
+ new_axes[last_idx] = new_axes[last_idx].resize(last_in // 2 + 1)
155
+ return NamedArray(out, tuple(new_axes))
156
+ elif isinstance(axis, Sequence) and not isinstance(axis, (str, Axis)):
157
+ axes_idx, sizes, new_axes = _multi_axis(a, {ax: None for ax in axis})
158
+ out = jfft.rfftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
159
+ last_idx = axes_idx[-1]
160
+ last_in = sizes[-1]
161
+ new_axes[last_idx] = new_axes[last_idx].resize(last_in // 2 + 1)
162
+ return NamedArray(out, tuple(new_axes))
163
+ else:
164
+ idx, ax, n = _single_axis(a, axis) # type: ignore[arg-type]
165
+ out = jfft.rfft(a.array, n=n, axis=idx, norm=norm)
166
+ length = n if n is not None else ax.size
167
+ new_axis = ax.resize(length // 2 + 1)
168
+ axes = list(a.axes)
169
+ axes[idx] = new_axis
170
+ return NamedArray(out, tuple(axes))
171
+
172
+
173
+ def _single_axis_irfft(a: NamedArray, axis: AxisSelector | None):
174
+ if axis is None:
175
+ idx = a.ndim - 1
176
+ in_ax = a.axes[idx]
177
+ length = (in_ax.size - 1) * 2
178
+ out_ax = in_ax.resize(length)
179
+ n = None
180
+ elif isinstance(axis, Axis):
181
+ idx = a.axis_indices(axis.name)
182
+ if idx is None:
183
+ raise ValueError(f"Axis {axis} not found in {a.axes}")
184
+ out_ax = axis
185
+ n = axis.size
186
+ else:
187
+ idx = a.axis_indices(axis)
188
+ if idx is None:
189
+ raise ValueError(f"Axis {axis} not found in {a.axes}")
190
+ in_ax = a.axes[idx]
191
+ length = (in_ax.size - 1) * 2
192
+ out_ax = in_ax.resize(length)
193
+ n = None
194
+ return idx, out_ax, n
195
+
196
+
197
+ def _multi_axis_irfft(a: NamedArray, axis: AxisMapping):
198
+ axes_idx: MutableSequence[int] = []
199
+ sizes: MutableSequence[int] = []
200
+ new_axes = list(a.axes)
201
+ items = list(axis.items())
202
+ for i, (key, val) in enumerate(items):
203
+ idx = a.axis_indices(key)
204
+ if idx is None:
205
+ raise ValueError(f"Axis {key} not found in {a.axes}")
206
+ ax = a.axes[idx]
207
+ if isinstance(val, Axis):
208
+ size = val.size
209
+ new_axes[idx] = val
210
+ elif val is None:
211
+ if i == len(items) - 1:
212
+ size = (ax.size - 1) * 2
213
+ else:
214
+ size = ax.size
215
+ new_axes[idx] = ax.resize(size)
216
+ else:
217
+ size = int(val)
218
+ new_axes[idx] = ax.resize(size)
219
+ axes_idx.append(idx)
220
+ sizes.append(size)
221
+ return list(axes_idx), list(sizes), new_axes
222
+
223
+
224
+ def irfft(
225
+ a: NamedArray,
226
+ axis: AxisSelector | Sequence[AxisSelector] | AxisMapping | None = None,
227
+ norm: str | None = None,
228
+ ) -> NamedArray:
229
+ """Named version of :func:`jax.numpy.fft.irfft`."""
230
+
231
+ if isinstance(axis, Mapping):
232
+ axes_idx, sizes, new_axes = _multi_axis_irfft(a, axis)
233
+ out = jfft.irfftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
234
+ return NamedArray(out, tuple(new_axes))
235
+ elif isinstance(axis, Sequence) and not isinstance(axis, (str, Axis)):
236
+ axes_idx, sizes, new_axes = _multi_axis_irfft(a, {ax: None for ax in axis})
237
+ out = jfft.irfftn(a.array, s=tuple(sizes), axes=tuple(axes_idx), norm=norm)
238
+ return NamedArray(out, tuple(new_axes))
239
+ else:
240
+ idx, out_ax, n = _single_axis_irfft(a, axis) # type: ignore[arg-type]
241
+ out = jfft.irfft(a.array, n=n, axis=idx, norm=norm)
242
+ axes = list(a.axes)
243
+ axes[idx] = out_ax
244
+ return NamedArray(out, tuple(axes))
245
+
246
+
247
+ def hfft(
248
+ a: NamedArray,
249
+ axis: AxisSelector | Sequence[AxisSelector] | AxisMapping | None = None,
250
+ norm: str | None = None,
251
+ ) -> NamedArray:
252
+ """Named version of :func:`jax.numpy.fft.hfft`.
253
+
254
+ Only a single axis is supported; passing a dictionary with more than one
255
+ entry will raise an error.
256
+ """
257
+
258
+ if isinstance(axis, Mapping):
259
+ if len(axis) != 1:
260
+ raise ValueError("hfft only supports a single axis")
261
+ key, val = next(iter(axis.items()))
262
+ if isinstance(val, Axis):
263
+ axis = val
264
+ elif val is None:
265
+ axis = key
266
+ else:
267
+ name = key.name if isinstance(key, Axis) else key
268
+ axis = Axis(name, int(val))
269
+
270
+ idx, out_ax, n = _single_axis_irfft(a, axis) # type: ignore[arg-type]
271
+ out = jfft.hfft(a.array, n=n, axis=idx, norm=norm)
272
+ axes = list(a.axes)
273
+ axes[idx] = out_ax
274
+ return NamedArray(out, tuple(axes))
275
+
276
+
277
+ def ihfft(
278
+ a: NamedArray,
279
+ axis: AxisSelector | Sequence[AxisSelector] | AxisMapping | None = None,
280
+ norm: str | None = None,
281
+ ) -> NamedArray:
282
+ """Named version of :func:`jax.numpy.fft.ihfft`.
283
+
284
+ Only a single axis is supported; passing a dictionary with more than one
285
+ entry will raise an error.
286
+ """
287
+
288
+ if isinstance(axis, Mapping):
289
+ if len(axis) != 1:
290
+ raise ValueError("ihfft only supports a single axis")
291
+ key, val = next(iter(axis.items()))
292
+ if isinstance(val, Axis):
293
+ axis = val
294
+ elif val is None:
295
+ axis = key
296
+ else:
297
+ name = key.name if isinstance(key, Axis) else key
298
+ axis = Axis(name, int(val))
299
+
300
+ idx, ax, n = _single_axis(a, axis) # type: ignore[arg-type]
301
+ out = jfft.ihfft(a.array, n=n, axis=idx, norm=norm)
302
+ length = n if n is not None else ax.size // 2 + 1
303
+ new_axis = ax.resize(length)
304
+ axes = list(a.axes)
305
+ axes[idx] = new_axis
306
+ return NamedArray(out, tuple(axes))
307
+
308
+
309
+ def fftshift(x: NamedArray, axes: AxisSelection | None = None) -> NamedArray:
310
+ """Named version of :func:`jax.numpy.fft.fftshift`."""
311
+
312
+ if axes is None:
313
+ out = jfft.fftshift(x.array)
314
+ else:
315
+ idxs = x.axis_indices(axes)
316
+ if isinstance(idxs, tuple):
317
+ if any(i is None for i in idxs):
318
+ raise ValueError(f"Axis {axes} not found in {x.axes}")
319
+ elif idxs is None:
320
+ raise ValueError(f"Axis {axes} not found in {x.axes}")
321
+ out = jfft.fftshift(x.array, axes=idxs)
322
+ return NamedArray(out, x.axes)
323
+
324
+
325
+ def ifftshift(x: NamedArray, axes: AxisSelection | None = None) -> NamedArray:
326
+ """Named version of :func:`jax.numpy.fft.ifftshift`."""
327
+
328
+ if axes is None:
329
+ out = jfft.ifftshift(x.array)
330
+ else:
331
+ idxs = x.axis_indices(axes)
332
+ if isinstance(idxs, tuple):
333
+ if any(i is None for i in idxs):
334
+ raise ValueError(f"Axis {axes} not found in {x.axes}")
335
+ elif idxs is None:
336
+ raise ValueError(f"Axis {axes} not found in {x.axes}")
337
+ out = jfft.ifftshift(x.array, axes=idxs)
338
+ return NamedArray(out, x.axes)
339
+
340
+
341
+ def fftfreq(axis: Axis, d: float = 1.0) -> NamedArray:
342
+ """Named version of :func:`jax.numpy.fft.fftfreq`."""
343
+
344
+ return NamedArray(jfft.fftfreq(axis.size, d), (axis,))
345
+
346
+
347
+ def rfftfreq(axis: Axis, d: float = 1.0) -> NamedArray:
348
+ """Named version of :func:`jax.numpy.fft.rfftfreq`."""
349
+
350
+ new_axis = axis.resize(axis.size // 2 + 1)
351
+ return NamedArray(jfft.rfftfreq(axis.size, d), (new_axis,))
352
+
353
+
354
+ __all__ = [
355
+ "fft",
356
+ "ifft",
357
+ "rfft",
358
+ "irfft",
359
+ "hfft",
360
+ "ihfft",
361
+ "fftfreq",
362
+ "rfftfreq",
363
+ "fftshift",
364
+ "ifftshift",
365
+ ]
@@ -0,0 +1,81 @@
1
+ import jax.numpy as jnp
2
+ import jax.numpy.fft as jfft
3
+
4
+ import haliax as hax
5
+ from haliax import Axis
6
+
7
+
8
+ def test_fft_axis_options():
9
+ N = Axis("n", 8)
10
+ x = hax.arange(N, dtype=jnp.float32)
11
+
12
+ # string axis
13
+ assert jnp.allclose(hax.fft(x, axis="n").array, jfft.fft(x.array))
14
+
15
+ # Axis object with resize
16
+ N2 = Axis("n", 16)
17
+ f = hax.fft(x, axis=N2)
18
+ assert f.axes[0] == N2
19
+ assert jnp.allclose(f.array, jfft.fft(x.array, n=16))
20
+
21
+ r = hax.rfft(x)
22
+ assert r.axes[0].size == 5
23
+ assert jnp.allclose(r.array, jfft.rfft(x.array))
24
+
25
+ ir = hax.irfft(r)
26
+ assert ir.axes[0].size == 8
27
+ assert jnp.allclose(ir.array, jfft.irfft(jfft.rfft(x.array)))
28
+
29
+ h = hax.hfft(r)
30
+ assert h.axes[0].size == 8
31
+ assert jnp.allclose(h.array, jfft.hfft(jfft.rfft(x.array)))
32
+
33
+ ih = hax.ihfft(x)
34
+ assert ih.axes[0].size == 5
35
+ assert jnp.allclose(ih.array, jfft.ihfft(x.array))
36
+
37
+
38
+ def test_fft_freq_and_shift():
39
+ N = Axis("n", 8)
40
+ x = hax.arange(N)
41
+
42
+ f = hax.fftfreq(N)
43
+ assert f.axes == (N,)
44
+ assert jnp.allclose(f.array, jfft.fftfreq(8))
45
+
46
+ rf = hax.rfftfreq(N)
47
+ assert rf.axes[0].size == 5
48
+ assert jnp.allclose(rf.array, jfft.rfftfreq(8))
49
+
50
+ shifted = hax.fftshift(x)
51
+ assert jnp.allclose(shifted.array, jfft.fftshift(x.array))
52
+ unshifted = hax.ifftshift(shifted)
53
+ assert jnp.allclose(unshifted.array, x.array)
54
+
55
+
56
+ def test_fft_multi_axis():
57
+ X = Axis("x", 4)
58
+ Y = Axis("y", 6)
59
+ Z = Axis("z", 8)
60
+ arr = hax.arange((X, Y, Z), dtype=jnp.float32)
61
+
62
+ f = hax.fft(arr, axis={"y": None, "z": None})
63
+ assert jnp.allclose(f.array, jfft.fftn(arr.array, axes=(1, 2)))
64
+
65
+ f_seq = hax.fft(arr, axis=("y", "z"))
66
+ assert jnp.allclose(f_seq.array, jfft.fftn(arr.array, axes=(1, 2)))
67
+ assert f_seq.axes == f.axes
68
+
69
+ rf = hax.rfft(arr, axis={"y": None, "z": None})
70
+ assert rf.axes[2].size == 5
71
+ assert jnp.allclose(rf.array, jfft.rfftn(arr.array, axes=(1, 2)))
72
+
73
+ irf = hax.irfft(rf, axis={"y": None, "z": Z})
74
+ assert jnp.allclose(
75
+ irf.array, jfft.irfftn(jfft.rfftn(arr.array, axes=(1, 2)), s=(Y.size, Z.size), axes=(1, 2))
76
+ )
77
+
78
+ # resizing via dict values
79
+ f2 = hax.fft(arr, axis={"y": 4, "z": None})
80
+ assert f2.axes[1].size == 4
81
+ assert jnp.allclose(f2.array, jfft.fftn(arr.array, s=(4, 8), axes=(1, 2)))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes