topozarr-core 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.
@@ -0,0 +1,282 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
10
+
11
+ [[package]]
12
+ name = "crossbeam-deque"
13
+ version = "0.8.6"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
16
+ dependencies = [
17
+ "crossbeam-epoch",
18
+ "crossbeam-utils",
19
+ ]
20
+
21
+ [[package]]
22
+ name = "crossbeam-epoch"
23
+ version = "0.9.18"
24
+ source = "registry+https://github.com/rust-lang/crates.io-index"
25
+ checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
26
+ dependencies = [
27
+ "crossbeam-utils",
28
+ ]
29
+
30
+ [[package]]
31
+ name = "crossbeam-utils"
32
+ version = "0.8.21"
33
+ source = "registry+https://github.com/rust-lang/crates.io-index"
34
+ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
35
+
36
+ [[package]]
37
+ name = "either"
38
+ version = "1.16.0"
39
+ source = "registry+https://github.com/rust-lang/crates.io-index"
40
+ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
41
+
42
+ [[package]]
43
+ name = "heck"
44
+ version = "0.5.0"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
47
+
48
+ [[package]]
49
+ name = "libc"
50
+ version = "0.2.186"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
53
+
54
+ [[package]]
55
+ name = "matrixmultiply"
56
+ version = "0.3.10"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
59
+ dependencies = [
60
+ "autocfg",
61
+ "rawpointer",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "ndarray"
66
+ version = "0.17.2"
67
+ source = "registry+https://github.com/rust-lang/crates.io-index"
68
+ checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
69
+ dependencies = [
70
+ "matrixmultiply",
71
+ "num-complex",
72
+ "num-integer",
73
+ "num-traits",
74
+ "portable-atomic",
75
+ "portable-atomic-util",
76
+ "rawpointer",
77
+ "rayon",
78
+ ]
79
+
80
+ [[package]]
81
+ name = "num-complex"
82
+ version = "0.4.6"
83
+ source = "registry+https://github.com/rust-lang/crates.io-index"
84
+ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
85
+ dependencies = [
86
+ "num-traits",
87
+ ]
88
+
89
+ [[package]]
90
+ name = "num-integer"
91
+ version = "0.1.46"
92
+ source = "registry+https://github.com/rust-lang/crates.io-index"
93
+ checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
94
+ dependencies = [
95
+ "num-traits",
96
+ ]
97
+
98
+ [[package]]
99
+ name = "num-traits"
100
+ version = "0.2.19"
101
+ source = "registry+https://github.com/rust-lang/crates.io-index"
102
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
103
+ dependencies = [
104
+ "autocfg",
105
+ ]
106
+
107
+ [[package]]
108
+ name = "numpy"
109
+ version = "0.28.0"
110
+ source = "registry+https://github.com/rust-lang/crates.io-index"
111
+ checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2"
112
+ dependencies = [
113
+ "libc",
114
+ "ndarray",
115
+ "num-complex",
116
+ "num-integer",
117
+ "num-traits",
118
+ "pyo3",
119
+ "pyo3-build-config",
120
+ "rustc-hash",
121
+ ]
122
+
123
+ [[package]]
124
+ name = "once_cell"
125
+ version = "1.21.4"
126
+ source = "registry+https://github.com/rust-lang/crates.io-index"
127
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
128
+
129
+ [[package]]
130
+ name = "portable-atomic"
131
+ version = "1.13.1"
132
+ source = "registry+https://github.com/rust-lang/crates.io-index"
133
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
134
+
135
+ [[package]]
136
+ name = "portable-atomic-util"
137
+ version = "0.2.7"
138
+ source = "registry+https://github.com/rust-lang/crates.io-index"
139
+ checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
140
+ dependencies = [
141
+ "portable-atomic",
142
+ ]
143
+
144
+ [[package]]
145
+ name = "proc-macro2"
146
+ version = "1.0.106"
147
+ source = "registry+https://github.com/rust-lang/crates.io-index"
148
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
149
+ dependencies = [
150
+ "unicode-ident",
151
+ ]
152
+
153
+ [[package]]
154
+ name = "pyo3"
155
+ version = "0.28.3"
156
+ source = "registry+https://github.com/rust-lang/crates.io-index"
157
+ checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
158
+ dependencies = [
159
+ "libc",
160
+ "once_cell",
161
+ "portable-atomic",
162
+ "pyo3-build-config",
163
+ "pyo3-ffi",
164
+ "pyo3-macros",
165
+ ]
166
+
167
+ [[package]]
168
+ name = "pyo3-build-config"
169
+ version = "0.28.3"
170
+ source = "registry+https://github.com/rust-lang/crates.io-index"
171
+ checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
172
+ dependencies = [
173
+ "target-lexicon",
174
+ ]
175
+
176
+ [[package]]
177
+ name = "pyo3-ffi"
178
+ version = "0.28.3"
179
+ source = "registry+https://github.com/rust-lang/crates.io-index"
180
+ checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
181
+ dependencies = [
182
+ "libc",
183
+ "pyo3-build-config",
184
+ ]
185
+
186
+ [[package]]
187
+ name = "pyo3-macros"
188
+ version = "0.28.3"
189
+ source = "registry+https://github.com/rust-lang/crates.io-index"
190
+ checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
191
+ dependencies = [
192
+ "proc-macro2",
193
+ "pyo3-macros-backend",
194
+ "quote",
195
+ "syn",
196
+ ]
197
+
198
+ [[package]]
199
+ name = "pyo3-macros-backend"
200
+ version = "0.28.3"
201
+ source = "registry+https://github.com/rust-lang/crates.io-index"
202
+ checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
203
+ dependencies = [
204
+ "heck",
205
+ "proc-macro2",
206
+ "pyo3-build-config",
207
+ "quote",
208
+ "syn",
209
+ ]
210
+
211
+ [[package]]
212
+ name = "quote"
213
+ version = "1.0.45"
214
+ source = "registry+https://github.com/rust-lang/crates.io-index"
215
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
216
+ dependencies = [
217
+ "proc-macro2",
218
+ ]
219
+
220
+ [[package]]
221
+ name = "rawpointer"
222
+ version = "0.2.1"
223
+ source = "registry+https://github.com/rust-lang/crates.io-index"
224
+ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
225
+
226
+ [[package]]
227
+ name = "rayon"
228
+ version = "1.12.0"
229
+ source = "registry+https://github.com/rust-lang/crates.io-index"
230
+ checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
231
+ dependencies = [
232
+ "either",
233
+ "rayon-core",
234
+ ]
235
+
236
+ [[package]]
237
+ name = "rayon-core"
238
+ version = "1.13.0"
239
+ source = "registry+https://github.com/rust-lang/crates.io-index"
240
+ checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
241
+ dependencies = [
242
+ "crossbeam-deque",
243
+ "crossbeam-utils",
244
+ ]
245
+
246
+ [[package]]
247
+ name = "rustc-hash"
248
+ version = "2.1.2"
249
+ source = "registry+https://github.com/rust-lang/crates.io-index"
250
+ checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
251
+
252
+ [[package]]
253
+ name = "syn"
254
+ version = "2.0.117"
255
+ source = "registry+https://github.com/rust-lang/crates.io-index"
256
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
257
+ dependencies = [
258
+ "proc-macro2",
259
+ "quote",
260
+ "unicode-ident",
261
+ ]
262
+
263
+ [[package]]
264
+ name = "target-lexicon"
265
+ version = "0.13.5"
266
+ source = "registry+https://github.com/rust-lang/crates.io-index"
267
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
268
+
269
+ [[package]]
270
+ name = "topozarr-core"
271
+ version = "0.1.0"
272
+ dependencies = [
273
+ "ndarray",
274
+ "numpy",
275
+ "pyo3",
276
+ ]
277
+
278
+ [[package]]
279
+ name = "unicode-ident"
280
+ version = "1.0.24"
281
+ source = "registry+https://github.com/rust-lang/crates.io-index"
282
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "topozarr-core"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ description = "Block-reduction kernel for topozarr pyramid generation"
7
+ readme = "README.md"
8
+
9
+ [lib]
10
+ name = "topozarr_core"
11
+ crate-type = ["cdylib"]
12
+
13
+ [dependencies]
14
+ pyo3 = { version = "0.28", features = ["extension-module", "abi3-py312"] }
15
+ numpy = "0.28"
16
+ ndarray = { version = "0.17", features = ["rayon"] }
17
+
18
+ [profile.release]
19
+ lto = true
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: topozarr-core
3
+ Version: 0.1.0
4
+ Classifier: Programming Language :: Python :: 3.12
5
+ Classifier: Programming Language :: Python :: 3.13
6
+ Classifier: Programming Language :: Python :: 3.14
7
+ Classifier: Programming Language :: Rust
8
+ Classifier: Topic :: Scientific/Engineering
9
+ Requires-Dist: numpy
10
+ Summary: Block-reduction kernel for topozarr pyramid generation
11
+ License-Expression: MIT
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
14
+ Project-URL: Repository, https://github.com/carbonplan/topozarr
15
+
16
+ # topozarr-core
17
+
18
+ Rust block-reduction kernel for [topozarr](https://github.com/carbonplan/topozarr) pyramid generation.
19
+
20
+ This package provides the low-level numeric kernel only. Install
21
+ [`topozarr`](https://pypi.org/project/topozarr/) for the user-facing API.
22
+
@@ -0,0 +1,6 @@
1
+ # topozarr-core
2
+
3
+ Rust block-reduction kernel for [topozarr](https://github.com/carbonplan/topozarr) pyramid generation.
4
+
5
+ This package provides the low-level numeric kernel only. Install
6
+ [`topozarr`](https://pypi.org/project/topozarr/) for the user-facing API.
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "topozarr-core"
3
+ version = "0.1.0"
4
+ description = "Block-reduction kernel for topozarr pyramid generation"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = ["numpy"]
8
+ license = "MIT"
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3.12",
11
+ "Programming Language :: Python :: 3.13",
12
+ "Programming Language :: Python :: 3.14",
13
+ "Programming Language :: Rust",
14
+ "Topic :: Scientific/Engineering",
15
+ ]
16
+
17
+ [project.urls]
18
+ Repository = "https://github.com/carbonplan/topozarr"
19
+
20
+ [build-system]
21
+ requires = ["maturin>=1.7,<2"]
22
+ build-backend = "maturin"
23
+
24
+ [tool.maturin]
25
+ module-name = "topozarr_core"
26
+ features = ["pyo3/extension-module"]
@@ -0,0 +1,230 @@
1
+ use ndarray::{ArrayD, ArrayViewD, IxDyn, Zip};
2
+ use numpy::{
3
+ IntoPyArray, PyArrayDescrMethods, PyArrayDyn, PyArrayMethods, PyUntypedArray,
4
+ PyUntypedArrayMethods,
5
+ };
6
+ use pyo3::exceptions::{PyTypeError, PyValueError};
7
+ use pyo3::prelude::*;
8
+
9
+ #[derive(Clone, Copy, PartialEq)]
10
+ enum Method {
11
+ Mean,
12
+ Max,
13
+ Min,
14
+ Sum,
15
+ }
16
+
17
+ impl Method {
18
+ fn parse(s: &str) -> PyResult<Self> {
19
+ match s {
20
+ "mean" => Ok(Method::Mean),
21
+ "max" => Ok(Method::Max),
22
+ "min" => Ok(Method::Min),
23
+ "sum" => Ok(Method::Sum),
24
+ _ => Err(PyValueError::new_err(format!(
25
+ "method must be one of 'mean', 'max', 'min', 'sum'; got {s:?}"
26
+ ))),
27
+ }
28
+ }
29
+ }
30
+
31
+ /// Element types the kernel dispatches over. `nan()` is `None` for integers.
32
+ trait Element: Copy + Send + Sync + PartialOrd + 'static {
33
+ const ZERO: Self;
34
+ fn to_f64(self) -> f64;
35
+ fn from_f64(v: f64) -> Self;
36
+ fn is_nan(self) -> bool;
37
+ fn nan() -> Option<Self>;
38
+ }
39
+
40
+ macro_rules! impl_element_int {
41
+ ($($t:ty),*) => {$(
42
+ impl Element for $t {
43
+ const ZERO: Self = 0;
44
+ fn to_f64(self) -> f64 { self as f64 }
45
+ fn from_f64(v: f64) -> Self { v as $t }
46
+ fn is_nan(self) -> bool { false }
47
+ fn nan() -> Option<Self> { None }
48
+ }
49
+ )*};
50
+ }
51
+
52
+ macro_rules! impl_element_float {
53
+ ($($t:ty),*) => {$(
54
+ impl Element for $t {
55
+ const ZERO: Self = 0.0;
56
+ fn to_f64(self) -> f64 { self as f64 }
57
+ fn from_f64(v: f64) -> Self { v as $t }
58
+ fn is_nan(self) -> bool { self.is_nan() }
59
+ fn nan() -> Option<Self> { Some(<$t>::NAN) }
60
+ }
61
+ )*};
62
+ }
63
+
64
+ impl_element_int!(u8, u16, i16, i32, i64);
65
+ impl_element_float!(f32, f64);
66
+
67
+ #[inline]
68
+ fn is_missing<T: Element>(v: T, fill: Option<T>) -> bool {
69
+ v.is_nan() || fill.is_some_and(|f| v == f)
70
+ }
71
+
72
+ /// Value emitted for a window with zero valid elements.
73
+ /// Unreachable for integer dtypes without a fill value (nothing can be missing).
74
+ #[inline]
75
+ fn all_missing_result<T: Element>(fill: Option<T>) -> T {
76
+ fill.or_else(T::nan).unwrap_or(T::ZERO)
77
+ }
78
+
79
+ fn reduce_window<T: Element>(
80
+ w: &ArrayViewD<T>,
81
+ method: Method,
82
+ fill: Option<T>,
83
+ skipna: bool,
84
+ ) -> T {
85
+ match method {
86
+ Method::Mean | Method::Sum => {
87
+ let mut acc = 0.0f64;
88
+ let mut count = 0usize;
89
+ for &v in w.iter() {
90
+ if skipna && is_missing(v, fill) {
91
+ continue;
92
+ }
93
+ acc += v.to_f64();
94
+ count += 1;
95
+ }
96
+ if count == 0 {
97
+ // sum over an all-missing window is 0, matching numpy nansum
98
+ // and xarray's skipna sum; mean is fill/NaN
99
+ return match method {
100
+ Method::Sum => T::ZERO,
101
+ _ => all_missing_result(fill),
102
+ };
103
+ }
104
+ if method == Method::Mean {
105
+ acc /= count as f64;
106
+ }
107
+ T::from_f64(acc)
108
+ }
109
+ Method::Max | Method::Min => {
110
+ let mut best: Option<T> = None;
111
+ for &v in w.iter() {
112
+ if skipna {
113
+ if is_missing(v, fill) {
114
+ continue;
115
+ }
116
+ } else if v.is_nan() {
117
+ return v;
118
+ }
119
+ best = Some(match best {
120
+ None => v,
121
+ Some(b) => {
122
+ let take = if method == Method::Max { v > b } else { v < b };
123
+ if take {
124
+ v
125
+ } else {
126
+ b
127
+ }
128
+ }
129
+ });
130
+ }
131
+ best.unwrap_or_else(|| all_missing_result(fill))
132
+ }
133
+ }
134
+ }
135
+
136
+ fn reduce<T: Element>(
137
+ a: ArrayViewD<T>,
138
+ stride: &[usize],
139
+ method: Method,
140
+ fill: Option<T>,
141
+ skipna: bool,
142
+ ) -> ArrayD<T> {
143
+ // Output: max(n // s, 1) per axis. Windows are min(s, n) wide so an axis
144
+ // smaller than its stride still yields one window; exact_chunks drops
145
+ // trailing partial windows, reproducing coarsen(boundary="trim").
146
+ let out_shape: Vec<usize> = a
147
+ .shape()
148
+ .iter()
149
+ .zip(stride)
150
+ .map(|(&n, &s)| (n / s).max(1))
151
+ .collect();
152
+ let window: Vec<usize> = a
153
+ .shape()
154
+ .iter()
155
+ .zip(stride)
156
+ .map(|(&n, &s)| s.min(n).max(1))
157
+ .collect();
158
+
159
+ let mut out = ArrayD::<T>::from_elem(IxDyn(&out_shape), T::ZERO);
160
+ Zip::from(&mut out)
161
+ .and(a.exact_chunks(IxDyn(&window)))
162
+ .par_for_each(|o, w| *o = reduce_window(&w.into_dyn(), method, fill, skipna));
163
+ out
164
+ }
165
+
166
+ #[pyfunction]
167
+ #[pyo3(signature = (a, stride, method, fill_value=None, skipna=true))]
168
+ fn block_reduce<'py>(
169
+ py: Python<'py>,
170
+ a: &Bound<'py, PyUntypedArray>,
171
+ stride: Vec<usize>,
172
+ method: &str,
173
+ fill_value: Option<f64>,
174
+ skipna: bool,
175
+ ) -> PyResult<Bound<'py, PyAny>> {
176
+ let method = Method::parse(method)?;
177
+ let ndim = a.ndim();
178
+ if ndim == 0 || ndim > 4 {
179
+ return Err(PyValueError::new_err(format!(
180
+ "array must have 1-4 dimensions, got {ndim}"
181
+ )));
182
+ }
183
+ if stride.len() != ndim {
184
+ return Err(PyValueError::new_err(format!(
185
+ "stride length {} does not match array ndim {ndim}",
186
+ stride.len()
187
+ )));
188
+ }
189
+ if stride.contains(&0) {
190
+ return Err(PyValueError::new_err("stride entries must be >= 1"));
191
+ }
192
+
193
+ macro_rules! dispatch {
194
+ ($t:ty) => {{
195
+ let arr = a.cast::<PyArrayDyn<$t>>()?;
196
+ let ro = arr.readonly();
197
+ let view = ro.as_array();
198
+ let fill = fill_value.map(<$t as Element>::from_f64);
199
+ let out = py.detach(|| reduce(view, &stride, method, fill, skipna));
200
+ Ok(out.into_pyarray(py).into_any())
201
+ }};
202
+ }
203
+
204
+ let dtype = a.dtype();
205
+ if dtype.is_equiv_to(&numpy::dtype::<f32>(py)) {
206
+ dispatch!(f32)
207
+ } else if dtype.is_equiv_to(&numpy::dtype::<f64>(py)) {
208
+ dispatch!(f64)
209
+ } else if dtype.is_equiv_to(&numpy::dtype::<i16>(py)) {
210
+ dispatch!(i16)
211
+ } else if dtype.is_equiv_to(&numpy::dtype::<i32>(py)) {
212
+ dispatch!(i32)
213
+ } else if dtype.is_equiv_to(&numpy::dtype::<i64>(py)) {
214
+ dispatch!(i64)
215
+ } else if dtype.is_equiv_to(&numpy::dtype::<u8>(py)) {
216
+ dispatch!(u8)
217
+ } else if dtype.is_equiv_to(&numpy::dtype::<u16>(py)) {
218
+ dispatch!(u16)
219
+ } else {
220
+ Err(PyTypeError::new_err(format!(
221
+ "unsupported dtype {dtype}; expected one of u8, u16, i16, i32, i64, f32, f64"
222
+ )))
223
+ }
224
+ }
225
+
226
+ #[pymodule]
227
+ fn topozarr_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
228
+ m.add_function(wrap_pyfunction!(block_reduce, m)?)?;
229
+ Ok(())
230
+ }