atlas-python 0.13.1__tar.gz → 0.14.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.
- {atlas_python-0.13.1 → atlas_python-0.14.0}/Cargo.lock +2 -2
- {atlas_python-0.13.1 → atlas_python-0.14.0}/Cargo.toml +1 -1
- {atlas_python-0.13.1 → atlas_python-0.14.0}/PKG-INFO +1 -1
- {atlas_python-0.13.1 → atlas_python-0.14.0}/README.md +18 -22
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/Cargo.toml +1 -1
- atlas_python-0.14.0/docs/README.md +67 -0
- atlas_python-0.14.0/docs/architecture.md +104 -0
- atlas_python-0.14.0/docs/data-model.md +108 -0
- atlas_python-0.14.0/docs/metadata.md +90 -0
- atlas_python-0.14.0/docs/pruning-index.md +159 -0
- atlas_python-0.14.0/docs/python-xarray.md +89 -0
- atlas_python-0.14.0/docs/storage-layout.md +98 -0
- atlas_python-0.14.0/docs/write-path.md +101 -0
- atlas_python-0.14.0/examples/bench_pruning.rs +75 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/pyproject.toml +1 -1
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/pruning/bitmap.rs +6 -43
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/pruning/column.rs +11 -33
- atlas_python-0.14.0/src/pruning/mod.rs +264 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/pruning/value.rs +29 -1
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/store/durability.rs +8 -112
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/store/mod.rs +216 -71
- {atlas_python-0.13.1 → atlas_python-0.14.0}/tests/integration.rs +82 -74
- atlas_python-0.13.1/src/pruning/format.rs +0 -270
- atlas_python-0.13.1/src/pruning/io.rs +0 -266
- atlas_python-0.13.1/src/pruning/mod.rs +0 -463
- {atlas_python-0.13.1 → atlas_python-0.14.0}/.github/workflows/atlas-python-docs.yaml +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/.github/workflows/atlas-python-release.yaml +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/.github/workflows/atlas-rust-release.yaml +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/.github/workflows/ci.yaml +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/.gitignore +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/CONTRIBUTING.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/LICENSE +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/.python-version +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/LICENSE +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/README.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/benchmarks/README.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/benchmarks/_common.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/benchmarks/bench_collection.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/benchmarks/generate_bench_charts.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/assets/bench_gridded.svg +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/assets/bench_profile.svg +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/benchmarks.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/examples.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/attributes.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/bulk-reads.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/cloud-storage.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/codecs-and-meta.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/dask.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/datasets-and-arrays.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/dtypes.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/durability.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/shared-arrays.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/stats.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/guides/xarray.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/index.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/installation.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/quickstart.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/reference/atlas.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/reference/dataset-view.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/reference/xarray-accessor.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/docs/vs-zarr-netcdf.md +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/01_basics.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/02_xarray.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/03_dask_streaming.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/04_meta_formats.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/05_codecs.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/06_stats_scan.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/07_shared_arrays.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/08_object_store.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/examples/09_missing_data.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/mkdocs.yml +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/attr.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/dataset.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/dtype.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/error.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/lib.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/logging.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/runtime.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/src/store.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/tests/test_smoke.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/atlas-python/tests/test_xarray.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/examples/lifecycle.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/examples/sensor_fleet.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/examples/weather_store.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/python/atlas/__init__.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/python/atlas/__init__.pyi +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/python/atlas/py.typed +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/python/atlas/store.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/python/atlas/xarray.py +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/array.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/config.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/dataset/cache.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/dataset/mod.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/dataset/pending_attrs.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/dataset/view.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/error.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/lib.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/meta/mod.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/meta/persist.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/meta/schema.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/meta/store_meta.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/meta/type_index.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/schema/array.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/schema/attr.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/schema/dtype.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/schema/mod.rs +0 -0
- {atlas_python-0.13.1 → atlas_python-0.14.0}/src/store/bulk_read.rs +0 -0
|
@@ -71,7 +71,7 @@ dependencies = [
|
|
|
71
71
|
|
|
72
72
|
[[package]]
|
|
73
73
|
name = "atlas-python"
|
|
74
|
-
version = "0.
|
|
74
|
+
version = "0.14.0"
|
|
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.14.0"
|
|
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.14.0"
|
|
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"
|
|
@@ -14,6 +14,8 @@ A directory-based store for thousands of named datasets, each holding N-dimensio
|
|
|
14
14
|
> **Looking for Python?** The Python bindings live in [`atlas-python/`](atlas-python/) — `pip install atlas-python`, then `import atlas`. They add a NumPy-native API and first-class [xarray](https://docs.xarray.dev) integration. See the [atlas-python README](atlas-python/README.md) for usage.
|
|
15
15
|
>
|
|
16
16
|
> The rest of this document covers **how the format works and the Rust crate**.
|
|
17
|
+
>
|
|
18
|
+
> **Architecture deep-dive?** [`docs/`](docs/) has focused walkthroughs with diagrams — [architecture](docs/architecture.md), [storage layout](docs/storage-layout.md), [data model](docs/data-model.md), [metadata](docs/metadata.md), [write path & durability](docs/write-path.md), [the pruning index](docs/pruning-index.md), and [Python/xarray](docs/python-xarray.md).
|
|
17
19
|
|
|
18
20
|
---
|
|
19
21
|
|
|
@@ -26,7 +28,6 @@ Each dataset is a named group of N-dimensional arrays with typed attributes —
|
|
|
26
28
|
```text
|
|
27
29
|
my_store/
|
|
28
30
|
├── atlas.json ← interned per-dataset schema + attribute-key namespace (JSON)
|
|
29
|
-
├── pruning.idx ← columnar min/max/count index across all datasets (see below)
|
|
30
31
|
├── _global/
|
|
31
32
|
│ └── data.af ← dataset-level (global) attribute values, one entry per dataset
|
|
32
33
|
├── temperature/
|
|
@@ -37,7 +38,7 @@ my_store/
|
|
|
37
38
|
└── data.af
|
|
38
39
|
```
|
|
39
40
|
|
|
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.
|
|
41
|
+
`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. There is **no separate index file** — the cross-dataset pruning index is built on demand from the `.af` statistics (see below), so it can never go stale and adds nothing to a write.
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
@@ -45,7 +46,7 @@ my_store/
|
|
|
45
46
|
|
|
46
47
|
`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.
|
|
47
48
|
|
|
48
|
-
A single `Atlas::flush()` drains the buffered attributes into their `.af` files, walks every cached `ArrayFile` (writing deltas + stats),
|
|
49
|
+
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). There is no index to write — the pruning index is derived from the `.af` stats on demand.
|
|
49
50
|
|
|
50
51
|
`DatasetView` is a borrowed handle into the atlas's shared meta — it has no `flush()` of its own.
|
|
51
52
|
|
|
@@ -87,28 +88,20 @@ Each array variable gets its own subdirectory with a single `data.af` binary fil
|
|
|
87
88
|
- **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.
|
|
88
89
|
- **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.
|
|
89
90
|
|
|
90
|
-
###
|
|
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.
|
|
91
|
+
### Pruning index (built on demand — no index file)
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
The per-array `.af` stats above answer "what's the min/max of `temperature` in *this* dataset?" one dataset at a time. The **pruning index** answers the **cross-dataset** question — "which of my 1 000 000 datasets could possibly contain a value above 25?" — as a single vectorised scan.
|
|
95
94
|
|
|
96
|
-
|
|
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.
|
|
95
|
+
There is **no `pruning.idx` file**. The `.af` stats *are* the source of truth, so the index is a denormalized pivot of data that already exists — Atlas rebuilds it **on demand** for the requested columns instead of persisting (and maintaining) a copy:
|
|
100
96
|
|
|
101
|
-
|
|
97
|
+
- `pruning_index(cols)` reads each requested array's `StatsFile` once and scatters its per-dataset entries into a flat, length-N column by dataset ordinal. Cost is proportional to the *requested* columns, not the (possibly 50 000+) total — so wide collections stay cheap.
|
|
98
|
+
- **One logical row per dataset**, positional — row *i* is the dataset at ordinal *i*. Deleted datasets keep their slot and are hidden by a liveness mask.
|
|
99
|
+
- **Columns**: one per array, per global attribute, and per (array, attribute) pair. Each holds, per dataset, a `present` bit, a `stats_valid` bit, `min`, `max`, `row_count`, `null_count`. Attribute columns now carry each dataset's **value** as a point range `[v, v]`, so you can range-prune on attributes too.
|
|
100
|
+
- Always consistent (no epoch, nothing to go stale), zero extra write memory, and nothing to write at `flush()`. Columns are built in parallel and the `name → ordinal` pivot map is cached across calls.
|
|
102
101
|
|
|
103
102
|
```rust
|
|
104
103
|
use atlas::{ColumnKey, StatVal};
|
|
105
104
|
|
|
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
105
|
// Fetch just the columns you need. The returned index is self-describing:
|
|
113
106
|
// it carries the liveness mask and the row↔name mapping.
|
|
114
107
|
let key = ColumnKey::array("temperature");
|
|
@@ -119,9 +112,12 @@ if let Some(view) = index.view(&key) {
|
|
|
119
112
|
println!("candidate: {}", index.dataset_name(row).unwrap());
|
|
120
113
|
}
|
|
121
114
|
}
|
|
115
|
+
|
|
116
|
+
// column_summaries() folds each column's collection-wide min/max/present_count
|
|
117
|
+
// (touches every column — a decide-what-to-read helper, not a hot path).
|
|
122
118
|
```
|
|
123
119
|
|
|
124
|
-
`min`/`max` keep the type the statistic was computed with (string min/max are lexicographic bytes; timestamps are integer nanoseconds)
|
|
120
|
+
`min`/`max` keep the type the statistic was computed with (string min/max are lexicographic bytes; timestamps are integer nanoseconds). **See [`docs/pruning-index.md`](docs/pruning-index.md) for the full design, diagrams, and benchmarks.**
|
|
125
121
|
|
|
126
122
|
---
|
|
127
123
|
|
|
@@ -196,7 +192,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
196
192
|
| **Array** | An N-dimensional typed array with named dimensions and an optional chunk shape. |
|
|
197
193
|
| **Attribute** | A typed scalar attached to a dataset (metadata, not array data). |
|
|
198
194
|
| **Array file** | One `data.af` file per variable name, shared across all datasets that define that variable. |
|
|
199
|
-
| **Pruning index** |
|
|
195
|
+
| **Pruning index** | A columnar min/max/count table across all datasets, **built on demand** from the `.af` stats (no index file). Query it with `Atlas::pruning_index(&[ColumnKey])` / `column_summaries()` to prune to candidate datasets without reading array data. |
|
|
200
196
|
| **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
197
|
| **Compact** | `Atlas::compact()` rewrites every cached `.af` file to reclaim space after deletes, and renumbers dataset row ordinals. |
|
|
202
198
|
| **StoreConfig** | Configuration passed to `Atlas::create`. Currently holds the compression `Codec`. |
|
|
@@ -263,7 +259,7 @@ Reading `temperature` for 1 000 datasets means opening exactly **one file**. Thi
|
|
|
263
259
|
| Cross-dataset column scan | Slow (N file opens) | Slow (N directory opens) | Fast (1 file open) |
|
|
264
260
|
| Partial reads | Yes | Yes | Yes |
|
|
265
261
|
| Statistics (min/max/nulls) | No | No | Yes (persisted on flush) |
|
|
266
|
-
| Cross-dataset stat pruning | No | No | Yes (
|
|
262
|
+
| Cross-dataset stat pruning | No | No | Yes (on-demand columnar index, vectorised) |
|
|
267
263
|
| Self-describing metadata | Yes | Yes | Yes (`atlas.json`) |
|
|
268
264
|
| Language support | C/Python/Julia/… | Python/Java/… | Rust |
|
|
269
265
|
| Mutable after write | Limited | Yes | Yes (chunked overwrites + compact) |
|
|
@@ -315,7 +311,7 @@ The decoded cache means repeated reads of the same chunk cost only a hash-map lo
|
|
|
315
311
|
|
|
316
312
|
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.
|
|
317
313
|
|
|
318
|
-
For **cross-dataset** query planning, the [
|
|
314
|
+
For **cross-dataset** query planning, the [pruning index](#pruning-index-built-on-demand--no-index-file) turns "which datasets could match?" into a vectorised scan over the array statistics. It's built on demand: `pruning_index(&[…])` reads just the requested columns' `.af` stats and pivots them into flat per-dataset vectors (columns built in parallel, the ordinal map cached); `column_summaries()` folds every column's collection-wide range to rule whole columns out. So a query engine can prune a fleet of datasets to a handful of candidates before opening a single `.af` file — with nothing to persist or keep in sync. See [`docs/pruning-index.md`](docs/pruning-index.md).
|
|
319
315
|
|
|
320
316
|
### Compression
|
|
321
317
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Atlas — Architecture Documentation
|
|
2
|
+
|
|
3
|
+
Atlas (**A**ggregated **T**ensor **L**arge **A**rray **S**tore) is a
|
|
4
|
+
directory-based store for **thousands to millions of named datasets**, each a
|
|
5
|
+
collection of N-dimensional arrays with attributes, backed by object storage
|
|
6
|
+
(local filesystem, S3, …).
|
|
7
|
+
|
|
8
|
+
This folder explains how the pieces fit together. Read the docs in order for a
|
|
9
|
+
full picture, or jump to a topic.
|
|
10
|
+
|
|
11
|
+
| # | Doc | What it covers |
|
|
12
|
+
|---|-----|----------------|
|
|
13
|
+
| 1 | [architecture.md](architecture.md) | The crates, the layers, and how a call flows top to bottom |
|
|
14
|
+
| 2 | [storage-layout.md](storage-layout.md) | What's on disk: `atlas.json`, `<array>/data.af`, `_global` |
|
|
15
|
+
| 3 | [data-model.md](data-model.md) | Datasets, arrays, array-name sharing, ordinals, attributes, dtypes |
|
|
16
|
+
| 4 | [metadata.md](metadata.md) | `atlas.json` / `StoreMeta`: schema interning, the type index, tombstones |
|
|
17
|
+
| 5 | [write-path.md](write-path.md) | create → write → flush → compact, buffering, and the durability boundary |
|
|
18
|
+
| 6 | [pruning-index.md](pruning-index.md) | The flat statistics table, **built on demand** from the array files |
|
|
19
|
+
| 7 | [python-xarray.md](python-xarray.md) | The `atlas-python` bindings and the xarray integration |
|
|
20
|
+
|
|
21
|
+
## The 10,000-foot view
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ Consumers: Rust API • Python (xarray) • CLI/notebooks │
|
|
26
|
+
└───────────────────────────────┬───────────────────────────────┘
|
|
27
|
+
│
|
|
28
|
+
┌───────────────────────────────▼───────────────────────────────┐
|
|
29
|
+
│ atlas-rust — the store │
|
|
30
|
+
│ │
|
|
31
|
+
│ Atlas one store: create/open, flush, compact, │
|
|
32
|
+
│ pruning_index (query stats across datasets) │
|
|
33
|
+
│ DatasetView one dataset: define/write/read arrays + attrs │
|
|
34
|
+
│ StoreMeta the schema + ordinals (atlas.json) │
|
|
35
|
+
│ Pruning a flat stats table, assembled on demand │
|
|
36
|
+
└───────────────────────────────┬───────────────────────────────┘
|
|
37
|
+
│ typed columnar arrays + stats
|
|
38
|
+
┌───────────────────────────────▼───────────────────────────────┐
|
|
39
|
+
│ array-format — one physical file per array name │
|
|
40
|
+
│ chunked · compressed · delta layers · per-dataset statistics │
|
|
41
|
+
└───────────────────────────────┬───────────────────────────────┘
|
|
42
|
+
│
|
|
43
|
+
┌───────────────────────────────▼───────────────────────────────┐
|
|
44
|
+
│ object_store — local FS • S3 • GCS • Azure • memory │
|
|
45
|
+
└───────────────────────────────────────────────────────────────┘
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The one idea that makes Atlas different
|
|
49
|
+
|
|
50
|
+
**Datasets that share an array name are co-located in one physical file, keyed
|
|
51
|
+
by dataset name.** A thousand sensors that each record `temperature` don't make
|
|
52
|
+
a thousand files — they make **one** `temperature/data.af`, with a thousand
|
|
53
|
+
entries inside. This is what lets Atlas scan a variable *across* datasets
|
|
54
|
+
cheaply, and it's the backbone of the [pruning index](pruning-index.md).
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
1,000 datasets ── each declares "temperature" and "salinity" ──►
|
|
58
|
+
|
|
59
|
+
store/
|
|
60
|
+
├── temperature/data.af ← 1,000 entries (one per dataset)
|
|
61
|
+
└── salinity/data.af ← 1,000 entries (one per dataset)
|
|
62
|
+
|
|
63
|
+
NOT: store/ds0/…, store/ds1/…, … (1,000 directories)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [data-model.md](data-model.md) for why, and [pruning-index.md](pruning-index.md)
|
|
67
|
+
for what it buys you.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# 1. Architecture
|
|
2
|
+
|
|
3
|
+
Atlas is three layers stacked on object storage. Each layer has one job.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
7
|
+
│ atlas-python (PyO3 bindings + xarray integration) │
|
|
8
|
+
│ • Atlas / DatasetView wrappers • ds.atlas.write(store) │
|
|
9
|
+
│ • numpy ⇄ atlas dtype mapping • add_xarray_dataset(ds, name) │
|
|
10
|
+
│ Releases the GIL, moves array data zero-copy into the core. │
|
|
11
|
+
└───────────────────────────────────┬──────────────────────────────────┘
|
|
12
|
+
│
|
|
13
|
+
┌───────────────────────────────────▼──────────────────────────────────┐
|
|
14
|
+
│ atlas-rust (the core — this crate) │
|
|
15
|
+
│ │
|
|
16
|
+
│ store/ Atlas: create/open, flush, compact, delete, queries │
|
|
17
|
+
│ dataset/ DatasetView: define/write/read arrays, get/set attrs │
|
|
18
|
+
│ meta/ StoreMeta: the schema, ordinals, type index (atlas.json) │
|
|
19
|
+
│ pruning/ the flat statistics table, assembled on demand │
|
|
20
|
+
│ schema/ DType, Attr, ArraySchema — the type system │
|
|
21
|
+
│ array.rs AtlasArray: the lazy per-array-file handle + cache │
|
|
22
|
+
│ config.rs Codec, MetaFormat, StoreConfig, TypeMismatchPolicy │
|
|
23
|
+
│ error.rs Error / Result │
|
|
24
|
+
└───────────────────────────────────┬──────────────────────────────────┘
|
|
25
|
+
│
|
|
26
|
+
┌───────────────────────────────────▼──────────────────────────────────┐
|
|
27
|
+
│ array-format (sibling crate — the columnar file format) │
|
|
28
|
+
│ ArrayFile: one physical file holding many datasets' entries for one │
|
|
29
|
+
│ array name. Chunked, per-chunk compressed, append-friendly via delta │
|
|
30
|
+
│ layers, with a per-dataset StatsFile and per-entry attributes. │
|
|
31
|
+
└───────────────────────────────────┬──────────────────────────────────┘
|
|
32
|
+
│
|
|
33
|
+
┌───────────────────────────────────▼──────────────────────────────────┐
|
|
34
|
+
│ object_store (local FS, S3, GCS, Azure, in-memory) │
|
|
35
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Who owns what
|
|
39
|
+
|
|
40
|
+
| Layer | Owns | Does **not** touch |
|
|
41
|
+
|-------|------|--------------------|
|
|
42
|
+
| `atlas-python` | ergonomics, numpy/xarray glue | storage, stats |
|
|
43
|
+
| `atlas-rust` | the *collection* — which datasets exist, their schema, cross-dataset queries | how one array's bytes are laid out |
|
|
44
|
+
| `array-format` | one physical array file — chunking, compression, stats, attributes | anything about *other* arrays or the collection |
|
|
45
|
+
| `object_store` | bytes in and out of a backend | structure |
|
|
46
|
+
|
|
47
|
+
The boundary that matters: **atlas-rust knows about the collection; array-format
|
|
48
|
+
knows about a single file.** Atlas never reaches into an array's byte layout;
|
|
49
|
+
array-format never knows two arrays belong to the same dataset.
|
|
50
|
+
|
|
51
|
+
## The central types
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Atlas ──────────────► one store (a directory / prefix)
|
|
55
|
+
│ create_dataset(name) -> DatasetView
|
|
56
|
+
│ open_dataset(name) -> DatasetView
|
|
57
|
+
│ flush() / compact()
|
|
58
|
+
│ pruning_index(cols) -> PruningIndex (cross-dataset stats)
|
|
59
|
+
│ read_array_across(…) -> stacked arrays (one var, many datasets)
|
|
60
|
+
│
|
|
61
|
+
├── StoreMeta (in memory, persisted as atlas.json)
|
|
62
|
+
│ datasets: name → schema (insertion order = ordinal, tombstoned)
|
|
63
|
+
│ schema_pool, type index, codec, meta_epoch
|
|
64
|
+
│
|
|
65
|
+
├── ArrayCache (name → AtlasArray, lazy)
|
|
66
|
+
│ one handle per physical array file, opened on first use
|
|
67
|
+
│
|
|
68
|
+
└── ordinal_map cache (name → ordinal, for the pruning pivot)
|
|
69
|
+
|
|
70
|
+
DatasetView ────────► one dataset inside a store
|
|
71
|
+
│ define_array / write_array / read_array
|
|
72
|
+
│ set_attribute / get_attribute (dataset-global)
|
|
73
|
+
│ set_array_attribute / … (per-array)
|
|
74
|
+
└── all mutations buffer in memory; nothing hits disk until Atlas::flush()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How a call flows
|
|
78
|
+
|
|
79
|
+
**Write** (`view.write_array("temp", …)`):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
DatasetView.write_array
|
|
83
|
+
└─ ArrayCache.get_or_insert("temp") → AtlasArray (lazy handle)
|
|
84
|
+
└─ AtlasArray.get().await → opens/creates temp/data.af
|
|
85
|
+
└─ ArrayFile.write_array → buffers into the pending delta
|
|
86
|
+
(spills 4 MiB blocks to a tempfile;
|
|
87
|
+
nothing durable yet)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Durability boundary** (`atlas.flush()`): commits every touched `ArrayFile`'s
|
|
91
|
+
pending delta to a sidecar layer, computes each array's `StatsFile`, and writes
|
|
92
|
+
`atlas.json`. See [write-path.md](write-path.md).
|
|
93
|
+
|
|
94
|
+
**Cross-dataset query** (`atlas.pruning_index([temp])`): reads `temp/data.af`'s
|
|
95
|
+
`StatsFile`, pivots the per-dataset entries into a flat length-N column by
|
|
96
|
+
ordinal. **No separate index is stored** — see [pruning-index.md](pruning-index.md).
|
|
97
|
+
|
|
98
|
+
## Runtime & concurrency
|
|
99
|
+
|
|
100
|
+
- Everything is `async` on Tokio. `Atlas` is `Send + Sync`.
|
|
101
|
+
- Each physical array file is guarded by its own `tokio::sync::RwLock`, so
|
|
102
|
+
different arrays are independent — the basis for the parallel pruning reads.
|
|
103
|
+
- Store metadata is a `parking_lot::Mutex` held only for short, non-`await`
|
|
104
|
+
critical sections.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# 3. Data Model
|
|
2
|
+
|
|
3
|
+
## Datasets and arrays
|
|
4
|
+
|
|
5
|
+
A **dataset** is a named collection of **arrays** (N-dimensional, typed) plus
|
|
6
|
+
attributes. Think of one NetCDF file → one dataset; its variables → arrays.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
dataset "GL_PR_BO_JLKU"
|
|
10
|
+
├── array TIME datetime64[ns] dims: [time]
|
|
11
|
+
├── array LATITUDE float32 dims: [time]
|
|
12
|
+
├── array TEMP float64 dims: [time, depth]
|
|
13
|
+
├── attr platform_code = "BO" (dataset-global)
|
|
14
|
+
└── TEMP.units = "degree_Celsius" (per-array attribute)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Array-name sharing (the core idea)
|
|
18
|
+
|
|
19
|
+
Two datasets that declare an array with the **same name** are stored in the
|
|
20
|
+
**same physical file**, keyed by dataset name. The store holds *N* physical
|
|
21
|
+
arrays where *N* = number of distinct array names, regardless of dataset count.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
ds "jan" ── TEMP, PSAL ──┐
|
|
25
|
+
ds "feb" ── TEMP, PSAL ──┤──► TEMP/data.af { jan, feb, mar }
|
|
26
|
+
ds "mar" ── TEMP, PSAL ──┘ PSAL/data.af { jan, feb, mar }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This is what makes "read one variable across all datasets" and the pruning
|
|
30
|
+
index cheap: the variable's data (and stats) are already gathered in one file.
|
|
31
|
+
|
|
32
|
+
**Corollary — normalize names.** If `jan` calls it `TEMP` and `feb` calls it
|
|
33
|
+
`TEMP_SBE`, they don't co-locate — you get a file per variant. For heterogeneous
|
|
34
|
+
ingest (per-file/station-specific names), map names to a shared schema *before*
|
|
35
|
+
writing. A store with a handful of shared array names scales to millions of
|
|
36
|
+
datasets; a store with per-dataset-unique names does not.
|
|
37
|
+
|
|
38
|
+
## Ordinals — a dataset's fixed row number
|
|
39
|
+
|
|
40
|
+
Every dataset has an **ordinal**: its position in the insertion-ordered dataset
|
|
41
|
+
list. The ordinal is a dataset's row in every cross-dataset view (the pruning
|
|
42
|
+
index, `read_array_across`).
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
ordinal 0 1 2 3
|
|
46
|
+
dataset ds0 ds1 ds2 ds3
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Ordinals are **stable**: deleting a dataset *tombstones* its slot (a liveness
|
|
50
|
+
bit flips to false) rather than removing it, so no later dataset's ordinal
|
|
51
|
+
shifts. The one operation that renumbers is [`compact`](write-path.md), which
|
|
52
|
+
drops tombstones and closes the gaps.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
delete ds1:
|
|
56
|
+
ordinal 0 1(dead) 2 3
|
|
57
|
+
dataset ds0 — ds2 ds3 ← ds2, ds3 keep ordinals 2, 3
|
|
58
|
+
|
|
59
|
+
compact:
|
|
60
|
+
ordinal 0 1 2
|
|
61
|
+
dataset ds0 ds2 ds3 ← renumbered, gaps closed
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Attributes
|
|
65
|
+
|
|
66
|
+
Two scopes, both key→value:
|
|
67
|
+
|
|
68
|
+
- **Dataset-global** (`set_attribute`) — e.g. `platform_code`, `cruise_id`.
|
|
69
|
+
Stored on the dataset's entry in `_global/data.af`.
|
|
70
|
+
- **Per-array** (`set_array_attribute`) — e.g. `units`, `valid_range`.
|
|
71
|
+
Stored on the array's entry in `<array>/data.af`.
|
|
72
|
+
|
|
73
|
+
Values are scalars or lists (`Attr`): bool, ints, floats, string, binary, and
|
|
74
|
+
list variants. The pruning index can range-prune on **scalar** attribute values
|
|
75
|
+
(a per-dataset value becomes a point range `[v, v]`); list-valued attributes are
|
|
76
|
+
tracked as present without a range.
|
|
77
|
+
|
|
78
|
+
## The type system
|
|
79
|
+
|
|
80
|
+
Atlas array dtypes (`DType`):
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
UInt8/16/32/64 Int8/16/32/64 Float32/64
|
|
84
|
+
String TimestampNs (nanoseconds since epoch)
|
|
85
|
+
List { child }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The numpy/xarray mapping (in `atlas-python`) covers these plus
|
|
89
|
+
`datetime64[ns]` → `TimestampNs` and `timedelta64` → `Int64` (+ a marker for
|
|
90
|
+
round-trip). Object/bytes/unicode → `String`.
|
|
91
|
+
|
|
92
|
+
### Type widening across datasets
|
|
93
|
+
|
|
94
|
+
Datasets sharing an array name may disagree on exact dtype; Atlas computes a
|
|
95
|
+
**merged type** for the collection:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
widen(Int32, Int64) = Int64
|
|
99
|
+
widen(Int32, Float32) = Float64 (int + float promotes to f64)
|
|
100
|
+
widen(UInt16, UInt32) = UInt32
|
|
101
|
+
widen(String, TimestampNs) = String (timestamps render as strings)
|
|
102
|
+
widen(List<Int8>, List<Int16>) = List<Int16> (element-wise)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Incompatible combinations (e.g. a numeric vs. a string array under the same
|
|
106
|
+
name) are **rejected at define time** under `TypeMismatchPolicy::Error`, or
|
|
107
|
+
warned under `::Warn`. The merged schema is what `merged_schema()` and the
|
|
108
|
+
pruning index's collection-wide summaries report.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# 4. Metadata (`atlas.json` / `StoreMeta`)
|
|
2
|
+
|
|
3
|
+
`StoreMeta` is the in-memory model of the collection's *structure*; `atlas.json`
|
|
4
|
+
is its on-disk form. It holds no bulk data and no attribute values — only
|
|
5
|
+
schema, ordinals, and the type index.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
StoreMeta
|
|
9
|
+
├── version format version
|
|
10
|
+
├── codec default Codec for new arrays (Zstd / Lz4 / none)
|
|
11
|
+
├── meta_epoch monotonic counter, bumped on every save
|
|
12
|
+
│
|
|
13
|
+
├── datasets: IndexMap<name, Arc<DatasetSchema>> ← insertion order = ordinal
|
|
14
|
+
│ (includes tombstones — never removed except by compact)
|
|
15
|
+
├── live: Vec<bool> ← liveness bit per ordinal
|
|
16
|
+
│
|
|
17
|
+
├── schema_pool: HashMap<hash, Vec<Arc<DatasetSchema>>> ← interning pool
|
|
18
|
+
└── index: TypeIndex ← derived: name → merged type
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Schema interning
|
|
22
|
+
|
|
23
|
+
Most collections are **homogeneous** — every dataset from one NetCDF folder has
|
|
24
|
+
the same variables with the same dtypes. Storing that schema once per dataset
|
|
25
|
+
would be wasteful, so identical `DatasetSchema`s are **interned**: deduplicated
|
|
26
|
+
by content hash into `schema_pool`, and every dataset holds an `Arc` to the
|
|
27
|
+
shared instance.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
1,000 identical datasets ──► schema_pool: { hash → [ one DatasetSchema ] }
|
|
31
|
+
datasets: 1,000 × Arc(same schema)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Editing a dataset's schema after interning uses `Arc::make_mut`, copying it out
|
|
35
|
+
of the pool first (copy-on-write), so shared instances are never mutated.
|
|
36
|
+
|
|
37
|
+
A `DatasetSchema` is:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
DatasetSchema
|
|
41
|
+
├── arrays: IndexMap<name, ArraySchema> (dtype, shape, chunks, dims, codec, fill)
|
|
42
|
+
├── global_attrs: IndexMap<key, DTypeS> (attribute-key namespace)
|
|
43
|
+
└── array_attrs: IndexMap<array, IndexMap<key, DTypeS>>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Tombstones and ordinals
|
|
47
|
+
|
|
48
|
+
Deletes are logical. `unrecord_dataset` clears the liveness bit; the entry stays
|
|
49
|
+
in `datasets` so ordinals below and above it don't shift. Enumeration goes
|
|
50
|
+
through the `live_*` methods so dead entries never leak:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
live_datasets() → (ordinal, name, schema) for live rows only
|
|
54
|
+
row_slots() → total slots incl. tombstones = the pruning row count
|
|
55
|
+
live_count() → live datasets only
|
|
56
|
+
live_mask() → Vec<bool>, applied by pruning views to hide deleted rows
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`compact` is the only thing that physically drops tombstones and renumbers (see
|
|
60
|
+
[write-path.md](write-path.md)).
|
|
61
|
+
|
|
62
|
+
## The type index
|
|
63
|
+
|
|
64
|
+
`TypeIndex` is **derived** from `datasets` (never set directly). It maps each
|
|
65
|
+
array name / attribute key to its collection-wide **merged type**, so that:
|
|
66
|
+
|
|
67
|
+
- defining an array can be checked against what other datasets already declared
|
|
68
|
+
(widen or reject — see [data-model.md](data-model.md));
|
|
69
|
+
- `merged_schema()` can report the whole collection's unified shape without
|
|
70
|
+
re-scanning every dataset.
|
|
71
|
+
|
|
72
|
+
It is rebuilt whenever the dataset set changes.
|
|
73
|
+
|
|
74
|
+
## `meta_epoch`
|
|
75
|
+
|
|
76
|
+
A monotonic counter bumped on every `save` (flush/compact). Historically it also
|
|
77
|
+
tagged a persisted pruning index so a stale index could be detected. **The
|
|
78
|
+
pruning index is now built on demand** (there's nothing persisted to go stale),
|
|
79
|
+
so `meta_epoch` is purely a save/version counter today.
|
|
80
|
+
|
|
81
|
+
## On-disk format (`MetaFormat`)
|
|
82
|
+
|
|
83
|
+
| Format | File | Use |
|
|
84
|
+
|--------|------|-----|
|
|
85
|
+
| `Json` (default) | `atlas.json` | human-readable, diff-able |
|
|
86
|
+
| `MsgPack` | `atlas.msgpack` | compact binary |
|
|
87
|
+
| `MsgPack` + compression | `atlas.msgpack.zst` / `.lz4` | large collections |
|
|
88
|
+
|
|
89
|
+
On `open`, the format and codec are **auto-detected** from the files present —
|
|
90
|
+
no config needed to reopen.
|