coordax 0.2.1__tar.gz → 0.2.2__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 (25) hide show
  1. {coordax-0.2.1 → coordax-0.2.2}/PKG-INFO +1 -1
  2. {coordax-0.2.1 → coordax-0.2.2}/coordax/__init__.py +1 -1
  3. {coordax-0.2.1 → coordax-0.2.2}/coordax/coordinate_systems.py +33 -17
  4. coordax-0.2.2/coordax/experimental.py +22 -0
  5. {coordax-0.2.1 → coordax-0.2.2}/coordax/fields.py +48 -37
  6. {coordax-0.2.1 → coordax-0.2.2}/coordax/named_axes.py +32 -32
  7. {coordax-0.2.1 → coordax-0.2.2}/coordax/named_axes_test.py +6 -6
  8. {coordax-0.2.1 → coordax-0.2.2}/coordax/ndarrays.py +21 -7
  9. {coordax-0.2.1 → coordax-0.2.2}/coordax/testing.py +3 -3
  10. {coordax-0.2.1 → coordax-0.2.2}/coordax.egg-info/PKG-INFO +1 -1
  11. {coordax-0.2.1 → coordax-0.2.2}/coordax.egg-info/SOURCES.txt +1 -0
  12. {coordax-0.2.1 → coordax-0.2.2}/pyproject.toml +1 -1
  13. {coordax-0.2.1 → coordax-0.2.2}/LICENSE +0 -0
  14. {coordax-0.2.1 → coordax-0.2.2}/README.md +0 -0
  15. {coordax-0.2.1 → coordax-0.2.2}/coordax/coordinate_systems_test.py +0 -0
  16. {coordax-0.2.1 → coordax-0.2.2}/coordax/coords.py +0 -0
  17. {coordax-0.2.1 → coordax-0.2.2}/coordax/doctest_test.py +0 -0
  18. {coordax-0.2.1 → coordax-0.2.2}/coordax/fields_test.py +0 -0
  19. {coordax-0.2.1 → coordax-0.2.2}/coordax/jax_datetime_integration_test.py +0 -0
  20. {coordax-0.2.1 → coordax-0.2.2}/coordax/utils.py +0 -0
  21. {coordax-0.2.1 → coordax-0.2.2}/coordax/xarray_test.py +0 -0
  22. {coordax-0.2.1 → coordax-0.2.2}/coordax.egg-info/dependency_links.txt +0 -0
  23. {coordax-0.2.1 → coordax-0.2.2}/coordax.egg-info/requires.txt +0 -0
  24. {coordax-0.2.1 → coordax-0.2.2}/coordax.egg-info/top_level.txt +0 -0
  25. {coordax-0.2.1 → coordax-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coordax
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Coordinate axes for scientific computing in JAX
5
5
  Author-email: Google LLC <noreply@google.com>
6
6
  License-Expression: Apache-2.0
@@ -53,4 +53,4 @@ from coordax.ndarrays import (
53
53
  )
54
54
  import coordax.testing # pylint: disable=unused-import
55
55
 
56
- __version__ = '0.2.1' # keep sync with pyproject.toml
56
+ __version__ = '0.2.2' # keep sync with pyproject.toml
@@ -46,7 +46,7 @@ Sequence = collections.abc.Sequence
46
46
  @functools.partial(utils.export, module='coordax.coords')
47
47
  @dataclasses.dataclass(frozen=True)
48
48
  class NoCoordinateMatch:
49
- """For use when a coordinate does not match an xarray.Coordinates object."""
49
+ """For use when no Coordax coordinate matches xarray coordinate."""
50
50
 
51
51
  reason: str
52
52
 
@@ -56,13 +56,13 @@ class Coordinate(abc.ABC):
56
56
  """Abstract class for coordinate objects.
57
57
 
58
58
  Coordinate subclasses are expected to obey several invariants:
59
- 1. Dimension names may not be repeated: `len(set(dims)) == len(dims)`
60
- 2. All dimensions must be named: `len(shape) == len(dims)`
59
+ 1. Dimension names may not be repeated: ``len(set(dims)) == len(dims)``
60
+ 2. All dimensions must be named: ``len(shape) == len(dims)``
61
61
 
62
62
  Every non-abstract Coordinate subclass must be registered as a "static"
63
63
  pytree node, e.g., by decorating the class with
64
- `@jax.tree_util.register_static`. Static pytrees nodes must implement
65
- `__hash__` and `__eq__` according to the requirements of keys in Python
64
+ ``@jax.tree_util.register_static``. Static pytrees nodes must implement
65
+ ``__hash__`` and ``__eq__`` according to the requirements of keys in Python
66
66
  dictionaries. This is easiest to acheive with frozen dataclasses, but care
67
67
  must be taken when working with np.ndarray attributes.
68
68
  """
@@ -73,7 +73,7 @@ class Coordinate(abc.ABC):
73
73
  """Dimension names of the coordinate.
74
74
 
75
75
  All subclasses must return a tuple of dimension names as strings, with the
76
- exception of `DummyAxis`.
76
+ exception of ``DummyAxis``.
77
77
  """
78
78
  raise NotImplementedError()
79
79
 
@@ -135,8 +135,12 @@ class Coordinate(abc.ABC):
135
135
  values.
136
136
 
137
137
  Returns:
138
- A matching instance of this coordinate or `NoCoordinateMatch` if this
139
- coordinate does not match the xarray dimensions and coordinates.
138
+ A matching instance of this coordinate or ``NoCoordinateMatch`` if this
139
+ coordinate type does not match the xarray dimensions and coordinates.
140
+
141
+ See also:
142
+ :func:`coordax.from_xarray`
143
+ :func:`coordax.coords.from_xarray`
140
144
  """
141
145
  raise NotImplementedError('from_xarray not implemented')
142
146
 
@@ -513,6 +517,7 @@ class LabeledAxis(Coordinate):
513
517
  @functools.partial(utils.export, module='coordax.coords')
514
518
  def compose(*coordinates: Coordinate) -> Coordinate:
515
519
  # pylint: disable=line-too-long
520
+ # fmt: off
516
521
  """Compose coordinates into a unified coordinate system.
517
522
 
518
523
  Args:
@@ -528,6 +533,7 @@ def compose(*coordinates: Coordinate) -> Coordinate:
528
533
  >>> cx.coords.compose(x, y)
529
534
  CartesianProduct(coordinates=(coordax.SizedAxis('x', size=2), coordax.SizedAxis('y', size=3)))
530
535
  """
536
+ # fmt: on
531
537
  product = CartesianProduct(coordinates)
532
538
  match len(product.coordinates):
533
539
  case 0:
@@ -544,7 +550,8 @@ def insert_axes(
544
550
  indices_to_axes: dict[int, Coordinate],
545
551
  ) -> Coordinate:
546
552
  # pylint: disable=line-too-long
547
- """Returns `coordinate` with extra axes inserted at specified positions.
553
+ # fmt: off
554
+ """Returns ``coordinate`` with extra axes inserted at specified positions.
548
555
 
549
556
  Args:
550
557
  coordinate: The coordinate system to modify.
@@ -561,6 +568,7 @@ def insert_axes(
561
568
  >>> cx.coords.insert_axes(x, {1: z})
562
569
  CartesianProduct(coordinates=(coordax.SizedAxis('x', size=2), coordax.SizedAxis('z', size=4)))
563
570
  """
571
+ # fmt: on
564
572
  indices_to_axes = indices_to_axes.copy()
565
573
  ndim = coordinate.ndim + len(indices_to_axes)
566
574
  normalize_idx = lambda i: i + ndim if i < 0 else i
@@ -581,7 +589,8 @@ def replace_axes(
581
589
  replace_with: Coordinate,
582
590
  ) -> Coordinate:
583
591
  # pylint: disable=line-too-long
584
- """Returns `coordinate` with `to_replace` replaced by `replace_with`.
592
+ # fmt: off
593
+ """Returns ``coordinate`` with ``to_replace`` replaced by ``replace_with``.
585
594
 
586
595
  Args:
587
596
  coordinate: The coordinate system to modify.
@@ -589,10 +598,10 @@ def replace_axes(
589
598
  replace_with: The new coordinate to insert.
590
599
 
591
600
  Returns:
592
- A new coordinate object with `to_replace` substituted by `replace_with`.
601
+ A new coordinate object with ``to_replace`` substituted by ``replace_with``.
593
602
 
594
603
  Raises:
595
- ValueError: If `to_replace` is not a contiguous part of `coordinate`.
604
+ ValueError: If ``to_replace`` is not a contiguous part of ``coordinate``.
596
605
 
597
606
  Examples:
598
607
  >>> import coordax as cx
@@ -605,6 +614,7 @@ def replace_axes(
605
614
  >>> cx.coords.replace_axes(xy, x, z)
606
615
  CartesianProduct(coordinates=(coordax.SizedAxis('z', size=4), coordax.SizedAxis('y', size=3)))
607
616
  """
617
+ # fmt: on
608
618
  if not to_replace.dims:
609
619
  raise ValueError(f'`to_replace` must have dimensions, got {to_replace}')
610
620
 
@@ -632,10 +642,11 @@ def from_xarray(
632
642
  coord_types: Sequence[type[Coordinate]] = (LabeledAxis, DummyAxis),
633
643
  ) -> Coordinate:
634
644
  # pylint: disable=line-too-long
635
- """Convert the coordinates of an xarray.DataArray into a coordax.Coordinate.
645
+ # fmt: off
646
+ """Convert coordinates of an ``xarray.DataArray`` into ``coordax.Coordinate``.
636
647
 
637
648
  Args:
638
- data_array: xarray.DataArray whose coordinates should be converted.
649
+ data_array: ``xarray.DataArray`` whose coordinates should be converted.
639
650
  coord_types: sequence of ``coordax.Coordinate`` subclasses with
640
651
  ``from_xarray`` methods defined. The first coordinate class that returns a
641
652
  coordinate object (indicating a match) will be used. By default,
@@ -643,8 +654,9 @@ def from_xarray(
643
654
  objects.
644
655
 
645
656
  Returns:
646
- A coordax.Coordinate object representing the coordinates of the input
647
- DataArray.
657
+ A coordax.Coordinate object, constructed from any of the indicated types
658
+ (plus ``CartesianProduct`` and ``Scalar``), representing the coordinates of
659
+ the input DataArray.
648
660
 
649
661
  Raises:
650
662
  ValueError: if no matching coordinate is found.
@@ -654,9 +666,13 @@ def from_xarray(
654
666
  >>> import xarray as xr
655
667
  >>> import numpy as np
656
668
  >>> da = xr.DataArray(np.zeros((2, 3)), dims=('x', 'y'), coords={'x': [1, 2]})
657
- >>> cx.coords.from_xarray(da) # doctest: +ELLIPSIS
669
+ >>> cx.coords.from_xarray(da)
658
670
  CartesianProduct(coordinates=(coordax.LabeledAxis('x', ticks=array([1, 2])), coordax.DummyAxis('y', size=3)))
671
+
672
+ See also:
673
+ :func:`coordax.from_xarray`
659
674
  """
675
+ # fmt: on
660
676
  dims = data_array.dims
661
677
  coords = []
662
678
 
@@ -0,0 +1,22 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Experimental parts of Coordax."""
15
+
16
+ # Note: import <name> as <name> is required for names to be exported.
17
+ # See PEP 484 & https://github.com/jax-ml/jax/issues/7570
18
+ # pylint: disable=g-multiple-import,useless-import-alias,g-importing-member,unused-import
19
+ from coordax.ndarrays import (
20
+ NDArray as NDArray,
21
+ register_ndarray as register_ndarray,
22
+ )
@@ -76,7 +76,7 @@ def _axes_attrs(field: Field) -> str:
76
76
 
77
77
  @utils.export
78
78
  def new_axis_name(field: Field, excluded_names: set[str] | None = None) -> str:
79
- """Returns axis name that is not present in `field` or `excluded_names`.
79
+ """Returns axis name that is not present in ``field`` or ``excluded_names``.
80
80
 
81
81
  Args:
82
82
  field: The field to generate a new axis name for.
@@ -106,7 +106,7 @@ def new_axis_name(field: Field, excluded_names: set[str] | None = None) -> str:
106
106
 
107
107
  @utils.export
108
108
  def tmp_axis_name(field: Field, excluded_names: set[str] | None = None) -> str:
109
- """Deprecated alias for `new_axis_name`."""
109
+ """Deprecated alias for ``new_axis_name``."""
110
110
  warnings.warn(
111
111
  'cx.tmp_axis_name() is deprecated, use cx.new_axis_name() instead',
112
112
  DeprecationWarning,
@@ -125,7 +125,7 @@ def cmap(
125
125
  vmap: Callable = jax.vmap, # pylint: disable=g-bare-generic
126
126
  ) -> Callable[..., Any]:
127
127
  # fmt: off
128
- """Vectorizes `fun` over coordinate dimensions of ``Field`` inputs.
128
+ """Vectorizes ``fun`` over coordinate dimensions of ``Field`` inputs.
129
129
 
130
130
  ``cmap`` is a "coordinate vectorizing map". It wraps an ordinary
131
131
  positional-axis-based function so that it accepts ``Field`` objects as input
@@ -244,7 +244,7 @@ def cpmap(
244
244
  coordax compatible with custom objects (e.g. neural net modules).
245
245
 
246
246
  Returns:
247
- A function that applies `fun` to positional axes of the inputs while
247
+ A function that applies ``fun`` to positional axes of the inputs while
248
248
  vectorizing over all coordinate dimensions. The coordinate order of the
249
249
  inputs is preserved in the outputs.
250
250
 
@@ -264,8 +264,8 @@ def cpmap(
264
264
  Traceback (most recent call last):
265
265
  ...
266
266
  ValueError: 'same_as_input' for out_axes requires all NamedArray inputs with
267
- named axes to have the same `named_axes`. Found multiple distinct
268
- `named_axes`:
267
+ named axes to have the same named_axes. Found multiple distinct
268
+ named_axes on inputs:
269
269
  [{'x': 0, 'y': 1}, {'y': 0, 'x': 1}]
270
270
 
271
271
  See also:
@@ -314,8 +314,8 @@ def _cmap_with_doc(
314
314
  )
315
315
 
316
316
  docstr = (
317
- f'Dimension-vectorized version of `{fun_name}`. Takes similar arguments'
318
- f' as `{fun_name}` but accepts and returns Fields in place of arrays.'
317
+ f'Dimension-vectorized version of ``{fun_name}``. Takes similar arguments'
318
+ f' as ``{fun_name}`` but accepts and returns Fields in place of arrays.'
319
319
  )
320
320
  if fun_doc:
321
321
  docstr += f'\n\nOriginal documentation:\n\n{fun_doc}'
@@ -405,8 +405,8 @@ def _wrap_array_method(name):
405
405
  wrapped_func.__module__ = __name__
406
406
  wrapped_func.__doc__ = (
407
407
  'Name-vectorized version of array method'
408
- f' `{name} <numpy.ndarray.{name}>`. Takes similar arguments as'
409
- f' `{name} <numpy.ndarray.{name}>` but accepts and returns Fields'
408
+ f' ``{name} <numpy.ndarray.{name}>``. Takes similar arguments as'
409
+ f' ``{name} <numpy.ndarray.{name}>`` but accepts and returns Fields'
410
410
  ' in place of regular arrays.'
411
411
  )
412
412
  return wrapped_func
@@ -421,6 +421,7 @@ def _in_treescope_abbreviation_mode() -> bool:
421
421
  @jax.tree_util.register_pytree_node_class
422
422
  class Field:
423
423
  # pylint: disable=line-too-long
424
+ # fmt: off
424
425
  """An array with optional named dimensions and associated coordinates.
425
426
 
426
427
  Examples:
@@ -457,6 +458,7 @@ class Field:
457
458
  >>> field.coordinate
458
459
  CartesianProduct(coordinates=(coordax.SizedAxis('x', size=2), coordax.DummyAxis('y', size=3), coordax.DummyAxis(None, size=4)))
459
460
  """
461
+ # fmt on
460
462
 
461
463
  _named_array: named_axes_lib.NamedArray
462
464
  _axes: dict[str, Coordinate]
@@ -472,11 +474,11 @@ class Field:
472
474
  Args:
473
475
  data: the underlying data array.
474
476
  dims: optional tuple of dimension names, with the same length as
475
- `data.ndim`. Strings indicate named axes, and may not be repeated.
476
- `None` indicates positional axes. If `dims` is not provided, all axes
477
- are positional.
477
+ ``data.ndim``. Strings indicate named axes, and may not be repeated.
478
+ ``None`` indicates positional axes. If ``dims`` is not provided, all
479
+ axes are positional.
478
480
  axes: optional mapping from dimension names to associated
479
- `coordax.Coordinate` objects.
481
+ ``coordax.Coordinate`` objects.
480
482
 
481
483
  Examples:
482
484
  >>> import coordax as cx
@@ -645,8 +647,9 @@ class Field:
645
647
  def unwrap(self, *names: str | Coordinate) -> Array:
646
648
  """Extracts underlying data from a field without named dimensions.
647
649
 
648
- This is effectively syntactic sugar for `assert field.named_dims == names`,
649
- followed by returning `field.data`.
650
+ This is effectively syntactic sugar for ``assert field.named_dims ==
651
+ names``,
652
+ followed by returning ``field.data``.
650
653
 
651
654
  Args:
652
655
  *names: Names of dimensions to check against.
@@ -655,7 +658,7 @@ class Field:
655
658
  The underlying data array.
656
659
 
657
660
  Raises:
658
- ValueError: If the field has named dimensions that do not match `names`.
661
+ ValueError: If the field has named dimensions that do not match ``names``.
659
662
 
660
663
  Examples:
661
664
  >>> import coordax as cx
@@ -678,7 +681,8 @@ class Field:
678
681
  return self.data
679
682
 
680
683
  def _validate_matching_coords(
681
- self, dims_or_coords: Sequence[str | Coordinate | types.EllipsisType],
684
+ self,
685
+ dims_or_coords: Sequence[str | Coordinate | types.EllipsisType],
682
686
  ):
683
687
  """Validate that given coordinates are all found on this field."""
684
688
  axes = []
@@ -716,6 +720,8 @@ class Field:
716
720
 
717
721
  Untag by name:
718
722
 
723
+ >>> field.untag('x')
724
+ <Field dims=(None, 'y') shape=(2, 3) axes={} >
719
725
  >>> field.untag('y')
720
726
  <Field dims=('x', None) shape=(2, 3) axes={'x': SizedAxis} >
721
727
 
@@ -741,8 +747,8 @@ class Field:
741
747
  Args:
742
748
  *names: Names or coordinates to assign to the positional axes. The total
743
749
  number of dimensions corresponding to these objects must match the
744
- number of positional axes in the field unless `...` is used to indicate
745
- positions of untagged axes.
750
+ number of positional axes in the field unless ``...`` is used to
751
+ indicate positions of untagged axes.
746
752
 
747
753
  Returns:
748
754
  A new Field with the specified dimensions tagged.
@@ -754,18 +760,18 @@ class Field:
754
760
  >>> field.tag('x', 'y')
755
761
  <Field dims=('x', 'y') shape=(2, 3) axes={} >
756
762
 
757
- Tagging with `Coordinate` objects adds them to the field:
763
+ Tagging with ``Coordinate`` objects adds them to the field:
758
764
 
759
765
  >>> x = cx.SizedAxis('x', 2)
760
766
  >>> field.tag(x, 'y')
761
767
  <Field dims=('x', 'y') shape=(2, 3) axes={'x': SizedAxis} >
762
768
 
763
- `None` leaves a dimension untagged:
769
+ ``None`` leaves a dimension untagged:
764
770
 
765
771
  >>> field.tag('x', None)
766
772
  <Field dims=('x', None) shape=(2, 3) axes={} >
767
773
 
768
- Ellipsis `...` can be indicate dimensions that should not be named:
774
+ Ellipsis ``...`` can be indicate dimensions that should not be named:
769
775
 
770
776
  >>> field.tag(..., 'y')
771
777
  <Field dims=(None, 'y') shape=(2, 3) axes={} >
@@ -775,7 +781,7 @@ class Field:
775
781
  >>> field.tag('x') # doctest: +NORMALIZE_WHITESPACE
776
782
  Traceback (most recent call last):
777
783
  ...
778
- ValueError: there must be exactly as many dimensions given to `tag` as
784
+ ValueError: there must be exactly as many dimensions given to ``tag`` as
779
785
  there are positional axes in the array, but got ('x',) for 2 positional
780
786
  axes.
781
787
 
@@ -790,7 +796,8 @@ class Field:
790
796
  CartesianProduct(axes={'x': SizedAxis, 'y': SizedAxis})
791
797
  >>> field = cx.Field(jnp.zeros((2, 3)))
792
798
  >>> field.tag(xy)
793
- <Field dims=('x', 'y') shape=(2, 3) axes={'x': SizedAxis, 'y': SizedAxis} >
799
+ <Field dims=('x', 'y') shape=(2, 3) axes={'x': SizedAxis, 'y': SizedAxis}
800
+ >
794
801
 
795
802
  See also:
796
803
  :meth:`coordax.Field.untag`
@@ -812,12 +819,14 @@ class Field:
812
819
 
813
820
  # Note: Can't call this "transpose" like Xarray, to avoid conflicting with the
814
821
  # positional only ndarray method.
815
- def order_as(self, *axis_order: str | Coordinate | types.EllipsisType) -> Field:
822
+ def order_as(
823
+ self, *axis_order: str | Coordinate | types.EllipsisType
824
+ ) -> Field:
816
825
  """Returns a field with the axes in the given order.
817
826
 
818
827
  Args:
819
828
  *axis_order: The desired order of axes, specified by name or coordinate.
820
- `...` may be used once, to indicate all other dimensions in order of
829
+ ``...`` may be used once, to indicate all other dimensions in order of
821
830
  appearance on this array.
822
831
 
823
832
  Returns:
@@ -839,13 +848,13 @@ class Field:
839
848
  return result
840
849
 
841
850
  def broadcast_like(self, other: Self | Coordinate) -> Self:
842
- """Returns a field broadcasted like `other`.
851
+ """Returns a field broadcasted like ``other``.
843
852
 
844
853
  Args:
845
854
  other: The field or coordinate to broadcast to.
846
855
 
847
856
  Returns:
848
- A new Field broadcasted to match `other`.
857
+ A new Field broadcasted to match ``other``.
849
858
 
850
859
  Examples:
851
860
  >>> import coordax as cx
@@ -1010,7 +1019,7 @@ class Field:
1010
1019
  def field(array: ArrayLike, *names: str | Coordinate | None) -> Field:
1011
1020
  """Wraps a positional array as a ``Field``.
1012
1021
 
1013
- `cx.field(array, *names)` is a shortcut for `cx.Field(array).tag(*names)`.
1022
+ ``cx.field(array, *names)`` is a shortcut for ``cx.Field(array).tag(*names)``.
1014
1023
 
1015
1024
  Args:
1016
1025
  array: the array to wrap.
@@ -1037,7 +1046,7 @@ def field(array: ArrayLike, *names: str | Coordinate | None) -> Field:
1037
1046
 
1038
1047
  @utils.export
1039
1048
  def wrap(array: ArrayLike, *names: str | Coordinate | None) -> Field:
1040
- """Deprecated alias for `cx.field`."""
1049
+ """Deprecated alias for ``cx.field``."""
1041
1050
  warnings.warn(
1042
1051
  'cx.wrap() is deprecated, use cx.field() instead',
1043
1052
  DeprecationWarning,
@@ -1048,7 +1057,7 @@ def wrap(array: ArrayLike, *names: str | Coordinate | None) -> Field:
1048
1057
 
1049
1058
  @utils.export
1050
1059
  def wrap_like(array: ArrayLike, other: Field) -> Field:
1051
- """Wraps `array` with the same coordinates as `other`."""
1060
+ """Wraps ``array`` with the same coordinates as ``other``."""
1052
1061
  warnings.warn(
1053
1062
  'cx.wrap_like() is deprecated, use cx.field(array, other.coordinate) '
1054
1063
  'instead of cx.wrap_like(array, other)',
@@ -1068,6 +1077,7 @@ def from_xarray(
1068
1077
  coord_types: Sequence[type[Coordinate]] = (LabeledAxis, DummyAxis),
1069
1078
  ) -> Field:
1070
1079
  # pylint: disable=g-import-not-at-top,line-too-long
1080
+ # fmt: off
1071
1081
  """Create a coordax.Field from an xarray.DataArray.
1072
1082
 
1073
1083
  Args:
@@ -1092,6 +1102,7 @@ def from_xarray(
1092
1102
  See also:
1093
1103
  :func:`coordax.coords.from_xarray`
1094
1104
  """
1105
+ # fmt: on
1095
1106
  data = data_array.data
1096
1107
  coord = coordinate_systems.from_xarray(data_array, coord_types)
1097
1108
  return field(data, coord)
@@ -1099,19 +1110,19 @@ def from_xarray(
1099
1110
 
1100
1111
  @utils.export
1101
1112
  def is_field(value) -> TypeGuard[Field]:
1102
- """Returns True if `value` is of type `Field`."""
1113
+ """Returns True if ``value`` is of type ``Field``."""
1103
1114
  return isinstance(value, Field)
1104
1115
 
1105
1116
 
1106
1117
  @utils.export
1107
1118
  def shape_struct_field(*axes: Coordinate) -> Field:
1108
- """Returns a Field with `axes` and a ShapeDtypeStruct in place of data.
1119
+ """Returns a Field with ``axes`` and a ShapeDtypeStruct in place of data.
1109
1120
 
1110
1121
  Args:
1111
1122
  *axes: The coordinates to use for the field.
1112
1123
 
1113
1124
  Returns:
1114
- A Field with `ShapeDtypeStruct` data and the given axes.
1125
+ A Field with ``ShapeDtypeStruct`` data and the given axes.
1115
1126
 
1116
1127
  Examples:
1117
1128
  >>> import coordax as cx
@@ -1142,12 +1153,12 @@ def get_coordinate(
1142
1153
  field: coordax.Field from which the coordinate will be extracted.
1143
1154
  missing_axes: controls how axes without coorinates are handled. Options are:
1144
1155
 
1145
- * ``'dummy'``: uses DummyAxis for dimensions without a coordinate.
1156
+ * ``'dummy'``: uses ``DummyAxis`` for dimensions without a coordinate.
1146
1157
  * ``'skip'``: ignores dimensions without a coordinate.
1147
1158
  * ``'error'``: raises if dimensions without a coordinate are present.
1148
1159
 
1149
1160
  Returns:
1150
- Coordinate associated with the `field`.
1161
+ Coordinate associated with the ``field``.
1151
1162
 
1152
1163
  Examples:
1153
1164
  >>> import coordax as cx
@@ -15,8 +15,8 @@
15
15
 
16
16
  This module is intended to be nearly a drop-in replacement for Penzai's
17
17
  NamedArray, but with an alternative, simpler implementation. Dimensions are
18
- specified by a tuple, where each element is either a string or `None`. `None` is
19
- used to indicate strictly positional dimensions.
18
+ specified by a tuple, where each element is either a string or ``None``.
19
+ ``None`` is used to indicate strictly positional dimensions.
20
20
 
21
21
  Some code and documentation is adapted from penzai.core.named_axes.
22
22
  """
@@ -49,7 +49,7 @@ def attrs_summary_type(
49
49
  named_array: NamedArray,
50
50
  inspect_data: bool,
51
51
  ) -> tuple[str, str, str]:
52
- """Returns a summary of a `named_array` and its data type."""
52
+ """Returns a summary of a ``named_array`` and its data type."""
53
53
  if isinstance(named_array.data, jax.Array) and not isinstance(
54
54
  named_array.data, jax.core.Tracer
55
55
  ):
@@ -111,8 +111,8 @@ class NamedArrayAdapter(ndarray_adapters.NDArrayAdapter['NamedArray']):
111
111
  bad_names = set(mask.dims) - set(array.dims)
112
112
  if bad_names:
113
113
  raise ValueError(
114
- 'Valid mask must be broadcastable to the shape of `array`, but it'
115
- f' had extra axis names {bad_names}'
114
+ 'Valid mask must be broadcastable to the shape of ``array``, but '
115
+ f'it had extra axis names {bad_names}'
116
116
  )
117
117
  mask = mask.order_as(*(d for d in array.dims if d in mask.dims))
118
118
  mask_data = mask.data
@@ -240,8 +240,8 @@ def _normalize_out_axes(
240
240
  named_axes_list = [arr.named_axes for arr in named_arrays_with_axes]
241
241
  raise ValueError(
242
242
  "'same_as_input' for out_axes requires all NamedArray inputs with"
243
- ' named axes to have the same `named_axes`. Found multiple'
244
- f' distinct `named_axes`:\n{named_axes_list}'
243
+ ' named axes to have the same named_axes. Found multiple'
244
+ f' distinct named_axes on inputs:\n{named_axes_list}'
245
245
  )
246
246
  [unique_named_axes] = unique_named_axes
247
247
  return dict(unique_named_axes)
@@ -304,22 +304,22 @@ def nmap(
304
304
  """Automatically vectorizes ``fun`` over named dimensions.
305
305
 
306
306
  ``nmap`` is a "named dimension vectorizing map". It wraps an ordinary
307
- positional-axis-based function so that it accepts NamedArrays as input and
308
- produces NamedArrays as output, and vectorizes over all of named dimensions,
307
+ positional-axis-based function so that it accepts ``NamedArray``s as input and
308
+ produces ``NamedArray``s as output, and vectorizes over all of named dimensions,
309
309
  calling the original function with positionally-indexed slices corresponding
310
- to each argument's `positional_shape`.
310
+ to each argument's ``positional_shape``.
311
311
 
312
- Unlike `jax.vmap`, the axes to vectorize over are inferred
312
+ Unlike ``jax.vmap``, the axes to vectorize over are inferred
313
313
  automatically from the named dimensions in the NamedArray inputs, rather
314
314
  than being specified as part of the mapping transformation. Specifically, each
315
315
  dimension name that appears in any of the arguments is vectorized over jointly
316
316
  across all arguments that include that dimension, and is then included as a
317
317
  named dimension in the output. To make an axis visible to ``fun``, you can
318
- call `untag` on the argument and pass the axis name(s) of interest; ``fun``
318
+ call ``untag`` on the argument and pass the axis name(s) of interest; ``fun``
319
319
  will then see those axes as positional axes instead of mapping over them.
320
320
 
321
- `untag` and ``nmap`` are together the primary ways to apply individual
322
- operations to axes of a NamedArray. `tag` can then be used on the result to
321
+ ``untag`` and ``nmap`` are together the primary ways to apply individual
322
+ operations to axes of a NamedArray. ``tag`` can then be used on the result to
323
323
  re-bind names to positional axes.
324
324
 
325
325
  Within ``fun``, any mapped-over axes will be accessible using standard JAX
@@ -347,8 +347,8 @@ def nmap(
347
347
 
348
348
  Returns:
349
349
  An automatically-vectorized version of ``fun``, which can optionally be
350
- called with NamedArrays instead of ordinary arrays, and which will always
351
- return NamedArrays for each of its output leaves. Any argument (or PyTree
350
+ called with ``NamedArray``s instead of ordinary arrays, and which will always
351
+ return ``NamedArray``s for each of its output leaves. Any argument (or PyTree
352
352
  leaf of an argument) that is a NamedArray will have its named dimensions
353
353
  vectorized over; ``fun`` will then be called with batch tracers
354
354
  corresponding to slices of the input array that are shaped like
@@ -452,8 +452,8 @@ def _nmap_with_doc(
452
452
  return jax.tree.map(wrap_output, result, is_leaf=is_array)
453
453
 
454
454
  docstr = (
455
- f'Dimension-vectorized version of `{fun_name}`. Takes similar arguments'
456
- f' as `{fun_name}` but accepts and returns NamedArray objects in place '
455
+ f'Dimension-vectorized version of ``{fun_name}``. Takes similar arguments'
456
+ f' as ``{fun_name}`` but accepts and returns NamedArray objects in place '
457
457
  'of arrays.'
458
458
  )
459
459
  if fun_doc:
@@ -536,8 +536,8 @@ def _wrap_array_method(
536
536
  wrapped_func.__module__ = __name__
537
537
  wrapped_func.__doc__ = (
538
538
  'Name-vectorized version of array method'
539
- f' `{name} <numpy.ndarray.{name}>`. Takes similar arguments as'
540
- f' `{name} <numpy.ndarray.{name}>` but accepts and returns NamedArray'
539
+ f' ``{name} <numpy.ndarray.{name}>``. Takes similar arguments as'
540
+ f' ``{name} <numpy.ndarray.{name}>`` but accepts and returns NamedArray'
541
541
  ' objects in place of regular arrays.'
542
542
  )
543
543
  return wrapped_func
@@ -560,7 +560,7 @@ class _ShapedLeaf:
560
560
 
561
561
 
562
562
  def _tmp_axis_name(x: NamedArray, excluded_names: set[str]) -> str:
563
- """Returns axis name that is not present in `x` or `excluded_names`."""
563
+ """Returns axis name that is not present in ``x`` or ``excluded_names``."""
564
564
  for i in range(x.ndim):
565
565
  name = f'tmp_axis_{i}'
566
566
  if name not in excluded_names and name not in x.named_dims:
@@ -582,9 +582,9 @@ class NamedArray:
582
582
 
583
583
  Attributes:
584
584
  data: the underlying data array.
585
- dims: tuple of dimension names, with the same length as `data.ndim`. Strings
586
- indicate named axes, and may not be repeated. `None` indicates positional
587
- axes.
585
+ dims: tuple of dimension names, with the same length as ``data.ndim``.
586
+ Strings indicate named axes, and may not be repeated. ``None`` indicates
587
+ positional axes.
588
588
  """
589
589
 
590
590
  _data: Array
@@ -600,9 +600,9 @@ class NamedArray:
600
600
  Arguments:
601
601
  data: the underlying data array.
602
602
  dims: optional tuple of dimension names, with the same length as
603
- `data.ndim`. Strings indicate named axes, and may not be repeated.
604
- `None` indicates positional axes. If `dims` is not provided, all axes
605
- are positional.
603
+ ``data.ndim``. Strings indicate named axes, and may not be repeated.
604
+ ``None`` indicates positional axes. If ``dims`` is not provided, all
605
+ axes are positional.
606
606
  """
607
607
  data = ndarrays.to_array(data)
608
608
  if dims is None:
@@ -804,8 +804,8 @@ class NamedArray:
804
804
  def untag(self, *dims: str) -> Self:
805
805
  """Removes the requested dimension names.
806
806
 
807
- `untag` can only be called on a `NamedArray` that does not have any
808
- positional axes. It produces a new `NamedArray` where the axes with the
807
+ ``untag`` can only be called on a ``NamedArray`` that does not have any
808
+ positional axes. It produces a new ``NamedArray`` where the axes with the
809
809
  requested dimension names are now treated as positional instead.
810
810
 
811
811
  Args:
@@ -846,12 +846,12 @@ class NamedArray:
846
846
  def order_as(self, *dims: str | types.EllipsisType) -> Self:
847
847
  """Reorder the dimensions of an array.
848
848
 
849
- All dimensions must be named. Use `tag` first to name any positional axes.
849
+ All dimensions must be named. Use ``tag`` first to name any positional axes.
850
850
 
851
851
  Args:
852
852
  *dims: dimension names that appear on this array, in the desired order on
853
- the result. `...` may be used once, to indicate all other dimensions in
854
- order of appearance on this array.
853
+ the result. ``...`` may be used once, to indicate all other dimensions
854
+ in order of appearance on this array.
855
855
 
856
856
  Returns:
857
857
  Array with transposed data and reordered dimensions, as indicated.
@@ -110,7 +110,7 @@ class NamedAxesTest(parameterized.TestCase):
110
110
  with self.assertRaisesWithLiteralMatch(
111
111
  TypeError,
112
112
  'data must be a np.ndarray, jax.Array or a duck-typed array registered'
113
- ' with coordax.register_ndarray(), got dict: {}',
113
+ ' with coordax.experimental.register_ndarray(), got dict: {}',
114
114
  ):
115
115
  named_axes.NamedArray({})
116
116
  with self.assertRaisesRegex(
@@ -784,9 +784,9 @@ class NamedAxesTest(parameterized.TestCase):
784
784
  ValueError,
785
785
  re.escape(
786
786
  "'same_as_input' for out_axes requires all NamedArray inputs with"
787
- ' named axes to have the same `named_axes`. Found multiple'
788
- " distinct `named_axes`:\n[{'x': 0, 'y': 1, 'z': 2}, {'z': 0,"
789
- " 'y': 1, 'x': 2}]"
787
+ ' named axes to have the same named_axes. Found multiple'
788
+ " distinct named_axes on inputs:\n"
789
+ "[{'x': 0, 'y': 1, 'z': 2}, {'z': 0, 'y': 1, 'x': 2}]"
790
790
  ),
791
791
  ):
792
792
  named_axes.nmap(lambda x, y: x, out_axes='same_as_input')(array1, array2)
@@ -796,8 +796,8 @@ class NamedAxesTest(parameterized.TestCase):
796
796
  with self.assertRaisesWithLiteralMatch(
797
797
  ValueError,
798
798
  "'same_as_input' for out_axes requires all NamedArray inputs with"
799
- ' named axes to have the same `named_axes`. Found multiple'
800
- " distinct `named_axes`:\n[{'x': 0}, {'x': 0, 'y': 1}]"
799
+ ' named axes to have the same named_axes. Found multiple'
800
+ " distinct named_axes on inputs:\n[{'x': 0}, {'x': 0, 'y': 1}]"
801
801
  ):
802
802
  named_axes.nmap(lambda x, y: x, out_axes='same_as_input')(array1, array2)
803
803
 
@@ -16,8 +16,10 @@
16
16
  from __future__ import annotations
17
17
 
18
18
  import abc
19
+ import functools
19
20
  from typing import Any, Callable, Self, TypeVar
20
21
 
22
+ from coordax import utils
21
23
  import jax
22
24
  import numpy as np
23
25
 
@@ -28,10 +30,15 @@ except ImportError:
28
30
 
29
31
 
30
32
  # TODO(shoyer): should this be a protocol?
33
+ @functools.partial(utils.export, module='coordax.experimental')
31
34
  class NDArray(abc.ABC):
32
35
  """Abstract base class for non-JAX arrays that can be used with Coordax.
33
36
 
34
- To register a new NDArray, use `coordax.register_ndarray()`.
37
+ THIS FEATURE IS EXPERIMENTAL, AND THE DETAILS ARE SUBJECT TO CHANGE. We
38
+ currently use it only to support jax-datetime, but expect it might be useful
39
+ in the future for other array types, e.g., with arbitrary units.
40
+
41
+ To register a new NDArray, use :func:`coordax.experimental.register_ndarray`.
35
42
 
36
43
  NDArray instances are also expected to be registered as JAX pytree nodes:
37
44
  https://docs.jax.dev/en/latest/pytrees.html#extending-pytrees
@@ -45,12 +52,12 @@ class NDArray(abc.ABC):
45
52
  @property
46
53
  @abc.abstractmethod
47
54
  def size(self) -> int:
48
- """Size of this array, typically `math.prod(self.shape)`."""
55
+ """Size of this array, typically ``math.prod(self.shape)``."""
49
56
 
50
57
  @property
51
58
  @abc.abstractmethod
52
59
  def ndim(self) -> int:
53
- """Number of dimensions in this array, typcially `len(self.shape)`."""
60
+ """Number of dimensions in this array, typcially ``len(self.shape)``."""
54
61
 
55
62
  @abc.abstractmethod
56
63
  def transpose(self, axes: tuple[int, ...]) -> Self:
@@ -58,7 +65,7 @@ class NDArray(abc.ABC):
58
65
 
59
66
  # Note: __getitem__ is not yet used by Coordax, but we will likely want it in
60
67
  # the near the future.
61
- # TODO(shoyer): Figure out the precise type for `value`.
68
+ # TODO(shoyer): Figure out the precise type for ``value``.
62
69
  @abc.abstractmethod
63
70
  def __getitem__(self, value) -> Self:
64
71
  """Index this array, returning a new array."""
@@ -72,7 +79,7 @@ Array = np.ndarray | jax.Array | NDArray
72
79
 
73
80
 
74
81
  def to_array(data: ArrayLike) -> Array:
75
- """Returns an NDArray compatible with JAX and Coordax.
82
+ """Returns an unlabeled NDArray compatible with JAX and Coordax.
76
83
 
77
84
  Args:
78
85
  data: a scalar, NumPy array, JAX array, or registered NDArray.
@@ -93,7 +100,8 @@ def to_array(data: ArrayLike) -> Array:
93
100
  if not isinstance(data, (np.ndarray, jax.Array, NDArray)):
94
101
  raise TypeError(
95
102
  'data must be a np.ndarray, jax.Array or a duck-typed array registered '
96
- f'with coordax.register_ndarray(), got {type(data).__name__}: {data}'
103
+ 'with coordax.experimental.register_ndarray(), got '
104
+ f'{type(data).__name__}: {data}'
97
105
  )
98
106
 
99
107
  return data
@@ -119,13 +127,19 @@ _FROM_NUMPY_FUNCS: list[
119
127
  ] = []
120
128
 
121
129
 
130
+ @functools.partial(utils.export, module='coordax.experimental')
122
131
  def register_ndarray(
123
132
  array_type: type[T],
124
133
  is_matching_numpy_array: Callable[[np.ndarray], bool],
125
134
  to_numpy: Callable[[T], np.ndarray],
126
135
  from_numpy: Callable[[np.ndarray], T],
127
136
  ) -> type[T]:
128
- """Registers a new duck-typed array type corresponding to a numpy dtype."""
137
+ # pylint: disable=g-doc-args
138
+ """Registers a new duck-typed array type corresponding to a numpy dtype.
139
+
140
+ See also:
141
+ :class:`coordax.experimental.NDArray`
142
+ """
129
143
  NDArray.register(array_type)
130
144
  _TO_NUMPY_FUNCS.append((array_type, to_numpy))
131
145
  _FROM_NUMPY_FUNCS.append((is_matching_numpy_array, from_numpy))
@@ -20,7 +20,7 @@ def assert_field_properties(
20
20
  rtol: float | None = 1e-5,
21
21
  atol: float | None = 1e-5,
22
22
  ):
23
- """Asserts that a Field has expected properties."""
23
+ """Asserts that a ``Field`` has expected properties."""
24
24
  if data is not None:
25
25
  if atol is None and rtol is None:
26
26
  chex.assert_trees_all_equal(actual.data, data)
@@ -48,7 +48,7 @@ def assert_fields_allclose(
48
48
  rtol: float = 1e-5,
49
49
  atol: float = 1e-5,
50
50
  ):
51
- """Asserts that two Fields are close and have matching coordinates."""
51
+ """Asserts that two ``Field``s are close and have matching coordinates."""
52
52
  assert_field_properties(
53
53
  actual=actual,
54
54
  data=desired.data,
@@ -63,7 +63,7 @@ def assert_fields_allclose(
63
63
 
64
64
 
65
65
  def assert_fields_equal(actual: coordax.Field, desired: coordax.Field):
66
- """Asserts that two Fields are equal and have matching coordinates."""
66
+ """Asserts that two ``Field``s are equal and have matching coordinates."""
67
67
  assert_field_properties(
68
68
  actual=actual,
69
69
  data=desired.data,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coordax
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Coordinate axes for scientific computing in JAX
5
5
  Author-email: Google LLC <noreply@google.com>
6
6
  License-Expression: Apache-2.0
@@ -6,6 +6,7 @@ coordax/coordinate_systems.py
6
6
  coordax/coordinate_systems_test.py
7
7
  coordax/coords.py
8
8
  coordax/doctest_test.py
9
+ coordax/experimental.py
9
10
  coordax/fields.py
10
11
  coordax/fields_test.py
11
12
  coordax/jax_datetime_integration_test.py
@@ -7,7 +7,7 @@ packages = ["coordax"]
7
7
 
8
8
  [project]
9
9
  name = "coordax"
10
- version = "0.2.1" # keep sync with __init__.py
10
+ version = "0.2.2" # keep sync with __init__.py
11
11
  description = "Coordinate axes for scientific computing in JAX"
12
12
  authors = [
13
13
  {name = "Google LLC", email = "noreply@google.com"},
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes