atlas-python 0.9.1__tar.gz → 0.12.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.12.0/.github/workflows/atlas-python-docs.yaml +88 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/.gitignore +5 -1
- {atlas_python-0.9.1 → atlas_python-0.12.0}/CONTRIBUTING.md +1 -1
- {atlas_python-0.9.1 → atlas_python-0.12.0}/Cargo.lock +4 -4
- {atlas_python-0.9.1 → atlas_python-0.12.0}/Cargo.toml +2 -2
- {atlas_python-0.9.1 → atlas_python-0.12.0}/PKG-INFO +44 -20
- {atlas_python-0.9.1 → atlas_python-0.12.0}/README.md +38 -15
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/Cargo.toml +1 -1
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/README.md +43 -19
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/benchmarks/README.md +4 -4
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/benchmarks/bench_collection.py +9 -9
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/benchmarks.md +4 -4
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/examples.md +2 -1
- atlas_python-0.12.0/atlas-python/docs/guides/attributes.md +115 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/bulk-reads.md +8 -8
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/cloud-storage.md +7 -7
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/dask.md +6 -6
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/datasets-and-arrays.md +51 -8
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/dtypes.md +5 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/durability.md +2 -2
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/shared-arrays.md +1 -1
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/stats.md +5 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/xarray.md +53 -8
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/index.md +11 -11
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/installation.md +5 -2
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/quickstart.md +5 -5
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/reference/xarray-accessor.md +2 -2
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/vs-zarr-netcdf.md +2 -2
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/02_xarray.py +6 -6
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/03_dask_streaming.py +3 -3
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/08_object_store.py +4 -4
- atlas_python-0.12.0/atlas-python/examples/09_missing_data.py +101 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/mkdocs.yml +4 -0
- atlas_python-0.12.0/atlas-python/src/attr.rs +95 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/dataset.rs +56 -10
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/error.rs +3 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/store.rs +82 -15
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/tests/test_smoke.py +1 -1
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/tests/test_xarray.py +220 -48
- {atlas_python-0.9.1 → atlas_python-0.12.0}/examples/lifecycle.rs +4 -4
- {atlas_python-0.9.1 → atlas_python-0.12.0}/examples/sensor_fleet.rs +2 -2
- {atlas_python-0.9.1 → atlas_python-0.12.0}/examples/weather_store.rs +17 -17
- {atlas_python-0.9.1 → atlas_python-0.12.0}/pyproject.toml +1 -1
- {atlas_python-0.9.1 → atlas_python-0.12.0}/python/atlas/__init__.pyi +62 -12
- {atlas_python-0.9.1 → atlas_python-0.12.0}/python/atlas/xarray.py +154 -29
- {atlas_python-0.9.1 → atlas_python-0.12.0}/src/array.rs +29 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/src/config.rs +28 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/src/dataset.rs +429 -75
- {atlas_python-0.9.1 → atlas_python-0.12.0}/src/error.rs +28 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/src/lib.rs +25 -10
- atlas_python-0.12.0/src/meta.rs +745 -0
- atlas_python-0.12.0/src/schema.rs +436 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/src/store.rs +138 -10
- {atlas_python-0.9.1 → atlas_python-0.12.0}/tests/integration.rs +352 -20
- atlas_python-0.9.1/atlas-python/docs/guides/attributes.md +0 -103
- atlas_python-0.9.1/atlas-python/src/attr.rs +0 -60
- atlas_python-0.9.1/src/meta.rs +0 -398
- atlas_python-0.9.1/src/schema.rs +0 -164
- {atlas_python-0.9.1 → atlas_python-0.12.0}/.github/workflows/atlas-python-release.yaml +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/.github/workflows/atlas-rust-release.yaml +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/.github/workflows/ci.yaml +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/LICENSE +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/.python-version +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/LICENSE +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/benchmarks/_common.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/benchmarks/generate_bench_charts.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/assets/bench_gridded.svg +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/assets/bench_profile.svg +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/guides/codecs-and-meta.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/reference/atlas.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/docs/reference/dataset-view.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/01_basics.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/04_meta_formats.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/05_codecs.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/06_stats_scan.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/examples/07_shared_arrays.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/dtype.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/lib.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/logging.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/src/runtime.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/python/atlas/__init__.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.12.0}/python/atlas/py.typed +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: atlas-python-docs
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
version:
|
|
6
|
+
description: "Docs version to publish (e.g. 1.2.3). Leave blank to publish as 'dev'."
|
|
7
|
+
required: false
|
|
8
|
+
type: string
|
|
9
|
+
alias:
|
|
10
|
+
description: "Alias to update (e.g. latest)."
|
|
11
|
+
required: false
|
|
12
|
+
default: dev
|
|
13
|
+
type: string
|
|
14
|
+
release:
|
|
15
|
+
types: [published]
|
|
16
|
+
push:
|
|
17
|
+
tags:
|
|
18
|
+
- "v*" # triggers on tags like v1.0.0, v2.3, etc.
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write
|
|
21
|
+
jobs:
|
|
22
|
+
deploy:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- name: Configure Git Credentials
|
|
27
|
+
run: |
|
|
28
|
+
git config user.name github-actions[bot]
|
|
29
|
+
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
|
30
|
+
|
|
31
|
+
- name: Extract version
|
|
32
|
+
id: vars
|
|
33
|
+
env:
|
|
34
|
+
INPUT_VERSION: ${{ github.event.inputs.version }}
|
|
35
|
+
INPUT_ALIAS: ${{ github.event.inputs.alias }}
|
|
36
|
+
run: |
|
|
37
|
+
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
|
38
|
+
# Tag push or release: refs/tags/v1.2.3 -> version=1.2.3, alias=latest
|
|
39
|
+
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
40
|
+
ALIAS=latest
|
|
41
|
+
else
|
|
42
|
+
# Manual run: use provided inputs, falling back to 'dev'
|
|
43
|
+
VERSION="${INPUT_VERSION:-dev}"
|
|
44
|
+
ALIAS="${INPUT_ALIAS:-dev}"
|
|
45
|
+
fi
|
|
46
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
47
|
+
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
48
|
+
|
|
49
|
+
- name: Show version
|
|
50
|
+
run: |
|
|
51
|
+
echo "Deploying docs version '${{ steps.vars.outputs.version }}' (alias '${{ steps.vars.outputs.alias }}')"
|
|
52
|
+
|
|
53
|
+
- uses: actions/setup-python@v6
|
|
54
|
+
with:
|
|
55
|
+
python-version: "3.12"
|
|
56
|
+
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
|
57
|
+
- uses: actions/cache@v4
|
|
58
|
+
with:
|
|
59
|
+
key: mkdocs-material-${{ env.cache_id }}
|
|
60
|
+
path: ~/.cache
|
|
61
|
+
restore-keys: |
|
|
62
|
+
mkdocs-material-
|
|
63
|
+
- run: pip install mkdocs-material mkdocstrings[python] mike
|
|
64
|
+
# Build & install the package so the compiled `atlas._atlas` extension is
|
|
65
|
+
# importable — mkdocstrings preloads it to resolve the re-exported API.
|
|
66
|
+
# Editable (-e) so maturin places the compiled `_atlas.*.so` into the
|
|
67
|
+
# python-source tree (atlas-python/python/atlas/), which is where griffe's
|
|
68
|
+
# `paths: ["python"]` search path looks for it. A plain install puts it in
|
|
69
|
+
# site-packages, outside that search path, and the alias fails to resolve.
|
|
70
|
+
# ubuntu-latest ships a Rust toolchain, so maturin can build from source.
|
|
71
|
+
- run: pip install -e ./atlas-python
|
|
72
|
+
- env:
|
|
73
|
+
VERSION: ${{ steps.vars.outputs.version }}
|
|
74
|
+
ALIAS: ${{ steps.vars.outputs.alias }}
|
|
75
|
+
run: |
|
|
76
|
+
git fetch origin gh-pages --depth=1 || true
|
|
77
|
+
# mike rejects an alias identical to the version, so only pass it when it differs
|
|
78
|
+
if [[ "$ALIAS" == "$VERSION" ]]; then
|
|
79
|
+
mike deploy --push --update-aliases -F atlas-python/mkdocs.yml "$VERSION"
|
|
80
|
+
else
|
|
81
|
+
mike deploy --push --update-aliases -F atlas-python/mkdocs.yml "$VERSION" "$ALIAS"
|
|
82
|
+
fi
|
|
83
|
+
# The site root must always redirect to `latest`, so only (re)write the
|
|
84
|
+
# root index.html when this run deploys the `latest` alias (releases).
|
|
85
|
+
# Manual `dev` runs are published at /dev/ but never touch the root.
|
|
86
|
+
if [[ "$ALIAS" == "latest" ]]; then
|
|
87
|
+
mike set-default --push -F atlas-python/mkdocs.yml latest
|
|
88
|
+
fi
|
|
@@ -28,4 +28,8 @@ raw_smoke_tests/
|
|
|
28
28
|
pyatlas/python/pyatlas/_pyatlas.pyd
|
|
29
29
|
pyatlas/python/pyatlas/_pyatlas.abi3.so
|
|
30
30
|
playground/
|
|
31
|
-
data/
|
|
31
|
+
data/
|
|
32
|
+
atlas-python/python/atlas/_atlas.pyd
|
|
33
|
+
atlas-python/python/atlas/_atlas.abi3.so
|
|
34
|
+
atlas-python/python/atlas/_atlas*.so
|
|
35
|
+
atlas-python/python/atlas/_atlas.pdb
|
|
@@ -83,7 +83,7 @@ Key design points:
|
|
|
83
83
|
- **Sync Python API, backed by an internal multi-threaded tokio runtime.** Each blocking call uses `py.allow_threads(|| RT.block_on(...))` so other Python threads can run.
|
|
84
84
|
- **Type dispatch via macros.** Atlas's read/write methods are generic over dtype (`T: ArrayElement`); the bindings dispatch at runtime via a `numeric_dispatch!` macro in [pyatlas/src/dataset.rs](pyatlas/src/dataset.rs).
|
|
85
85
|
- **`numpy`-zero-copy on the numeric path.** Python `np.ndarray` ↔ `ndarray::ArrayView` via the `numpy` crate.
|
|
86
|
-
- **xarray integration** lives in [pyatlas/python/pyatlas/xarray.py](pyatlas/python/pyatlas/xarray.py). The `Atlas.
|
|
86
|
+
- **xarray integration** lives in [pyatlas/python/pyatlas/xarray.py](pyatlas/python/pyatlas/xarray.py). The `Atlas.add_xarray_dataset` Rust pymethod and the `ds.atlas.write` accessor both delegate to the `_write_xarray_new_dataset` helper. dask-backed variables are streamed one chunk at a time via `arr.blocks[idx].compute()`.
|
|
87
87
|
|
|
88
88
|
---
|
|
89
89
|
|
|
@@ -28,9 +28,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
|
28
28
|
|
|
29
29
|
[[package]]
|
|
30
30
|
name = "array-format"
|
|
31
|
-
version = "0.
|
|
31
|
+
version = "0.11.0"
|
|
32
32
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
-
checksum = "
|
|
33
|
+
checksum = "f894cd59132aa6a00c360fef55469100c39da19c556bbf44a46bd647f7252fae"
|
|
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.12.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.12.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.12.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"
|
|
@@ -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.11.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.12.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -118,18 +118,18 @@ ds = xr.Dataset(
|
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
with atlas.Atlas.create("/tmp/my_store") as store:
|
|
121
|
-
store.
|
|
121
|
+
store.add_xarray_dataset(ds, "jan_2024") # store-side method
|
|
122
122
|
ds.atlas.write(store, "jan_2025") # xarray accessor (same effect)
|
|
123
123
|
|
|
124
124
|
# Read back as an xr.Dataset
|
|
125
125
|
store = atlas.Atlas.open("/tmp/my_store")
|
|
126
|
-
ds_back = store.
|
|
126
|
+
ds_back = store.open_as_xarray_dataset("jan_2024")
|
|
127
127
|
xr.testing.assert_identical(ds, ds_back)
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
### Bulk ingestion
|
|
131
131
|
|
|
132
|
-
`
|
|
132
|
+
`add_xarray_dataset` never flushes by itself — N consecutive calls accumulate in memory and a single
|
|
133
133
|
`flush()` (or the `with` exit) persists everything.
|
|
134
134
|
|
|
135
135
|
```python
|
|
@@ -138,35 +138,35 @@ import glob, os, atlas, xarray as xr
|
|
|
138
138
|
with atlas.Atlas.create("/tmp/store") as store:
|
|
139
139
|
for nc_path in sorted(glob.glob("*.nc")):
|
|
140
140
|
name = os.path.splitext(os.path.basename(nc_path))[0]
|
|
141
|
-
store.
|
|
141
|
+
store.add_xarray_dataset(xr.open_dataset(nc_path), name)
|
|
142
142
|
# One delta file per array name across the whole batch (not one per file).
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
### Streaming dask-backed writes
|
|
146
146
|
|
|
147
147
|
If a variable's `.data` is a `dask.array.Array` (e.g. from `xr.open_dataset(path, chunks=...)`
|
|
148
|
-
or `ds.chunk({...})`), `
|
|
148
|
+
or `ds.chunk({...})`), `add_xarray_dataset` / `ds.atlas.write` stream **one dask block at a time**
|
|
149
149
|
into the store rather than materialising the whole array. The dask chunk shape becomes the
|
|
150
150
|
on-disk `chunk_shape`, so the layout maps 1:1. Peak memory ≈ one chunk per variable.
|
|
151
151
|
|
|
152
152
|
```python
|
|
153
153
|
ds = xr.open_dataset("big.nc", chunks={"time": 100, "lat": -1, "lon": -1})
|
|
154
154
|
with atlas.Atlas.create("/tmp/store") as store:
|
|
155
|
-
store.
|
|
155
|
+
store.add_xarray_dataset(ds, "big") # streams chunk-by-chunk
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
Pass `chunks={var: [...]}` to `
|
|
158
|
+
Pass `chunks={var: [...]}` to `add_xarray_dataset` / `ds.atlas.write` to override the on-disk chunk
|
|
159
159
|
shape independently of dask's chunking.
|
|
160
160
|
|
|
161
161
|
### Lazy dask-backed reads
|
|
162
162
|
|
|
163
|
-
`store.
|
|
163
|
+
`store.open_as_xarray_dataset(name)` returns each variable dask-backed whenever it was stored with non-trivial
|
|
164
164
|
chunking (`chunk_shape != shape`); the dask `chunks` tuple mirrors the on-disk chunk grid and each
|
|
165
165
|
on-disk chunk is one dask task. Full-shape arrays (and 0-D scalars) come back eager as numpy. Call
|
|
166
166
|
`.compute()` to materialise, or slice / `map_blocks` to operate lazily.
|
|
167
167
|
|
|
168
168
|
```python
|
|
169
|
-
ds_back = atlas.Atlas.open("/tmp/store").
|
|
169
|
+
ds_back = atlas.Atlas.open("/tmp/store").open_as_xarray_dataset("big")
|
|
170
170
|
ds_back["temperature"].data # -> dask.array.Array
|
|
171
171
|
ds_back["temperature"][0:100].compute() # reads exactly one chunk
|
|
172
172
|
```
|
|
@@ -179,15 +179,35 @@ picklable, so call `.compute()` before handing off to distributed/multiprocessin
|
|
|
179
179
|
| Item | How it's stored |
|
|
180
180
|
| --- | --- |
|
|
181
181
|
| Each coord / data variable | A separate array, with `dims` mapped 1:1. |
|
|
182
|
-
| Dataset attrs | Dataset attributes, plain keys. |
|
|
183
|
-
| Per-variable attrs |
|
|
184
|
-
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). |
|
|
182
|
+
| Dataset attrs | Dataset-level (global) attributes, plain keys. |
|
|
183
|
+
| Per-variable attrs | Real per-variable attributes on each variable's array. |
|
|
184
|
+
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). See [Missing data](#missing-data). |
|
|
185
185
|
| Coord vs data_var distinction | JSON list in the internal `_pyatlas_coords` attr. |
|
|
186
186
|
| Non-scalar attr values (list, ndarray) | JSON-encoded string with a `json:` prefix marker. |
|
|
187
187
|
|
|
188
|
-
Each `
|
|
188
|
+
Each `add_xarray_dataset` / `ds.atlas.write` creates a *new* dataset — there is no append-into-existing
|
|
189
189
|
mode.
|
|
190
190
|
|
|
191
|
+
### Missing data
|
|
192
|
+
|
|
193
|
+
When a dataset is opened with `mask_and_scale=True` (xarray's default), masked cells become `NaN`
|
|
194
|
+
(floats) / `NaT` (datetimes) and `_FillValue` is moved into `var.encoding`. So those cells are
|
|
195
|
+
recorded as **null** (counted in `null_count`, excluded from min/max stats), arrays default to a
|
|
196
|
+
sentinel fill on write: `NaN` for floats, `NaT` for `datetime64[ns]`, and `""` for strings (integers
|
|
197
|
+
have none). Missing string cells (`None`/`NaN`) are substituted with the string fill and a warning is
|
|
198
|
+
emitted, since a string can't be stored as null directly.
|
|
199
|
+
|
|
200
|
+
Override per write with `fill_value` — a bare scalar (numeric arrays) or a `{var: scalar}` dict
|
|
201
|
+
(`None` disables the default for that var); an explicit CF `_FillValue` attribute still takes
|
|
202
|
+
precedence over the default:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
store.add_xarray_dataset(ds, "jan_2024", fill_value={"temperature": -9999.0})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
See the [xarray guide](https://github.com/maris-development/atlas/blob/main/atlas-python/docs/guides/xarray.md#fill-values-and-missing-data)
|
|
209
|
+
for the full resolution order.
|
|
210
|
+
|
|
191
211
|
## Supported dtypes
|
|
192
212
|
|
|
193
213
|
| numpy dtype | atlas dtype |
|
|
@@ -221,8 +241,9 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
221
241
|
| `list_datasets() -> list[str]` | All dataset names. |
|
|
222
242
|
| `list_arrays() -> list[str]` | Distinct array names across datasets. |
|
|
223
243
|
| `dataset_exists(name) -> bool` | Existence check. |
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
244
|
+
| `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
|
+
| `open_as_xarray_dataset(name) -> xr.Dataset` | Read a dataset back (chunked vars come back dask-backed). |
|
|
246
|
+
| `open_as_many_xarray_dataset(names, concat_dim="dataset") -> xr.Dataset` | Open many datasets stacked along `concat_dim` (eager numpy). |
|
|
226
247
|
| `flush()` | The single durability boundary — persist everything. |
|
|
227
248
|
| `close()` | Alias for `flush()`; also the `with`-block exit. |
|
|
228
249
|
| `compact()` | Reclaim tombstoned space across cached array files. |
|
|
@@ -240,8 +261,10 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
240
261
|
| `delete_array(name)` | Tombstone the array within this dataset. |
|
|
241
262
|
| `array_meta(name) -> dict \| None` | `{"dtype", "shape", "chunk_shape", "dimension_names"}`. |
|
|
242
263
|
| `array_stats(name) -> dict \| None` | `{"row_count", "null_count", "min", "max"}` — populated after `flush()`. |
|
|
243
|
-
| `set_attribute(key, value, dtype=None)` | Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`).
|
|
244
|
-
| `get_attribute(key)` / `attributes()` | Single attribute or dict of all. |
|
|
264
|
+
| `set_attribute(key, value, dtype=None)` | Set a dataset-level (global) attribute. Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`). Values are stored in the `_global` `.af` file. |
|
|
265
|
+
| `get_attribute(key)` / `attributes()` | Single global attribute or dict of all. |
|
|
266
|
+
| `set_array_attribute(array, key, value, dtype=None)` | Set a per-variable attribute on `array` (e.g. `units`); `KeyError` if the array isn't defined. |
|
|
267
|
+
| `get_array_attribute(array, key)` / `array_attributes(array)` | Single per-variable attribute or dict of all, for `array`. |
|
|
245
268
|
|
|
246
269
|
`DatasetView` does **not** expose its own `flush` / `compact` — both go through the parent `Atlas`.
|
|
247
270
|
|
|
@@ -250,13 +273,14 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
250
273
|
Runnable, self-contained scripts (each writes to a temp directory):
|
|
251
274
|
|
|
252
275
|
- [01_basics.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/01_basics.py) — create a store, define arrays, set attributes, reopen, read back.
|
|
253
|
-
- [02_xarray.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/02_xarray.py) — round-trip an `xr.Dataset` via both `store.
|
|
276
|
+
- [02_xarray.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/02_xarray.py) — round-trip an `xr.Dataset` via both `store.add_xarray_dataset(...)` and the `ds.atlas.write(...)` accessor.
|
|
254
277
|
- [03_dask_streaming.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/03_dask_streaming.py) — stream a dask-chunked `xr.Dataset` in one chunk at a time.
|
|
278
|
+
- [09_missing_data.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/09_missing_data.py) — masked/missing cells with default `NaN` / `NaT` / `""` fills, `null_count`, and `fill_value=` overrides.
|
|
255
279
|
|
|
256
280
|
## Performance
|
|
257
281
|
|
|
258
282
|
ATLAS is tuned for collections of many similarly-shaped datasets. On a "1000 datasets" benchmark
|
|
259
|
-
against netCDF4 and Zarr v3, the bulk read paths (`Atlas.
|
|
283
|
+
against netCDF4 and Zarr v3, the bulk read paths (`Atlas.open_as_many_xarray_dataset` /
|
|
260
284
|
`Atlas.read_array_across_stacked`) beat Zarr by ~2.8× on large chunked slice reads, and on small
|
|
261
285
|
per-dataset workloads ATLAS leads on both reads and writes. See the
|
|
262
286
|
[benchmarks](https://github.com/maris-development/atlas/tree/main/atlas-python/benchmarks) for the full
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# ATLAS — Aggregated Tensor Large Array Store
|
|
2
2
|
|
|
3
|
+
[](https://github.com/maris-development/atlas/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://crates.io/crates/atlas-rust)
|
|
5
|
+
[](https://docs.rs/atlas-rust)
|
|
6
|
+
[](https://pypi.org/project/atlas-python/)
|
|
7
|
+
[](https://maris-development.github.io/atlas/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
3
10
|
A directory-based store for thousands of named datasets, each holding N-dimensional typed arrays. Built on top of the [`array-format`](https://github.com/robinskil/array-format) (`.af`) binary format, with configurable compression (Zstd, LZ4, or none), chunked I/O, and an [`object_store`](https://crates.io/crates/object_store) backend that works on local disk, S3, GCS, Azure Blob, and in-memory.
|
|
4
11
|
|
|
5
12
|
**Think of it as a "zip" for N-dimensional datasets.** Where a `.zip` bundles many files into one archive, ATLAS gathers many NetCDF / Zarr-style datasets — anything that's a set of named N-dimensional arrays — into a single high-performance collection. But it's more than a bundle: instead of storing each dataset whole, ATLAS lays the data out **variable-first**, so every dataset's `temperature` lives in one file. That makes it cheap to scan or slice a single variable across thousands of datasets at once, while still reading back any individual dataset as a normal NetCDF/xarray-like object.
|
|
@@ -14,26 +21,30 @@ A directory-based store for thousands of named datasets, each holding N-dimensio
|
|
|
14
21
|
|
|
15
22
|
`atlas` is designed for workloads where you have a large collection of similarly-shaped datasets — such as one dataset per time step, sensor station, or simulation run — and you want to query a single variable (e.g. `temperature`) across all of them efficiently.
|
|
16
23
|
|
|
17
|
-
Each dataset is a named group of N-dimensional arrays with typed per-
|
|
24
|
+
Each dataset is a named group of N-dimensional arrays with typed attributes — both dataset-level (global) attributes and per-variable attributes. Datasets that share an array name (e.g. every `jan_2024`, `feb_2024`, … all have `temperature`) are stored together in the same physical file, keyed by dataset name inside the file.
|
|
18
25
|
|
|
19
26
|
```text
|
|
20
27
|
my_store/
|
|
21
|
-
├── atlas.json ← dataset
|
|
28
|
+
├── atlas.json ← interned per-dataset schema + attribute-key namespace (JSON)
|
|
29
|
+
├── _global/
|
|
30
|
+
│ └── data.af ← dataset-level (global) attribute values, one entry per dataset
|
|
22
31
|
├── temperature/
|
|
23
|
-
│ └── data.af ← one ArrayFile
|
|
32
|
+
│ └── data.af ← one ArrayFile: temperature + its per-variable attributes, per dataset
|
|
24
33
|
├── pressure/
|
|
25
34
|
│ └── data.af
|
|
26
35
|
└── time/
|
|
27
36
|
└── data.af
|
|
28
37
|
```
|
|
29
38
|
|
|
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
|
+
|
|
30
41
|
---
|
|
31
42
|
|
|
32
43
|
## Durability model
|
|
33
44
|
|
|
34
|
-
`atlas.json` is read **once** when the store is opened or created. Every subsequent mutation — `create_dataset`, `define_array`, `set_attribute`, `delete_array`, `delete_dataset` — only touches
|
|
45
|
+
`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.
|
|
35
46
|
|
|
36
|
-
A single `Atlas::flush()` walks every cached `ArrayFile` (writing deltas + stats) and then serialises the in-memory
|
|
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).
|
|
37
48
|
|
|
38
49
|
`DatasetView` is a borrowed handle into the atlas's shared meta — it has no `flush()` of its own.
|
|
39
50
|
|
|
@@ -43,14 +54,25 @@ A single `Atlas::flush()` walks every cached `ArrayFile` (writing deltas + stats
|
|
|
43
54
|
|
|
44
55
|
### `atlas.json`
|
|
45
56
|
|
|
46
|
-
The
|
|
57
|
+
The schema is a plain JSON file written on `Atlas::flush()` / `Atlas::close()`. It stores:
|
|
58
|
+
|
|
59
|
+
- **Store version** — for format upgrades. The current version is `2`; stores written by an older atlas are rejected on open (re-export to upgrade).
|
|
60
|
+
- **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
|
+
- **Dataset registry** — a map of dataset name → schema-pool index.
|
|
62
|
+
- **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()`.
|
|
63
|
+
|
|
64
|
+
When the same array name (or attribute key) appears in more than one dataset with different types, the merged type is **widened**. Widening is allowed only within numeric types (e.g. `int16` ∪ `int32` → `int32`; `int32` ∪ `float32` → `float64`) or between `string` and `timestamp` (→ `string`).
|
|
65
|
+
|
|
66
|
+
Any other combination — e.g. an `int32` array in one dataset and a `string` array under the same name in another — can't merge. The dataset is **still stored** under its own type (each dataset keeps the type it declared, and its data reads back normally); the merged schema simply keeps the **first-seen** type. Whether that's reported as a warning or an error is set by `StoreConfig::on_type_mismatch`:
|
|
67
|
+
|
|
68
|
+
| `TypeMismatchPolicy` | Behaviour |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `Warn` (default) | Logs a `tracing` warning and carries on — an ingest of many heterogeneous files won't abort because one disagrees. |
|
|
71
|
+
| `Error` | `define_array` / `set_attribute` / `set_array_attribute` return `Error::TypeMismatch`. |
|
|
47
72
|
|
|
48
|
-
|
|
49
|
-
- **Dataset names** — the complete list of datasets in the store.
|
|
50
|
-
- **Per-dataset attributes** — typed key-value pairs serialized as plain JSON values: bool, 64-bit integer, 64-bit float, UTF-8 string, or an RFC 3339 nanosecond-precision timestamp string (e.g. `"2023-11-15T07:33:20.123456789Z"`). Atlas's `Attr` enum has five variants (`Bool`/`Int64`/`Float64`/`String`/`TimestampNanoseconds`); there are no narrow integer/float types on disk.
|
|
51
|
-
- **Array schemas** — per array: dtype, shape, chunk shape, named dimensions, and the codec used when the array was first written.
|
|
73
|
+
The policy is a **per-session** choice, not an on-disk property: pass it to `Atlas::create`, or to `Atlas::open_with_config` / `Atlas::open_path_with_config` when opening an existing collection (plain `open` / `open_path` use the default). Note that these open variants honour only `on_type_mismatch` — the codec and metadata format are always detected from disk.
|
|
52
74
|
|
|
53
|
-
Because `atlas.json` is human-readable and self-describing, you can inspect or audit the
|
|
75
|
+
Attribute **values** are **not** in `atlas.json` — only their key names are (as part of the schema). Values live in the `.af` files and are read via the `array-format` attribute API. Because `atlas.json` is human-readable and self-describing, you can still inspect or audit the collection's schema with any JSON tool without needing the library.
|
|
54
76
|
|
|
55
77
|
### `<array_name>/data.af`
|
|
56
78
|
|
|
@@ -60,6 +82,7 @@ Each array variable gets its own subdirectory with a single `data.af` binary fil
|
|
|
60
82
|
- **Chunked layout** — arrays are split into chunks of a user-specified shape, so partial reads and writes touch only the relevant blocks.
|
|
61
83
|
- **Configurable compression** — each block is compressed with the codec set when the store was created (default: Zstd; also LZ4 and uncompressed). The codec is persisted in `atlas.json` and restored automatically on `open` — no need to pass it again. Block target size is 8 MiB.
|
|
62
84
|
- **Per-array fill value** — `define_array` accepts an optional `FillValue` (one of `Bool`/`Int`/`UInt`/`Float`/`String`). Unwritten cells read back as the fill value, and any *written* cell equal to it is counted as a null in the persisted stats (see below). Fill values are stored in the per-array footer; `atlas.json` is not extended.
|
|
85
|
+
- **Attribute values** — per-variable attributes (e.g. `units`) are stored in the footer of the variable's own `data.af`, keyed by dataset. Dataset-level (global) attributes live the same way in the reserved `_global/data.af`. Atlas's `Attr` type mirrors `array-format`'s `AttributeValue` (bool, sized ints/uints, `f32`/`f64`, string, binary, and typed lists) plus a nanosecond timestamp variant stored as an RFC 3339 string.
|
|
63
86
|
- **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.
|
|
64
87
|
- **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.
|
|
65
88
|
|
|
@@ -257,7 +280,7 @@ Choose LZ4 when decompression throughput matters more than storage size (e.g. la
|
|
|
257
280
|
|
|
258
281
|
### Write path
|
|
259
282
|
|
|
260
|
-
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 `
|
|
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.
|
|
261
284
|
|
|
262
285
|
### Compaction
|
|
263
286
|
|
|
@@ -301,7 +324,7 @@ the `(0:25, 0:25, 0:12)` slice down to chunk-level reads.
|
|
|
301
324
|
| **atlas-bulk** | 1 store, `read_array_across_stacked` with slice push-down | **2.12** | 59 | 6387 |
|
|
302
325
|
| **atlas + `--use-dask`** | 1 store, dask-threaded per-dataset `view.read_arrays(...)` | **3.21** | 60 | 6387 |
|
|
303
326
|
| zarr | 1000 separate stores, `xr.open_mfdataset(parallel=True).isel(...)` | 5.99 | 38 | 6392 |
|
|
304
|
-
| atlas (default) | 1 store, serial per-dataset `
|
|
327
|
+
| atlas (default) | 1 store, serial per-dataset `open_as_xarray_dataset(...).isel(...).load()` | 10.23 | 51 | 6387 |
|
|
305
328
|
| netcdf | 1000 `.nc` files, `xr.open_mfdataset(parallel=True).isel(...)` | 13.91 | 122 | 5596 |
|
|
306
329
|
|
|
307
330
|
#### `--case profile --datasets 1000` (~67 MB raw — per-dataset overhead dominates)
|
|
@@ -332,9 +355,9 @@ For tiny-shape datasets the comparison is dominated by per-dataset overhead, whi
|
|
|
332
355
|
|
|
333
356
|
### What the numbers say
|
|
334
357
|
|
|
335
|
-
- **Reads on gridded** (decompression-dominated, with realistic chunking + slice push-down): `atlas-bulk` reads in **2.12s vs zarr's 6.00s — 2.8× faster**. `atlas + --use-dask` (using the new `view.read_arrays(...)` fast path) hits **3.21s — 1.9× faster than zarr**. The win comes from atlas's structural advantage (one open of one file per variable, in-memory metadata) combined with APIs that bypass `
|
|
358
|
+
- **Reads on gridded** (decompression-dominated, with realistic chunking + slice push-down): `atlas-bulk` reads in **2.12s vs zarr's 6.00s — 2.8× faster**. `atlas + --use-dask` (using the new `view.read_arrays(...)` fast path) hits **3.21s — 1.9× faster than zarr**. The win comes from atlas's structural advantage (one open of one file per variable, in-memory metadata) combined with APIs that bypass `open_as_xarray_dataset`'s xr.Dataset + per-chunk dask graph overhead. zarr and netcdf both push the slice down through `open_mfdataset(...).isel(...)` via dask graph optimization — they're not penalised by the chunking; atlas just wins by amortising metadata and skipping per-dataset Python overhead.
|
|
336
359
|
- **Reads on profile** (overhead-dominated): atlas wins by an order of magnitude. `atlas-bulk` is **~50× faster than zarr** (0.08s vs 4.07s) because the per-dataset I/O is small enough that everything is overhead — and atlas's structural design is built for exactly that case. Default serial `atlas` is still ~12× faster than zarr.
|
|
337
|
-
- **Default `atlas.
|
|
360
|
+
- **Default `atlas.open_as_xarray_dataset` iteration is currently SLOW on chunked storage** (10.23s vs zarr's 6.00s). It goes through `open_as_xarray_dataset(name)` which returns dask-backed arrays per chunk (8 chunks/var × 3 vars × 1000 datasets = 24,000 dask delayed tasks just to build the graph). Dask graph overhead exceeds the parallelism win. **The fix is to use `view.read_arrays(vars, start, shape)` for per-dataset slice reads** — that's what `atlas + --use-dask` does internally and why it hits 3.21s. For cross-dataset reads of the *same* slice from many datasets, prefer `Atlas.open_as_many_xarray_dataset` / `read_array_across_stacked` (the `atlas-bulk` row).
|
|
338
361
|
- **Workload sensitivity**: `--use-dask` helps when per-dataset decompression is the bottleneck and *hurts* when per-dataset overhead is the bottleneck (profile: default `atlas` 0.32s → dask 2.03s). Picking the right API for the workload matters more than picking dask vs serial.
|
|
339
362
|
- **Writes on gridded**: zarr is fastest (~31s vs atlas's ~50s) — each `to_zarr(...)` just dumps bytes into a per-dataset subtree, while atlas does more bookkeeping per call (schema registration, per-dataset attrs into the shared `atlas.msgpack.zst`, block addressing in the shared array file). Atlas still beats netcdf decisively.
|
|
340
363
|
- **Writes on profile**: atlas wins ~12× (0.91s vs zarr's 11.43s, netcdf's 2.98s). At small per-dataset sizes the metadata write dominates, and atlas's amortised single-flush model wins.
|
|
@@ -86,18 +86,18 @@ ds = xr.Dataset(
|
|
|
86
86
|
)
|
|
87
87
|
|
|
88
88
|
with atlas.Atlas.create("/tmp/my_store") as store:
|
|
89
|
-
store.
|
|
89
|
+
store.add_xarray_dataset(ds, "jan_2024") # store-side method
|
|
90
90
|
ds.atlas.write(store, "jan_2025") # xarray accessor (same effect)
|
|
91
91
|
|
|
92
92
|
# Read back as an xr.Dataset
|
|
93
93
|
store = atlas.Atlas.open("/tmp/my_store")
|
|
94
|
-
ds_back = store.
|
|
94
|
+
ds_back = store.open_as_xarray_dataset("jan_2024")
|
|
95
95
|
xr.testing.assert_identical(ds, ds_back)
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
### Bulk ingestion
|
|
99
99
|
|
|
100
|
-
`
|
|
100
|
+
`add_xarray_dataset` never flushes by itself — N consecutive calls accumulate in memory and a single
|
|
101
101
|
`flush()` (or the `with` exit) persists everything.
|
|
102
102
|
|
|
103
103
|
```python
|
|
@@ -106,35 +106,35 @@ import glob, os, atlas, xarray as xr
|
|
|
106
106
|
with atlas.Atlas.create("/tmp/store") as store:
|
|
107
107
|
for nc_path in sorted(glob.glob("*.nc")):
|
|
108
108
|
name = os.path.splitext(os.path.basename(nc_path))[0]
|
|
109
|
-
store.
|
|
109
|
+
store.add_xarray_dataset(xr.open_dataset(nc_path), name)
|
|
110
110
|
# One delta file per array name across the whole batch (not one per file).
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
### Streaming dask-backed writes
|
|
114
114
|
|
|
115
115
|
If a variable's `.data` is a `dask.array.Array` (e.g. from `xr.open_dataset(path, chunks=...)`
|
|
116
|
-
or `ds.chunk({...})`), `
|
|
116
|
+
or `ds.chunk({...})`), `add_xarray_dataset` / `ds.atlas.write` stream **one dask block at a time**
|
|
117
117
|
into the store rather than materialising the whole array. The dask chunk shape becomes the
|
|
118
118
|
on-disk `chunk_shape`, so the layout maps 1:1. Peak memory ≈ one chunk per variable.
|
|
119
119
|
|
|
120
120
|
```python
|
|
121
121
|
ds = xr.open_dataset("big.nc", chunks={"time": 100, "lat": -1, "lon": -1})
|
|
122
122
|
with atlas.Atlas.create("/tmp/store") as store:
|
|
123
|
-
store.
|
|
123
|
+
store.add_xarray_dataset(ds, "big") # streams chunk-by-chunk
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
Pass `chunks={var: [...]}` to `
|
|
126
|
+
Pass `chunks={var: [...]}` to `add_xarray_dataset` / `ds.atlas.write` to override the on-disk chunk
|
|
127
127
|
shape independently of dask's chunking.
|
|
128
128
|
|
|
129
129
|
### Lazy dask-backed reads
|
|
130
130
|
|
|
131
|
-
`store.
|
|
131
|
+
`store.open_as_xarray_dataset(name)` returns each variable dask-backed whenever it was stored with non-trivial
|
|
132
132
|
chunking (`chunk_shape != shape`); the dask `chunks` tuple mirrors the on-disk chunk grid and each
|
|
133
133
|
on-disk chunk is one dask task. Full-shape arrays (and 0-D scalars) come back eager as numpy. Call
|
|
134
134
|
`.compute()` to materialise, or slice / `map_blocks` to operate lazily.
|
|
135
135
|
|
|
136
136
|
```python
|
|
137
|
-
ds_back = atlas.Atlas.open("/tmp/store").
|
|
137
|
+
ds_back = atlas.Atlas.open("/tmp/store").open_as_xarray_dataset("big")
|
|
138
138
|
ds_back["temperature"].data # -> dask.array.Array
|
|
139
139
|
ds_back["temperature"][0:100].compute() # reads exactly one chunk
|
|
140
140
|
```
|
|
@@ -147,15 +147,35 @@ picklable, so call `.compute()` before handing off to distributed/multiprocessin
|
|
|
147
147
|
| Item | How it's stored |
|
|
148
148
|
| --- | --- |
|
|
149
149
|
| Each coord / data variable | A separate array, with `dims` mapped 1:1. |
|
|
150
|
-
| Dataset attrs | Dataset attributes, plain keys. |
|
|
151
|
-
| Per-variable attrs |
|
|
152
|
-
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). |
|
|
150
|
+
| Dataset attrs | Dataset-level (global) attributes, plain keys. |
|
|
151
|
+
| Per-variable attrs | Real per-variable attributes on each variable's array. |
|
|
152
|
+
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). See [Missing data](#missing-data). |
|
|
153
153
|
| Coord vs data_var distinction | JSON list in the internal `_pyatlas_coords` attr. |
|
|
154
154
|
| Non-scalar attr values (list, ndarray) | JSON-encoded string with a `json:` prefix marker. |
|
|
155
155
|
|
|
156
|
-
Each `
|
|
156
|
+
Each `add_xarray_dataset` / `ds.atlas.write` creates a *new* dataset — there is no append-into-existing
|
|
157
157
|
mode.
|
|
158
158
|
|
|
159
|
+
### Missing data
|
|
160
|
+
|
|
161
|
+
When a dataset is opened with `mask_and_scale=True` (xarray's default), masked cells become `NaN`
|
|
162
|
+
(floats) / `NaT` (datetimes) and `_FillValue` is moved into `var.encoding`. So those cells are
|
|
163
|
+
recorded as **null** (counted in `null_count`, excluded from min/max stats), arrays default to a
|
|
164
|
+
sentinel fill on write: `NaN` for floats, `NaT` for `datetime64[ns]`, and `""` for strings (integers
|
|
165
|
+
have none). Missing string cells (`None`/`NaN`) are substituted with the string fill and a warning is
|
|
166
|
+
emitted, since a string can't be stored as null directly.
|
|
167
|
+
|
|
168
|
+
Override per write with `fill_value` — a bare scalar (numeric arrays) or a `{var: scalar}` dict
|
|
169
|
+
(`None` disables the default for that var); an explicit CF `_FillValue` attribute still takes
|
|
170
|
+
precedence over the default:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
store.add_xarray_dataset(ds, "jan_2024", fill_value={"temperature": -9999.0})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
See the [xarray guide](https://github.com/maris-development/atlas/blob/main/atlas-python/docs/guides/xarray.md#fill-values-and-missing-data)
|
|
177
|
+
for the full resolution order.
|
|
178
|
+
|
|
159
179
|
## Supported dtypes
|
|
160
180
|
|
|
161
181
|
| numpy dtype | atlas dtype |
|
|
@@ -189,8 +209,9 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
189
209
|
| `list_datasets() -> list[str]` | All dataset names. |
|
|
190
210
|
| `list_arrays() -> list[str]` | Distinct array names across datasets. |
|
|
191
211
|
| `dataset_exists(name) -> bool` | Existence check. |
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
212
|
+
| `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
|
+
| `open_as_xarray_dataset(name) -> xr.Dataset` | Read a dataset back (chunked vars come back dask-backed). |
|
|
214
|
+
| `open_as_many_xarray_dataset(names, concat_dim="dataset") -> xr.Dataset` | Open many datasets stacked along `concat_dim` (eager numpy). |
|
|
194
215
|
| `flush()` | The single durability boundary — persist everything. |
|
|
195
216
|
| `close()` | Alias for `flush()`; also the `with`-block exit. |
|
|
196
217
|
| `compact()` | Reclaim tombstoned space across cached array files. |
|
|
@@ -208,8 +229,10 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
208
229
|
| `delete_array(name)` | Tombstone the array within this dataset. |
|
|
209
230
|
| `array_meta(name) -> dict \| None` | `{"dtype", "shape", "chunk_shape", "dimension_names"}`. |
|
|
210
231
|
| `array_stats(name) -> dict \| None` | `{"row_count", "null_count", "min", "max"}` — populated after `flush()`. |
|
|
211
|
-
| `set_attribute(key, value, dtype=None)` | Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`).
|
|
212
|
-
| `get_attribute(key)` / `attributes()` | Single attribute or dict of all. |
|
|
232
|
+
| `set_attribute(key, value, dtype=None)` | Set a dataset-level (global) attribute. Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`). Values are stored in the `_global` `.af` file. |
|
|
233
|
+
| `get_attribute(key)` / `attributes()` | Single global attribute or dict of all. |
|
|
234
|
+
| `set_array_attribute(array, key, value, dtype=None)` | Set a per-variable attribute on `array` (e.g. `units`); `KeyError` if the array isn't defined. |
|
|
235
|
+
| `get_array_attribute(array, key)` / `array_attributes(array)` | Single per-variable attribute or dict of all, for `array`. |
|
|
213
236
|
|
|
214
237
|
`DatasetView` does **not** expose its own `flush` / `compact` — both go through the parent `Atlas`.
|
|
215
238
|
|
|
@@ -218,13 +241,14 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
218
241
|
Runnable, self-contained scripts (each writes to a temp directory):
|
|
219
242
|
|
|
220
243
|
- [01_basics.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/01_basics.py) — create a store, define arrays, set attributes, reopen, read back.
|
|
221
|
-
- [02_xarray.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/02_xarray.py) — round-trip an `xr.Dataset` via both `store.
|
|
244
|
+
- [02_xarray.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/02_xarray.py) — round-trip an `xr.Dataset` via both `store.add_xarray_dataset(...)` and the `ds.atlas.write(...)` accessor.
|
|
222
245
|
- [03_dask_streaming.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/03_dask_streaming.py) — stream a dask-chunked `xr.Dataset` in one chunk at a time.
|
|
246
|
+
- [09_missing_data.py](https://github.com/maris-development/atlas/blob/main/atlas-python/examples/09_missing_data.py) — masked/missing cells with default `NaN` / `NaT` / `""` fills, `null_count`, and `fill_value=` overrides.
|
|
223
247
|
|
|
224
248
|
## Performance
|
|
225
249
|
|
|
226
250
|
ATLAS is tuned for collections of many similarly-shaped datasets. On a "1000 datasets" benchmark
|
|
227
|
-
against netCDF4 and Zarr v3, the bulk read paths (`Atlas.
|
|
251
|
+
against netCDF4 and Zarr v3, the bulk read paths (`Atlas.open_as_many_xarray_dataset` /
|
|
228
252
|
`Atlas.read_array_across_stacked`) beat Zarr by ~2.8× on large chunked slice reads, and on small
|
|
229
253
|
per-dataset workloads ATLAS leads on both reads and writes. See the
|
|
230
254
|
[benchmarks](https://github.com/maris-development/atlas/tree/main/atlas-python/benchmarks) for the full
|