atlas-python 0.12.0__tar.gz → 0.13.1__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.
- {atlas_python-0.12.0 → atlas_python-0.13.1}/.gitignore +2 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/Cargo.lock +4 -4
- {atlas_python-0.12.0 → atlas_python-0.13.1}/Cargo.toml +2 -2
- {atlas_python-0.12.0 → atlas_python-0.13.1}/PKG-INFO +40 -2
- {atlas_python-0.12.0 → atlas_python-0.13.1}/README.md +58 -7
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/Cargo.toml +1 -1
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/README.md +39 -1
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/datasets-and-arrays.md +21 -0
- atlas_python-0.13.1/atlas-python/docs/guides/stats.md +177 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/index.md +5 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/attr.rs +5 -5
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/dataset.rs +22 -17
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/error.rs +7 -3
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/store.rs +182 -52
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/tests/test_smoke.py +43 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/tests/test_xarray.py +64 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/pyproject.toml +1 -1
- {atlas_python-0.12.0 → atlas_python-0.13.1}/python/atlas/__init__.py +2 -1
- {atlas_python-0.12.0 → atlas_python-0.13.1}/python/atlas/__init__.pyi +77 -1
- atlas_python-0.13.1/python/atlas/store.py +106 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/python/atlas/xarray.py +81 -9
- {atlas_python-0.12.0 → atlas_python-0.13.1}/src/config.rs +43 -1
- atlas_python-0.13.1/src/dataset/cache.rs +54 -0
- atlas_python-0.13.1/src/dataset/mod.rs +11 -0
- atlas_python-0.13.1/src/dataset/pending_attrs.rs +244 -0
- atlas_python-0.12.0/src/dataset.rs → atlas_python-0.13.1/src/dataset/view.rs +54 -178
- {atlas_python-0.12.0 → atlas_python-0.13.1}/src/error.rs +16 -5
- {atlas_python-0.12.0 → atlas_python-0.13.1}/src/lib.rs +2 -0
- atlas_python-0.13.1/src/meta/mod.rs +21 -0
- atlas_python-0.13.1/src/meta/persist.rs +449 -0
- atlas_python-0.13.1/src/meta/schema.rs +230 -0
- atlas_python-0.13.1/src/meta/store_meta.rs +668 -0
- atlas_python-0.13.1/src/meta/type_index.rs +175 -0
- atlas_python-0.13.1/src/pruning/bitmap.rs +121 -0
- atlas_python-0.13.1/src/pruning/column.rs +414 -0
- atlas_python-0.13.1/src/pruning/format.rs +270 -0
- atlas_python-0.13.1/src/pruning/io.rs +266 -0
- atlas_python-0.13.1/src/pruning/mod.rs +463 -0
- atlas_python-0.13.1/src/pruning/value.rs +129 -0
- atlas_python-0.13.1/src/schema/array.rs +43 -0
- atlas_python-0.13.1/src/schema/attr.rs +252 -0
- atlas_python-0.13.1/src/schema/dtype.rs +277 -0
- atlas_python-0.13.1/src/schema/mod.rs +17 -0
- atlas_python-0.13.1/src/store/bulk_read.rs +228 -0
- atlas_python-0.13.1/src/store/durability.rs +282 -0
- atlas_python-0.12.0/src/store.rs → atlas_python-0.13.1/src/store/mod.rs +119 -366
- atlas_python-0.13.1/tests/integration.rs +1518 -0
- atlas_python-0.12.0/atlas-python/docs/guides/stats.md +0 -92
- atlas_python-0.12.0/src/meta.rs +0 -745
- atlas_python-0.12.0/src/schema.rs +0 -436
- atlas_python-0.12.0/tests/integration.rs +0 -746
- {atlas_python-0.12.0 → atlas_python-0.13.1}/.github/workflows/atlas-python-docs.yaml +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/.github/workflows/atlas-python-release.yaml +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/.github/workflows/atlas-rust-release.yaml +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/.github/workflows/ci.yaml +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/CONTRIBUTING.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/LICENSE +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/.python-version +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/LICENSE +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/benchmarks/README.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/benchmarks/_common.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/benchmarks/bench_collection.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/benchmarks/generate_bench_charts.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/assets/bench_gridded.svg +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/assets/bench_profile.svg +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/benchmarks.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/examples.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/attributes.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/bulk-reads.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/cloud-storage.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/codecs-and-meta.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/dask.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/dtypes.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/durability.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/shared-arrays.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/guides/xarray.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/installation.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/quickstart.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/reference/atlas.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/reference/dataset-view.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/reference/xarray-accessor.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/docs/vs-zarr-netcdf.md +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/01_basics.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/02_xarray.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/03_dask_streaming.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/04_meta_formats.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/05_codecs.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/06_stats_scan.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/07_shared_arrays.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/08_object_store.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/examples/09_missing_data.py +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/mkdocs.yml +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/dtype.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/lib.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/logging.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/src/runtime.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/examples/lifecycle.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/examples/sensor_fleet.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/examples/weather_store.rs +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/python/atlas/py.typed +0 -0
- {atlas_python-0.12.0 → atlas_python-0.13.1}/src/array.rs +0 -0
|
@@ -28,9 +28,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
|
28
28
|
|
|
29
29
|
[[package]]
|
|
30
30
|
name = "array-format"
|
|
31
|
-
version = "0.
|
|
31
|
+
version = "0.12.0"
|
|
32
32
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
-
checksum = "
|
|
33
|
+
checksum = "f7379c3303a9b9f0693ccd040d85da31e302a601efeafae8a6cb69341e42de41"
|
|
34
34
|
dependencies = [
|
|
35
35
|
"bytes",
|
|
36
36
|
"futures",
|
|
@@ -71,7 +71,7 @@ dependencies = [
|
|
|
71
71
|
|
|
72
72
|
[[package]]
|
|
73
73
|
name = "atlas-python"
|
|
74
|
-
version = "0.
|
|
74
|
+
version = "0.13.1"
|
|
75
75
|
dependencies = [
|
|
76
76
|
"atlas-rust",
|
|
77
77
|
"ndarray",
|
|
@@ -86,7 +86,7 @@ dependencies = [
|
|
|
86
86
|
|
|
87
87
|
[[package]]
|
|
88
88
|
name = "atlas-rust"
|
|
89
|
-
version = "0.
|
|
89
|
+
version = "0.13.1"
|
|
90
90
|
dependencies = [
|
|
91
91
|
"array-format",
|
|
92
92
|
"chrono",
|
|
@@ -5,7 +5,7 @@ members = ["atlas-python"]
|
|
|
5
5
|
# Registry name on crates.io (`atlas` is taken). The library is still
|
|
6
6
|
# imported as `atlas` — see `[lib]` below.
|
|
7
7
|
name = "atlas-rust"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.13.1"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
description = "Directory-based store for thousands of N-dimensional datasets local or remote using object storage."
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -22,7 +22,7 @@ all-features = true
|
|
|
22
22
|
rustdoc-args = ["--cfg", "docsrs"]
|
|
23
23
|
|
|
24
24
|
[dependencies]
|
|
25
|
-
array-format = "0.
|
|
25
|
+
array-format = "0.12.0"
|
|
26
26
|
object_store = "0.13"
|
|
27
27
|
serde = { version = "1", features = ["derive"] }
|
|
28
28
|
serde_json = "1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlas-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -220,6 +220,40 @@ for the full resolution order.
|
|
|
220
220
|
- `bool` is available as an *attribute* type but not as an array dtype.
|
|
221
221
|
- `binary`, `list[...]`, `fixed_size_list[...,N]` are reserved for a later release.
|
|
222
222
|
|
|
223
|
+
## Cross-dataset pruning
|
|
224
|
+
|
|
225
|
+
`ds.array_stats(name)` answers "what's the range of `temperature` in *this* dataset?" one at a
|
|
226
|
+
time. To ask "which of my 10 000 datasets could contain a value above 25?" as a single
|
|
227
|
+
vectorised scan, use the **pruning index** — a flattened, columnar view of the per-dataset
|
|
228
|
+
statistics, one row per dataset:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
import numpy as np
|
|
232
|
+
|
|
233
|
+
store = atlas.Atlas.open("/tmp/my_store")
|
|
234
|
+
|
|
235
|
+
# Footer only — every column's collection-wide min/max, no column data fetched.
|
|
236
|
+
summ = store.column_summaries()
|
|
237
|
+
if summ["temperature"]["max"] < 25.0:
|
|
238
|
+
... # nothing can match — skip the column entirely
|
|
239
|
+
|
|
240
|
+
# Fetch just the columns you need; each is a dict of numpy arrays over the row space.
|
|
241
|
+
idx = store.pruning_index(arrays=["temperature"])
|
|
242
|
+
col = idx["columns"]["temperature"]
|
|
243
|
+
|
|
244
|
+
ok = col["present"] & col["stats_valid"] & idx["live"] # exclude gaps + deleted rows
|
|
245
|
+
hits = np.where(ok & (col["max"] > 25.0))[0]
|
|
246
|
+
candidates = [idx["datasets"][i] for i in hits] # row -> dataset name
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The return value is self-describing: `idx["datasets"][i]` names row `i`, `idx["live"]` is the
|
|
250
|
+
delete mask (always `&` it in), and datasets that don't declare a column are explicit gaps
|
|
251
|
+
(`present` is `False`, with `row_count` 0). Only the columns you ask for are fetched from
|
|
252
|
+
storage, so the cost is independent of how many other columns the collection has. `min`/`max`
|
|
253
|
+
keep their source type (strings come back as `bytes`, timestamps as int64 nanoseconds). See the
|
|
254
|
+
[stats & scans guide](https://github.com/maris-development/atlas/blob/main/atlas-python/docs/guides/stats.md)
|
|
255
|
+
for the full API, including `global_attrs=` / `array_attrs=` columns.
|
|
256
|
+
|
|
223
257
|
## Cloud / object storage
|
|
224
258
|
|
|
225
259
|
With the `cloud` extra, `Atlas.open` / `Atlas.create` accept an
|
|
@@ -244,9 +278,13 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
244
278
|
| `add_xarray_dataset(ds, name, chunks=None, fill_value=None)` | Append an `xarray.Dataset` (does **not** flush). `fill_value` overrides the per-array fill (scalar or `{var: scalar}`); see [Missing data](#missing-data). |
|
|
245
279
|
| `open_as_xarray_dataset(name) -> xr.Dataset` | Read a dataset back (chunked vars come back dask-backed). |
|
|
246
280
|
| `open_as_many_xarray_dataset(names, concat_dim="dataset") -> xr.Dataset` | Open many datasets stacked along `concat_dim` (eager numpy). |
|
|
281
|
+
| `pruning_index(arrays=None, global_attrs=None, array_attrs=None) -> dict` | Flattened statistics for **only** the requested columns; see [Cross-dataset pruning](#cross-dataset-pruning). |
|
|
282
|
+
| `column_summaries() -> dict` | Every column's collection-wide min/max, read from the index footer alone (no column data fetched). |
|
|
283
|
+
| `dataset_row(name) -> int \| None` | The dataset's fixed row ordinal in the pruning index. |
|
|
284
|
+
| `row_slots() -> int` | Total row slots (live + tombstoned) — the pruning index's height. |
|
|
247
285
|
| `flush()` | The single durability boundary — persist everything. |
|
|
248
286
|
| `close()` | Alias for `flush()`; also the `with`-block exit. |
|
|
249
|
-
| `compact()` | Reclaim tombstoned space across cached array files. |
|
|
287
|
+
| `compact()` | Reclaim tombstoned space across cached array files, and renumber row ordinals. |
|
|
250
288
|
| `__enter__` / `__exit__` | Context-manager support (`__exit__` calls `close()`). |
|
|
251
289
|
|
|
252
290
|
### `atlas.DatasetView`
|
|
@@ -26,6 +26,7 @@ Each dataset is a named group of N-dimensional arrays with typed attributes —
|
|
|
26
26
|
```text
|
|
27
27
|
my_store/
|
|
28
28
|
├── atlas.json ← interned per-dataset schema + attribute-key namespace (JSON)
|
|
29
|
+
├── pruning.idx ← columnar min/max/count index across all datasets (see below)
|
|
29
30
|
├── _global/
|
|
30
31
|
│ └── data.af ← dataset-level (global) attribute values, one entry per dataset
|
|
31
32
|
├── temperature/
|
|
@@ -36,7 +37,7 @@ my_store/
|
|
|
36
37
|
└── data.af
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
`atlas.json` holds only the **schema** of the collection. Attribute **values** live in the `.af` files: per-variable attributes on the variable's own file, and dataset-level attributes in the reserved `_global` file.
|
|
40
|
+
`atlas.json` holds only the **schema** of the collection. Attribute **values** live in the `.af` files: per-variable attributes on the variable's own file, and dataset-level attributes in the reserved `_global` file. `pruning.idx` is a derived, columnar statistics index — everything you need to prune a query to a handful of candidate datasets without opening a single `.af` file.
|
|
40
41
|
|
|
41
42
|
---
|
|
42
43
|
|
|
@@ -44,7 +45,7 @@ my_store/
|
|
|
44
45
|
|
|
45
46
|
`atlas.json` is read **once** when the store is opened or created. Every subsequent mutation — `create_dataset`, `define_array`, `set_attribute`, `set_array_attribute`, `delete_array`, `delete_dataset` — only touches in-memory state; array writes buffer inside the per-array in-memory layer and attribute writes buffer in a pending-attribute map. **Nothing reaches disk until `Atlas::flush()` (or `Atlas::close()`).** Dropping an `Atlas` without flushing abandons every pending in-memory write.
|
|
46
47
|
|
|
47
|
-
A single `Atlas::flush()` drains the buffered attributes into their `.af` files, walks every cached `ArrayFile` (writing deltas + stats), and then serialises the in-memory schema to `atlas.json`. This gives one durability boundary for the whole store: N datasets ⇒ one delta file per touched array name (not one per dataset) and one `atlas.json` rewrite (not N).
|
|
48
|
+
A single `Atlas::flush()` drains the buffered attributes into their `.af` files, walks every cached `ArrayFile` (writing deltas + stats), fills and writes the `pruning.idx` statistics index, and then serialises the in-memory schema to `atlas.json`. This gives one durability boundary for the whole store: N datasets ⇒ one delta file per touched array name (not one per dataset) and one `atlas.json` rewrite (not N).
|
|
48
49
|
|
|
49
50
|
`DatasetView` is a borrowed handle into the atlas's shared meta — it has no `flush()` of its own.
|
|
50
51
|
|
|
@@ -56,7 +57,7 @@ A single `Atlas::flush()` drains the buffered attributes into their `.af` files,
|
|
|
56
57
|
|
|
57
58
|
The schema is a plain JSON file written on `Atlas::flush()` / `Atlas::close()`. It stores:
|
|
58
59
|
|
|
59
|
-
- **Store version** — for format upgrades. The current version is `
|
|
60
|
+
- **Store version** — for format upgrades. The current version is `3`; stores written by an older atlas are rejected on open with a clear `Error::UnsupportedVersion` (re-export to upgrade). Version 3 added the per-dataset **row ordinals + tombstone mask** that back the pruning index.
|
|
60
61
|
- **Interned schema pool** — each *distinct* per-dataset schema is stored once and referenced by index. A dataset's schema is its array schemas (per array: dtype, shape, chunk shape, named dimensions, codec) plus its **attribute-key namespace** (the names of its global and per-variable attribute keys). A collection of thousands of homogeneous datasets stores its schema a single time.
|
|
61
62
|
- **Dataset registry** — a map of dataset name → schema-pool index.
|
|
62
63
|
- **Merged schema** — a collection-wide summary listing every unique array (with its dtype widened across all datasets, its dimensions, and its per-variable attribute types) and every global attribute type. Descriptive only — reads always use each dataset's own schema — but handy for tools that want one view of "what's in here." Also exposed programmatically via `Atlas::merged_schema()`.
|
|
@@ -86,6 +87,52 @@ Each array variable gets its own subdirectory with a single `data.af` binary fil
|
|
|
86
87
|
- **Persisted statistics** — on `Atlas::flush()`, min, max, null count, and row count are computed per array per dataset and stored alongside the data. Cells equal to the array's fill value are tallied in `null_count` and excluded from `min`/`max` (NaN fills match NaN cells by bit pattern). Statistics survive store reopening.
|
|
87
88
|
- **In-memory caches** — a 256 MiB decoded block cache and a 64 MiB raw I/O cache sit in front of the object store for repeated reads.
|
|
88
89
|
|
|
90
|
+
### `pruning.idx`
|
|
91
|
+
|
|
92
|
+
The per-array `.af` stats above answer "what's the min/max of `temperature` in *this* dataset?" one dataset at a time. `pruning.idx` answers the **cross-dataset** question — "which of my 10 000 datasets could possibly contain a value above 25?" — as a single vectorised scan, without opening any `.af` file.
|
|
93
|
+
|
|
94
|
+
It is a flattened, **columnar** index, written at the store root on every `flush()`:
|
|
95
|
+
|
|
96
|
+
- **One logical row per dataset**, positional — row *i* is the dataset at ordinal *i*. Deleted datasets keep their slot (see [Deletes](#deletes-and-compaction) below) and are hidden by a liveness mask, so a row number never changes underneath a cached index.
|
|
97
|
+
- **One column per array, per global attribute, and per (array, attribute) pair.** Each column holds, for every dataset: a `present` bit, a `stats_valid` bit, `min`, `max`, `row_count`, and `null_count`. Datasets that don't declare that array/attribute are **explicit gaps** (`present = false`), so columns stay aligned no matter how heterogeneous the collection is.
|
|
98
|
+
- **Compacted and column-addressable.** Sparsity is the norm (~26 % density on a real 10 000-dataset collection), so on disk each column stores values only for its present rows, keyed by the bitmap. Columns are compressed individually (Zstd by default, via `StoreConfig::pruning_compression`) and located through a footer directory — reading 2 of 200 columns fetches ~2/200ths of the file via ranged reads, never the whole thing.
|
|
99
|
+
- **Guarded against staleness.** The footer carries a `meta_epoch` that must match `atlas.json`; an index left behind by a crash between the two writes is rejected (`Error::CorruptIndex`) rather than silently mis-attributing rows.
|
|
100
|
+
|
|
101
|
+
The read API is a two-step funnel — rule columns out from the footer alone, then fetch only the survivors:
|
|
102
|
+
|
|
103
|
+
```rust
|
|
104
|
+
use atlas::{ColumnKey, StatVal};
|
|
105
|
+
|
|
106
|
+
// Footer only — every column's collection-wide min/max, no column data fetched.
|
|
107
|
+
for (key, summary) in store.column_summaries().await? {
|
|
108
|
+
// e.g. skip a column whose global max can't satisfy the predicate.
|
|
109
|
+
let _ = summary.might_match(|_, hi| hi > &StatVal::Float(25.0));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Fetch just the columns you need. The returned index is self-describing:
|
|
113
|
+
// it carries the liveness mask and the row↔name mapping.
|
|
114
|
+
let key = ColumnKey::array("temperature");
|
|
115
|
+
let index = store.pruning_index(std::slice::from_ref(&key)).await?;
|
|
116
|
+
if let Some(view) = index.view(&key) {
|
|
117
|
+
// Deleted and absent rows are already excluded.
|
|
118
|
+
for row in view.candidates(|_, hi| hi > &StatVal::Float(25.0)) {
|
|
119
|
+
println!("candidate: {}", index.dataset_name(row).unwrap());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`min`/`max` keep the type the statistic was computed with (string min/max are lexicographic bytes; timestamps are integer nanoseconds); a column's collection-wide *declared* type is the merged dtype from `atlas.json`. **Attribute columns currently record presence only** — which datasets carry the key — not attribute values; filtering on an attribute's *value* through the index isn't wired up yet.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Deletes and compaction
|
|
129
|
+
|
|
130
|
+
`delete_dataset` and `delete_array` are **logical**. A deleted dataset is *tombstoned* — its slot stays in `atlas.json` (so every later dataset keeps its row ordinal) and a liveness bit is cleared; its entries in the shared `.af` files are marked deleted but the bytes remain. Reads, `list_datasets`, the merged schema, and the pruning index all skip tombstoned datasets, so a delete is invisible to consumers immediately, but nothing is reclaimed yet.
|
|
131
|
+
|
|
132
|
+
`Atlas::compact()` is the garbage collector. It rewrites every cached `.af` file with the tombstoned regions dropped, **renumbers** the surviving datasets to close the ordinal holes, and rebuilds the pruning index against the new numbering. It is the only operation that changes a dataset's row ordinal — so a row number cached from before a compact is stale afterwards.
|
|
133
|
+
|
|
134
|
+
Re-creating a deleted name reuses its slot (and pruning-index row), starting from a clean schema — the revived dataset never inherits the previous occupant's stats.
|
|
135
|
+
|
|
89
136
|
---
|
|
90
137
|
|
|
91
138
|
## Quick start
|
|
@@ -149,8 +196,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
149
196
|
| **Array** | An N-dimensional typed array with named dimensions and an optional chunk shape. |
|
|
150
197
|
| **Attribute** | A typed scalar attached to a dataset (metadata, not array data). |
|
|
151
198
|
| **Array file** | One `data.af` file per variable name, shared across all datasets that define that variable. |
|
|
152
|
-
| **
|
|
153
|
-
| **
|
|
199
|
+
| **Pruning index** | `pruning.idx` — a columnar min/max/count index across all datasets. Query it with `Atlas::pruning_index(&[ColumnKey])` / `column_summaries()` to prune to candidate datasets without reading array data. |
|
|
200
|
+
| **Flush** | `Atlas::flush()` — the single durability boundary. Persists every cached array file, the pruning index, and rewrites `atlas.json` from the in-memory `StoreMeta`. Must be called explicitly (or via `Atlas::close()` / Python's `with atlas:`). |
|
|
201
|
+
| **Compact** | `Atlas::compact()` rewrites every cached `.af` file to reclaim space after deletes, and renumbers dataset row ordinals. |
|
|
154
202
|
| **StoreConfig** | Configuration passed to `Atlas::create`. Currently holds the compression `Codec`. |
|
|
155
203
|
| **Codec** | Compression codec for new array blocks: `Codec::Zstd` (default), `Codec::Lz4`, or `Codec::Uncompressed`. Persisted in `atlas.json`; `open` reads it automatically. |
|
|
156
204
|
|
|
@@ -215,6 +263,7 @@ Reading `temperature` for 1 000 datasets means opening exactly **one file**. Thi
|
|
|
215
263
|
| Cross-dataset column scan | Slow (N file opens) | Slow (N directory opens) | Fast (1 file open) |
|
|
216
264
|
| Partial reads | Yes | Yes | Yes |
|
|
217
265
|
| Statistics (min/max/nulls) | No | No | Yes (persisted on flush) |
|
|
266
|
+
| Cross-dataset stat pruning | No | No | Yes (`pruning.idx`, vectorised) |
|
|
218
267
|
| Self-describing metadata | Yes | Yes | Yes (`atlas.json`) |
|
|
219
268
|
| Language support | C/Python/Julia/… | Python/Java/… | Rust |
|
|
220
269
|
| Mutable after write | Limited | Yes | Yes (chunked overwrites + compact) |
|
|
@@ -262,10 +311,12 @@ Two caches sit in front of the object store:
|
|
|
262
311
|
|
|
263
312
|
The decoded cache means repeated reads of the same chunk cost only a hash-map lookup. Both caches are shared across all `DatasetView`s that open the same `Atlas`.
|
|
264
313
|
|
|
265
|
-
### Persisted statistics
|
|
314
|
+
### Persisted statistics and pruning
|
|
266
315
|
|
|
267
316
|
Min, max, null count, and row count are computed and persisted on every `Atlas::flush()`. Downstream systems can read these statistics from the opened `DatasetView` without touching array data at all — useful for query planning, dashboards, or data-quality checks.
|
|
268
317
|
|
|
318
|
+
For **cross-dataset** query planning, the [`pruning.idx`](#pruningidx) index turns "which datasets could match?" into a vectorised scan over compacted, column-addressed statistics. A footer-only `column_summaries()` rules whole columns out for free; `pruning_index(&[…])` then fetches just the requested columns via ranged reads. On a real 10 000-dataset collection the whole index is ~0.3 MiB compressed, and a single-column query reads it in a couple of milliseconds — so a query engine can prune a fleet of datasets to a handful of candidates before opening a single `.af` file.
|
|
319
|
+
|
|
269
320
|
### Compression
|
|
270
321
|
|
|
271
322
|
The codec is chosen once at store creation via `StoreConfig` and written into `atlas.json`. `Atlas::open` reads it from there, so callers never need to repeat the codec choice. Each array also records its own codec in `atlas.json`, so a store can theoretically hold arrays written with different codecs if the schema is migrated.
|
|
@@ -280,7 +331,7 @@ Choose LZ4 when decompression throughput matters more than storage size (e.g. la
|
|
|
280
331
|
|
|
281
332
|
### Write path
|
|
282
333
|
|
|
283
|
-
Writes are buffered in-memory across the whole atlas. Calling `Atlas::flush()` compresses and writes every modified block across every cached array file, then rewrites `atlas.json` atomically (a single `PUT`). The write path scales with the number of modified chunks, not the number of datasets, and N consecutive `add_xarray_dataset` / `create_dataset` calls amortise to a single flush.
|
|
334
|
+
Writes are buffered in-memory across the whole atlas. Attribute writes buffer in a shared, **interned** pending map — identical values (a NetCDF collection is ~94 % duplicate attribute values) are stored once and shared, so the buffer stays small during a large ingest. Calling `Atlas::flush()` compresses and writes every modified block across every cached array file, writes the pruning index, then rewrites `atlas.json` atomically (a single `PUT`). The write path scales with the number of modified chunks, not the number of datasets, and N consecutive `add_xarray_dataset` / `create_dataset` calls amortise to a single flush.
|
|
284
335
|
|
|
285
336
|
### Compaction
|
|
286
337
|
|
|
@@ -188,6 +188,40 @@ for the full resolution order.
|
|
|
188
188
|
- `bool` is available as an *attribute* type but not as an array dtype.
|
|
189
189
|
- `binary`, `list[...]`, `fixed_size_list[...,N]` are reserved for a later release.
|
|
190
190
|
|
|
191
|
+
## Cross-dataset pruning
|
|
192
|
+
|
|
193
|
+
`ds.array_stats(name)` answers "what's the range of `temperature` in *this* dataset?" one at a
|
|
194
|
+
time. To ask "which of my 10 000 datasets could contain a value above 25?" as a single
|
|
195
|
+
vectorised scan, use the **pruning index** — a flattened, columnar view of the per-dataset
|
|
196
|
+
statistics, one row per dataset:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import numpy as np
|
|
200
|
+
|
|
201
|
+
store = atlas.Atlas.open("/tmp/my_store")
|
|
202
|
+
|
|
203
|
+
# Footer only — every column's collection-wide min/max, no column data fetched.
|
|
204
|
+
summ = store.column_summaries()
|
|
205
|
+
if summ["temperature"]["max"] < 25.0:
|
|
206
|
+
... # nothing can match — skip the column entirely
|
|
207
|
+
|
|
208
|
+
# Fetch just the columns you need; each is a dict of numpy arrays over the row space.
|
|
209
|
+
idx = store.pruning_index(arrays=["temperature"])
|
|
210
|
+
col = idx["columns"]["temperature"]
|
|
211
|
+
|
|
212
|
+
ok = col["present"] & col["stats_valid"] & idx["live"] # exclude gaps + deleted rows
|
|
213
|
+
hits = np.where(ok & (col["max"] > 25.0))[0]
|
|
214
|
+
candidates = [idx["datasets"][i] for i in hits] # row -> dataset name
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The return value is self-describing: `idx["datasets"][i]` names row `i`, `idx["live"]` is the
|
|
218
|
+
delete mask (always `&` it in), and datasets that don't declare a column are explicit gaps
|
|
219
|
+
(`present` is `False`, with `row_count` 0). Only the columns you ask for are fetched from
|
|
220
|
+
storage, so the cost is independent of how many other columns the collection has. `min`/`max`
|
|
221
|
+
keep their source type (strings come back as `bytes`, timestamps as int64 nanoseconds). See the
|
|
222
|
+
[stats & scans guide](https://github.com/maris-development/atlas/blob/main/atlas-python/docs/guides/stats.md)
|
|
223
|
+
for the full API, including `global_attrs=` / `array_attrs=` columns.
|
|
224
|
+
|
|
191
225
|
## Cloud / object storage
|
|
192
226
|
|
|
193
227
|
With the `cloud` extra, `Atlas.open` / `Atlas.create` accept an
|
|
@@ -212,9 +246,13 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
212
246
|
| `add_xarray_dataset(ds, name, chunks=None, fill_value=None)` | Append an `xarray.Dataset` (does **not** flush). `fill_value` overrides the per-array fill (scalar or `{var: scalar}`); see [Missing data](#missing-data). |
|
|
213
247
|
| `open_as_xarray_dataset(name) -> xr.Dataset` | Read a dataset back (chunked vars come back dask-backed). |
|
|
214
248
|
| `open_as_many_xarray_dataset(names, concat_dim="dataset") -> xr.Dataset` | Open many datasets stacked along `concat_dim` (eager numpy). |
|
|
249
|
+
| `pruning_index(arrays=None, global_attrs=None, array_attrs=None) -> dict` | Flattened statistics for **only** the requested columns; see [Cross-dataset pruning](#cross-dataset-pruning). |
|
|
250
|
+
| `column_summaries() -> dict` | Every column's collection-wide min/max, read from the index footer alone (no column data fetched). |
|
|
251
|
+
| `dataset_row(name) -> int \| None` | The dataset's fixed row ordinal in the pruning index. |
|
|
252
|
+
| `row_slots() -> int` | Total row slots (live + tombstoned) — the pruning index's height. |
|
|
215
253
|
| `flush()` | The single durability boundary — persist everything. |
|
|
216
254
|
| `close()` | Alias for `flush()`; also the `with`-block exit. |
|
|
217
|
-
| `compact()` | Reclaim tombstoned space across cached array files. |
|
|
255
|
+
| `compact()` | Reclaim tombstoned space across cached array files, and renumber row ordinals. |
|
|
218
256
|
| `__enter__` / `__exit__` | Context-manager support (`__exit__` calls `close()`). |
|
|
219
257
|
|
|
220
258
|
### `atlas.DatasetView`
|
|
@@ -56,6 +56,27 @@ jan = atlas.open_dataset("jan_2024")
|
|
|
56
56
|
atlas.delete_dataset("feb_2024")
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## Deleting datasets
|
|
60
|
+
|
|
61
|
+
`delete_dataset` is **logical**. The dataset immediately disappears from
|
|
62
|
+
`list_datasets()`, reads, the merged schema, and the pruning index — but under
|
|
63
|
+
the hood it is *tombstoned*: its slot stays in `atlas.json` and its bytes stay
|
|
64
|
+
in the shared array files. This keeps every other dataset's **row ordinal
|
|
65
|
+
stable** (`dataset_row(name)` never shifts under you, which is what lets a
|
|
66
|
+
cached [pruning index](stats.md) stay valid across a delete).
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
atlas.delete_dataset("feb_2024")
|
|
70
|
+
atlas.dataset_exists("feb_2024") # False — invisible immediately
|
|
71
|
+
atlas.row_slots() # still counts the dead slot until compact
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Call [`atlas.compact()`](durability.md) to actually reclaim the space: it
|
|
75
|
+
rewrites the array files without the tombstoned regions and **renumbers** the
|
|
76
|
+
surviving datasets to close the ordinal holes — the only operation that changes
|
|
77
|
+
a row ordinal. Re-creating a deleted name reuses its slot with a clean schema
|
|
78
|
+
(the revived dataset never inherits the old one's data or stats).
|
|
79
|
+
|
|
59
80
|
## Declaring an array
|
|
60
81
|
|
|
61
82
|
`define_array` records the schema (dtype, dims, shape, chunking, fill
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Stats and scans
|
|
2
|
+
|
|
3
|
+
Atlas computes and persists per-array summary statistics on every flush.
|
|
4
|
+
You can scan thousands of datasets without reading any raw chunks.
|
|
5
|
+
|
|
6
|
+
## What's tracked
|
|
7
|
+
|
|
8
|
+
`view.array_stats(name)` returns a dict (or `None` if the array doesn't
|
|
9
|
+
exist in this dataset, or hasn't been flushed yet):
|
|
10
|
+
|
|
11
|
+
| Key | Meaning |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `row_count` | The array's full logical size — the product of its shape. |
|
|
14
|
+
| `null_count` | Cells equal to the `fill_value`, **plus every cell never written**. |
|
|
15
|
+
| `min` | Minimum across written, non-fill cells. |
|
|
16
|
+
| `max` | Maximum across written, non-fill cells. |
|
|
17
|
+
|
|
18
|
+
`row_count` is capacity, not how much you wrote. An array declared with shape
|
|
19
|
+
`[10]` reports `row_count == 10` even if you only ever wrote 5 cells — the
|
|
20
|
+
other 5 show up in `null_count`, since they read back as the fill value:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
ds.define_array("t", dtype="int32", dims=["i"], shape=[10], chunk_shape=[5], fill_value=0)
|
|
24
|
+
ds.write_array("t", start=[0], data=np.array([1, 2, 3, 4, 5], dtype=np.int32))
|
|
25
|
+
atlas.flush()
|
|
26
|
+
ds.array_stats("t")
|
|
27
|
+
# {"row_count": 10, "null_count": 5, "min": 1, "max": 5}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
So `row_count - null_count` is the count of real values.
|
|
31
|
+
|
|
32
|
+
Stats are populated **after** [`atlas.flush()`](durability.md). Between a
|
|
33
|
+
`define_array` / `write_array` and the next flush, `array_stats(name)`
|
|
34
|
+
returns `None`.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
ds.write_array("readings", start=[0], data=values)
|
|
38
|
+
ds.array_stats("readings") # None — not flushed yet
|
|
39
|
+
atlas.flush()
|
|
40
|
+
ds.array_stats("readings") # {"row_count": ..., "null_count": ..., "min": ..., "max": ...}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Stats live in a per-array `{array}/data.stats` sidecar, next to that array's
|
|
44
|
+
`data.af` — **not** in `atlas.json`, which holds only schemas.
|
|
45
|
+
|
|
46
|
+
## Cross-dataset scans: the pruning index
|
|
47
|
+
|
|
48
|
+
`array_stats` answers for one array in one dataset. To ask "which datasets
|
|
49
|
+
could possibly match this?" across a whole collection, use the **pruning
|
|
50
|
+
index** — a flattened, columnar view with one row per dataset:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
store = atlas.Atlas.open("/tmp/store")
|
|
54
|
+
|
|
55
|
+
idx = store.pruning_index(arrays=["readings"])
|
|
56
|
+
col = idx["columns"]["readings"]
|
|
57
|
+
|
|
58
|
+
# Vectorized: no Python loop over datasets.
|
|
59
|
+
ok = col["present"] & col["stats_valid"] & idx["live"]
|
|
60
|
+
hits = np.where(ok & (col["max"] > 25.0))[0]
|
|
61
|
+
candidates = [idx["datasets"][i] for i in hits]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each column is a dict of numpy arrays, all the same length as `idx["rows"]`:
|
|
65
|
+
|
|
66
|
+
| Key | Meaning |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `present` | The dataset at this row declares the array/attribute. |
|
|
69
|
+
| `stats_valid` | `min`/`max` are meaningful here. |
|
|
70
|
+
| `min`, `max` | The per-dataset range. |
|
|
71
|
+
| `row_count`, `null_count` | As above — **both 0** where `present` is `False`. |
|
|
72
|
+
|
|
73
|
+
Row `i` is the dataset at ordinal `i` (`store.dataset_row(name)`), and
|
|
74
|
+
`idx["datasets"][i]` maps back to its name. Datasets that don't declare the
|
|
75
|
+
array are **explicit gaps** rather than missing entries, so columns stay
|
|
76
|
+
aligned across the collection no matter how heterogeneous it is. A dataset
|
|
77
|
+
without the array contributes `row_count == 0`, which is a real answer you can
|
|
78
|
+
sum or filter on directly.
|
|
79
|
+
|
|
80
|
+
`present` and `stats_valid` are separate on purpose: `stats_valid` is `False`
|
|
81
|
+
where there is no usable range even though the dataset does declare the array
|
|
82
|
+
— `List` dtypes, which carry no statistics at all, and rows written but not
|
|
83
|
+
yet flushed. Their `row_count` / `null_count` are still valid, so check
|
|
84
|
+
`present` for "does it have this?" and `stats_valid` before touching
|
|
85
|
+
`min`/`max`.
|
|
86
|
+
|
|
87
|
+
Deleted datasets keep their row and are masked by `idx["live"]`. Always `&` it
|
|
88
|
+
in, or a deleted dataset's values will widen your result.
|
|
89
|
+
|
|
90
|
+
### Read only what you need
|
|
91
|
+
|
|
92
|
+
`pruning.idx` is column-addressed: asking for two columns fetches two byte
|
|
93
|
+
ranges, not the file. On a 10 000-dataset collection with 781 columns, reading
|
|
94
|
+
one column takes ~2 ms.
|
|
95
|
+
|
|
96
|
+
Cheaper still, `column_summaries()` reads the **footer only** — every column's
|
|
97
|
+
collection-wide min/max and present count, no column data at all:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
summaries = store.column_summaries() # ~2 ms for 781 columns
|
|
101
|
+
if summaries["readings"]["max"] < 25.0:
|
|
102
|
+
... # no dataset can match; skip fetching the column entirely
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The index is compressed with zstd by default
|
|
106
|
+
(`StoreConfig.pruning_compression`) — on that same 10 000-dataset collection
|
|
107
|
+
it is 0.29 MB rather than 17.4 MB. Blocks are compressed individually, so
|
|
108
|
+
single-column reads stay ranged whatever codec you pick, and the codec is
|
|
109
|
+
recorded in the index itself so readers adapt without being told.
|
|
110
|
+
|
|
111
|
+
### Types
|
|
112
|
+
|
|
113
|
+
Statistics keep the type they were computed with — nothing is cast. `min` and
|
|
114
|
+
`max` come back as `int64`, `uint64`, `float64`, or a list of `bytes | None`,
|
|
115
|
+
chosen from what the column actually holds. A column that is `int32` in some
|
|
116
|
+
datasets and `float64` in others promotes to `float64` at the numpy boundary.
|
|
117
|
+
|
|
118
|
+
For a column's collection-wide *declared* type, use
|
|
119
|
+
[`merged_schema()`](datasets-and-arrays.md) — the index itself stores no dtype.
|
|
120
|
+
|
|
121
|
+
### Attribute columns
|
|
122
|
+
|
|
123
|
+
Attribute columns currently record **presence only**: `present` tells you which
|
|
124
|
+
datasets carry the key, but `stats_valid` is `False` and `min`/`max` are unset.
|
|
125
|
+
Filtering on attribute *values* through the index isn't wired up yet — read
|
|
126
|
+
them per dataset with `attributes()` for now.
|
|
127
|
+
|
|
128
|
+
[`examples/06_stats_scan.py`](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/06_stats_scan.py)
|
|
129
|
+
runs a per-dataset scan across 32 sensor datasets.
|
|
130
|
+
|
|
131
|
+
## How `null_count` works with `fill_value`
|
|
132
|
+
|
|
133
|
+
A "null" in atlas is **a cell whose stored value equals the array's
|
|
134
|
+
`fill_value`**. Both *unwritten* cells (which read back as the fill value)
|
|
135
|
+
and *written* cells whose value happens to match contribute to the count.
|
|
136
|
+
|
|
137
|
+
For float arrays the natural choice is `fill_value=float("nan")` — NaN
|
|
138
|
+
doesn't compare equal to itself by IEEE rules, but atlas special-cases
|
|
139
|
+
NaN, so a written NaN counts as a null.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
ds.define_array("temp", dtype="float32", ..., fill_value=float("nan"))
|
|
143
|
+
ds.write_array("temp", start=[0], data=np.array([1.0, np.nan, 3.0], dtype=np.float32))
|
|
144
|
+
atlas.flush()
|
|
145
|
+
ds.array_stats("temp")
|
|
146
|
+
# {"row_count": 3, "null_count": 1, "min": 1.0, "max": 3.0}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For integer arrays, pick a sentinel value (`-1`, `np.iinfo(dtype).min`,
|
|
150
|
+
etc.) — any *written* cell equal to it will also be counted as null. Pick
|
|
151
|
+
a value that can't appear in real data.
|
|
152
|
+
|
|
153
|
+
When ingesting via [`add_xarray_dataset`](xarray.md#fill-values-and-missing-data)
|
|
154
|
+
you don't set these by hand: float arrays default to a `NaN` fill, datetimes
|
|
155
|
+
to `NaT`, and strings to `""`, so cells masked by `mask_and_scale=True` are
|
|
156
|
+
counted as null automatically. Pass `fill_value=` to override.
|
|
157
|
+
|
|
158
|
+
## What stats don't include
|
|
159
|
+
|
|
160
|
+
- **No mean, sum, stdev, or quantiles.** If you need those, read the array
|
|
161
|
+
and compute them with numpy / dask. Atlas's stats are designed to be
|
|
162
|
+
cheap-on-write and cheap-on-read, not a full analytics engine.
|
|
163
|
+
- **No per-dimension reductions.** `min` / `max` are scalars over the
|
|
164
|
+
whole array, not per-row / per-column.
|
|
165
|
+
- **No per-chunk zone maps.** Statistics are per array per dataset; there is
|
|
166
|
+
no finer granularity to prune within a large array.
|
|
167
|
+
- **`List` / `FixedSizeList` arrays get no statistics at all** — not even
|
|
168
|
+
counts.
|
|
169
|
+
|
|
170
|
+
Strings and timestamps *are* covered. String `min`/`max` come back as `bytes`
|
|
171
|
+
and order lexicographically; timestamps come back as integer nanoseconds since
|
|
172
|
+
the epoch:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
ds.array_stats("station")["min"] # b"AAA01"
|
|
176
|
+
np.array(ds.array_stats("time")["min"]).astype("datetime64[ns]")
|
|
177
|
+
```
|
|
@@ -40,6 +40,11 @@ with atlas.Atlas.create("/tmp/my_store", codec="zstd") as store:
|
|
|
40
40
|
- **Bulk cross-dataset APIs** — `open_as_many_xarray_dataset` and
|
|
41
41
|
`read_array_across_stacked` collapse N per-dataset reads into one PyO3
|
|
42
42
|
call, sharing a single file handle.
|
|
43
|
+
- **Cross-dataset pruning** — a columnar min/max/count index
|
|
44
|
+
(`store.pruning_index(...)` / `column_summaries()`) prunes "which of my
|
|
45
|
+
10 000 datasets could match this predicate?" to a handful of candidates
|
|
46
|
+
with a single vectorised scan, without opening any array file. See
|
|
47
|
+
[Stats and scans](guides/stats.md).
|
|
43
48
|
- **Sync API, GIL-released** — a multi-threaded tokio runtime backs every
|
|
44
49
|
blocking call; the GIL is released so other Python threads can run.
|
|
45
50
|
- **Local fs or cloud storage** — pass a path string for local, or an
|
|
@@ -8,19 +8,19 @@ pub fn py_to_attr(value: &Bound<'_, PyAny>, dtype_hint: Option<&str>) -> PyResul
|
|
|
8
8
|
return py_to_attr_typed(value, hint);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
if let Ok(b) = value.
|
|
11
|
+
if let Ok(b) = value.cast::<PyBool>() {
|
|
12
12
|
return Ok(Attr::Bool(b.is_true()));
|
|
13
13
|
}
|
|
14
|
-
if value.
|
|
14
|
+
if value.cast::<PyString>().is_ok() {
|
|
15
15
|
return Ok(Attr::String(value.extract::<String>()?));
|
|
16
16
|
}
|
|
17
|
-
if value.
|
|
17
|
+
if value.cast::<PyBytes>().is_ok() {
|
|
18
18
|
return Ok(Attr::Binary(value.extract::<Vec<u8>>()?));
|
|
19
19
|
}
|
|
20
|
-
if value.
|
|
20
|
+
if value.cast::<PyInt>().is_ok() {
|
|
21
21
|
return Ok(Attr::Int64(value.extract::<i64>()?));
|
|
22
22
|
}
|
|
23
|
-
if value.
|
|
23
|
+
if value.cast::<PyFloat>().is_ok() {
|
|
24
24
|
return Ok(Attr::Float64(value.extract::<f64>()?));
|
|
25
25
|
}
|
|
26
26
|
Err(PyValueError::new_err(format!(
|