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.
Files changed (79) hide show
  1. atlas_python-0.10.0/.github/workflows/atlas-python-docs.yaml +88 -0
  2. {atlas_python-0.9.1 → atlas_python-0.10.0}/.gitignore +5 -1
  3. {atlas_python-0.9.1 → atlas_python-0.10.0}/CONTRIBUTING.md +1 -1
  4. {atlas_python-0.9.1 → atlas_python-0.10.0}/Cargo.lock +4 -4
  5. {atlas_python-0.9.1 → atlas_python-0.10.0}/Cargo.toml +2 -2
  6. {atlas_python-0.9.1 → atlas_python-0.10.0}/PKG-INFO +38 -16
  7. {atlas_python-0.9.1 → atlas_python-0.10.0}/README.md +11 -4
  8. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/Cargo.toml +1 -1
  9. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/README.md +37 -15
  10. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/README.md +4 -4
  11. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/bench_collection.py +9 -9
  12. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/benchmarks.md +4 -4
  13. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/examples.md +2 -1
  14. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/attributes.md +3 -3
  15. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/bulk-reads.md +8 -8
  16. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/cloud-storage.md +7 -7
  17. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/dask.md +6 -6
  18. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/dtypes.md +5 -0
  19. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/durability.md +2 -2
  20. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/shared-arrays.md +1 -1
  21. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/stats.md +5 -0
  22. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/xarray.md +53 -8
  23. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/index.md +11 -11
  24. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/installation.md +5 -2
  25. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/quickstart.md +5 -5
  26. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/reference/xarray-accessor.md +2 -2
  27. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/vs-zarr-netcdf.md +2 -2
  28. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/02_xarray.py +6 -6
  29. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/03_dask_streaming.py +3 -3
  30. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/08_object_store.py +4 -4
  31. atlas_python-0.10.0/atlas-python/examples/09_missing_data.py +101 -0
  32. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/mkdocs.yml +4 -0
  33. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/dataset.rs +3 -3
  34. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/store.rs +17 -8
  35. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/tests/test_smoke.py +1 -1
  36. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/tests/test_xarray.py +205 -36
  37. {atlas_python-0.9.1 → atlas_python-0.10.0}/pyproject.toml +1 -1
  38. {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/__init__.pyi +16 -6
  39. {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/xarray.py +136 -13
  40. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/store.rs +1 -1
  41. {atlas_python-0.9.1 → atlas_python-0.10.0}/.github/workflows/atlas-python-release.yaml +0 -0
  42. {atlas_python-0.9.1 → atlas_python-0.10.0}/.github/workflows/atlas-rust-release.yaml +0 -0
  43. {atlas_python-0.9.1 → atlas_python-0.10.0}/.github/workflows/ci.yaml +0 -0
  44. {atlas_python-0.9.1 → atlas_python-0.10.0}/LICENSE +0 -0
  45. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/.python-version +0 -0
  46. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/LICENSE +0 -0
  47. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/_common.py +0 -0
  48. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/benchmarks/generate_bench_charts.py +0 -0
  49. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/assets/bench_gridded.svg +0 -0
  50. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/assets/bench_profile.svg +0 -0
  51. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/codecs-and-meta.md +0 -0
  52. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/guides/datasets-and-arrays.md +0 -0
  53. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/reference/atlas.md +0 -0
  54. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/docs/reference/dataset-view.md +0 -0
  55. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/01_basics.py +0 -0
  56. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/04_meta_formats.py +0 -0
  57. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/05_codecs.py +0 -0
  58. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/06_stats_scan.py +0 -0
  59. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/examples/07_shared_arrays.py +0 -0
  60. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/attr.rs +0 -0
  61. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/dtype.rs +0 -0
  62. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/error.rs +0 -0
  63. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/lib.rs +0 -0
  64. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/logging.rs +0 -0
  65. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/src/runtime.rs +0 -0
  66. {atlas_python-0.9.1 → atlas_python-0.10.0}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
  67. {atlas_python-0.9.1 → atlas_python-0.10.0}/examples/lifecycle.rs +0 -0
  68. {atlas_python-0.9.1 → atlas_python-0.10.0}/examples/sensor_fleet.rs +0 -0
  69. {atlas_python-0.9.1 → atlas_python-0.10.0}/examples/weather_store.rs +0 -0
  70. {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/__init__.py +0 -0
  71. {atlas_python-0.9.1 → atlas_python-0.10.0}/python/atlas/py.typed +0 -0
  72. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/array.rs +0 -0
  73. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/config.rs +0 -0
  74. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/dataset.rs +0 -0
  75. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/error.rs +0 -0
  76. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/lib.rs +0 -0
  77. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/meta.rs +0 -0
  78. {atlas_python-0.9.1 → atlas_python-0.10.0}/src/schema.rs +0 -0
  79. {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.add_xr_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()`.
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.9.0"
31
+ version = "0.10.0"
32
32
  source = "registry+https://github.com/rust-lang/crates.io-index"
33
- checksum = "d91c6d4d26f667272f8930d41bc95118716e12858baf115940a626510e7b5873"
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.9.1"
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.9.1"
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.9.1"
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.9.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.9.1
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.add_xr_dataset(ds, "jan_2024") # store-side method
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.to_xarray("jan_2024")
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
- `add_xr_dataset` never flushes by itself — N consecutive calls accumulate in memory and a single
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.add_xr_dataset(xr.open_dataset(nc_path), name)
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({...})`), `add_xr_dataset` / `ds.atlas.write` stream **one dask block at a time**
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.add_xr_dataset(ds, "big") # streams chunk-by-chunk
155
+ store.add_xarray_dataset(ds, "big") # streams chunk-by-chunk
156
156
  ```
157
157
 
158
- Pass `chunks={var: [...]}` to `add_xr_dataset` / `ds.atlas.write` to override the on-disk chunk
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.to_xarray(name)` returns each variable dask-backed whenever it was stored with non-trivial
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").to_xarray("big")
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 `add_xr_dataset` / `ds.atlas.write` creates a *new* dataset — there is no append-into-existing
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
- | `add_xr_dataset(ds, name, chunks=None)` | Append an `xarray.Dataset` (does **not** flush). |
225
- | `to_xarray(name) -> xr.Dataset` | Read a dataset back (chunked vars come back dask-backed). |
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.add_xr_dataset(...)` and the `ds.atlas.write(...)` accessor.
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.to_xarray_many` /
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
+ [![CI](https://github.com/maris-development/atlas/actions/workflows/ci.yaml/badge.svg)](https://github.com/maris-development/atlas/actions/workflows/ci.yaml)
4
+ [![crates.io](https://img.shields.io/crates/v/atlas-rust.svg?logo=rust)](https://crates.io/crates/atlas-rust)
5
+ [![docs.rs](https://img.shields.io/docsrs/atlas-rust?logo=docsdotrs&label=docs.rs)](https://docs.rs/atlas-rust)
6
+ [![PyPI](https://img.shields.io/pypi/v/atlas-python.svg?logo=pypi&logoColor=white)](https://pypi.org/project/atlas-python/)
7
+ [![Python docs](https://img.shields.io/badge/docs-atlas--python-blue?logo=materialformkdocs&logoColor=white)](https://maris-development.github.io/atlas/)
8
+ [![License](https://img.shields.io/crates/l/atlas-rust.svg)](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 `add_xr_dataset` / `create_dataset` calls amortise to a single flush.
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 `to_xarray(...).isel(...).load()` | 10.23 | 51 | 6387 |
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 `to_xarray`'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.
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.to_xarray` iteration is currently SLOW on chunked storage** (10.23s vs zarr's 6.00s). It goes through `to_xarray(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.to_xarray_many` / `read_array_across_stacked` (the `atlas-bulk` row).
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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "atlas-python"
3
- version = "0.9.1"
3
+ version = "0.10.0"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -86,18 +86,18 @@ ds = xr.Dataset(
86
86
  )
87
87
 
88
88
  with atlas.Atlas.create("/tmp/my_store") as store:
89
- store.add_xr_dataset(ds, "jan_2024") # store-side method
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.to_xarray("jan_2024")
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
- `add_xr_dataset` never flushes by itself — N consecutive calls accumulate in memory and a single
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.add_xr_dataset(xr.open_dataset(nc_path), name)
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({...})`), `add_xr_dataset` / `ds.atlas.write` stream **one dask block at a time**
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.add_xr_dataset(ds, "big") # streams chunk-by-chunk
123
+ store.add_xarray_dataset(ds, "big") # streams chunk-by-chunk
124
124
  ```
125
125
 
126
- Pass `chunks={var: [...]}` to `add_xr_dataset` / `ds.atlas.write` to override the on-disk chunk
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.to_xarray(name)` returns each variable dask-backed whenever it was stored with non-trivial
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").to_xarray("big")
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 `add_xr_dataset` / `ds.atlas.write` creates a *new* dataset — there is no append-into-existing
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
- | `add_xr_dataset(ds, name, chunks=None)` | Append an `xarray.Dataset` (does **not** flush). |
193
- | `to_xarray(name) -> xr.Dataset` | Read a dataset back (chunked vars come back dask-backed). |
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.add_xr_dataset(...)` and the `ds.atlas.write(...)` accessor.
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.to_xarray_many` /
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 `to_xarray` or `view.read_arrays`) | (use `read_array_across_stacked`) | — | — |
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 `to_xarray(name).isel(slice).load()`.
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 `to_xarray(...).isel(...).load()`, avoiding the
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 `add_xr_dataset` already does this
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 `to_xarray(name)`
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.add_xr_dataset(
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 to_xarray's xr.Dataset + dask-graph build overhead. The
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.to_xarray(f"ds_{i:04d}")
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 to_xarray_many) ────────────────────────────
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.to_xarray_many(...)` call — the atlas-native equivalent of
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.add_xr_dataset(
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
- # `to_xarray_many(...).isel(...)` would also work but slices in numpy
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.to_xarray_many "
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.to_xarray(name).isel(...).load()` — isn't in the chart; it scales
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 `to_xarray(...).isel(...)` slice chain composes with the
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.to_xarray_many`](reference/atlas.md) /
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** → `to_xarray(name).isel(...).load()`. Most
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