Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ae00901
notebooks: binder-runnable reader demos (hhdc_viewer, waveform_viewer)
espg Aug 26, 2026
1b96153
notebooks: share the viewers in viewers.py; say 'contain' in the cove…
espg Aug 26, 2026
347e72e
viewers: binned tensor mode in the 3-D view, checked by default
espg Aug 26, 2026
bb797b2
notebooks: archive the stale examples off main; drop leaked .npy arti…
espg Aug 26, 2026
c0f38f8
fold review: ruff check + format clean on both notebooks (issue #328)
espg Aug 27, 2026
a92d84a
fold review: bin the shard's tensors as they arrive, not all at once …
espg Aug 27, 2026
dd59f40
fold review: put binned points at cell centres, not corners (issue #328)
espg Aug 27, 2026
a3bae14
fold review: grid_xy returns (col, row) so east and north are true (i…
espg Aug 27, 2026
ec66ce9
fold review: bound the figure registry in both interactive views (iss…
espg Aug 27, 2026
cf62491
fold review: split ATL03's chunk side from GEDI's gside (issue #328)
espg Aug 27, 2026
20cdf48
fold review: cache the digest reads and make the empty-cell guard rea…
espg Aug 27, 2026
eefbb1b
fold review: normalize both sensors before ranking on the weaker one …
espg Aug 27, 2026
2912736
fold review: export says whether it got the z resolution it asked for…
espg Aug 27, 2026
3f8919b
fold review: stream the cube export instead of holding the shard in R…
espg Aug 27, 2026
21da9fc
fold review: header says one %pip install and names the one zagg impo…
espg Aug 27, 2026
487fa7f
fold review: cross-link the notebook pair, drop the dangling 07_minim…
espg Aug 27, 2026
96f6d25
fold review: ignore the notebook tensor exports (issue #328)
espg Aug 27, 2026
0df2d62
fold review: say 'no zagg public API', not 'nothing from zagg' (issue…
espg Aug 27, 2026
5ab6913
fold review: drop the duplicated reader-only paragraph (issue #328)
espg Aug 27, 2026
62ca510
fold review: pin the archived-notebook links to a commit permalink (i…
espg Aug 27, 2026
ef5497f
fold review: postBuild names the real zagg dependency and the shadowi…
espg Aug 27, 2026
4410762
fold review: let ipywidgets come from viz rather than reinstalling it…
espg Aug 27, 2026
5400b1d
fold review: say why open_shard's sweep is deliberate and no longer d…
espg Aug 27, 2026
eb7ef39
fold review: the block axes are face-local NE/NW, not compass east/no…
espg Aug 28, 2026
d3c26be
viewers: read the 3-D view's tensors per block, not per shard (PR #540)
espg Aug 28, 2026
616fe4d
notebooks: descending block list; split the export cell; read one blo…
espg Aug 28, 2026
b7fd245
notebooks: say what the timing cell is for, and how to swap the polyg…
espg Aug 28, 2026
b9d7631
viewers: sort the merged 2x2 ATL03 digest; the CDF reads means as int…
espg Aug 28, 2026
d217dc8
notebooks: rewrite the coincident-waveform prose; cite the GEDI gain;…
espg Aug 28, 2026
0d08fd0
notebooks: report reads honestly; co-registered voxel exports (PR #540)
espg Aug 28, 2026
d1372ae
notebooks: unstick the nth slider; block list from the tally, sorted …
espg Aug 29, 2026
7141cf4
notebooks: opacity control, joint-cell ordering, shared shard pick, c…
espg Aug 29, 2026
f33fecc
notebooks: trim the added prose; show both z windows instead of quoti…
espg Aug 29, 2026
06b001b
notebooks: move exports to export.py; isotropic 128^3 chips with repo…
espg Aug 29, 2026
7f40f46
notebooks: reframe the export section around the two decisions it for…
espg Aug 29, 2026
cbd89c9
notebooks: scope the range-bin claim to the examples (PR #540)
espg Aug 29, 2026
7ebed91
notebooks: open_shard -> viewer_stats; the read path is handles plus …
espg Aug 29, 2026
e59ce04
notebooks: paired_blocks helper; drop BLOCK_ORDER and the grid consta…
espg Aug 29, 2026
2ff7a37
viewers: shade by weight ramps down from the slider, not up to opaque…
espg Aug 29, 2026
a076d57
fold review: fit_window clipped and mis-binned silently; unparsable m…
espg Aug 29, 2026
b1b3ab1
viewers: revert continuous_update; bound the nth slider to the block'…
espg Aug 29, 2026
603a6db
viewers: read waveform digests a block at a time; the slider does no …
espg Aug 29, 2026
5933e1b
viewers: keep column sums and gains, not block tensors (1.25 GiB -> ~…
espg Aug 29, 2026
2c7fb10
viewers: opacity fires on release; nth needs no suppression now it re…
espg Aug 29, 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
10 changes: 10 additions & 0 deletions .binder/postBuild
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ else
HATCH_VCS_PRETEND_VERSION="0.0.0+binder" \
python -m pip install --no-cache-dir ".[analysis,catalog,viz]"
fi

# The reader-only demos (notebooks/hhdc_viewer.ipynb, notebooks/waveform_viewer.ipynb)
# run on mortie + moczarr and never import zagg internals, so their runtime is NOT a
Comment thread
espg marked this conversation as resolved.
Outdated
# zagg extra -- each notebook carries its own `%pip install` line and stays runnable
# outside Binder. Two of those packages must be in the IMAGE rather than installed
# from a cell, because they ship Jupyter frontend extensions that a running kernel
# cannot pick up: ipympl (the `%matplotlib widget` backend hhdc_viewer's rotatable
# 3-D view needs) and ipywidgets (the sliders and dropdowns both use). moczarr itself
# is pure Python, so the notebooks' own `%pip install` covers it either way.
python -m pip install --no-cache-dir ipympl ipywidgets
Comment thread
espg marked this conversation as resolved.
Outdated
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,13 @@ The notebooks under `notebooks/` run on [Binder](https://mybinder.org/v2/gh/engl
| `jupyterhub_example.ipynb` | Drive the API from a science hub; read & visualize a published result | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/englacial/zagg/main?urlpath=lab/tree/notebooks/jupyterhub_example.ipynb) |
| `cryocloud_example.ipynb` | End-to-end ISMIP6 read + **AWS Lambda fan-out** on CryoCloud | **not Binder-runnable** (needs live AWS + Earthdata credentials) |
| `cost_reporting.ipynb` | Max / estimated / actual invoke cost + progress-bar dispatch wrapper | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/englacial/zagg/main?urlpath=lab/tree/notebooks/cost_reporting.ipynb) |
| `hhdc_viewer.ipynb` | Polygon → MOC → shard → **rotatable paired 3-D view** (ATL03 + GEDI) → numpy tensors | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/englacial/zagg/main?urlpath=lab/tree/notebooks/hhdc_viewer.ipynb) |
Comment thread
espg marked this conversation as resolved.
Outdated
| `waveform_viewer.ipynb` | The cell-level join: one GEDI footprint against the 2×2 ATL03 cells beneath it, both from stored digests | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/englacial/zagg/main?urlpath=lab/tree/notebooks/waveform_viewer.ipynb) |

`cryocloud_example.ipynb` is the only Lambda demo; it dispatches to a deployed AWS Lambda and reads private-account S3 via the CryoCloud IRSA role, so it cannot run on Binder.

`hhdc_viewer.ipynb` and `waveform_viewer.ipynb` are **reader-only**: they run on `mortie` + `moczarr[zagg]` with no zagg-internal imports, reading the anonymous public [source.coop](https://source.coop/englacial/zagg) demo stores (ICESat-2 ATL03 + GEDI L1B over California and the NEON AOP sites). Each carries its own `%pip install` line, so they run outside Binder unchanged. They are split in two because `hhdc_viewer` needs `%matplotlib widget` for its rotatable 3-D view and `waveform_viewer` needs `%matplotlib inline` — the two backends collide in one kernel.
Comment thread
espg marked this conversation as resolved.
Outdated
Comment thread
espg marked this conversation as resolved.
Outdated

## Project Structure

```
Expand Down
239 changes: 239 additions & 0 deletions notebooks/hhdc_viewer.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "039315dd",
"metadata": {},
"source": [
"# polygon \u2192 MOC \u2192 shard \u2192 3-D \u2192 numpy\n",
"\n",
"The whole zagg read stack in five functions, two `pip install`s, and zero\n",
"credentials. A geojson polygon becomes a morton MOC; the MOC checks itself\n",
"against the store's own coverage; the covered shards open with timings; one\n",
"shard renders in 3-D (ATL03 + GEDI together); the current view exports to a\n",
"numpy tensor and saves to disk. Everything below is reader-side \u2014 `mortie`\n",
"for the geometry, `moczarr` for the store \u2014 running anonymously against\n",
"public S3, binder-ready."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f67cef5b",
"metadata": {},
"outputs": [],
"source": [
"%pip install -q mortie \"moczarr[zagg]>=0.7\" matplotlib ipympl ipywidgets\n",
"%matplotlib widget\n",
"\n",
"import resource\n",
"import time\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import moczarr as mz\n",
"from mortie import moc\n",
"\n",
"# The drawing lives in viewers.py beside this notebook, so the cells below stay\n",
"# about the READ path. Both demo notebooks share it.\n",
"from viewers import BLOCK_ORDER, view3d\n",
"\n",
"# One store per product, each appendable. Coverage answers which ground the\n",
"# store holds; the store name never does.\n",
"STORES = {\n",
" \"atl03\": (\"s3://us-west-2.opendata.source.coop/englacial/zagg/demo/atl03_tdigest_o9.zarr\", \"19/h_tdigest_signal\"),\n",
" \"gedi\": (\"s3://us-west-2.opendata.source.coop/englacial/zagg/demo/gedi_flux_o9.zarr\", \"18/rx_flux\"),\n",
"}\n",
"S3 = {\"region\": \"us-west-2\", \"anonymous\": True}\n",
"\n",
"rss = lambda: resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 2**20 # noqa: E731"
]
},
{
"cell_type": "markdown",
"id": "4cafa82b",
"metadata": {},
"source": [
"## One polygon in, covered shards out"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "933b33b2",
"metadata": {},
"outputs": [],
"source": [
"aoi = {\"features\": [{\"geometry\": {\"coordinates\": [[ # a ~4 km box on the SERC tract\n",
" [-76.56, 38.87], [-76.50, 38.87], [-76.50, 38.91], [-76.56, 38.91], [-76.56, 38.87]\n",
"]]}}]}\n",
"\n",
"q = moc(aoi)\n",
"shards = None\n",
"for name, (root, _field) in STORES.items():\n",
" assert mz.coverage_moc(root, **S3).contains(q), f\"{name} does not contain the polygon\"\n",
" ids = set(mz.candidate_shards(root, aoi=q, **S3))\n",
" shards = ids if shards is None else shards & ids\n",
"shards = sorted(shards)\n",
"shards"
]
},
{
"cell_type": "markdown",
"id": "1634ec9a",
"metadata": {},
"source": [
"## Open one shard \u2014 every dataset, timed"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0300cfdb",
"metadata": {},
"outputs": [],
"source": [
"def open_shard(shard):\n",
" \"\"\"Open the shard's leaf in every store; print open+sweep timing and RSS.\"\"\"\n",
" handles = {}\n",
" for name, (root, field) in STORES.items():\n",
" t0, r0 = time.perf_counter(), rss()\n",
" store = mz.open_leaf(root, shard, **S3)\n",
" _, element = mz.open_ragged(store, field)\n",
" n = sum(len(v) for _, v in mz.read_ragged(store, field))\n",
" print(f\"{name:6s} open+sweep {time.perf_counter() - t0:5.1f}s \"\n",
" f\"(+{rss() - r0:4.0f} MB) \u2014 {n:,} centroids, element {element}\")\n",
" handles[name] = (store, field)\n",
" return handles\n",
"\n",
"\n",
"handles = open_shard(shards[0])"
]
},
{
"cell_type": "markdown",
"id": "138d32bc",
"metadata": {},
"source": [
"## The 3-D view \u2014 both sensors, exact centroids, time-aware"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e1b012c",
"metadata": {},
"outputs": [],
"source": [
"view = view3d(handles, shards[0])\n"
]
},
{
"cell_type": "markdown",
"id": "e70211aa",
"metadata": {},
"source": [
"## Export what you see \u2014 a numpy tensor, shaped your way"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68119f92",
"metadata": {},
"outputs": [],
"source": [
"def export(view, sensor, n_bins=64, resolution=1.0, fit=\"degrade_resolution\"):\n",
" \"\"\"The block on screen -> (rows, cols, n_bins) numpy tensor + z metadata.\n",
"\n",
" `n_bins`/`resolution` set the vertical shape; the xy shape follows the\n",
" sensor's cell order within the o12 block -- 2**(cell_order - 12) a side,\n",
" so 128 for ATL03's o19 cells and 64 for GEDI's o18.\n",
" \"\"\"\n",
" store, field = handles[sensor]\n",
" t, mask, (z0, dz), w = next(\n",
" b for b in mz.read_tensors(store, field, n_bins=n_bins, resolution=resolution,\n",
" block_order=BLOCK_ORDER, fit=fit)\n",
" if int(b[3]) == int(view.block)\n",
" )\n",
" print(f\"{sensor}: {t.shape} tensor, z = {z0:.1f} m + bin * {dz:g} m\")\n",
" return t, {\"z0\": z0, \"dz\": dz, \"block\": mz.morton_decimal(w)}\n",
"\n",
"\n",
"gedi, meta = export(view, \"gedi\", n_bins=128, resolution=0.5)\n",
"np.save(f\"gedi_{meta['block']}.npy\", gedi)\n",
"\n",
"atl03, meta = export(view, \"atl03\") # default 64 x 1 m bins\n",
"np.save(f\"atl03_{meta['block']}.npy\", atl03)"
]
},
{
"cell_type": "code",
"id": "585bbd96",
"source": [
"# ---- second export: every block of the shard as a 128 x 128 x 128 cube ----\n",
"# ATL03's cells are o19, so an o12 block is 2**(19-12) = 128 cells a side;\n",
"# ask for 128 z-bins at 1 m and the tensor is a cube. One `read_tensors`\n",
"# sweep yields one cube per populated block, so a shard exports a SET.\n",
"#\n",
"# `fit=\"degrade_resolution\"` keeps a block whose trimmed relief exceeds 128 m\n",
"# rather than refusing it -- the z gain comes back per block, so the ones that\n",
"# did not fit at 1 m say so instead of silently pretending.\n",
"cubes = {}\n",
"for tensor, mask, (z0, dz), w in mz.read_tensors(\n",
" *handles[\"atl03\"], n_bins=128, resolution=1.0, block_order=BLOCK_ORDER,\n",
" fit=\"degrade_resolution\",\n",
"):\n",
" cubes[mz.morton_decimal(w)] = {\"tensor\": tensor, \"z0\": z0, \"dz\": dz,\n",
" \"cells\": int(mask.astype(bool).sum())}\n",
"\n",
"exact = [b for b, c in cubes.items() if c[\"dz\"] == 1.0]\n",
"print(f\"{len(cubes)} blocks -> {len(cubes)} cubes of {next(iter(cubes.values()))['tensor'].shape}\")\n",
"print(f\" {len(exact)} at the requested 1 m bins; \"\n",
" f\"{len(cubes) - len(exact)} degraded to fit their relief in 128 bins\")\n",
"for b, c in sorted(cubes.items())[:5]:\n",
" print(f\" {b} z = {c['z0']:8.1f} m + bin * {c['dz']:.2f} m \"\n",
" f\"{c['cells']:5,} populated cells {c['tensor'].nbytes / 2**20:.1f} MiB\")\n",
"\n",
"np.savez_compressed(\n",
" f\"atl03_cubes_{shards[0]}.npz\",\n",
" **{b: c[\"tensor\"] for b, c in cubes.items()},\n",
")\n",
"print(f\"saved atl03_cubes_{shards[0]}.npz\")"
],
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "99e36ee1",
"metadata": {},
"source": [
"Five functions, two libraries, one polygon \u2014 coverage, shards, timings,\n",
"the paired 3-D view, and tensors on disk."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading