Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
c53f31b
Record fast backend catalog ADR #18
bjhardcastle May 3, 2026
ae2e904
Add backend catalog reader seam #19
bjhardcastle May 3, 2026
a5ba85d
Add SQLite catalog snapshot cache #20
bjhardcastle May 3, 2026
907e7e0
Add obstore range reader and HDF5 probe #21
bjhardcastle May 3, 2026
51d1793
Migrate scalar HDF5 table schema path #22
bjhardcastle May 3, 2026
8ad34c2
Fix previous
bjhardcastle May 3, 2026
bf24a82
Support rich HDF5 schema snapshots
bjhardcastle May 3, 2026
832606b
Cover fast HDF5 metadata and TimeSeries schemas
bjhardcastle May 3, 2026
bb1d7d0
Preserve fast schema multi-file semantics
bjhardcastle May 3, 2026
ec8a1ab
Add remote schema budget coverage
bjhardcastle May 3, 2026
96e996f
Warn on remote internal path traversal
bjhardcastle May 3, 2026
0e5cf2f
Exclude network integration tests by default
bjhardcastle May 3, 2026
391d481
Implement Zarr schema backend reader
bjhardcastle May 3, 2026
d968f32
Replace HDF5 schema path with metadata parser
bjhardcastle May 3, 2026
647ebeb
Speed up fast HDF5 schema batching
bjhardcastle May 3, 2026
4074232
Cache obstore stores across schema reads
bjhardcastle May 3, 2026
14250dd
Reuse HDF5 source identities in process
bjhardcastle May 3, 2026
70750eb
Add aggregate schema latency benchmark
bjhardcastle May 3, 2026
e685878
Add same-source HDF5 schema scan helper
bjhardcastle May 3, 2026
0300642
Make S3 region handling bucket-aware
bjhardcastle May 3, 2026
c9dfd72
Use fast catalog for table materialization
bjhardcastle May 3, 2026
bd7f714
Optimize indexed subset materialization
bjhardcastle May 3, 2026
4096837
Remove implicit HDF5 schema follow-up warming
bjhardcastle May 3, 2026
fec17fe
Reuse scan catalog snapshots during materialization
bjhardcastle May 3, 2026
aa687b7
Materialize contiguous HDF5 scalars with ranges
bjhardcastle May 3, 2026
e19a870
Materialize indexed HDF5 columns with ranges
bjhardcastle May 3, 2026
853827e
Add catalog-backed HDF5 path summaries
bjhardcastle May 3, 2026
be4b3ef
Use explicit HDF5 multi-table scans for SQL context
bjhardcastle May 3, 2026
c18f770
Add lazynwb CLI paths command (#45)
bjhardcastle May 3, 2026
c954803
Add project-local CLI TOML config (#46)
bjhardcastle May 3, 2026
939870e
Add recursive local source discovery (#47)
bjhardcastle May 3, 2026
cf9eb05
Fix fast backend selection for S3 HDF5
bjhardcastle May 3, 2026
98a877a
Resolve DANDI CLI sources (#48)
bjhardcastle May 3, 2026
360100e
List SQL-ready NWB tables in CLI (#49)
bjhardcastle May 3, 2026
bf519d8
Inspect NWB table schemas in CLI (#50)
bjhardcastle May 3, 2026
750d7af
Preview bounded NWB table rows in CLI (#51)
bjhardcastle May 3, 2026
6614c0a
Execute bounded SQL queries in CLI (#52)
bjhardcastle May 3, 2026
71b5baf
Emit source-aware CLI agent context (#53)
bjhardcastle May 4, 2026
1ab4b9b
Document Polars predicate head boundary (#43)
bjhardcastle May 4, 2026
63938fd
Share DynamicTable schema rules (#58)
bjhardcastle May 4, 2026
8a3ccea
Centralize storage option normalization (#60)
bjhardcastle May 4, 2026
975fcd1
Record CLI usage error contract (#54)
bjhardcastle May 4, 2026
642cc7c
Plan indexed HDF5 range reads (#61)
bjhardcastle May 4, 2026
39d877a
Add Zarr catalog path summaries (#62)
bjhardcastle May 4, 2026
a3226e1
Share Zarr metadata catalog with attrs (#63)
bjhardcastle May 4, 2026
e88f08b
Add CLI usage error recovery fields (#64)
bjhardcastle May 4, 2026
09f04c8
Return metadata from get_internal_paths
bjhardcastle May 4, 2026
90ad4d8
Fix remote Zarr NWB opening and schema reads
bjhardcastle May 4, 2026
cd80982
#65 Split internal path discovery API
bjhardcastle May 4, 2026
014562b
Fix DANDI URL resolution (#67)
bjhardcastle May 5, 2026
9982234
Make scan_dandiset delegate to scan_nwb (#68)
bjhardcastle May 5, 2026
b0e320f
Add DANDI sample integration framework (#69)
bjhardcastle May 5, 2026
f2a5999
Verify DANDI table access (#70)
bjhardcastle May 5, 2026
fca83c9
Verify DANDI TimeSeries access (#71)
bjhardcastle May 5, 2026
ff8d235
Verify DANDI metadata access (#72)
bjhardcastle May 5, 2026
0036e92
Document URI-first DANDI workflow (#73)
bjhardcastle May 5, 2026
8c6d889
Fix Python 3.14 CI failures
bjhardcastle May 5, 2026
6b47594
Update DANDI source discovery API
bjhardcastle May 5, 2026
808ba21
Prepare 1.0.0.dev1 release [skip actions]
bjhardcastle May 7, 2026
d5190e6
Fix TimeSeries container discovery (#80)
bjhardcastle May 9, 2026
e6094da
Preserve timestamps-only TimeSeries discovery (#80)
bjhardcastle May 9, 2026
add8a2b
Add global Polars config
bjhardcastle May 12, 2026
567241e
Add general NWB container getter
bjhardcastle May 12, 2026
f9ae5e9
Rm FileIOConfig
bjhardcastle May 12, 2026
5ad9010
Prepare 1.0.0.dev2 release [skip actions]
bjhardcastle May 12, 2026
16224ee
Handle Polars dynamic TopK predicates
bjhardcastle May 13, 2026
01b5147
Prepare 1.0.0.dev3 release [skip actions]
bjhardcastle May 13, 2026
ebebae8
Add remote Zarr v2 benchmark design
bjhardcastle May 15, 2026
a99c171
Add remote Zarr schema benchmark
bjhardcastle May 15, 2026
e5e4d08
Make Zarr benchmark metric parseable
bjhardcastle May 15, 2026
c2f1754
Ignore autoresearch loop state
bjhardcastle May 15, 2026
783f232
Add batched Zarr schema snapshots
bjhardcastle May 15, 2026
4f684aa
Add Zarr v2 chunk planner
bjhardcastle May 15, 2026
c8d62b3
Use obstore for consolidated Zarr metadata
bjhardcastle May 15, 2026
b75071f
Wire batched Zarr schema warming
bjhardcastle May 15, 2026
fe09493
Add native Zarr chunk transfer engine
bjhardcastle May 15, 2026
87630e5
Add native Zarr exact-array reader hook
bjhardcastle May 15, 2026
00d28c0
Add fast remote Zarr handoff doc
bjhardcastle May 15, 2026
099746e
Release 1.0.0.dev4 with Zarr v3 support [skip actions]
bjhardcastle May 15, 2026
30c67a0
Route native Zarr reads into public workflows
bjhardcastle May 15, 2026
e6ee931
Prepare 1.0.0.dev5 release [skip actions]
bjhardcastle May 15, 2026
428f83d
Handle Zarr metadata failures in HTML repr
bjhardcastle May 19, 2026
e40c10b
Fix metadata paths in HTML repr
bjhardcastle May 19, 2026
95b1e28
Optimize HDF5/Zarr prefix seams
bjhardcastle Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ cython_debug/

.sandbox.py
.scratch/
.autoresearch/

tests/files/
.github/copilot-instructions.md
.github/copilot-instructions.md
168 changes: 146 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ lazynwb.get_df('my_file.nwb', '/trials')
lazynwb.get_df('s3://bucket/session.nwb', '/trials')

# DANDI archive
lf = lazynwb.scan_dandiset('000363', '/trials')
dandi_sources = lazynwb.get_dandi_sources('000363', version='0.231012.2129')
lf = lazynwb.scan_nwb(dandi_sources, '/trials')
```

### Basic benchmarks
Expand Down Expand Up @@ -125,6 +126,19 @@ to reproduce or run against your own files:
python benchmarks/streaming_benchmark.py [NWB_PATH]
```

Schema inference latency for the remote dynamic-routing workload can be measured
with [benchmarks/schema_benchmark.py](benchmarks/schema_benchmark.py):
```
uv run python benchmarks/schema_benchmark.py
LAZYNWB_SCHEMA_BENCH_JSON=metrics.json uv run python benchmarks/schema_benchmark.py
LAZYNWB_SCHEMA_BENCH_UNITS_SOURCES_FILE=tests/paths.txt uv run python benchmarks/schema_benchmark.py
```

The schema benchmark uses an isolated temporary catalog cache, defaults to
anonymous public object-store access, and reports cold/warm totals plus per-source
range GET counts, fetched bytes, and timings. Useful environment variables are
documented in the script docstring.

## Why not to use lazynwb

- some convenience features of `pynwb` will not be available, for example object references in tables
Expand All @@ -145,6 +159,18 @@ df = lazynwb.get_df('my_file.nwb', '/intervals/trials')
Use `get_internal_paths` to find available paths if you're not sure what's in a file:
```python
lazynwb.get_internal_paths('my_file.nwb')
# ['/intervals/trials', '/processing/behavior/running_speed', '/units', ...]
```

Use `get` when you want lazynwb to choose the return type from the NWB container:
```python
df = lazynwb.get('my_file.nwb', '/units') # pandas DataFrame by default
ts = lazynwb.get('my_file.nwb', '/processing/behavior/running_speed', exact_path=True)
```

Force DataFrame materialization, even for a TimeSeries container:
```python
df = lazynwb.get('my_file.nwb', '/processing/behavior/running_speed', exact_path=True, as_df=True)
```

---
Expand Down Expand Up @@ -285,6 +311,13 @@ Get a table as polars:
df = nwb.get_df('/units', as_polars=True)
```

Use the general accessor when you want tables as DataFrames and TimeSeries as
`TimeSeries` objects:
```python
result = nwb.get('/processing/behavior/running_speed', exact_path=True)
df = nwb.get('/processing/behavior/running_speed', exact_path=True, as_df=True)
```

Get a summary of everything in the file:
```python
nwb.describe()
Expand Down Expand Up @@ -348,9 +381,16 @@ Returns columns including `identifier`, `session_id`, `session_start_time`,
See what's inside an NWB file:
```python
paths = lazynwb.get_internal_paths('my_file.nwb')
# {'/acquisition/lick_sensor_events/data': <HDF5 dataset ...>,
# '/intervals/trials': <HDF5 group ...>,
# '/units': <HDF5 group ...>,
# ['/acquisition/lick_sensor_events',
# '/intervals/trials',
# '/processing/behavior/running_speed',
# '/units',
# ...]

path_info = lazynwb.get_internal_path_info('my_file.nwb')
# {'/acquisition/lick_sensor_events': {'is_timeseries': True, 'is_group': True, ...},
# '/intervals/trials': {'is_group': True, 'attrs': {'colnames': ...}, ...},
# '/units': {'is_group': True, 'attrs': {'colnames': ...}, ...},
# ...}
```

Expand Down Expand Up @@ -435,52 +475,136 @@ df = lazynwb.get_df('az://my-container/data/file.nwb', '/units')
df = lazynwb.get_df('https://example.com/data/file.nwb', '/units')
```

Configure cloud access via `lazynwb.file_io.config`:
Configure global defaults via `lazynwb.config`:
```python
from lazynwb.file_io import config
from lazynwb import config

config.use_polars = True # return Polars by default from get_df/get_metadata_df
config.use_obstore = True # use obstore for S3/GCS/Azure (default: False)
config.use_remfile = False # use remfile for HTTP byte-range requests (default: True)
config.anon = True # anonymous access across backends
config.fsspec_storage_options = {"region": "us-west-2"} # backend-specific extras if needed
config.fsspec_storage_options = {"request_payer": True} # backend-specific extras if needed
config.disable_cache = False # disable FileAccessor caching (default: False)
```

For normal AWS S3 buckets, the region belongs to the bucket, not the caller's
current AWS session. The fast HDF5 range reader discovers and caches bucket
regions per bucket, so avoid setting a generic `{"region": "..."}` for workflows
that may mix buckets from different AWS regions. Keep explicit region or endpoint
settings for S3-compatible storage such as localstack, MinIO, or R2, where they
describe that custom service rather than an AWS bucket location.

---

## DANDI archive

Scan a table across all NWB files in a DANDI dandiset:
Use DANDI as a URI discovery step: resolve NWB asset sources with
`get_dandi_sources`, then pass those sources to the regular `lazynwb` APIs.
This keeps DANDI-specific version and asset resolution separate from table,
TimeSeries, and metadata reads.

Draft-only dandisets need an explicit draft version. This example uses the
DANDI:001637 draft sample:
```python
lf = lazynwb.scan_dandiset(
dandiset_id='000363',
table_path='/units',
version='0.231012.2129',
max_assets=10, # limit number of files (useful for testing)
import lazynwb

dandi_sources = lazynwb.get_dandi_sources('001637', version='draft')
```

Known DANDI asset IDs can be resolved directly, avoiding asset listing:
```python
dandi_sources = lazynwb.get_dandi_sources(
'001637',
version='draft',
asset_ids=[
'ca248278-e1b2-4896-ad1c-900e4506cd04',
'1e37bc82-fd23-4cb5-a253-e794cea932ba',
],
)
df = lf.collect()
```

Filter which assets to include:
Published versions can be pinned for reproducible analyses:
```python
lf = lazynwb.scan_dandiset(
published_sources = lazynwb.get_dandi_sources(
'000363',
version='0.231012.2129',
)
```

Use the resolved sources with `get_df` for eager table reads. Large array columns
are excluded by default, so this reads scalar columns unless you opt in to
specific arrays:
```python
units = lazynwb.get_df(
dandi_sources[:2],
'/units',
asset_filter=lambda asset: 'probe' in asset['path'],
include_column_names=['id', 'firing_rate'],
)
```

Use the same sources with `scan_nwb` when you want Polars projections and filters
to bound the remote reads before collection:
```python
import polars as pl

lf = lazynwb.scan_nwb(dandi_sources, '/units', infer_schema_length=2)
units = (
lf
.filter(pl.col('firing_rate') > 1.0)
.select('id', 'firing_rate', 'spike_times', '_nwb_path')
.collect()
)
```

Get S3 URLs for all NWB files in a dandiset:
Use a single resolved source for TimeSeries discovery and bounded reads. The
returned `data` and `timestamps` objects stay lazy until you slice them:
```python
urls = lazynwb.get_dandiset_s3_urls('000363')
timeseries_by_path = lazynwb.get_timeseries(dandi_sources[0], match_all=True)
path, ts = next(iter(timeseries_by_path.items()))
preview = ts.data[:1000]
```

Open a single DANDI asset:
Use the resolved sources for metadata across the same DANDI asset set:
```python
accessor = lazynwb.from_dandi_asset(
metadata = lazynwb.get_metadata_df(dandi_sources, as_polars=True)
```

Performance expectations for DANDI workflows are the same as for other remote
NWB files:

- `get_dandi_sources` reads DANDI asset metadata and resolves object-store
sources; it does not read NWB table or array payloads.
- `get_df` avoids full large array reads by default with
`exclude_array_columns=True`. Use `include_column_names` or
`exclude_column_names` to keep table reads narrow.
- `scan_nwb` lets Polars push down `.select(...)` projections and `.filter(...)`
predicates before `.collect()`, which is the preferred path for large remote
tables.
- TimeSeries data and timestamps are returned as backend arrays. Slice bounded
ranges such as `ts.data[:1000]`; avoid `ts.data[:]` unless you intend to read
the full remote array.
- Metadata and internal-path discovery use bounded catalog reads where possible,
so they should not require broad raw data traversal on supported remote HDF5
sources.

Limit DANDI source discovery before handing sources to table or TimeSeries APIs:
```python
dandi_sources = lazynwb.get_dandi_sources(
dandiset_id='000363',
asset_id='21c622b7-6d8e-459b-98e8-b968a97a1585',
version='0.231012.2129',
max_assets=10,
)
lf = lazynwb.scan_nwb(dandi_sources, '/units', infer_schema_length=2)
```

Prefer the source-first form for new workflows because the same resolved sources can
feed `get_df`, `scan_nwb`, `get_timeseries`, and `get_metadata_df`.

The opt-in DANDI:001637 integration checks exercise the draft sample against
tables, TimeSeries, and metadata without enabling those multi-GB remote reads in
the default test suite:
```bash
uv run pytest tests/test_dandi.py tests/test_dandi_tables.py tests/test_dandi_timeseries.py tests/test_dandi_metadata.py --run-dandi-integration -m "integration and dandi_sample"
```

---
Expand Down
9 changes: 9 additions & 0 deletions autoresearch.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Autoresearch configuration
# Generated by autoresearch CLI v0.3.3

branch = "codex/fast-remote-zarr-v2"
eval_command = "uv run python benchmarks/zarr_schema_benchmark.py --max-sources 1 --json-output /tmp/lazynwb-zarr-schema-benchmark.json"
metric_direction = "lower"
metric_name = "cold_zarr_schema_seconds"
target_file = "src/lazynwb/_zarr/reader.py"
time_budget = "10m"
Loading