atlas-python 0.9.1__tar.gz → 0.10.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.10.0/.github/workflows/atlas-python-docs.yaml +88 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/.gitignore +5 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/CONTRIBUTING.md +1 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/Cargo.lock +4 -4
- {atlas_python-0.9.1 → atlas_python-0.10.0}/Cargo.toml +2 -2
- {atlas_python-0.9.1 → atlas_python-0.10.0}/PKG-INFO +38 -16
- {atlas_python-0.9.1 → atlas_python-0.10.0}/README.md +11 -4
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/Cargo.toml +1 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/README.md +37 -15
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/README.md +4 -4
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/bench_collection.py +9 -9
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/benchmarks.md +4 -4
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/examples.md +2 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/attributes.md +3 -3
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/bulk-reads.md +8 -8
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/cloud-storage.md +7 -7
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/dask.md +6 -6
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/dtypes.md +5 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/durability.md +2 -2
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/shared-arrays.md +1 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/stats.md +5 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/xarray.md +53 -8
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/index.md +11 -11
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/installation.md +5 -2
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/quickstart.md +5 -5
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/reference/xarray-accessor.md +2 -2
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/vs-zarr-netcdf.md +2 -2
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/02_xarray.py +6 -6
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/03_dask_streaming.py +3 -3
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/08_object_store.py +4 -4
- atlas_python-0.10.0/atlas-python/examples/09_missing_data.py +101 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/mkdocs.yml +4 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/dataset.rs +3 -3
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/store.rs +17 -8
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/tests/test_smoke.py +1 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/tests/test_xarray.py +205 -36
- {atlas_python-0.9.1 → atlas_python-0.10.0}/pyproject.toml +1 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/__init__.pyi +16 -6
- {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/xarray.py +136 -13
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/store.rs +1 -1
- {atlas_python-0.9.1 → atlas_python-0.10.0}/.github/workflows/atlas-python-release.yaml +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/.github/workflows/atlas-rust-release.yaml +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/.github/workflows/ci.yaml +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/LICENSE +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/.python-version +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/LICENSE +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/_common.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/generate_bench_charts.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/assets/bench_gridded.svg +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/assets/bench_profile.svg +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/codecs-and-meta.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/datasets-and-arrays.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/reference/atlas.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/reference/dataset-view.md +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/01_basics.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/04_meta_formats.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/05_codecs.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/06_stats_scan.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/07_shared_arrays.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/attr.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/dtype.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/error.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/lib.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/logging.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/runtime.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/examples/lifecycle.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/examples/sensor_fleet.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/examples/weather_store.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/__init__.py +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/py.typed +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/array.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/config.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/dataset.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/error.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/lib.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/meta.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/src/schema.rs +0 -0
- {atlas_python-0.9.1 → atlas_python-0.10.0}/tests/integration.rs +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.10.0"
|
|
32
32
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
-
checksum = "
|
|
33
|
+
checksum = "a637a3b5f23ba45eb53f09e643b363163114f0e94d0f6f05f330134cb86d381b"
|
|
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.10.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.10.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.10.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.10.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.10.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
|
```
|
|
@@ -181,13 +181,33 @@ picklable, so call `.compute()` before handing off to distributed/multiprocessin
|
|
|
181
181
|
| Each coord / data variable | A separate array, with `dims` mapped 1:1. |
|
|
182
182
|
| Dataset attrs | Dataset attributes, plain keys. |
|
|
183
183
|
| Per-variable attrs | Flattened as `{var}.{attr}` at the dataset attr level. |
|
|
184
|
-
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). |
|
|
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. |
|
|
@@ -250,13 +271,14 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
250
271
|
Runnable, self-contained scripts (each writes to a temp directory):
|
|
251
272
|
|
|
252
273
|
- [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.
|
|
274
|
+
- [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
275
|
- [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.
|
|
276
|
+
- [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
277
|
|
|
256
278
|
## Performance
|
|
257
279
|
|
|
258
280
|
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.
|
|
281
|
+
against netCDF4 and Zarr v3, the bulk read paths (`Atlas.open_as_many_xarray_dataset` /
|
|
260
282
|
`Atlas.read_array_across_stacked`) beat Zarr by ~2.8× on large chunked slice reads, and on small
|
|
261
283
|
per-dataset workloads ATLAS leads on both reads and writes. See the
|
|
262
284
|
[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.
|
|
@@ -257,7 +264,7 @@ Choose LZ4 when decompression throughput matters more than storage size (e.g. la
|
|
|
257
264
|
|
|
258
265
|
### Write path
|
|
259
266
|
|
|
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 `
|
|
267
|
+
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
268
|
|
|
262
269
|
### Compaction
|
|
263
270
|
|
|
@@ -301,7 +308,7 @@ the `(0:25, 0:25, 0:12)` slice down to chunk-level reads.
|
|
|
301
308
|
| **atlas-bulk** | 1 store, `read_array_across_stacked` with slice push-down | **2.12** | 59 | 6387 |
|
|
302
309
|
| **atlas + `--use-dask`** | 1 store, dask-threaded per-dataset `view.read_arrays(...)` | **3.21** | 60 | 6387 |
|
|
303
310
|
| zarr | 1000 separate stores, `xr.open_mfdataset(parallel=True).isel(...)` | 5.99 | 38 | 6392 |
|
|
304
|
-
| atlas (default) | 1 store, serial per-dataset `
|
|
311
|
+
| atlas (default) | 1 store, serial per-dataset `open_as_xarray_dataset(...).isel(...).load()` | 10.23 | 51 | 6387 |
|
|
305
312
|
| netcdf | 1000 `.nc` files, `xr.open_mfdataset(parallel=True).isel(...)` | 13.91 | 122 | 5596 |
|
|
306
313
|
|
|
307
314
|
#### `--case profile --datasets 1000` (~67 MB raw — per-dataset overhead dominates)
|
|
@@ -332,9 +339,9 @@ For tiny-shape datasets the comparison is dominated by per-dataset overhead, whi
|
|
|
332
339
|
|
|
333
340
|
### What the numbers say
|
|
334
341
|
|
|
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 `
|
|
342
|
+
- **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
343
|
- **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.
|
|
344
|
+
- **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
345
|
- **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
346
|
- **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
347
|
- **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
|
```
|
|
@@ -149,13 +149,33 @@ picklable, so call `.compute()` before handing off to distributed/multiprocessin
|
|
|
149
149
|
| Each coord / data variable | A separate array, with `dims` mapped 1:1. |
|
|
150
150
|
| Dataset attrs | Dataset attributes, plain keys. |
|
|
151
151
|
| Per-variable attrs | Flattened as `{var}.{attr}` at the dataset attr level. |
|
|
152
|
-
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). |
|
|
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. |
|
|
@@ -218,13 +239,14 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
218
239
|
Runnable, self-contained scripts (each writes to a temp directory):
|
|
219
240
|
|
|
220
241
|
- [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.
|
|
242
|
+
- [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
243
|
- [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.
|
|
244
|
+
- [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
245
|
|
|
224
246
|
## Performance
|
|
225
247
|
|
|
226
248
|
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.
|
|
249
|
+
against netCDF4 and Zarr v3, the bulk read paths (`Atlas.open_as_many_xarray_dataset` /
|
|
228
250
|
`Atlas.read_array_across_stacked`) beat Zarr by ~2.8× on large chunked slice reads, and on small
|
|
229
251
|
per-dataset workloads ATLAS leads on both reads and writes. See the
|
|
230
252
|
[benchmarks](https://github.com/maris-development/atlas/tree/main/atlas-python/benchmarks) for the full
|
|
@@ -49,7 +49,7 @@ replace them, so one run can compare multiple variants head-to-head.
|
|
|
49
49
|
|
|
50
50
|
| Backend | Always | Added by `--atlas-bulk` | Added by `--netcdf-groups` | Added by `--zarr-groups` |
|
|
51
51
|
|---|---|---|---|---|
|
|
52
|
-
| atlas | 1 store, N datasets (iterate `
|
|
52
|
+
| atlas | 1 store, N datasets (iterate `open_as_xarray_dataset` or `view.read_arrays`) | (use `read_array_across_stacked`) | — | — |
|
|
53
53
|
| atlas-bulk | — | (this row) | — | — |
|
|
54
54
|
| netcdf | N separate `.nc` files | — | 1 `.nc` file w/ N netCDF4 groups | — |
|
|
55
55
|
| netcdf-groups | — | — | (this row) | — |
|
|
@@ -65,7 +65,7 @@ read pattern (not the layout) — see "Read pattern" below.
|
|
|
65
65
|
Read the slice from every dataset. Each backend uses its canonical
|
|
66
66
|
"many datasets" idiom:
|
|
67
67
|
|
|
68
|
-
- **atlas (default, no `--use-dask`)** — iterate `
|
|
68
|
+
- **atlas (default, no `--use-dask`)** — iterate `open_as_xarray_dataset(name).isel(slice).load()`.
|
|
69
69
|
Returns a full xr.Dataset per dataset and slices in xarray. On chunked
|
|
70
70
|
storage this pays per-chunk dask graph build overhead; slow on `gridded`.
|
|
71
71
|
- **atlas (with `--use-dask`)** — fast path: each dask worker calls
|
|
@@ -100,14 +100,14 @@ When set, `--use-dask` does three things:
|
|
|
100
100
|
per-dataset slice load is wrapped in `dask.delayed` and dispatched via
|
|
101
101
|
`dask.compute(*, scheduler="threads")`. For atlas specifically, this
|
|
102
102
|
path uses `view.read_arrays(vars, start, shape)` (the fast Rust dict
|
|
103
|
-
path) rather than `
|
|
103
|
+
path) rather than `open_as_xarray_dataset(...).isel(...).load()`, avoiding the
|
|
104
104
|
xr.Dataset + dask graph overhead that dominates default `atlas`.
|
|
105
105
|
2. **Reads (mfdataset paths)** — default netcdf, default zarr: already use
|
|
106
106
|
dask internally; the flag only constrains the thread pool to
|
|
107
107
|
`--dask-workers` if specified.
|
|
108
108
|
3. **Writes** — `generate_dataset()` returns dask-backed `xr.DataArray`s
|
|
109
109
|
(2 chunks along `time`). Each backend's xarray write triggers a dask
|
|
110
|
-
compute that streams chunks. Atlas's `
|
|
110
|
+
compute that streams chunks. Atlas's `add_xarray_dataset` already does this
|
|
111
111
|
chunk-by-chunk; netCDF/zarr `to_*` handle it transparently.
|
|
112
112
|
|
|
113
113
|
Use `--dask-workers N` to set the thread count; defaults to dask's default
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Compare atlas / netCDF / zarr on a 1000-dataset collection workload.
|
|
2
2
|
|
|
3
3
|
Each backend uses its canonical "many datasets" layout and read pattern:
|
|
4
|
-
atlas — 1 store, 1000 datasets; iterate `
|
|
4
|
+
atlas — 1 store, 1000 datasets; iterate `open_as_xarray_dataset(name)`
|
|
5
5
|
netCDF — 1000 .nc files; `xr.open_mfdataset(files, ...)`
|
|
6
6
|
zarr — 1 store, 1000 groups; iterate `xr.open_zarr(..., group=name)`
|
|
7
7
|
|
|
@@ -76,7 +76,7 @@ def bench_atlas(
|
|
|
76
76
|
meta_compression="zstd",
|
|
77
77
|
) as store:
|
|
78
78
|
for i in range(n_datasets):
|
|
79
|
-
store.
|
|
79
|
+
store.add_xarray_dataset(
|
|
80
80
|
generate_dataset(i, case, use_dask=use_dask),
|
|
81
81
|
f"ds_{i:04d}",
|
|
82
82
|
chunks=chunks_per_var,
|
|
@@ -97,7 +97,7 @@ def bench_atlas(
|
|
|
97
97
|
from dask.delayed import delayed
|
|
98
98
|
|
|
99
99
|
# Fast path: per-dataset slice reads via `view.read_arrays(...)`
|
|
100
|
-
# bypass
|
|
100
|
+
# bypass open_as_xarray_dataset's xr.Dataset + dask-graph build overhead. The
|
|
101
101
|
# dask scheduler still parallelises across datasets; each task does
|
|
102
102
|
# just one Rust call per variable with the slice already applied.
|
|
103
103
|
start = [s.start for s in indexers.values()]
|
|
@@ -114,7 +114,7 @@ def bench_atlas(
|
|
|
114
114
|
else:
|
|
115
115
|
with time_block() as rt:
|
|
116
116
|
for i in range(n_datasets):
|
|
117
|
-
ds = store.
|
|
117
|
+
ds = store.open_as_xarray_dataset(f"ds_{i:04d}")
|
|
118
118
|
_ = ds[var_names].isel(indexers).load()
|
|
119
119
|
progress_tick(i, n_datasets, "read")
|
|
120
120
|
log_phase("atlas", "read", f"{rt.elapsed:.3f}s")
|
|
@@ -122,7 +122,7 @@ def bench_atlas(
|
|
|
122
122
|
return BackendResult(name="atlas", write_s=wt.elapsed, read_s=rt.elapsed, size_bytes=size)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
# ── atlas (bulk, single-call
|
|
125
|
+
# ── atlas (bulk, single-call open_as_many_xarray_dataset) ────────────────────────────
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def bench_atlas_bulk(
|
|
@@ -134,7 +134,7 @@ def bench_atlas_bulk(
|
|
|
134
134
|
dask_workers: int | None = None,
|
|
135
135
|
) -> BackendResult:
|
|
136
136
|
"""Same write phase as `bench_atlas`; read phase is one
|
|
137
|
-
`Atlas.
|
|
137
|
+
`Atlas.open_as_many_xarray_dataset(...)` call — the atlas-native equivalent of
|
|
138
138
|
`xr.open_mfdataset`. Stacked Dataset is sliced and `.load()`-ed in one
|
|
139
139
|
dask compute."""
|
|
140
140
|
import dask.config as dask_config
|
|
@@ -158,7 +158,7 @@ def bench_atlas_bulk(
|
|
|
158
158
|
meta_compression="zstd",
|
|
159
159
|
) as store:
|
|
160
160
|
for i, name in enumerate(names):
|
|
161
|
-
store.
|
|
161
|
+
store.add_xarray_dataset(
|
|
162
162
|
generate_dataset(i, case, use_dask=use_dask),
|
|
163
163
|
name,
|
|
164
164
|
chunks=chunks_per_var,
|
|
@@ -179,7 +179,7 @@ def bench_atlas_bulk(
|
|
|
179
179
|
# Push the slice down through the low-level API so atlas only decompresses
|
|
180
180
|
# the chunks overlapping the slice (matches what zarr/netcdf's
|
|
181
181
|
# `open_mfdataset(...).isel(...).load()` does via dask graph optimization).
|
|
182
|
-
# `
|
|
182
|
+
# `open_as_many_xarray_dataset(...).isel(...)` would also work but slices in numpy
|
|
183
183
|
# *after* decompressing the full per-dataset chunks, wasting work when
|
|
184
184
|
# `chunk_shape != shape`.
|
|
185
185
|
start = [s.start for s in indexers.values()]
|
|
@@ -544,7 +544,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
544
544
|
"--atlas-bulk",
|
|
545
545
|
action="store_true",
|
|
546
546
|
help=(
|
|
547
|
-
"Add an `atlas-bulk` row that reads via Atlas.
|
|
547
|
+
"Add an `atlas-bulk` row that reads via Atlas.open_as_many_xarray_dataset "
|
|
548
548
|
"(atlas-native equivalent of xr.open_mfdataset) instead of "
|
|
549
549
|
"iterating per-dataset. Additive — the default `atlas` row still runs."
|
|
550
550
|
),
|
|
@@ -23,7 +23,7 @@ actually reach for:
|
|
|
23
23
|
numpy (opt in with `--atlas-bulk`).
|
|
24
24
|
|
|
25
25
|
The default (serial) atlas read path — iterate
|
|
26
|
-
`view.
|
|
26
|
+
`view.open_as_xarray_dataset(name).isel(...).load()` — isn't in the chart; it scales
|
|
27
27
|
linearly with N and is fine for small fleets, but at the scales below
|
|
28
28
|
(100+ datasets) you should already be reaching for one of the parallel
|
|
29
29
|
paths. `--netcdf-groups` / `--zarr-groups` add layouts that use one file
|
|
@@ -96,7 +96,7 @@ threaded `dask.delayed` fan-out has more overhead than tokio's
|
|
|
96
96
|
- **`atlas + --use-dask`** picks up most of atlas-bulk's win without
|
|
97
97
|
giving up the dask graph: 1.6–4× faster than `zarr+dask` across the
|
|
98
98
|
sweep, and the natural choice when downstream `xr` code needs to stay
|
|
99
|
-
lazy (a `
|
|
99
|
+
lazy (a `open_as_xarray_dataset(...).isel(...)` slice chain composes with the
|
|
100
100
|
upstream graph, where `atlas-bulk` returns eager numpy). Reach for
|
|
101
101
|
serial `atlas` only when the fleet is small enough that the dask
|
|
102
102
|
fan-out overhead doesn't pay back.
|
|
@@ -146,14 +146,14 @@ Update the `RESULTS` dict in that script and re-run after a fresh sweep.
|
|
|
146
146
|
## API picker for reads (in rough order of speed)
|
|
147
147
|
|
|
148
148
|
- **Cross-dataset slice of the same vars across many datasets** →
|
|
149
|
-
[`Atlas.
|
|
149
|
+
[`Atlas.open_as_many_xarray_dataset`](reference/atlas.md) /
|
|
150
150
|
[`Atlas.read_array_across_stacked`](reference/atlas.md) (the
|
|
151
151
|
`atlas-bulk` path; one Rust call per variable).
|
|
152
152
|
- **Per-dataset slice reads inside a dask worker** →
|
|
153
153
|
[`view.read_arrays(vars, start, shape)`](reference/dataset-view.md)
|
|
154
154
|
(returns `dict[str, np.ndarray]`; skips xr.Dataset + per-chunk dask
|
|
155
155
|
graph). This is what `bench_atlas` with `--use-dask` does internally.
|
|
156
|
-
- **Natural xarray code** → `
|
|
156
|
+
- **Natural xarray code** → `open_as_xarray_dataset(name).isel(...).load()`. Most
|
|
157
157
|
ergonomic but pays per-chunk dask graph build overhead on chunked
|
|
158
158
|
storage.
|
|
159
159
|
|