labelarray 0.1.0__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.
- labelarray-0.1.0/CHANGELOG.md +25 -0
- labelarray-0.1.0/LICENSE +21 -0
- labelarray-0.1.0/MANIFEST.in +4 -0
- labelarray-0.1.0/PKG-INFO +187 -0
- labelarray-0.1.0/README.md +159 -0
- labelarray-0.1.0/docs/releasing.md +51 -0
- labelarray-0.1.0/pyproject.toml +51 -0
- labelarray-0.1.0/scripts/check.sh +73 -0
- labelarray-0.1.0/setup.cfg +4 -0
- labelarray-0.1.0/src/labelarray/__init__.py +82 -0
- labelarray-0.1.0/src/labelarray/__version__.py +1 -0
- labelarray-0.1.0/src/labelarray/_compiled.py +101 -0
- labelarray-0.1.0/src/labelarray/align.py +40 -0
- labelarray-0.1.0/src/labelarray/array.py +474 -0
- labelarray-0.1.0/src/labelarray/collection.py +267 -0
- labelarray-0.1.0/src/labelarray/contract.py +47 -0
- labelarray-0.1.0/src/labelarray/errors.py +21 -0
- labelarray-0.1.0/src/labelarray/grouped.py +259 -0
- labelarray-0.1.0/src/labelarray/index.py +470 -0
- labelarray-0.1.0/src/labelarray/labels.py +244 -0
- labelarray-0.1.0/src/labelarray/py.typed +0 -0
- labelarray-0.1.0/src/labelarray/sums.py +317 -0
- labelarray-0.1.0/src/labelarray/transforms.py +610 -0
- labelarray-0.1.0/src/labelarray.egg-info/PKG-INFO +187 -0
- labelarray-0.1.0/src/labelarray.egg-info/SOURCES.txt +37 -0
- labelarray-0.1.0/src/labelarray.egg-info/dependency_links.txt +1 -0
- labelarray-0.1.0/src/labelarray.egg-info/requires.txt +14 -0
- labelarray-0.1.0/src/labelarray.egg-info/top_level.txt +1 -0
- labelarray-0.1.0/tests/test_array.py +232 -0
- labelarray-0.1.0/tests/test_collection.py +59 -0
- labelarray-0.1.0/tests/test_contraction.py +59 -0
- labelarray-0.1.0/tests/test_grouping.py +121 -0
- labelarray-0.1.0/tests/test_label_indexes.py +361 -0
- labelarray-0.1.0/tests/test_label_lookups.py +214 -0
- labelarray-0.1.0/tests/test_library_errors.py +29 -0
- labelarray-0.1.0/tests/test_readme.py +21 -0
- labelarray-0.1.0/tests/test_standalone.py +75 -0
- labelarray-0.1.0/tests/test_sums.py +235 -0
- labelarray-0.1.0/tests/test_transforms.py +376 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
The first release on its own. labelarray was developed as the array layer
|
|
6
|
+
of tidy3a (`tidy3a.labelarray`) and leaves it unchanged in behaviour:
|
|
7
|
+
|
|
8
|
+
- `Array`: a NumPy array with named axes, exact labels, attributes, and a
|
|
9
|
+
name. Selection by positions and labels (`isel`, `sel`), `transpose`,
|
|
10
|
+
`rename`, reductions (`reduce`), and contraction (`dot`). NumPy's ufuncs,
|
|
11
|
+
`np.where`, and Python's operators broadcast by axis name under the exact
|
|
12
|
+
alignment rule.
|
|
13
|
+
- `Collection`: arrays sharing axes, one label array per axis, with
|
|
14
|
+
selection, assignment, joins (`join`), nearest-label matching
|
|
15
|
+
(`nearest`), and conversion to and from xarray.
|
|
16
|
+
- The exact label rule (`labels`) and label indexes: sorted, sort-once,
|
|
17
|
+
and grid indexes, chosen by `index_for`, with `register_index` for other
|
|
18
|
+
label types.
|
|
19
|
+
- Grouping: `Grouping`, `factorize`, the key transforms `Bins`,
|
|
20
|
+
`Duration`, `Round`, and `GridCells`, and grouped reductions
|
|
21
|
+
(`grouped.reduce`, `counts`, `broadcast`) bit-identical to reducing each
|
|
22
|
+
group on its own.
|
|
23
|
+
- Float32 sums and means correctly rounded in any memory layout (`sums`),
|
|
24
|
+
with an optional compiled path (`labelarray[fast]`) giving the same bits.
|
|
25
|
+
- One family of errors under `LabelArrayError`.
|
labelarray-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rigoberto Leyva Salmeron
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: labelarray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Labelled N-D arrays in pure NumPy, with exact label semantics and reproducible reductions
|
|
5
|
+
Author: Rigoberto Leyva Salmeron
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rleyvasal/labelarray
|
|
8
|
+
Keywords: numpy,arrays,labelled arrays,named axes,xarray,reproducibility
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=2.0
|
|
17
|
+
Provides-Extra: xarray
|
|
18
|
+
Requires-Dist: xarray>=2024.6; extra == "xarray"
|
|
19
|
+
Requires-Dist: pandas>=2.2.2; extra == "xarray"
|
|
20
|
+
Provides-Extra: fast
|
|
21
|
+
Requires-Dist: numba>=0.60; extra == "fast"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: xarray>=2024.6; extra == "dev"
|
|
24
|
+
Requires-Dist: pandas>=2.2.2; extra == "dev"
|
|
25
|
+
Requires-Dist: numba>=0.60; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# labelarray
|
|
30
|
+
|
|
31
|
+
Labelled N-D arrays in pure NumPy, with exact label semantics and
|
|
32
|
+
reproducible reductions. It needs NumPy only; xarray is used only to convert
|
|
33
|
+
to and from xarray objects. It is the array layer of
|
|
34
|
+
[tidy3a](https://github.com/rleyvasal/tidy3a), released on its own.
|
|
35
|
+
|
|
36
|
+
It is deliberately small. What it offers that other array libraries do not:
|
|
37
|
+
|
|
38
|
+
1. **Exact labels.** Labels match only when they are the same kind
|
|
39
|
+
(booleans, numbers, times, durations, text, bytes) and exactly equal, in
|
|
40
|
+
lookups, alignment, and joins. The integer `2**53 + 1` never matches the
|
|
41
|
+
float `2**53`; pandas, and so xarray, pair labels through float64.
|
|
42
|
+
2. **Reproducible reductions.** Float32 sums and means are correctly
|
|
43
|
+
rounded: the float32 nearest the exact result, the same in any memory
|
|
44
|
+
layout. Grouped reductions are bit-identical to reducing each group on
|
|
45
|
+
its own.
|
|
46
|
+
3. **Collections.** Arrays sharing axes, with one label array per axis,
|
|
47
|
+
joined and aligned under those rules.
|
|
48
|
+
|
|
49
|
+
**Status:** version `0.1.0`, the first release on its own; the API may still
|
|
50
|
+
change before 1.0. What changed in each release is in
|
|
51
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
52
|
+
|
|
53
|
+
## Installing
|
|
54
|
+
|
|
55
|
+
Requires Python 3.11 or newer.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install labelarray # NumPy only
|
|
59
|
+
pip install "labelarray[xarray]" # with xarray: to_xarray() and from_xarray()
|
|
60
|
+
pip install "labelarray[fast]" # with Numba: compiled grouped float32 sums and means
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Results are the same with or without `fast`; it only makes large grouped
|
|
64
|
+
sums and means faster.
|
|
65
|
+
|
|
66
|
+
## Arrays
|
|
67
|
+
|
|
68
|
+
An `Array` is a NumPy array with one name per axis, labels for any of its
|
|
69
|
+
axes, attributes, and a name:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import numpy as np
|
|
73
|
+
from labelarray import Array
|
|
74
|
+
|
|
75
|
+
air = Array(
|
|
76
|
+
np.array([[271.0, 272.0, 273.0], [281.0, 282.0, 283.0]], dtype="float32"),
|
|
77
|
+
dims=("lat", "time"),
|
|
78
|
+
coords={"lat": [10.0, 20.0], "time": np.arange("2026-01-01", "2026-01-04", dtype="datetime64[D]")},
|
|
79
|
+
attrs={"units": "K"},
|
|
80
|
+
name="air",
|
|
81
|
+
)
|
|
82
|
+
print(air.sel(lat=20.0).data) # [281. 282. 283.]
|
|
83
|
+
print(air.isel(time=[0, 2]).labels("time")) # ['2026-01-01' '2026-01-03']
|
|
84
|
+
print(air.reduce("mean", ["time"]).data) # [272. 282.]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`isel` takes positions, slices, or masks; `sel` takes labels, looked up
|
|
88
|
+
exactly; one label or position removes the axis, a list keeps it.
|
|
89
|
+
`transpose`, `rename` (axes), `reduce` (sum, mean, min, max, median, std,
|
|
90
|
+
var, any, all, count, with `na_rm` and `min_count`), and `dot` (the sum of
|
|
91
|
+
products over named axes) complete the methods.
|
|
92
|
+
|
|
93
|
+
NumPy's ufuncs, `np.where`, and Python's operators broadcast by axis name.
|
|
94
|
+
Labels on a shared axis must match: the same labels in another order are
|
|
95
|
+
matched by label, and anything else is an `AlignError`. There is no outer
|
|
96
|
+
join by default; ask for one with `Collection.join`.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import numpy as np
|
|
100
|
+
from labelarray import Array
|
|
101
|
+
|
|
102
|
+
a = Array([1.0, 2.0, 3.0], ("x",), {"x": [1, 2, 3]})
|
|
103
|
+
b = Array([30.0, 10.0, 20.0], ("x",), {"x": [3, 1, 2]})
|
|
104
|
+
print((a + b).data) # [11. 22. 33.]
|
|
105
|
+
print(np.sqrt(a * 4).data) # [2. 2.82842712 3.46410162]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Labels are copied once into a read-only array the library owns, and each
|
|
109
|
+
label array's index is built on first use and kept with it: a sorted index,
|
|
110
|
+
a sort-once index for unsorted labels, or a grid index for evenly spaced
|
|
111
|
+
numbers and times, which looks labels up arithmetically. `register_index`
|
|
112
|
+
adds index classes for other label types, under the same exactness
|
|
113
|
+
obligations.
|
|
114
|
+
|
|
115
|
+
## Collections
|
|
116
|
+
|
|
117
|
+
A `Collection` holds variables with different axes, each axis with one size
|
|
118
|
+
and at most one label array:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from labelarray import Array, Collection
|
|
122
|
+
|
|
123
|
+
left = Collection({"a": Array([1.0, 2.0, 3.0], ("k",))}, {"k": [1, 3, 5]})
|
|
124
|
+
right = Collection({"b": Array([10.0, 20.0], ("k",))}, {"k": [5, 1]})
|
|
125
|
+
both = left.join(right, "inner")
|
|
126
|
+
print(both.coords["k"]) # [1 5]
|
|
127
|
+
print(both.array("b").data) # [20. 10.]
|
|
128
|
+
print(left.join(right, "left").array("b").data) # [20. nan 10.]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`isel`, `sel`, `select` (variables), `assign`, `rename` (variables), `join`
|
|
132
|
+
(left, inner, or outer), `nearest` (each label matched to its nearest within
|
|
133
|
+
a tolerance), `to_dict`, `to_xarray`, and `from_xarray` are its methods.
|
|
134
|
+
Missing positions after a join promote integers and booleans as xarray
|
|
135
|
+
does, and attributes that conflict are dropped.
|
|
136
|
+
|
|
137
|
+
## Grouping
|
|
138
|
+
|
|
139
|
+
A `Grouping` assigns each position a group code; `factorize` makes one from
|
|
140
|
+
key values, optionally after a key transform (`Bins`, `Duration`, `Round`,
|
|
141
|
+
`GridCells`), with every missing key (NaN, NaT) one group, dropped unless
|
|
142
|
+
asked for. `grouped.reduce` reduces every group at once:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import numpy as np
|
|
146
|
+
from labelarray import Array, Grouping, factorize, grouped
|
|
147
|
+
|
|
148
|
+
intensity = Array(np.array([10, 20, 30, 40, 50], dtype="float32"), ("point",))
|
|
149
|
+
codes, labels = factorize(np.array([0, 0, 1, 1, 2]))
|
|
150
|
+
rings = Grouping(codes, labels)
|
|
151
|
+
print(grouped.reduce(intensity, "mean", rings, "point", "ring").data) # [15. 35. 50.]
|
|
152
|
+
print(grouped.counts(rings, "ring").data) # [2 2 1]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`grouped.broadcast` puts a result per group back on each position.
|
|
156
|
+
|
|
157
|
+
## Errors
|
|
158
|
+
|
|
159
|
+
Every error is a `LabelArrayError` (a `ValueError`): `LabelError` for labels
|
|
160
|
+
that cannot be found, ordered, or compared; `AlignError` for axes that
|
|
161
|
+
cannot be paired; `GroupError` for keys that cannot be grouped; and
|
|
162
|
+
`ShapeError` for axis names, sizes, and ranks that do not fit.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
From a checkout, install the development dependencies and run the tests:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
python -m pip install -e ".[dev]"
|
|
170
|
+
python -m pytest -q
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
xarray and pandas are test dependencies: the tests check the library against
|
|
174
|
+
xarray and its label lookups against pandas. Before pushing, run the CI
|
|
175
|
+
checks in fresh environments with only declared dependencies (requires
|
|
176
|
+
[uv](https://docs.astral.sh/uv/)):
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
scripts/check.sh
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The [release procedure](docs/releasing.md) describes the GitHub Release
|
|
183
|
+
workflow.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# labelarray
|
|
2
|
+
|
|
3
|
+
Labelled N-D arrays in pure NumPy, with exact label semantics and
|
|
4
|
+
reproducible reductions. It needs NumPy only; xarray is used only to convert
|
|
5
|
+
to and from xarray objects. It is the array layer of
|
|
6
|
+
[tidy3a](https://github.com/rleyvasal/tidy3a), released on its own.
|
|
7
|
+
|
|
8
|
+
It is deliberately small. What it offers that other array libraries do not:
|
|
9
|
+
|
|
10
|
+
1. **Exact labels.** Labels match only when they are the same kind
|
|
11
|
+
(booleans, numbers, times, durations, text, bytes) and exactly equal, in
|
|
12
|
+
lookups, alignment, and joins. The integer `2**53 + 1` never matches the
|
|
13
|
+
float `2**53`; pandas, and so xarray, pair labels through float64.
|
|
14
|
+
2. **Reproducible reductions.** Float32 sums and means are correctly
|
|
15
|
+
rounded: the float32 nearest the exact result, the same in any memory
|
|
16
|
+
layout. Grouped reductions are bit-identical to reducing each group on
|
|
17
|
+
its own.
|
|
18
|
+
3. **Collections.** Arrays sharing axes, with one label array per axis,
|
|
19
|
+
joined and aligned under those rules.
|
|
20
|
+
|
|
21
|
+
**Status:** version `0.1.0`, the first release on its own; the API may still
|
|
22
|
+
change before 1.0. What changed in each release is in
|
|
23
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
24
|
+
|
|
25
|
+
## Installing
|
|
26
|
+
|
|
27
|
+
Requires Python 3.11 or newer.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install labelarray # NumPy only
|
|
31
|
+
pip install "labelarray[xarray]" # with xarray: to_xarray() and from_xarray()
|
|
32
|
+
pip install "labelarray[fast]" # with Numba: compiled grouped float32 sums and means
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Results are the same with or without `fast`; it only makes large grouped
|
|
36
|
+
sums and means faster.
|
|
37
|
+
|
|
38
|
+
## Arrays
|
|
39
|
+
|
|
40
|
+
An `Array` is a NumPy array with one name per axis, labels for any of its
|
|
41
|
+
axes, attributes, and a name:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import numpy as np
|
|
45
|
+
from labelarray import Array
|
|
46
|
+
|
|
47
|
+
air = Array(
|
|
48
|
+
np.array([[271.0, 272.0, 273.0], [281.0, 282.0, 283.0]], dtype="float32"),
|
|
49
|
+
dims=("lat", "time"),
|
|
50
|
+
coords={"lat": [10.0, 20.0], "time": np.arange("2026-01-01", "2026-01-04", dtype="datetime64[D]")},
|
|
51
|
+
attrs={"units": "K"},
|
|
52
|
+
name="air",
|
|
53
|
+
)
|
|
54
|
+
print(air.sel(lat=20.0).data) # [281. 282. 283.]
|
|
55
|
+
print(air.isel(time=[0, 2]).labels("time")) # ['2026-01-01' '2026-01-03']
|
|
56
|
+
print(air.reduce("mean", ["time"]).data) # [272. 282.]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`isel` takes positions, slices, or masks; `sel` takes labels, looked up
|
|
60
|
+
exactly; one label or position removes the axis, a list keeps it.
|
|
61
|
+
`transpose`, `rename` (axes), `reduce` (sum, mean, min, max, median, std,
|
|
62
|
+
var, any, all, count, with `na_rm` and `min_count`), and `dot` (the sum of
|
|
63
|
+
products over named axes) complete the methods.
|
|
64
|
+
|
|
65
|
+
NumPy's ufuncs, `np.where`, and Python's operators broadcast by axis name.
|
|
66
|
+
Labels on a shared axis must match: the same labels in another order are
|
|
67
|
+
matched by label, and anything else is an `AlignError`. There is no outer
|
|
68
|
+
join by default; ask for one with `Collection.join`.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import numpy as np
|
|
72
|
+
from labelarray import Array
|
|
73
|
+
|
|
74
|
+
a = Array([1.0, 2.0, 3.0], ("x",), {"x": [1, 2, 3]})
|
|
75
|
+
b = Array([30.0, 10.0, 20.0], ("x",), {"x": [3, 1, 2]})
|
|
76
|
+
print((a + b).data) # [11. 22. 33.]
|
|
77
|
+
print(np.sqrt(a * 4).data) # [2. 2.82842712 3.46410162]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Labels are copied once into a read-only array the library owns, and each
|
|
81
|
+
label array's index is built on first use and kept with it: a sorted index,
|
|
82
|
+
a sort-once index for unsorted labels, or a grid index for evenly spaced
|
|
83
|
+
numbers and times, which looks labels up arithmetically. `register_index`
|
|
84
|
+
adds index classes for other label types, under the same exactness
|
|
85
|
+
obligations.
|
|
86
|
+
|
|
87
|
+
## Collections
|
|
88
|
+
|
|
89
|
+
A `Collection` holds variables with different axes, each axis with one size
|
|
90
|
+
and at most one label array:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from labelarray import Array, Collection
|
|
94
|
+
|
|
95
|
+
left = Collection({"a": Array([1.0, 2.0, 3.0], ("k",))}, {"k": [1, 3, 5]})
|
|
96
|
+
right = Collection({"b": Array([10.0, 20.0], ("k",))}, {"k": [5, 1]})
|
|
97
|
+
both = left.join(right, "inner")
|
|
98
|
+
print(both.coords["k"]) # [1 5]
|
|
99
|
+
print(both.array("b").data) # [20. 10.]
|
|
100
|
+
print(left.join(right, "left").array("b").data) # [20. nan 10.]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`isel`, `sel`, `select` (variables), `assign`, `rename` (variables), `join`
|
|
104
|
+
(left, inner, or outer), `nearest` (each label matched to its nearest within
|
|
105
|
+
a tolerance), `to_dict`, `to_xarray`, and `from_xarray` are its methods.
|
|
106
|
+
Missing positions after a join promote integers and booleans as xarray
|
|
107
|
+
does, and attributes that conflict are dropped.
|
|
108
|
+
|
|
109
|
+
## Grouping
|
|
110
|
+
|
|
111
|
+
A `Grouping` assigns each position a group code; `factorize` makes one from
|
|
112
|
+
key values, optionally after a key transform (`Bins`, `Duration`, `Round`,
|
|
113
|
+
`GridCells`), with every missing key (NaN, NaT) one group, dropped unless
|
|
114
|
+
asked for. `grouped.reduce` reduces every group at once:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import numpy as np
|
|
118
|
+
from labelarray import Array, Grouping, factorize, grouped
|
|
119
|
+
|
|
120
|
+
intensity = Array(np.array([10, 20, 30, 40, 50], dtype="float32"), ("point",))
|
|
121
|
+
codes, labels = factorize(np.array([0, 0, 1, 1, 2]))
|
|
122
|
+
rings = Grouping(codes, labels)
|
|
123
|
+
print(grouped.reduce(intensity, "mean", rings, "point", "ring").data) # [15. 35. 50.]
|
|
124
|
+
print(grouped.counts(rings, "ring").data) # [2 2 1]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`grouped.broadcast` puts a result per group back on each position.
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
Every error is a `LabelArrayError` (a `ValueError`): `LabelError` for labels
|
|
132
|
+
that cannot be found, ordered, or compared; `AlignError` for axes that
|
|
133
|
+
cannot be paired; `GroupError` for keys that cannot be grouped; and
|
|
134
|
+
`ShapeError` for axis names, sizes, and ranks that do not fit.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
From a checkout, install the development dependencies and run the tests:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python -m pip install -e ".[dev]"
|
|
142
|
+
python -m pytest -q
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
xarray and pandas are test dependencies: the tests check the library against
|
|
146
|
+
xarray and its label lookups against pandas. Before pushing, run the CI
|
|
147
|
+
checks in fresh environments with only declared dependencies (requires
|
|
148
|
+
[uv](https://docs.astral.sh/uv/)):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
scripts/check.sh
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The [release procedure](docs/releasing.md) describes the GitHub Release
|
|
155
|
+
workflow.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Releasing labelarray
|
|
2
|
+
|
|
3
|
+
The release workflow is `.github/workflows/release.yml`. Publishing a GitHub
|
|
4
|
+
Release starts it. It requires a tag matching the package version, runs the
|
|
5
|
+
reusable CI checks on that commit, then publishes the same source archive
|
|
6
|
+
and wheel that passed metadata and clean-install checks. The publishing job
|
|
7
|
+
downloads those artifacts without rebuilding them.
|
|
8
|
+
|
|
9
|
+
## One-time setup
|
|
10
|
+
|
|
11
|
+
In `rleyvasal/labelarray`, create a GitHub environment named `pypi` (done
|
|
12
|
+
2026-10-25, with no protection rules). Release restrictions and required
|
|
13
|
+
reviewers can be added in the repository settings later; naming the
|
|
14
|
+
environment in YAML does not itself configure approval rules.
|
|
15
|
+
|
|
16
|
+
Register a PyPI trusted publisher with these values:
|
|
17
|
+
|
|
18
|
+
| Field | Value |
|
|
19
|
+
|---|---|
|
|
20
|
+
| PyPI project | `labelarray` |
|
|
21
|
+
| GitHub owner | `rleyvasal` |
|
|
22
|
+
| GitHub repository | `labelarray` |
|
|
23
|
+
| Workflow filename | `release.yml` |
|
|
24
|
+
| GitHub environment | `pypi` |
|
|
25
|
+
|
|
26
|
+
For a new project, use a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/).
|
|
27
|
+
For an existing project you manage, [add the publisher to that project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/).
|
|
28
|
+
The workflow uses [trusted publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/)
|
|
29
|
+
with a short-lived identity token; no saved PyPI API token is required.
|
|
30
|
+
|
|
31
|
+
The PyPI publisher was registered as a pending publisher on 2026-10-25;
|
|
32
|
+
the first successful release creates the project.
|
|
33
|
+
|
|
34
|
+
## Prepare and publish a version
|
|
35
|
+
|
|
36
|
+
1. Set `src/labelarray/__version__.py` to the release version, for example
|
|
37
|
+
`0.1.0`. The workflow rejects development versions such as `0.1.0.dev0`.
|
|
38
|
+
2. Move the release's changelog entries under a version-and-date heading.
|
|
39
|
+
Update the README and first-release notes for the published version.
|
|
40
|
+
3. Run `scripts/check.sh`.
|
|
41
|
+
4. Commit the release changes and push them. Create the corresponding
|
|
42
|
+
version tag, for example `v0.1.0`, on that commit and push the tag.
|
|
43
|
+
5. Publish a GitHub Release for the tag. This triggers the version check,
|
|
44
|
+
Python 3.11–3.14 tests, minimum-dependency tests, and distribution checks.
|
|
45
|
+
6. Approve the `pypi` environment deployment if its configured rules require
|
|
46
|
+
approval. Confirm the publishing job succeeds and verify the version
|
|
47
|
+
listed on [PyPI](https://pypi.org/project/labelarray/).
|
|
48
|
+
|
|
49
|
+
If a check fails, the publishing job does not run. Fix the failure before
|
|
50
|
+
releasing. After a successful upload, PyPI does not allow replacing that
|
|
51
|
+
version's files; subsequent changes need a new version.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "labelarray"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Labelled N-D arrays in pure NumPy, with exact label semantics and reproducible reductions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Rigoberto Leyva Salmeron" }]
|
|
14
|
+
keywords = ["numpy", "arrays", "labelled arrays", "named axes", "xarray", "reproducibility"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
# NumPy 2's promotion rules are assumed throughout. NumPy is the only requirement.
|
|
22
|
+
dependencies = [
|
|
23
|
+
"numpy>=2.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# to_xarray() and from_xarray(). 2.2.2 is the first pandas built for NumPy 2;
|
|
28
|
+
# older wheels fail to import alongside numpy>=2.0.
|
|
29
|
+
xarray = ["xarray>=2024.6", "pandas>=2.2.2"]
|
|
30
|
+
# Compiled grouped float32 sums and means (labelarray.sums); the same results as without it, faster.
|
|
31
|
+
# 0.60 is the first Numba release for NumPy 2.0. Numba supports a new NumPy some months after it appears.
|
|
32
|
+
fast = ["numba>=0.60"]
|
|
33
|
+
# xarray and pandas: the references the tests compare with.
|
|
34
|
+
# numba: the tests run the compiled path; the minimum-dependency job runs the NumPy path.
|
|
35
|
+
dev = ["xarray>=2024.6", "pandas>=2.2.2", "numba>=0.60", "pytest>=7.0"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/rleyvasal/labelarray"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.dynamic]
|
|
41
|
+
version = { attr = "labelarray.__version__.__version__" }
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
labelarray = ["py.typed"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run what CI runs (.github/workflows/tests.yml), locally, before pushing.
|
|
3
|
+
#
|
|
4
|
+
# Each job gets a fresh environment with only labelarray's declared
|
|
5
|
+
# dependencies, so nothing installed on this machine (a newer xarray, tidy3a)
|
|
6
|
+
# can hide a failure that CI would see.
|
|
7
|
+
#
|
|
8
|
+
# scripts/check.sh all jobs
|
|
9
|
+
# scripts/check.sh 3.12 one Python version only
|
|
10
|
+
#
|
|
11
|
+
# Needs uv (https://docs.astral.sh/uv/); it downloads missing Pythons.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
cd "$(dirname "$0")/.."
|
|
15
|
+
work="$(mktemp -d)"
|
|
16
|
+
trap 'rm -rf "$work"' EXIT
|
|
17
|
+
|
|
18
|
+
versions=("${@:-3.11 3.12 3.13 3.14}")
|
|
19
|
+
versions=(${versions[@]})
|
|
20
|
+
failed=()
|
|
21
|
+
|
|
22
|
+
run() {
|
|
23
|
+
local name="$1"; shift
|
|
24
|
+
printf '\n── %s ──\n' "$name"
|
|
25
|
+
if "$@"; then
|
|
26
|
+
printf ' ok: %s\n' "$name"
|
|
27
|
+
else
|
|
28
|
+
printf ' FAILED: %s\n' "$name"
|
|
29
|
+
failed+=("$name")
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pytest_job() {
|
|
34
|
+
local python="$1"; shift
|
|
35
|
+
local env="$work/py$python-$RANDOM"
|
|
36
|
+
uv venv -q --python "$python" "$env" &&
|
|
37
|
+
uv pip install -q --python "$env/bin/python" "$@" &&
|
|
38
|
+
"$env/bin/python" -m pytest -q -p no:cacheprovider
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for python in "${versions[@]}"; do
|
|
42
|
+
run "pytest (Python $python)" pytest_job "$python" -e ".[dev]"
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [[ $# -eq 0 ]]; then
|
|
46
|
+
run "pytest (minimum NumPy and xarray)" \
|
|
47
|
+
pytest_job 3.11 "numpy==2.0.*" "pandas==2.2.2" "xarray==2024.6.0" pytest -e .
|
|
48
|
+
|
|
49
|
+
distribution_job() {
|
|
50
|
+
uv build -q --out-dir "$work/dist" &&
|
|
51
|
+
uvx twine check --strict "$work"/dist/* &&
|
|
52
|
+
uv venv -q --python 3.12 "$work/clean" &&
|
|
53
|
+
uv pip install -q --python "$work/clean/bin/python" "$work"/dist/*.whl &&
|
|
54
|
+
(cd "$work" && "$work/clean/bin/python" -c \
|
|
55
|
+
"import labelarray, numpy as np; a = labelarray.Array(np.arange(3.0), ('t',), {'t': [1, 2, 3]}); print(a.sel(t=2).data, labelarray.__version__)") &&
|
|
56
|
+
uv venv -q --python 3.12 "$work/full" &&
|
|
57
|
+
uv pip install -q --python "$work/full/bin/python" "$(ls "$work"/dist/*.whl)[xarray]" &&
|
|
58
|
+
(cd "$work" && "$work/full/bin/python" -c \
|
|
59
|
+
"import labelarray, numpy as np; a = labelarray.Array(np.arange(3.0), ('t',), {'t': [1, 2, 3]}); print(labelarray.Collection({'a': a}, a.coords).to_xarray())") &&
|
|
60
|
+
uv venv -q --python 3.12 "$work/fast" &&
|
|
61
|
+
uv pip install -q --python "$work/fast/bin/python" "$(ls "$work"/dist/*.whl)[fast]" &&
|
|
62
|
+
(cd "$work" && "$work/fast/bin/python" -c \
|
|
63
|
+
"import numpy as np; from labelarray import sums; v = np.ones(200_000, np.float32); assert sums._compiled('grouped_estimate') is not None, 'compiled kernel not loaded'; print(sums.reduce(v, 'mean', (0,), codes=np.zeros(200_000, np.intp), groups=1))")
|
|
64
|
+
}
|
|
65
|
+
run "source distribution, metadata, and installed wheel (plain, with the xarray extra, and with the fast extra)" distribution_job
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
printf '\n'
|
|
69
|
+
if [[ ${#failed[@]} -gt 0 ]]; then
|
|
70
|
+
printf 'FAILED: %s\n' "${failed[@]}"
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
printf 'All checks passed; safe to push.\n'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""labelarray: labelled N-D arrays in NumPy, with exact labels and reproducible reductions.
|
|
2
|
+
|
|
3
|
+
The public API:
|
|
4
|
+
|
|
5
|
+
- ``Array``: a NumPy array with named axes, exact labels, attributes, and a
|
|
6
|
+
name; NumPy's ufuncs, ``np.where``, and Python's operators broadcast by axis
|
|
7
|
+
name. ``Collection``: arrays sharing axes, one label array per axis.
|
|
8
|
+
``concat``, ``align``, and ``broadcast`` work on several arrays (``array``,
|
|
9
|
+
``collection``).
|
|
10
|
+
- The exact label rule: ``kind``, ``equal``, ``compare``, ``strict_subset``,
|
|
11
|
+
``exact_cast``, ``lossy_join_type`` (``labels``); label indexes and their
|
|
12
|
+
factory, ``index_for`` and ``register_index`` (``index``).
|
|
13
|
+
- Grouping: ``Grouping``, with ``reduce``, ``counts``, and ``broadcast``
|
|
14
|
+
applying it to an array (``grouped``); ``factorize`` and the key transforms
|
|
15
|
+
``Bins``, ``Duration``, ``Round``, and ``GridCells`` (``transforms``).
|
|
16
|
+
- Numbers: float32 sums and means correctly rounded (``sums``) and the
|
|
17
|
+
contraction behind ``Array.dot`` (``contract``).
|
|
18
|
+
- Errors: ``LabelArrayError`` and its ``LabelError``, ``AlignError``,
|
|
19
|
+
``GroupError``, and ``ShapeError`` (``errors``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from labelarray.__version__ import __version__
|
|
23
|
+
from labelarray.array import Array, align, broadcast, concat, label_index
|
|
24
|
+
from labelarray.collection import Collection
|
|
25
|
+
from labelarray.errors import (
|
|
26
|
+
AlignError,
|
|
27
|
+
GroupError,
|
|
28
|
+
LabelArrayError,
|
|
29
|
+
LabelError,
|
|
30
|
+
ShapeError,
|
|
31
|
+
)
|
|
32
|
+
from labelarray.grouped import Grouping
|
|
33
|
+
from labelarray.index import (
|
|
34
|
+
GridIndex,
|
|
35
|
+
LabelIndex,
|
|
36
|
+
SortedIndex,
|
|
37
|
+
SortOnceIndex,
|
|
38
|
+
index_for,
|
|
39
|
+
register_index,
|
|
40
|
+
)
|
|
41
|
+
from labelarray.labels import (
|
|
42
|
+
compare,
|
|
43
|
+
equal,
|
|
44
|
+
exact_cast,
|
|
45
|
+
kind,
|
|
46
|
+
lossy_join_type,
|
|
47
|
+
strict_subset,
|
|
48
|
+
)
|
|
49
|
+
from labelarray.transforms import Bins, Duration, GridCells, Round, factorize
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"AlignError",
|
|
53
|
+
"Array",
|
|
54
|
+
"Bins",
|
|
55
|
+
"Collection",
|
|
56
|
+
"Duration",
|
|
57
|
+
"GridCells",
|
|
58
|
+
"GridIndex",
|
|
59
|
+
"GroupError",
|
|
60
|
+
"Grouping",
|
|
61
|
+
"LabelArrayError",
|
|
62
|
+
"LabelError",
|
|
63
|
+
"LabelIndex",
|
|
64
|
+
"Round",
|
|
65
|
+
"ShapeError",
|
|
66
|
+
"SortOnceIndex",
|
|
67
|
+
"SortedIndex",
|
|
68
|
+
"__version__",
|
|
69
|
+
"align",
|
|
70
|
+
"broadcast",
|
|
71
|
+
"compare",
|
|
72
|
+
"concat",
|
|
73
|
+
"equal",
|
|
74
|
+
"exact_cast",
|
|
75
|
+
"factorize",
|
|
76
|
+
"index_for",
|
|
77
|
+
"kind",
|
|
78
|
+
"label_index",
|
|
79
|
+
"lossy_join_type",
|
|
80
|
+
"register_index",
|
|
81
|
+
"strict_subset",
|
|
82
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|