Skip to content

Commit 7319131

Browse files
Promote 0.4.0 beta: VizX notebook rendering, pmtiles_gbx reader, sample downloaders, XYZ rescale, Helios series (#48)
* docs(spec): Helios PMTiles tiling series + Overture data source design Approved brainstorming output for the 0.4.0 "twofer": three sequenced sub-projects — an Overture STAC-driven distributed data-source module (gbx.sample.overture), net-new VizX PMTiles/COG viewers + a reusable PMTiles inspector, and a config_nb-spined 3-notebook series (SF AOI, solar site-selection narrative) demonstrating MVT/XYZ/COG -> PMTiles. Co-authored-by: Isaac * docs(spec): Overture download outputs Volume + metadata Delta table Add a metadata Delta table output target to OvertureClient.download: one row per asset with a source/path column holding the Volumes path, UPSERTed via MERGE keyed by (theme, type, source). Matches the GeoBrix reader source/path convention so the table can drive distributed reads and a repair()-style re-download, mirroring the StacClient/eo-series pattern. Co-authored-by: Isaac * docs(spec): make Overture distribution Serverless-first + aggressive Serverless is the target, not Classic. Distribution now models the h3-rasterize fan-out: distributed Spark read of Overture parquet over the cloud path with bbox predicate pushdown writing the AOI subset in parallel (performant default), asset-level repartition(N,col) HTTP download as fallback, finer-than-file row-group/byte-range fan-out to avoid the file-count bottleneck, and the hard Serverless constraints (repartition by column only; no spark.conf/cache/persist/.rdd; TEMP TABLE = Serverless/18.1+). Co-authored-by: Isaac * docs(spec): VizX static fallbacks reuse contextily basemaps The plot_pmtiles vector static fallback and plot_cog render over a contextily basemap, consistent with the existing plot_static. contextily is already a [vizx] dependency, so no new dep and the static path stays visually consistent with the rest of VizX. Co-authored-by: Isaac * docs(spec): add performance methodology + knowledge-capture practice Standing practice for all tiling work: every light/Serverless gain is assessed for applicability to similar light-tier and same+similar heavy-tier functions, and captured as a reusable pattern in two homes — a version-controlled docs/superpowers/performance/ corpus and a thin pointer agent memory that links to it — kept distinct from the user-facing performance.mdx. Co-authored-by: Isaac * docs(spec): build perf corpus incrementally within Helios plans Per decision, no separate precursor: the docs/superpowers/performance/ corpus README + pattern files are created as each tiling gain is validated, with paired pointer memories added alongside; each SP plan carries a "capture validated gains" step. Co-authored-by: Isaac * docs(plans): Helios SP1 Overture, SP2 VizX viewers, SP3 series Three sequenced TDD implementation plans from the approved Helios spec. SP1 (12 tasks): OvertureClient STAC-driven distributed download + metadata Delta table. SP2 (10 tasks): plot_pmtiles/plot_cog + pmtiles_info. SP3 (8 tasks): config_nb-spined 3-notebook SF series. Corrected SP2 fallback semantics to interactive-by-default with auto-degrade when the base64 embed exceeds max_embed_mb (max_embed_mb=0 forces static); the draft had inverted it (and was internally inconsistent with its own interactive-default test). Co-authored-by: Isaac * docs(plan): SP3 INTERACTIVE_PLOTS toggle + native ST/H3 injection Add INTERACTIVE_PLOTS=False (GitHub-renderable static default) to config_nb, routing all notebook plot calls through toggle-aware helpers (static plot_pmtiles max_embed_mb=0 / plot_static / plot_cog; interactive plot_pmtiles / plot_interactive). Inject Databricks-native ST/H3 where it fits the solar narrative — NB01 roof area + H3 density, NB03 H3 slope aggregation — as the product on-ramp; NB02 left raster-only. Native names + geometry encoding flagged for confirmation and capability-gated. Co-authored-by: Isaac * docs(plan): SP3 resilient same-session WHL install + [overture] extra Keep the two-step install (force-reinstall --no-deps for fresh wheel bytes, then extras so deps resolve) with an inline rationale so it is not collapsed to one line; matches the eo-series / h3-rasterize pattern and is resilient to same-session wheel rebuilds. Add the [overture] extra (pystac, introduced by SP1) so OvertureClient imports cleanly. Co-authored-by: Isaac * docs(plan): SP3 Task 8 — Serverless end-to-end series run chain Add the target-environment run: a databricks-sdk runner + gbx:run:helios- series command that submits the series to Serverless as sequential notebook job tasks (nb01->nb02->nb03), each %run-ing config_nb (job tasks don't share Python state) with depends_on threading the Volume/Delta artifacts; emits the run_page_url. Complements Task 7's Docker cell-by-cell validation. Co-authored-by: Isaac * feat(sample): overture bbox-intersect + theme expansion helpers Foundation for the Overture discovery module: axis-aligned bbox intersection, bbox normalization, and the canonical theme->type map + expansion (None => all themes). Pure driver-side helpers, no Spark/network, unit-tested in isolation. Co-authored-by: Isaac * feat(sample): traverse Overture static STAC with injected opener traverse_catalog walks a pystac.Catalog-shaped root (children -> items), filters items by AOI bbox intersection, restricts to requested (theme,type) pairs, and emits one row per GeoParquet asset (theme/type/href/asset_bbox). Tested offline with an importable fake catalog (no network). Co-authored-by: Isaac * feat(sample): Overture release resolution + CLI fast-path resolve_release picks the latest release from catalog metadata (sorted overture:releases extra field, or catalog id) or passes an explicit pin through unchanged; raises ValueError when no release is discoverable. cli_discover shells out to the optional overturemaps CLI per (theme, type) pair and returns None when absent, signalling the caller to fall back to static STAC traversal. Runner is injectable for offline testing. Test file imports consolidated to top (isort gate). Co-authored-by: Isaac * feat(sample): OvertureClient.discover over static STAC OvertureClient mirrors StacClient's shape and injection seams. discover() resolves the release, expands themes (None=>all), tries the CLI fast-path (production only, treats None or empty result as fallback) then static traversal, and returns a typed DataFrame (theme/type/href/asset_bbox/release). Re-exported from sample/__init__. test_sample_bundle updated to reflect new __all__. Co-authored-by: Isaac * refactor(sample): drop dead opener guard in _open_catalog _open_catalog is only reached when self._catalog_opener is None (routing happens in _opener()), so the inner guard is unreachable and misleading. Simplify to always open the real catalog. Co-authored-by: Isaac * feat(sample): distributed Overture read + AOI rewrite default Performant default download path: distributed-read each asset's GeoParquet with a bbox-struct predicate pushdown (AOI rows only), write the subset per (theme,type) to the Volume, emit metadata. Repartition by column (never number-only) so the plan stays distributed on Serverless; verified via getNumPartitions > 1. Adds _meta_schema/_meta_dataframe/_META_COLS module helpers reused by the Task 6 fallback path. Co-authored-by: Isaac * test(sample): add regression for empty Overture meta schema contract Empty _meta_dataframe() already flows through the same withColumn/select chain as the non-empty path, so _META_COLS matches; this test locks that contract so Tasks 7/8 union logic can rely on schema consistency. Co-authored-by: Isaac * feat(sample): whole-file Overture download fallback path Asset-level HTTP-href download for when no cloud read is available: fan out by href (column-hash repartition, Serverless-safe), temp-file then sequential copy (Volume-safe), retry + parquet-open validate + idempotent skip. _get_fn injectable; tested offline with a fake fetcher serving real parquet bytes. Co-authored-by: Isaac * feat(sample): OvertureClient.download path selection Public download() routes cloud-readable assets to the distributed read+AOI-rewrite path and http hrefs to the whole-file fallback, returns the pinned metadata schema (source aliased as path), and is idempotent on re-run. Partitions default to asset count. Co-authored-by: Isaac * test(sample): cover download() http fallback routing + fix docstring The existing test_download_routes_cloud_to_distributed uses an absolute tmp_path href (starts with /), which the cloud predicate treats as a FUSE Volume path and routes to _download_distributed -- so download()'s routing to the HTTP fallback branch was untested, and the test comment claiming otherwise was misleading. Add test_download_routes_http_to_fallback: an http:// href fails the cloud predicate, so download() routes through _download_fallback (offline, via the injected fake _get_fn). Asserts the _META_COLS 10-column schema in order and is_out_file_valid. Correct the misleading comment and assert against _META_COLS instead of an inline list. Fix download()'s docstring: it claimed table= UPSERTs to a Delta table, which does not exist yet (Task 8) -- a caller passing table= today gets a silent no-op. Document table= as reserved/no-op and spell out the cloud predicate (/-prefix Volume + s3a:// schemes, http(s):// -> fallback). Co-authored-by: Isaac * feat(sample): metadata Delta MERGE for Overture downloads table=<name> persists/UPSERTs the per-asset metadata to a Delta table, idempotent MERGE keyed by (theme,type,source) (mirrors StacClient.repair). First run creates the table; re-runs update volatile cols so the catalog stays queryable and re-runnable without accumulating duplicate rows. Co-authored-by: Isaac * feat(sample): OvertureClient.read from dir or metadata table read() loads downloaded GeoParquet back into Spark from a Volume directory, a metadata Delta table name, or a metadata DataFrame (source/path column), with an optional bbox-struct AOI filter. A path-guard prevents spark.catalog.tableExists() ParseException on filesystem paths. The metadata table branch can directly drive distributed reads. Co-authored-by: Isaac * test(sample): cover read() table-name + empty; hoist import Fix three review findings on OvertureClient.read(): - Add test_read_from_table_name (Delta-gated, skips offline) to cover the table-name branch that had no test; verifies real row retrieval. - Add test_read_empty_metadata_raises (offline) to cover the IndexError regression in _read_paths([]); guard now raises a clear ValueError. - Hoist `from pyspark.sql import functions as F` from inside test_read_from_metadata_dataframe to the module-level import block (isort/E402 compliance). Co-authored-by: Isaac * feat(sample): download_overture_aoi one-shot convenience Module-level convenience that discovers an AOI's Overture assets and downloads them in one call (themes=None => all), passing the AOI bbox pushdown and optional metadata Delta table through. Re-exported from sample/__init__. Co-authored-by: Isaac * build(sample): light-CI-lock wiring for gbx.sample.overture Add pystac>=1.9,<2 to requirements-pyrx-ci.in (explicit pin; already present transitively via rio-tiler, now marked as a direct dep so supply-chain intent is clear) and requirements-dev-container.in (same reason, alongside pystac-client). Regenerate both hash-pinned .txt locks so the via annotation correctly shows -r <in-file>. Add the [overture] optional-dependencies extra to pyproject.toml (pystac>=1.9,<2), mirroring the [stac] extra pattern. Register test/sample in both required locations: 1. _LIGHT_TEST_DIRS in test/conftest.py (so the heavy CI phase skips it and avoids ModuleNotFoundError at collection time). 2. pytest dir list in .github/actions/pyrx_build/action.yml (so the light CI phase actually runs it). Co-authored-by: Isaac * fix(lint): sweep pre-existing F401/C901 + SP1 minor fixes Three pre-existing flake8 failures (gating CI lint): - test/ds/test_scratch_isolation.py: remove unused pytest import (F401) - test/ds/test_vector_reader_contract.py: drop unused _ShapefileReader import (F401) - src/databricks/labs/gbx/ds/vector.py: suppress C901 on _write_local_osgeo_gdb (complexity 26 > max 20; genuine refactor is out of scope for this wiring task, noqa is the least-surprise fix) SP1 accumulated minor fixes: - sample/overture.py _download_fallback: drop dead _validate/_max_tries local aliases; UDF closure captures validate/max_tries from the enclosing function scope directly - test/sample/test_overture.py: hoist module-body `import databricks.labs.gbx.sample.overture as ov` to module level with # noqa: E402 (consistent with existing importorskip pattern) - test/sample/test_sample_bundle.py: add download_overture_aoi to the expected __all__ set (was missing; sample/__init__.py exports it) Docker black drift (black 26.3.1 in container): - test/ds/test_vector_filename.py, test_vector_writer.py: reformatted Co-authored-by: Isaac * docs(perf): capture SP1 Overture AOI ingestion pattern (Task 12) Start the performance engineering corpus at docs/superpowers/performance/. First entry: serverless-aoi-ingestion-strategy — the pattern validated while building the Overture data source: distributed read-in-place with bbox-struct predicate pushdown + column-hash repartition, with a whole-file HTTP-href fallback. Captures the applicability matrix (light AOI downloaders: yes; heavy OGR/GDAL readers: N/A) and the honest evidence status (pattern/correctness confirmed; cluster-scale speedup not yet measured, deferred to the optional cluster smoke). Distinct from the existing serverless-fanout-repartition-by-column memory (which covers repartition mechanics); this entry covers the full AOI ingestion strategy and generalizes to future sample/ downloaders. Co-authored-by: Isaac * docs(spec): document download bbox= + per-asset write subdir Whole-branch review flagged the spec's download() signature omitted the bbox= AOI param the impl requires for distributed-read pushdown, and did not state the per-asset unique-subdirectory write contract that prevents sharded-type clobber (the C1 fix). Spec now matches the implemented surface. Co-authored-by: Isaac * fix(sample): per-asset Overture write target (no shard clobber) _download_distributed was writing all assets sharing the same (theme, type) to the same `out_dir/<theme>/<type>` path. The 2nd asset's mode("overwrite") deleted the 1st asset's output, so only the last shard survived; metadata source values collapsed (same path), hiding the loss in the MERGE key. Fix: each asset writes to `out_dir/<theme>/<type>/<token>` where `token = sha1(href.split("?")[0])[:12]` — stable, deterministic (same asset → same subdir across re-runs → idempotent MERGE key), unique per asset. mode("overwrite") is now safe and idempotent per asset. read() uses recursiveFileLookup and was already correct — no change needed. Adds test_download_distributed_multi_asset_no_clobber: two assets sharing (theme, type) → RED on old code (sources collapse to 1), GREEN after fix (both shards present, 3 AOI rows, 2 distinct sources, idempotency confirmed). Co-authored-by: Isaac * feat(pmtiles): driver-side pmtiles_info header inspector Spark-side PMTiles read is unsupported, so a local-driver header reader is needed by the VizX viewers for vector/raster type detection and the static fallback. Lazy re-export keeps heavy-tier imports pandas-free. Note: the brief's fixture used identical PNG bytes for z=0 and z=1, causing the PMTiles writer to deduplicate them (max_zoom collapses to 0). Fixed by using distinct per-zoom payloads in the raster fixture. Co-authored-by: Isaac * feat(vizx): pmtiles type-detect + archive-bytes helpers Implement _is_raster_type and _archive_bytes helpers with pinned CDN constants (maplibre-gl@4.7.1, pmtiles@3.2.1) for Task 2. Both TDD green. Co-authored-by: Isaac * feat(vizx): MapLibre+pmtiles.js HTML builder (base64 FileSource) CDN versions pinned (maplibre-gl 4.7.1, pmtiles 3.2.1); archive rides inline as a base64 in-browser FileSource so the map streams client-side with no tile server. Vector vs raster layer chosen from the header type. Co-authored-by: Isaac * fix(vizx): harden pmtiles HTML (script-escape, b64 newline, zoom range) Three SP2 Task 3 review findings addressed: - I1: escape </script> in embedded style JSON (replace "</" with "<\/") so an attacker-controlled style value cannot prematurely close the <script> block and inject arbitrary HTML/JS. - I2: strip \n/\r from archive_b64 at the top of _build_pmtiles_html so base64.encodebytes output (which adds newlines every 76 chars) never breaks the single-line `const b64 = "..."` literal. - M1: add minzoom/maxzoom to the raster layer dict in _default_style so archives whose min_zoom > 0 don't render a blank map until the user manually zooms in. Three new tests: test_build_html_script_injection_escaped, test_build_html_b64_newline_stripped, test_build_html_raster_layer_zoom_range. All 10 tests green; Docker lint clean (317 files unchanged). Co-authored-by: Isaac * feat(vizx): raster pmtiles static fallback via plot_raster Adds _lowest_zoom_tile (iterates all_tiles via MemorySource, picks the coarsest zoom) and _static_raster_fallback (hands tile bytes to plot_raster). all_tiles already yields ((z,x,y), payload) so tileid_to_zxy is not needed. TDD: 11/11 green, lint clean. Co-authored-by: Isaac * feat(vizx): vector pmtiles static fallback (MVT decode -> gdf) Decodes tile-local MVT features back to WGS-84 with the same tile-bounds math pyvx writes, then renders via plot_static over a contextily basemap. Co-authored-by: Isaac * feat(vizx): plot_pmtiles dispatch + base64 size guard Interactive MapLibre map by default; when the ~33%-inflated base64 embed would exceed max_embed_mb, fallback=True (default) degrades to the static render and fallback=False raises. max_embed_mb=0 forces static. Co-authored-by: Isaac * feat(vizx): plot_cog static COG viewer over contextily basemap Decimated/overview rasterio read; band= selects one band. Static-only (interactive raster-source injection deferred — COGs aren't PMTiles; convert to raster PMTiles + plot_pmtiles for an interactive map). Co-authored-by: Isaac * feat(vizx): export plot_pmtiles + plot_cog Add plot_pmtiles public export alongside existing plot_cog (from SP2 Task 7); ensure both are in __all__. Adds export-assertion test to verify availability. Heavy viz deps (matplotlib, rasterio, geopandas) remain lazy inside functions. Co-authored-by: Isaac * docs(vizx): document plot_pmtiles, plot_cog, pmtiles_info Add a "PMTiles viewers" section to vizx.mdx covering plot_pmtiles, plot_cog, and pmtiles_info with param tables and CodeFromTest snippets sourced from a new doc-test module. New doc-test (docs/tests/python/api/vizx_viewers.py) exercises all three functions with real in-memory fixtures and assertions — no network, no sample-data mount required. Runs in Docker via pytest. Also fixes _static_raster_fallback to strip the 'basemap' kwarg before forwarding to plot_raster, which does not accept it. Co-authored-by: Isaac * test(vizx): lock basemap-strip + tidy viewers doc-test The raster fallback test used a **kw-accepting lambda that passed whether or not the basemap kwarg was stripped, leaving plot_kw.pop("basemap") unprotected. Strengthen the monkeypatch to a named function that captures forwarded kwargs, pass basemap=False into _static_raster_fallback, and assert "basemap" not in captured_kw. Also: add basemap=False to the static doc-test example so it directly exercises the kwarg that triggered the bug; wrap both NamedTemporaryFile .tif usages in try/finally os.unlink to avoid leaking temp files. Co-authored-by: Isaac * docs(perf): capture PMTiles base64-embed rendering pattern (SP2 Task 10) SP2 VizX viewers introduced plot_pmtiles, which embeds the full PMTiles archive inline as a base64-decoded in-browser FileSource — no tile server, no remote range requests, offline-safe, GitHub-renderable via the static fallback. The size-guard (base64 ~+33% overhead, max_embed_mb=64) and the max_embed_mb=0 forced-static override are non-obvious engineering decisions worth capturing so future inline viewers don't rediscover them. Corpus entry: docs/superpowers/performance/notebook-pmtiles-rendering.md (problem → pattern → applicability matrix → offline test evidence). Heavy-tier verdict: N/A — driver-side light-tier-only pattern. Co-authored-by: Isaac * fix(vizx): clear error on unknown pmtiles type + precise map_kwargs docs M1: plot_pmtiles now raises ValueError immediately when tile_type is not in {mvt, png, jpeg, webp, avif}, naming the supported set. Single dispatch point covers both interactive and static paths before any HTML build or tile decode. Introduces _SUPPORTED_TILE_TYPES sentinel. I1: docstring and vizx.mdx table row for **map_kwargs now state that on the static path they are forwarded to the matching plotter (plot_raster for raster archives, plot_static for vector archives) and must be valid kwargs for that plotter; unrecognised kwargs raise TypeError. Not a behaviour change. New offline test: test_plot_pmtiles_unknown_tile_type_raises — monkeypatches pmtiles_info to return tile_type="unknown", asserts ValueError with message naming supported types. All 18 tests pass. Co-authored-by: Isaac * feat(helios): add notebook-series diagram generator + images Each of the three Helios notebooks (Vector Engine / MVT, Visual Basemap / XYZ, Analytical Core / COG+STAC) gets a data→tile→PMTiles→view flow diagram rendered as SVG + trimmed PNG. The generator (resources/images/helios.py) mirrors the eo-series.py palette, glyph, chip, and four-stage layout machinery exactly, with Helios-specific glyphs: building footprints, vector- tile grid, stacked-archive, map pin (NB01); aerial swatch, web-mercator globe, XYZ pyramid, map pin (NB02); contour DEM, COG catalog, hillshade relief, map pin (NB03). PNGs rendered via Chrome headless + PIL bbox-trim at 2× device scale (2960×1440). The notebooks/examples/helios/ directory scaffold is also created here for downstream tasks. Co-authored-by: Isaac * docs: landing Quick Start uses staged-wheel install, not bare pip geobrix isn't on PyPI yet, so `%pip install geobrix` on the homepage can't work. Replace it with the quoted PEP 508 named form installing geobrix[light] from a staged Volume wheel (matching installation.mdx and the notebook config cells), plus a note to stage the wheel first. Co-authored-by: Isaac * feat(helios): add config_nb spine for SF solar tiling series Shared %run setup for the Helios SF solar tiling series, built verbatim from the Task 1 brief: resilient two-step %pip install of the staged geobrix[light,stac,vizx,overture] wheel + %restart_python, lightweight tier (pyrx -> rx, pyvx -> vx) with heavyweight option-2 commented, OvertureClient + StacClient, set_conf_safe Serverless guard, viz/COG/ PMTiles + inspector imports, FORCE_REBUILD + INTERACTIVE_PLOTS=False toggles, ETL_DIR/HELIOS_DIR + SF_AOI_BBOX/SF_CITY_BBOX, and series-only helpers solar_score / finalize_delta / show_pmtiles / show_cog / show_raster that NB01-03 %run and depend on. config_nb is %run-only; the cell-by-cell harness cannot run the comment- first %pip cell or the Spark/dbutils cells in its isolated venv (same as the eo-series spine), so the spine is exercised end-to-end in Task 4's full-series validation. SP1/SP2 modules verified importable from source. Co-authored-by: Isaac * docs(plan): SP3 Task 6 add reciprocal cross-link sweep + inventory The Helios series touches many functions/writers; each function's docs page should link to the series example (mirroring the eo-series/ h3-rasterize convention). Add a Task 6 step with the concrete inventory of pages to touch (vectorx/raster/pmtiles/writers/vizx/stac/h3-raster/ sample-data) linking to ../notebooks/helios, canonical page per function. Co-authored-by: Isaac * feat(helios): add NB01 Overture buildings to vector PMTiles Implements the Helios series notebook 01 (Vector Engine): Overture Maps building footprints → gbx_st_asmvt_pyramid LATERAL fan-out → gbx_pmtiles_agg → sf_buildings.pmtiles on the Volume → show_pmtiles inline view. Composes with Databricks-native st_geomfromwkb/st_area/ st_centroid and h3_longlatash3 (gated behind a try/except so the tiling path stays green when native ST/H3 is unavailable, e.g. plain Spark in Docker). Overture discover/download/read cells gated behind GBX_HELIOS_OFFLINE env check with a 12-polygon synthetic SF fallback so the full tiling+PMTiles pipeline can be validated offline. Co-authored-by: Isaac * docs(spec): gbx_pmtiles_agg merges multi-feature vector tiles Design for fixing the first-wins dedup that drops all-but-one feature per vector tile, breaking the documented st_asmvt_pyramid -> pmtiles_agg vector pipeline. Merge same-(z,x,y) MVT blobs (decode+union per layer+ re-encode) for vector tile types; keep first-wins for raster; both tiers at parity (POLYGON multi-feature test). Per-tile simplification deferred. Co-authored-by: Isaac * docs(plan): gbx_pmtiles_agg vector-merge implementation plan 5-task TDD plan: light-tier merge (decode/union-per-layer/re-encode), heavy-tier MvtDecoder (OGR /vsimem read) + merge in PMTiles_Agg, POLYGON light-vs-heavy parity, docs, gains capture. Heavy unions features per layer name then concats distinct layers; raster keeps first-wins. No new deps. Holding execution pending the cross-aggregator drop-on-collision audit. Co-authored-by: Isaac * docs(spec): PV — heavy pmtiles_agg also fixes malformed dup-tile entries Cross-aggregator audit confirms the drop-on-collision flaw is UNIQUE to gbx_pmtiles_agg (all other _agg combine correctly, incl. gbx_st_asmvt). It also found the heavy tier writes a directory entry per tuple (no tileId dedup) -> duplicate (z,x,y) = malformed archive (affects raster too). The group-by-tileid design fixes both; add a heavy raster dup-tile regression. Co-authored-by: Isaac * fix(pmtiles): merge multi-feature MVT blobs per (z,x,y) in light tier gbx_pmtiles_agg was dropping all but the first feature per tile for vector (MVT) data, making the st_asmvt_pyramid→gbx_pmtiles_agg pipeline produce single-feature tiles for real datasets. This fix groups payloads by tileid, decodes and unions features per layer, and re-encodes one merged MVT blob. Raster first-wins is unchanged. Co-authored-by: Isaac * fix(pmtiles): merge multi-feature MVT blobs per (z,x,y) in heavy tier gbx_pmtiles_agg was dropping all but the first vector feature per tile when multiple rows shared the same (z,x,y) — the st_asmvt_pyramid → gbx_pmtiles_agg pipeline produced single-feature tiles for dense data. A second bug: the heavy encoder wrote a directory entry per tuple, so duplicate (z,x,y) produced two entries — a structurally invalid PMTiles archive (affects raster too, not just vector). Fix: group buffer.tiles by Hilbert tile id in eval; for MVT decode each blob (new MvtDecoder via OGR MVT driver on /vsimem/ .pbf), union features per layer name, re-encode one merged blob (MvtWriter.encode per layer, concatenated protobuf). Raster keeps first-wins. Exactly one directory entry is emitted per tile id in all cases. MvtWriter.ensureNativeLoaded() promoted to private[mvt] so MvtDecoder (same package) can call the shared JNI-load guard. TDD: MvtDecoderTest (4 tests, RED→GREEN), PMTiles_AggTest +4 tests (merge, polygon-type, raster-first-wins, raster-dup-entry regression) RED→GREEN; all 5 pre-existing PMTiles_AggTest tests stay green. Co-authored-by: Isaac * fix(mvt): worldToTileLocal in MvtDecoder — fix double-transform in merge MvtDecoder.decode returned OGR-decoded geometry in EPSG:3857 world coordinates, but MvtWriter.encode expected tile-local [0,extent] input and applied tileLocalToWorld internally. mergeMvtPayloads composed them in sequence, so world coords were fed to tileLocalToWorld a second time, producing coordinates wildly outside [0,extent] in every merged MVT tile. Added worldToTileLocal (inverse of MvtWriter.tileLocalToWorld) applied inside decode so all callers receive tile-local WKB. Strengthened PMTiles_AggTest to assert decoded coordinate values (not just count/type). Co-authored-by: Isaac * fix(mvt): revert worldToTileLocal — OGR already returns tile-local coords Investigation showed the premise was wrong: OGR MVT driver opened on a raw .pbf path (without z/x/y context) returns coordinates as raw integer tile-local values, not EPSG:3857 world metres. The decode → encode compose was already correct. The worldToTileLocal function incorrectly treated tile-local integers (~100) as world metres (~-19M) and re-scaled them to near-degenerate values (~2048), causing OGR to produce zero-sized polygons that were deduplicated in the merged tile. Reverted to original decode behaviour; updated scaladoc to document the actual tile-local coordinate contract explicitly. Co-authored-by: Isaac * fix(test): correct dropWhile gap in coordinate assertion dropWhile(_ < 200.0) leaves x=200 (first polygon max corner) as .head, so the second polygon minX check compared 200 to expected ~300 and failed. Changed the cutoff to 250 (midpoint between the two polygons' x ranges) to correctly skip all first-polygon x values and land on the second polygon's minX (~300). Co-authored-by: Isaac * docs: Databricks Labs logo + complementary teal hero banner Add the vendored Databricks Labs logo to the landing-page banner and set the banner background to a deep-teal gradient — the complement of the logo's red-orange (#FF5F46) — so the logo pops and white hero text stays readable. Logo vendored under static/img (not hot-linked). Co-authored-by: Isaac * fix(test): use range-spread assertion for coordinate round-trip Replaces the fragile dropWhile-based coordinate check with a simpler xMax - xMin > 200 assertion. OGR y-flips individual blob decodes but the full merge-encode-decode round-trip recovers original orientation; the explicit positional assertions were correct only by accident (they tested x, not y). The range-spread check is equivalent and immune to OGR y-flip behavior differences between read paths. Co-authored-by: Isaac * fix(cmd): single-quote-escape gbx:test:notebooks --path for parens/spaces The docker-exec command string passed the notebook --path unquoted, so a path with parentheses or spaces (e.g. "01. Vector Engine (MVT).ipynb") broke the inner bash -c. Escape it single-quote-safe. Verified for parens+space, embedded single-quote, and empty-path (no injection). Co-authored-by: Isaac * test(pmtiles): light-vs-heavy vector-merge parity (POLYGON multi-feature) Cross-tier parity gate: two POLYGON blobs for the same (z,x,y) tile must produce equivalent merged tiles from both light (_assemble_archive) and heavy (gbx_pmtiles_agg UDAF). Uses polygons, not points — required by the MVT tile-local contract to avoid false passes. Discovered and fixed a bug in MvtWriter.encode: gdal.GetMemFileBuffer returns null for files written by the OGR MVT driver to /vsimem/ (it only works for FileFromMemBuffer-created files). Switched to a real Java temp directory with Files.readAllBytes so the encoded .pbf bytes are always returned correctly, enabling PMTiles multi-feature tile merging to work. Co-authored-by: Isaac * docs(pmtiles): document MVT merge vs raster first-wins in gbx_pmtiles_agg User-facing note: vector tiles at the same (z,x,y) are merged into one multi-feature tile; raster tiles use the first. This makes the behaviour of the st_asmvt_pyramid→gbx_pmtiles_agg pipeline explicit in the reference. Co-authored-by: Isaac * docs(perf): capture pmtiles vector-merge correctness lessons Two reusable engineering lessons from the gbx_pmtiles_agg vector-merge fix: (1) tile aggregators must group by tile ID before writing — first-wins silently drops features in dense MVT pipelines; (2) gdal.GetMemFileBuffer returns NULL for OGR-written /vsimem/ paths — use real temp dir + Java file I/O for driver output. Audit confirmed no other _agg shares the drop-on-collision flaw. Co-authored-by: Isaac * fix(helios): NB01 portable mkdir + accurate merge note + unit/tier notes Replace dbutils.fs.mkdirs with os.makedirs(exist_ok=True) — portable and Volume-FUSE-safe; os is already imported in the offline-guard cell. Correct the misleading "first-write-wins" comments in Sections 4 and 5: gbx_pmtiles_agg now MERGES per-feature MVT blobs sharing a (z,x,y) (decode + union features per layer + re-encode) after the product fix; every building in a dense tile is preserved. The pipeline is correct as-written. Add a square-degrees unit note to Section 3b — st_area on EPSG:4326 returns deg², matching the roof_area_deg2 variable name already in the code. Add a tier-split LATERAL syntax note to Section 4 — light UDTF emits flat (z,x,y,mvt_bytes); heavy wraps them in a tile struct — so readers comparing function-info docs understand the column layout difference. Co-authored-by: Isaac * feat(helios): add NB02 NAIP aerial imagery to raster PMTiles Adds the second Helios notebook: NAIP → gbx_rst_to_webmercator → gbx_rst_xyzpyramid (z12-z16) → gbx_pmtiles_agg → raster PMTiles on a UC Volume. Includes offline fallback (GBX_HELIOS_OFFLINE) that copies the committed nyc_sentinel2_red_byte.tif sample so the full reproject → pyramid → PMTiles → view chain validates without network access. Corrects brief column names (z/x/y/bytes not zoom/tile_x/ tile_y/tile) and adds the driver arg to rst_fromcontent (GTiff). Co-authored-by: Isaac * fix(nb): NB02 heavy-tier struct note + CRS-aware NAIP window Heavy gbx_rst_xyzpyramid wraps output in a tile struct (t.tile.z/x/y/bytes) while light tier emits flat columns; add a one-line inline comment so readers switching to the heavy tier know which column form to use. NAIP COGs on Planetary Computer are in UTM, not WGS84; src.window() with raw lon/lat bbox silently produces a mislocated/empty crop. Fix the online path to transform_bounds("EPSG:4326", src.crs, ...) + window_from_bounds before reading. transform_bounds is a no-op when src.crs is already EPSG:4326, so the fix is safe for any source projection. Offline fallback is unchanged (Docker/CI path, uses committed sample corpus). Co-authored-by: Isaac * feat(helios): add NB03 DEM to COG+STAC + hillshade PMTiles Stage a USGS 3DEP DEM (with SRTM offline fallback), convert to Cloud-Optimized GeoTIFF via gbx_rst_cog_convert, catalog the COG into a queryable Delta table (StacClient is read-only; hand-built rows via finalize_delta), derive slope/aspect/hillshade (gbx_rst_slope / gbx_rst_aspect / gbx_rst_hillshade — corrected from brief's wrong rst_terrainslope names), aggregate onto H3 cells via gbx_rst_h3_rastertogridavg, apply solar_score per cell, gate native h3_centeraswkb behind a capability check for non-Photon envs, then reproject hillshade to web mercator and tile to raster PMTiles. Co-authored-by: Isaac * fix(helios): NB03 H3 SQL to light-tier LATERAL + CRS transform Cell 14 used the heavy-tier LATERAL VIEW explode(…[0]) pattern, which is invalid for the light (pyrx) UDTF that emits flat (band, cellID, measure) rows. Replace both slope_cells and aspect_cells queries with the canonical LATERAL form + WHERE t.band = 1. Also inline gbx_rst_transform(tile, 4326) before rastertogridavg: SRTM is already 4326 (no-op), but 3DEP may be EPSG:4269 (NAD83); this keeps the input CRS contract correct by construction. Cell 13 markdown updated to describe the actual UDTF contract (no [0], no LATERAL VIEW explode). Co-authored-by: Isaac * docs(helios): add notebook-series README Adds notebooks/examples/helios/README.md mirroring the eo-series structure: intro + solar site-selection narrative, lightweight-tier and data-source blockquotes, per-notebook highlights (with Databricks-native on-ramp notes for NB01/NB03), Files table, run order, ASCII data-flow diagram, Serverless execution strategy, key functions, Gotchas, and Related resources. Doc-voice clean (no wave/ subagent/SP# vocabulary). Co-authored-by: Isaac * docs(helios): Helios docs page + sidebar + reciprocal function-page links Adds the Helios notebook series documentation page (SF AOI → three PMTiles archives — vector MVT, raster XYZ, terrain hillshade) mirroring the eo-series.mdx structure. Sidebar entry added at position 4 in the Notebooks category. Reciprocal cross-link sweep (per the task brief): adds worked-example sentences and Related-links entries pointing to notebooks/helios from every canonical API/writer page that showcases a Helios-touched function: vectorx-functions (gbx_st_asmvt, gbx_st_asmvt_pyramid), raster-functions (gbx_rst_to_webmercator, gbx_rst_xyzpyramid, gbx_rst_cog_convert, terrain: slope/aspect/hillshade, and the Analysis section), pmtiles-functions (gbx_pmtiles_agg), writers/pmtiles and writers/overview (.write.format("pmtiles")), vizx (plot_pmtiles, plot_cog, pmtiles_info), stac (StacClient, alongside existing eo-series link), h3-raster-tessellation (gbx_rst_h3_rastertogridavg), and sample-data/overview (Overture data source). Co-authored-by: Isaac * docs(helios): NB03 diagram — STAC Delta padding, hillshade gradient, names Folder STAC Delta label gets headroom above and separation from the .cog files (pushed down). Slope/hillshade grid now lights from the top-left (sun) and darkens diagonally. Fix chip/footer names to the real registered functions gbx_rst_slope / gbx_rst_hillshade (were rst_terrainslope/_hillshade). Co-authored-by: Isaac * docs(helios): move diagram to its own cell + add multi-PMTiles note Extract each notebook's chip/diagram image out of the intro cell into its own markdown cell right after it (NB01-03). Also add a note to NB01's PMTiles section on producing many PMTiles via groupBy(shard).agg + a mosaic/tilejson catalog (the distributed-sharding alternative to one archive). Co-authored-by: Isaac * docs(helios): NB04 — distributed sharding & mosaic for PMTiles at scale Demonstrates the multi-archive alternative to NB01's single archive: shard_z=11 bit-shift partitions SF tiles into 4 bounded PMTiles files, gbx_pmtiles_agg grouped by shard produces one archive per key, and a Delta catalog + mosaic.json let a MapLibre client assemble the full multi-region map client-side without a tile-proxy server. Co-authored-by: Isaac * fix(nb04): spread offline buildings across all 4 z11 shards + fix buffer note All 12 original synthetic buildings landed in one z11 shard (11_327_791) because the cell comment had the lon boundary wrong (-122.50 is east of the actual split at -122.5195, hence x=327 not x=326). An offline run produced only one archive, defeating the multi-archive demo. Replace with 8 buildings (2 per shard) placed well clear of the z11 boundaries (lon=-122.5195, lat=37.7186): Outer Sunset + Inner Sunset fringe (11_326_791), Daly City + Westlake (11_326_792), Mission + Noe Valley (11_327_791), south SF east + Daly City east (11_327_792). Standalone shard-math verification confirms all four keys appear. Also corrects the cell-0 buffer-rule bullet, which conflated tile-clipping (done by the MVT encoder) with source-query buffering (not done in these demo notebooks). The new text states the demo omits buffering and says what a production pipeline must do instead. Co-authored-by: Isaac * feat(helios): add NB04 diagram — Distributed Sharding & Mosaic Add a 4th conceptual diagram for the Helios notebook series covering the distributed PMTiles sharding pattern: MVT pyramid → shard assignment via shiftright → per-shard gbx_pmtiles_agg → mosaic catalog (sf_building_shards Delta + mosaic.json). Violet/indigo theme (#6B4FA0) distinct from the existing blue/orange/teal. New glyphs: g_shard_grid (2×2 quadrant grid with z11 badge), g_multi_archive (4 small .pmtiles icons in a 2×2 layout), g_mosaic_catalog (reassembled 2×2 tile grid + mosaic.json badge). Series pill updated to "of 4" — all four PNGs regenerated at 2960×1440px. Docs in helios.mdx and README.md updated by user to cover NB04. Co-authored-by: Isaac * docs(helios): NB04 diagram own cell + 4.0 normalize + NB01 cross-ref Move NB04's conceptual diagram out of the intro cell into its own markdown cell right after it, matching the image-cell layout used by NB01-03. Normalize NB04 from nbformat 4.5 (cell ids on every cell) to 4.0 with no ids, so it matches the rest of the Helios series; all code-cell content is byte-identical, only the on-disk representation changed. Add a short note to NB01's "fold into one PMTiles archive" section pointing to NB04 for the multi-archive groupBy(shard).agg(gbx_pmtiles_agg) + mosaic catalog pattern — the cross-reference 361a2cab's message described but that did not actually land in the notebook. Co-authored-by: Isaac * refactor(images): role-based resources/images/ layout Flat resources/images/ had brand logos, diagram PNGs/SVGs, and generator scripts all co-mingled. Reorganise into brand/, diagrams/{helios,eo-series, h3-rasterize,xview,rasterx}/, and generators/ so each asset class lives in one place. readers/ and quickstart/ are unchanged. All cross-repo references (docs MDX, READMEs, notebooks, generator docstrings, QC check-diagram-coverage.py) updated to the new paths. Generator __file__- relative output paths patched so re-running any generator still writes into the correct diagrams/<topic>/ subdir. Co-authored-by: Isaac * docs(vizx): multi-layer viewer design spec Design for unified vector/raster/grid in-notebook viewers: a Layer model consumed by plot_static (matplotlib) and a MapLibre-consolidated plot_interactive (folium retired); the >64MB embed ladder grounded in the displayHTML-iframe CORS constraints; simplify_tiles() + simplify_tiles_spec with ephemeral-vs-durable modes and a tippecanoe/distributed/rasterio engine policy; a gating Phase-1.5 AnyWidget-on-Serverless spike with the zoom cut-over contingent on it; and the notebook/doc audit + Helios rewiring. Co-authored-by: Isaac * docs(vizx): fold review decisions + empirical CORS finding into spec Record the decided items (CARTO basemap + contextily retained for static, dynamic cut-over as an immediate follow-on on AnyWidget-spike pass, two-flavor simplify engine, red-carpet docs as a halo deliverable) and the empirical Files-API CORS result (cross-origin Range header blocked -> no in-notebook Volume range reads via the gateway; presigned-S3 is the only remaining in-notebook candidate). Add the spike list to run before planning and an SRI requirement for the vendored MapLibre/pmtiles.js. Co-authored-by: Isaac * docs(helios): point config_nb at geospatial_docs/geobrix data/helios Set schema_name to geobrix (was helios) and the HELIOS_DIR subdir to helios (was sf), so the series ETL tree resolves to /Volumes/geospatial_docs/geobrix/data/helios — matching the catalog/schema of the staged light-tier wheel config_nb installs and the Volume the VizX spike archive is generated into. Co-authored-by: Isaac * docs(vizx): record spike results — cut-over IN, indefinite-archive App-only Spike B (AnyWidget JS<->kernel comm on Serverless) PASSED -> the dynamic zoom cut-over is in scope as a Phase-1.5 follow-on. Spike A: a MANAGED volume yields no presigned URL -> indefinite single-archive in-notebook is Phase-2 (App); external-volume presigned path parked. Files-API Range header is CORS-blocked (prior finding). Plan can now be scoped with the cut-over included. Co-authored-by: Isaac * docs(vizx): implementation plan for the multi-layer viewer 16-task TDD plan from the approved design: Layer model, plot_static/ plot_interactive on MapLibre (folium retired), the >64MB ladder, two-flavor simplify_tiles + spec (tippecanoe/tile-join/rasterio), exports/extras/SRI, the Phase-1.5 AnyWidget dynamic cut-over, red-carpet docs, and the notebook/ doc audit + Helios NB02/NB03 overlays. Spikes resolved: cut-over IN, indefinite single-archive App-only. Co-authored-by: Isaac * docs(helios): hoist user settings to a block after restart in config_nb Move the variables a user edits — catalog_name / schema_name, FORCE_REBUILD, INTERACTIVE_PLOTS, and the AOI bboxes — into a single labeled "User settings" cell placed right after %restart_python (so the freshly installed wheel is active). The deeper USE CATALOG and ETL_DIR cells keep only their consuming logic. Nothing between restart and those cells uses the knobs at runtime, so the hoist is behavior-preserving. Co-authored-by: Isaac * docs(eo-series): hoist user settings to a block after %pip in config_nb Mirror the helios config_nb change: move catalog_name / schema_name and FORCE_REBUILD into a single labeled "User settings" cell right after the %pip install (where eo-series' implicit post-%pip restart lands; this series has no explicit %restart_python). The USE CATALOG cell keeps only its consuming logic. Behavior-preserving. Co-authored-by: Isaac * docs(vizx): expand plan Task 15 to showcase the new viewer, not just migrate Per request, the example-notebook audit now actively demonstrates the multi-layer interactive viewer on each notebook's final artifact: eo-series (merged kring raster / band-stacked tile + H3 grid overlay), xview (clipped aerial tile + detected-object boundaries), and h3-rasterize (opportunistic). Anchored to the real variables (kring_df, stacked_df, clip_raster). Co-authored-by: Isaac * feat(vizx): Layer model + vector/raster/grid/pmtiles constructors Foundational Task 1 of VizX viewer build: unified Layer dataclass with constructors for each visualization tier and as_layers coercion function. Co-authored-by: Isaac * test(vizx): cover as_layers coercion branches; drop unused import Add missing test coverage for bare-input coercion (PMTiles magic bytes, raster paths, ndarray) and empty-list rejection. Remove unused field import. Co-authored-by: Isaac * feat(vizx): plot_cog accepts ax= for static multi-layer overlay Thread ax=None through plot_cog and _render_cog so callers can draw a COG onto a shared Axes (needed by Task-3 static compositor). When ax is None, behaviour is unchanged (owns its own figure). Co-authored-by: Isaac * feat(vizx): plot_static accepts a Layer list (matplotlib compositor) Refactor plot_static to accept Layer / list[Layer] input via as_layers; adds _draw_one_layer dispatcher (vector/grid via geopandas, raster via plot_cog) so multiple layers render on one Axes in order. Legacy single-DataFrame keyword call is preserved unchanged. Co-authored-by: Isaac * docs(vizx): plan — static fallback decodes pmtiles, never drops a layer Co-authored-by: Isaac * fix(vizx): warn (not drop) on pmtiles in plot_static; guard empty layer list * pmtiles layers now trigger a stacklevel=2 warning instead of silently skipping * empty list/tuple input raises ValueError with clear message (not AttributeError) * added test_plot_static_empty_list_raises + test_plot_static_pmtiles_layer_warns Co-authored-by: Isaac * feat(vizx): MapLibre per-layer adapters (geojson/image/pmtiles) Converts a Layer into MapLibre GL sources+layers+embed_bytes: vector/grid→geojson (reproject+fill/line/circle by geom type), raster→image (rasterio decimate+PIL RGBA PNG+4-corner lon/lat), pmtiles→raster|vector source with _gbx_pmtiles sidecar for Task-5 builder. 20 tests green; full vizx suite 142/142. Co-authored-by: Isaac * docs(vizx): plan — Task 6 gates budget on assembled HTML size, not mixed embed_bytes Co-authored-by: Isaac * fix(vizx): derive pmtiles source-layer from archive metadata Hardcoded "buildings" rendered blank for any archive whose vector layer had a different name; now reads vector_layers[0].id from TileJSON metadata (falling back to "buildings" only for url-mode archives that cannot be pre-inspected). Narrows test_unknown_kind_raises to ValueError only and adds two new tests: source-layer derivation with layer_name="roads" and a unit test for _extract_vector_layer_names. Co-authored-by: Isaac * feat(vizx): self-contained multi-source MapLibre HTML builder Adds build_html() to _maplibre.py: merges N per-layer adapter outputs into one SRI-pinned HTML page with CARTO basemap, pmtiles:// protocol registration (embed FileSource or url PMTiles), and sidecar popping so MapLibre never sees the _gbx_pmtiles key. Six new test_build_html_* tests; 28/28 green. Co-authored-by: Isaac * fix(vizx): escape JSON in MapLibre <script>; make build_html non-mutating Add _json_for_script() to escape <, >, & preventing </script> breakout XSS from user DataFrame attribute values; make build_html read rather than pop the _gbx_pmtiles sidecar so callers can invoke it twice (idempotent). Co-authored-by: Isaac * feat(vizx): >64MB ladder (url->embed->[simplify hook]->static) with warnings prepare_layers() decides per layer whether to embed or fall back to static; url-mode pmtiles are always interactive; embedded archives exceeding budget trigger loud warnings naming offending layers and the three remedies. Co-authored-by: Isaac * feat(vizx): plot_interactive on MapLibre (layers); retire folium Replaces the folium-based plot_interactive with a MapLibre GL ladder (as_layers → prepare_layers → build_html); plot_pmtiles becomes a thin delegator to plot_interactive([pmtiles_layer(...)]). No folium import survives in vizx. Old folium-specific tests removed; pmtiles tests updated to reflect new delegation path and removed tile-type validation (unknown types now treated as vector per the new adapter behavior). Co-authored-by: Isaac * docs(vizx): plan — Task 12 also removes dead _build_pmtiles_html / version skew Co-authored-by: Isaac * feat(vizx): simplify_tiles_spec schema + validation Adds normalize_spec() to apply defaults and validate simplify configuration, supporting schema stability for future tile-simplification engines. Co-authored-by: Isaac * feat(vizx): simplify_tiles_from_source (tippecanoe / rasterio, budget-bounded) Vector path: GeoDataFrame/path/Spark DF → PMTiles via tippecanoe with budget_mb, min_z/max_z, drop_densest, cluster_distance from normalize_spec. Resolves real tippecanoe C binary (PyPI BIN_DIR, then PATH) to avoid the manylinux wrapper's SystemExit / SIGSEGV quirk under subprocess. Raster path: overview downsample to raster_max_px via rasterio → COG. distributed engine raises NotImplementedError. 7/7 tests RAN (not skipped). Co-authored-by: Isaac * feat(vizx): simplify_tiles_from_archive (tile-join down-zoom/trim) Add simplify_tiles_from_archive to _simplify.py: calls source-built tile-join to down-zoom an existing PMTiles archive without re-tiling from source. Add _resolve_tilejoin_bin / _probe_tilejoin_bin to find the working tile-join ELF (skipping Python wrappers and segfaulting manylinux binaries on emulated-x86 Docker). Test round-trips max_z=8 archive down to max_z=4 and asserts PMTiles magic bytes. Co-authored-by: Isaac * fix(vizx): warn (not log) when budget_mb ignored on archive trim Replaces silent log.warning with proper UserWarning for callers who cannot see logging output (notebooks/apps). Co-authored-by: Isaac * docs(vizx): plan — Task 11 budget-escalates archive simplify to source re-tile When tile-join zoom-trim leaves a tile over budget_mb, decode the archive's features and re-tile from source (tippecanoe drop-densest) to enforce the byte budget, instead of warning budget-ignored. Co-authored-by: Isaac * docs(vizx): plan — add Task 11b (embed-size audit) + 13b (predictive prefetch) Captures two design refinements: proactive up-front size auditing so the 64MB embed decision is never a surprise, and background neighbor-ring tile prefetch for the dynamic viewer (cache + daemon thread on the reactive Task-13 loop). Co-authored-by: Isaac * feat(vizx): wire simplify into ladder; archive budget-escalates to source re-tile _simplify_layer routes vector/grid/raster source layers to simplify_tiles_from_source and pmtiles archives to simplify_tiles_from_archive; prepare_layers rung-3 warns "simplified <label>" on success. simplify_tiles_from_archive escalates to source re-tile (decode max_z MVT → GeoDataFrame → tippecanoe) when budget_mb is explicitly set and zoom-trimmed tiles still exceed it; within-budget trim is silent. Co-authored-by: Isaac * docs(vizx): plan — explicit evict policy for the prefetch cache (Task 13b) Co-authored-by: Isaac * test(vizx): assert archive escalation reduces max tile sizes Strengthen test_archive_budget_escalation_retiles_from_source to verify that escalation actually reduces tile sizes when budget_mb is exceeded. Compare the escalated result's max tile size against the source archive's max tile size to confirm escalation had the intended effect. Use original 0.001 MB budget that triggers escalation and acknowledges tippecanoe's best-effort enforcement. Co-authored-by: Isaac * feat(vizx): proactive embed-size audit + report (audit_layers, dry_run) audit_layers() gives a dry pre-flight: per-layer embed_bytes + max_tile_bytes, total assembled-HTML size vs budget, fits bool, and verdict. prepare_layers now returns the same audit dict. plot_interactive always prints a one-line summary before rendering; dry_run=True returns the audit without rendering. Clarifies budget_mb as per-tile cap (tippecanoe), not total archive ceiling. Co-authored-by: Isaac * fix(vizx): audit max_tile_bytes reads real tiles (all_tiles); url verdict _max_tile_bytes was calling pmtiles_info (header-only, no 'tiles' key), so the tile list was always empty and the return was always None. Rewrite to iterate the archive via all_tiles + gzip-decompress, mirroring simplify_tiles_from_archive. Fix url verdict to require ≥1 pmtiles layer with every pmtiles layer url-mode (not all layers). Remove unreachable isinstance(str) branch from test_plot_interactive_dry_run. Add test_audit_max_tile_bytes_for_archive to close the coverage gap. Co-authored-by: Isaac * feat(vizx): export layer/simplify/audit API; [vizx] +tippecanoe +anywidget -folium; drop dead pmtiles html Add vector/raster/grid/pmtiles_layer, simplify_tiles_from_source/archive, and audit_layers to __init__.py exports + __all__; lazy-load simplify helpers (tippecanoe dep); compute real sha384 SRI hashes for maplibre@4.7.1 and pmtiles@3.2.0; remove _build_pmtiles_html + duplicate CDN constants from _pmtiles.py; drop 8 dead builder tests; recompile CI lock. Co-authored-by: Isaac * fix(vizx): SRI-pin the MapLibre CSS link; correct tippecanoe floor Adds _MAPLIBRE_CSS_SRI constant (sha384-...) and updates the CSS <link> with integrity attribute for security. Corrects tippecanoe>=1.36 (nonexistent version) to >=2.45,<3 to match reality on PyPI. Co-authored-by: Isaac * feat(vizx): Phase-1.5 dynamic zoom cut-over (AnyWidget overview+stream) Embeds a min_z..max_z PMTiles overview; JS moveend fires model.send above the seam; Python on_msg calls on_viewport and pushes detail back via a synced Unicode trait. Seam gate and comm pattern from Spike B. Co-authored-by: Isaac * fix(vizx): sync _esm to frontend (plain-str class attr) so dynamic widget renders traitlets.Unicode(_esm_val).tag(sync=False) caused anywidget's __init__ to see has_trait("_esm")==True and skip adding it as sync=True → _esm never reached the JS frontend → blank widget. Plain str class attr fixes this; removes the stale _GbxDynamicWidget._esm overwrite line and 6 DeprecationWarnings. Adds test_esm_is_synced_to_frontend (get_state() check) and strengthens test_custom_on_viewport_called (asserts detail trait is set after handler). Co-authored-by: Isaac * feat(vizx): predictive neighbor-ring tile prefetch for the dynamic viewer Adds _TileCache (LRU, prefetch-guard evict policy) and _PrefetchWorker (generation-token coalescing, daemon thread) so panning to an adjacent tile is served instantly from cache without re-tiling. Co-authored-by: Isaac * fix(vizx): bbox-windowed tiling for per-viewport detail + prefetch; no viewed-demotion Pass viewport bbox through to simplify_tiles_from_source so per-tile detail is actually clipped to the visible region. Add _tile_bbox helper (slippy-map formula) so _tiler_for_prefetch produces distinct archives per neighbor tile. Guard prefetch writes with put_if_absent to prevent a background prefetch from downgrading a tile already served as viewed. Co-authored-by: Isaac * docs(vizx): red-carpet multi-layer + ladder page with executable examples Adds vizx-layers.mdx: a narrative page teaching the >64MB embed-size ladder (URL→embed→simplify→static), multi-layer plot_static/plot_interactive examples, ephemeral-vs-durable simplify story, and scale guidance linking to Helios NB04 sharding and noting the Phase-2 App path. Includes executable doc-tests (5 passing in Docker), a decision-tree SVG/PNG diagram, sidebar entry, and a link from vizx.mdx. Co-authored-by: Isaac * docs(vizx): plan — Task 14b real interactive screenshots (docs/README/notebooks) Capture the MapLibre viewer in action via headless Chrome (chrome-devtools MCP, wait-for-canvas), embed in vizx-layers.mdx + README + a ~50% preview above each notebook's conditional interactive cell. Runs after Tasks 14 + 16. Co-authored-by: Isaac * docs(vizx): correct tippecanoe note; add dynamic viewer + App scale guidance [vizx] installs tippecanoe wheel (manylinux) — clusters/Serverless need no extra step; macOS may need brew. Dynamic cut-over and Databricks App roadmap added to scale guidance. raster_layer input clarified; simplify_from_archive mentioned near ephemeral/durable section; intro/example gap closed. Co-authored-by: Isaac * docs: migrate + showcase the multi-layer viewer in eo-series/xview/h3-rasterize Add INTERACTIVE_PLOTS toggle + plot_interactive([raster_layer, grid_layer/vector_layer]) showcase cells to NB03, NB04, xView, and h3-rasterize. Remove folium prose references from all four docs pages and three READMEs; retire folium comment in config_nb.ipynb. No folium references remain in the target trees. Co-authored-by: Isaac * docs(vizx): polish vizx-layers diagram (full bands, grouped connectors, no dup numbers) Full-height accent stripes on layer cards via SVG clipPath (fix a); full-width top band on Compositor (fix b); removed redundant numeric prefix from ladder-rung label text since the badge already shows it (fix c); replaced individual arrows with dashed group-box + single arrow per group (layer cards->Compositor, rungs 1-3->plot_interactive, rung 4->plot_static) for unambiguous connector clarity. Co-authored-by: Isaac * docs(vizx): center plot_interactive/plot_static boxes on their incoming arrows Each renderer box is now positioned so its vertical mid-line exactly matches the y of its incoming arrow (group mid-y for plot_interactive, rung-4 center-y for plot_static). Co-authored-by: Isaac * docs(vizx): move the embed-size ladder section to just after Layer types Co-authored-by: Isaac * fix(xview): as_gdf takes wkt_col not geom_col in the interactive showcase Co-authored-by: Isaac * docs(helios): real multi-layer overlays in NB02/NB03 + honest prose NB02/NB03 single-archive show_pmtiles calls never overlaid layers the prose claimed; add real plot_interactive([pmtiles_layer, ...]) overlay cells (INTERACTIVE_PLOTS-gated, standalone-guarded) and correct the README/helios.mdx/config_nb prose to match (folium -> MapLibre). Co-authored-by: Isaac * docs(vizx): match rung-4 pill + trim diagram canvas to content Fixed pill badge consistency: all 4 rungs now use a fixed PILL_W (108px) so every pill is identical in width, height, corner-radius, font, and right-alignment within its rung box. Removed bottom/right dead space: CANVAS_W/CANVAS_H are now computed inside render() from the actual content extents + uniform PAD, shrinking the canvas from 1480x720 to 1164x562 (2328x1124 @2x PNG). Co-authored-by: Isaac * docs(vizx): split reference into Overview + PMTiles + Rendering sub-pages Move detailed sections to focused sub-pages; vizx.mdx becomes a navigational overview with capability map and install/import anchors. Co-authored-by: Isaac * docs(vizx): real interactive multi-layer hero screenshot (headless Chrome render) Co-authored-by: Isaac * docs(vizx): embed interactive viewer screenshot in docs, README, notebooks Shows users what plot_interactive produces before they run any code — one hero image in vizx.mdx (intro), vizx-layers.mdx (multi-layer section), helios/README.md (top of page), and a markdown preview cell inserted directly above each conditional INTERACTIVE_PLOTS/show_pmtiles cell in 8 notebooks. Co-authored-by: Isaac * docs(vizx): split Rendering into Vector+Raster; export plot_interactive_dynamic; fix links Split vizx-rendering.mdx into vizx-vector.mdx (Interactive Maps → Static Maps → Adapters, pos 14) and vizx-raster.mdx (Raster rendering → Escape hatches, pos 15); sidebars.js updated accordingly. Add plot_interactive_dynamic to __all__ + lazy __getattr__ branch so bare import doesn't pull anywidget/traitlets; add test coverage. Fix 4 dangling anchor links: vizx#pmtiles-viewers → vizx-pmtiles, vizx#multi-layer-compositor → vizx-vector+vizx-raster, vizx#plot_static → vizx-vector#static-maps. Remove folium==0.20.0 from requirements-dev-container.in (already purged from [vizx] extra); .txt needs recompile in container. Co-authored-by: Isaac * docs(vizx): drop redundant PMTiles Viewers H2; promote fn sections; fix links Co-authored-by: Isaac * docs(vizx): remove the interactive hero screenshot (to be replaced later) Co-authored-by: Isaac * fix(sample): fix OvertureClient for real nested STAC catalog + add CLI path The live Overture STAC catalog is nested (root → release child → theme → type → item links), not flat. The old traverse_catalog only did catalog.get_children() → collection.get_items() and found nothing. resolve_release returned the root catalog id instead of a real release. Fixes: - traverse_catalog: navigate release→theme→type→item links; inject _item_loader seam for offline tests (avoids pystac.Item.from_file on fake:// hrefs in tests). - resolve_release: find the child with extra_fields.latest=True (or fall back to first child); raise only if no children at all. - OvertureClient: add _overturemaps_cli_path / _overturemaps_cli_available helpers + _download_via_cli (subprocess overturemaps download --stac with bbox pushdown, temp-assemble then shutil.copyfile for Volume safety). download() checks CLI before the is_cloud branch when bbox is set. - pyproject.toml: add overturemaps>=0.13 to [overture] extra. - Fake catalog updated to nested structure; tests updated accordingly. Co-authored-by: Isaac * fix(sample): idempotent CLI download + remove dead cli_discover Two fixes to OvertureClient's overturemaps CLI integration: 1. _download_via_cli now skips the subprocess when the target already exists and _is_valid_parquet() returns True, matching the same idempotency contract that _download_fallback already had. A new force=False kwarg on both _download_via_cli and download() lets callers opt out of the skip when they need a fresh fetch. 2. cli_discover in _overture_discover.py used --list-paths, a flag that does not exist in the overturemaps CLI; every call failed (nonzero returncode) and discover() silently fell back to traverse_catalog anyway. Remove cli_discover entirely and call traverse_catalog directly — eliminating one guaranteed-failing subprocess per theme on every discover() call. Tests: two new tests assert call-count behaviour for the idempotent skip and force=True override; cli_discover tests replaced by a traverse_catalog assertion that covers the same discovery contract. Co-authored-by: Isaac * perf(vizx): decode only min-zoom tiles in vector static fallback _static_vector_fallback decoded every tile at every zoom level; for a z12–z16 pyramid that meant ~5× the features and 5+ minutes of matplotlib rendering in a Helios notebook. The static path is a coarse driver-side overview, so decoding only the lowest zoom (min_zoom from the PMTiles header, with a scan-based fallback) is sufficient and cuts decode work by ~N_levels. Matches the existing raster fallback, which already uses _lowest_zoom_tile. Tests spy on _decode_mvt_to_geoms to assert only min-zoom calls occur, including the no-min_zoom-in-info fallback path. Co-authored-by: Isaac * fix(pyrx): make rst_xyzpyramid format/size/resampling optional in UDTF eval The SQL UDTF gbx_rst_xyzpyramid documents format/size/resampling as optional (3-arg call is valid), and the rst_xyzpyramid Python binding already defaults them — but _RstXyzPyramidUDTF.eval required all six arguments, so LATERAL gbx_rst_xyzpyramid(tile, min_z, max_z) failed at runtime with UDTF_EVAL_METHOD_ARGUMENTS_DO_NOT_MATCH_SIGNATURE. Add =None defaults to eval() (body already maps None→PNG/256/bilinear), mirroring _RstH3TessellateUDTF.eval(self, tile, resolution, mode=None). Regression test asserts the 3-arg SQL call succeeds and yields the same tile count as the full 6-arg explicit call. Co-authored-by: Isaac * fix(vizx): gunzip compressed PMTiles tiles before static decode PMTiles stores tiles per the archive's tile_compression; when it is gzip, the reader (a…
2 parents 3a7215e + 35f38c0 commit 7319131

210 files changed

Lines changed: 36933 additions & 1480 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/pyrx_build/action.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,6 @@ runs:
6464
# dirs via test/conftest.py collect_ignore). Every light test dir must be
6565
# listed: pyrx, ds, pyvx, pygx (light GridX), pmtiles_light (light
6666
# pmtiles_agg), stac (light STAC client, [stac] extra),
67-
# vizx (gbx.vizx, [vizx] extra). See test/conftest.py for the maintained condition.
68-
pytest test/pyrx test/ds test/pyvx test/pygx test/pmtiles_light test/stac test/vizx -m "not integration" -v
67+
# vizx (gbx.vizx, [vizx] extra), sample (gbx.sample.overture, [overture] extra).
68+
# See test/conftest.py for the maintained condition.
69+
pytest test/pyrx test/ds test/pyvx test/pygx test/pmtiles_light test/stac test/vizx test/sample -m "not integration" -v

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<img src="resources/images/GeoBriX.png" width="50%" />
1+
<img src="resources/images/brand/GeoBriX.png" width="50%" />
22

33
[![build](https://github.com/databrickslabs/geobrix/actions/workflows/build_main.yml/badge.svg)](https://github.com/databrickslabs/geobrix/actions/workflows/build_main.yml)
44
[![codecov](https://codecov.io/gh/databrickslabs/geobrix/branch/main/graph/badge.svg)](https://codecov.io/gh/databrickslabs/geobrix)
@@ -22,7 +22,7 @@
2222

2323
> **Full docs:** **https://databrickslabs.github.io/geobrix/** — this README is the 2-minute tour.
2424
25-
<img src="resources/images/geobrix_vision.png" width="70%" />
25+
<img src="resources/images/brand/geobrix_vision.png" width="70%" />
2626

2727
## Tiers
2828

@@ -31,7 +31,7 @@
3131

3232
## Packages
3333

34-
<img src="resources/images/RasterX.png" width="18%" /> <img src="resources/images/GridX.png" width="18%" /> <img src="resources/images/VectorX.png" width="18%" />
34+
<img src="resources/images/brand/RasterX.png" width="18%" /> <img src="resources/images/brand/GridX.png" width="18%" /> <img src="resources/images/brand/VectorX.png" width="18%" />
3535

3636
- **[RasterX](https://databrickslabs.github.io/geobrix/docs/api/raster-functions)** — raster I/O and analytics (gap-filling; the platform has no built-in raster). **Both tiers** — lightweight `pyrx` and heavyweight Scala.
3737
- **[GridX](https://databrickslabs.github.io/geobrix/docs/api/gridx-functions)** — BNG, Quadbin, and custom grids (pairs with native H3 for global hex). **Both tiers** — lightweight `pygx` and heavyweight Scala.

docs/docs/api/gridx-functions.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import packagesExamples from '!!raw-loader!../../tests/python/packages/examples.
1010
import gridxScalaCode from '!!raw-loader!../../tests/scala/packages/GridxPackageExamples.scala';
1111
import Tier from '@site/src/components/Tier';
1212
import { Impl } from '@site/src/components/Tier';
13-
import GridXIcon from '../../../resources/images/GridX.png';
13+
import GridXIcon from '../../../resources/images/brand/GridX.png';
1414

1515
# <img src={GridXIcon} alt="GridX" style={{height: '3em', width: 'auto', verticalAlign: 'middle', marginRight: '0.5rem'}} /> Function Reference
1616

@@ -88,7 +88,7 @@ Run this once before the examples below. It registers GridX (BNG) so you can use
8888
<CodeFromTest code={gridxFunctionsExamples} functionName="gridx_setup_example" language="python" source="docs/tests/python/api/gridx_functions.py" testFile="docs/tests/python/api/test_gridx_functions.py" outputConstant="gridx_setup_example_output" />
8989

9090
:::tip Map any GridX cell set with `plot_static`
91-
[`vizx.plot_static`](./vizx#plot_static) renders a column of cell ids straight from a Spark DataFrame as a **static, GitHub-renderable map over a basemap** — and it's the one helper that covers **every** GridX grid: pass `grid_system="quadbin"`, `"bng"`, or `"custom"` (as well as `"h3"`). Each grid's native CRS is handled for you (e.g. BNG's EPSG:27700 is reprojected for the basemap), and `"custom"` takes the same grid spec via `grid_conf=`. It's the quickest way to eyeball the cells the functions below produce. See [`plot_static`](./vizx#plot_static) for the full parameter list.
91+
[`vizx.plot_static`](./vizx-vector#static-maps) renders a column of cell ids straight from a Spark DataFrame as a **static, GitHub-renderable map over a basemap** — and it's the one helper that covers **every** GridX grid: pass `grid_system="quadbin"`, `"bng"`, or `"custom"` (as well as `"h3"`). Each grid's native CRS is handled for you (e.g. BNG's EPSG:27700 is reprojected for the basemap), and `"custom"` takes the same grid spec via `grid_conf=`. It's the quickest way to eyeball the cells the functions below produce. See [`plot_static`](./vizx-vector#static-maps) for the full parameter list.
9292
:::
9393

9494
---

docs/docs/api/h3-raster-tessellation.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,5 @@ GROUP BY cellid;
199199
```
200200

201201
Use Databricks-native `h3_coverash3` and `h3_tessellateaswkb` for the **vector** side of the same grid (polygon covering sets, geometry chips); use GeoBrix `rst_h3_tessellate` for the **raster** side.
202+
203+
See the [Helios notebooks](../notebooks/helios) for a worked example of `gbx_rst_h3_rastertogridavg` binning slope and aspect rasters into H3 cells to produce a per-cell `solar_score` via native Databricks SQL (NB03).

docs/docs/api/overview.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import CodeFromTest from '@site/src/components/CodeFromTest';
66
import overviewExamples from '!!raw-loader!../../tests/python/api/overview.py';
77
import scalaApiExamples from '!!raw-loader!../../tests/scala/api/ScalaApiExamples.scala';
88
import packagesExamples from '!!raw-loader!../../tests/python/packages/examples.py';
9-
import RasterXIcon from '../../../resources/images/RasterX.png';
10-
import GridXIcon from '../../../resources/images/GridX.png';
11-
import VectorXIcon from '../../../resources/images/VectorX.png';
12-
import VizXIcon from '../../../resources/images/VizX.png';
9+
import RasterXIcon from '../../../resources/images/brand/RasterX.png';
10+
import GridXIcon from '../../../resources/images/brand/GridX.png';
11+
import VectorXIcon from '../../../resources/images/brand/VectorX.png';
12+
import VizXIcon from '../../../resources/images/brand/VizX.png';
1313

1414
# Functions Overview
1515

1616
GeoBrix provides four specialized packages for different spatial processing needs. All packages expose Scala, Python, and SQL APIs backed by the same Spark columnar expressions.
1717

18-
![GeoBrix Vision](../../../resources/images/geobrix_vision.png)
18+
![GeoBrix Vision](../../../resources/images/brand/geobrix_vision.png)
1919

2020
## Available Packages
2121

docs/docs/api/pmtiles-functions.mdx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,24 @@ The 4-argument form omits the metadata JSON (defaults to `'{}'`):
179179

180180
<CodeFromTest code={pmtilesSqlCode} functionName="pmtiles_agg_4arg_sql_example" language="sql" source="docs/tests/python/api/pmtiles_functions_sql.py" testFile="docs/tests/python/api/test_pmtiles_functions_sql.py" />
181181

182+
### Duplicate tile coordinates
183+
184+
When multiple rows share the same tile coordinates `(z, x, y)`:
185+
186+
- **Vector (MVT) tiles** — features from all matching rows are combined into one multi-feature tile. Features from different layers are kept in their respective layers; attributes are preserved per feature.
187+
- **Raster tiles (PNG, JPEG, WebP)** — the first non-null tile is used; subsequent tiles for the same coordinates are ignored (raster images cannot be meaningfully combined).
188+
189+
Tile type is detected automatically from the content of the first non-null payload (see [Tile-type detection](#tile-type-detection) above).
190+
182191
### Typical pipelines
183192

184193
- **Raster pyramid:** `gbx_rst_xyzpyramid(tile, minZoom, maxZoom)` produces per-tile rows of PNG bytes — pipe straight into `gbx_pmtiles_agg`.
185-
- **Vector pyramid:** `gbx_st_asmvt_pyramid(geom_wkb, attrs, minZoom, maxZoom, layer)` produces per-tile MVT bytes — pipe straight into `gbx_pmtiles_agg`.
194+
- **Vector pyramid:** `gbx_st_asmvt_pyramid(geom_wkb, attrs, minZoom, maxZoom, layer)` produces per-tile MVT bytes — pipe straight into `gbx_pmtiles_agg`. Because `gbx_st_asmvt_pyramid` emits one row per feature, the aggregate merges all features that share a tile coordinate into a single multi-feature tile.
186195

187196
For pyramids that exceed the Spark cell ceiling, use the [PMTiles Writer](../writers/pmtiles) instead.
188197

198+
See the [Helios notebooks](../notebooks/helios) for a worked end-to-end example: `gbx_pmtiles_agg` packages vector MVT tiles (NB01), a raster XYZ pyramid (NB02), and a hillshade pyramid (NB03) into separate PMTiles archives over a San Francisco AOI.
199+
189200
---
190201

191202
## Serving from object storage
@@ -230,4 +241,5 @@ PMTiles is designed to be served as a single static file via HTTP `Range` reques
230241

231242
- [PMTiles Writer](../writers/pmtiles) — DataSource for streaming large pyramids to disk.
232243
- [Raster Functions](./raster-functions#rst_xyzpyramid) — Generate tile bytes with `gbx_rst_xyzpyramid`.
244+
- [Helios notebooks](../notebooks/helios) — worked end-to-end example using `gbx_pmtiles_agg` for all three data modalities (vector MVT, raster XYZ, hillshade).
233245
- [VectorX Function Reference](./vectorx-functions) — Generate MVT tiles with `gbx_st_asmvt_pyramid`.

docs/docs/api/raster-functions.mdx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import TabItem from '@theme/TabItem';
88
import CodeFromTest from '@site/src/components/CodeFromTest';
99
import Tier from '@site/src/components/Tier';
1010
import { Impl } from '@site/src/components/Tier';
11-
import RasterXIcon from '../../../resources/images/RasterX.png';
11+
import RasterXIcon from '../../../resources/images/brand/RasterX.png';
1212
import rasterxCode from '!!raw-loader!../../tests/python/api/rasterx_functions.py';
1313
import pyrxCode from '!!raw-loader!../../tests/python/api/pyrx_functions.py';
1414
import rasterxSqlCode from '!!raw-loader!../../tests/python/api/rasterx_functions_sql.py';
@@ -43,7 +43,7 @@ RasterX is GeoBrix's raster data processing package, providing comprehensive too
4343

4444
RasterX exposes 87+ SQL functions (registered as `gbx_rst_*`; available in Python and Scala as `rst_*`), organized into the following categories (see [rasterx/functions.scala](https://github.com/databrickslabs/geobrix/blob/main/src/main/scala/com/databricks/labs/gbx/rasterx/functions.scala)):
4545

46-
![RasterX function categories — Constructors, Accessors, Aggregators, Generators, Operations, H3 Grid](../../../resources/images/rasterx-function-categories.png)
46+
![RasterX function categories — Constructors, Accessors, Aggregators, Generators, Operations, H3 Grid](../../../resources/images/diagrams/rasterx/rasterx-function-categories.png)
4747

4848
- **Accessor Functions**: Read raster properties and metadata (bounds, dimensions, CRS, bands, pixel size, georeference, format, type, NoData, subdatasets, summary, etc.)
4949
- **Aggregator Functions**: Combine or merge rasters in group-by (combineavg_agg, derivedband_agg, merge_agg, h3_rasterize_agg)
@@ -1556,7 +1556,7 @@ Powered by **rasterio**.
15561556

15571557
## Web-Mercator Tile Output
15581558

1559-
Reproject rasters to EPSG:3857 (Web Mercator) and emit slippy-map XYZ tiles. Pair with [`gbx_pmtiles_agg`](./pmtiles-functions#pmtiles_agg) or the [PMTiles writer](../writers/pmtiles) to publish a raster pyramid as a single `.pmtiles` archive.
1559+
Reproject rasters to EPSG:3857 (Web Mercator) and emit slippy-map XYZ tiles. Pair with [`gbx_pmtiles_agg`](./pmtiles-functions#pmtiles_agg) or the [PMTiles writer](../writers/pmtiles) to publish a raster pyramid as a single `.pmtiles` archive. See the [Helios notebooks](../notebooks/helios) for a worked example: NAIP aerial scenes are reprojected and pyramided into a PMTiles archive in NB02.
15601560

15611561
### rst_to_webmercator
15621562

@@ -1645,7 +1645,7 @@ FROM <table>, LATERAL gbx_rst_polygonize(tile, band, connectedness) t
16451645

16461646
## Terrain Analysis {#terrain}
16471647

1648-
Thin wrappers around `gdal.DEMProcessing` for digital elevation model (DEM) derivatives. Each function takes a single-band DEM tile and returns a derived tile of the same footprint.
1648+
Thin wrappers around `gdal.DEMProcessing` for digital elevation model (DEM) derivatives. Each function takes a single-band DEM tile and returns a derived tile of the same footprint. See the [Helios notebooks](../notebooks/helios) for a worked example: `gbx_rst_slope`, `gbx_rst_aspect`, and `gbx_rst_hillshade` are applied to 3DEP DEMs in NB03 to produce terrain layers and a per-H3-cell solar score.
16491649

16501650
### rst_slope
16511651

@@ -1913,7 +1913,7 @@ Powered by **NumPy**. This tier keeps each passing pixel's original value and se
19131913

19141914
## Analysis
19151915

1916-
Higher-level analytical transforms wrapping single GDAL primitives — COG layout publishing, proximity surfaces, contour extraction, and viewshed analysis.
1916+
Higher-level analytical transforms wrapping single GDAL primitives — COG layout publishing, proximity surfaces, contour extraction, and viewshed analysis. See the [Helios notebooks](../notebooks/helios) for a worked example of `gbx_rst_cog_convert` converting 3DEP DEMs to COGs and cataloging them in a STAC Delta table (NB03).
19171917

19181918
### rst_cog_convert
19191919

@@ -2049,4 +2049,5 @@ For rendering tiles and building maps from results, see the [Visualization (`gbx
20492049
- [PMTiles Function Reference](./pmtiles-functions) — Aggregator (`gbx_pmtiles_agg`) for publishing tile pyramids
20502050
- [PMTiles Writer](../writers/pmtiles) — DataSource for streaming large pyramids to a single `.pmtiles` file
20512051
- [RasterX Readers](../readers/raster)
2052+
- [Helios notebooks](../notebooks/helios) — worked end-to-end example: Web Mercator reprojection, XYZ pyramid generation, COG conversion, and terrain analytics packaged into PMTiles archives over San Francisco.
20522053

docs/docs/api/stac.mdx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ Where a single-node STAC script serializes search requests and downloads, `StacC
1313
`StacClient` requires `geobrix[light,stac]`. The `[stac]` extra pulls in `pystac-client`, `planetary-computer`, `tenacity`, and `requests`. Serverless environment version 5 (Python 3.12) is required.
1414
:::
1515

16+
## Ready-made downloaders
17+
18+
For common sources you usually don't call `StacClient` directly — the `gbx.sample` package ships AOI-driven downloaders that wrap it (or the same distributed STAC pattern) with source-specific defaults, each following a **discover → download → read** flow:
19+
20+
| Downloader | Source | STAC backing |
21+
|---|---|---|
22+
| [Overture Maps Downloader](../sample-data/overture-downloader)`OvertureClient` | Overture Maps (buildings, places, …) | Overture's static STAC catalog via the `overturemaps` CLI |
23+
| [NAIP Aerial Imagery Downloader](../sample-data/naip-downloader)`NaipDownloader` | NAIP 1 m aerial imagery (US) | `StacClient` on Planetary Computer |
24+
| [3DEP Downloader (DEM)](../sample-data/dem-downloader)`DemDownloader` | USGS 3DEP elevation (US) | `StacClient` on Planetary Computer |
25+
26+
Reach for `StacClient` directly when you need a catalog these don't cover, or full control over the search → download → repair flow described below.
27+
1628
## Installation
1729

1830
```bash
@@ -161,7 +173,7 @@ repaired = client.repair(
161173

162174
## End-to-end example
163175

164-
This illustrates the full search → download → repair flow. The EO-series notebooks are the fully-executed, worked example — see [EO Series](../notebooks/eo-series).
176+
This illustrates the full search → download → repair flow. For fully-executed, worked examples see [EO Series](../notebooks/eo-series) (Sentinel-2 / Alaska) or [Helios](../notebooks/helios) (3DEP COGs over San Francisco, NB03).
165177

166178
```python
167179
from databricks.labs.gbx.stac import StacClient
@@ -230,7 +242,7 @@ files_df.write.mode("overwrite").saveAsTable("band_b02")
230242
```
231243

232244
:::note No doc-test backing for this page
233-
`StacClient` is a network integration client — it requires live STAC catalog access and real asset URLs. The illustrative code blocks above show the API surface; the [EO Series notebooks](../notebooks/eo-series) are the fully-executed, end-to-end example with real data.
245+
`StacClient` is a network integration client — it requires live STAC catalog access and real asset URLs. The illustrative code blocks above show the API surface; the [EO Series notebooks](../notebooks/eo-series) and the [Helios notebooks](../notebooks/helios) are fully-executed, end-to-end examples with real data.
234246
:::
235247

236248
---
@@ -249,5 +261,6 @@ The following are explicitly out of scope for the initial `StacClient`:
249261
## See also
250262

251263
- [EO Series notebooks](../notebooks/eo-series) — the worked end-to-end example (search → download → repair → tessellate → stack).
264+
- [Helios notebooks](../notebooks/helios)`StacClient` used to discover and download 3DEP DEMs from Planetary Computer, converted to COGs and cataloged in a STAC Delta table (NB03).
252265
- [Execution Tiers](./execution-tiers) — lightweight vs heavyweight comparison.
253266
- [RasterX Function Reference](./raster-functions)`rst_h3_tessellate`, `rst_fromcontent`, `rst_merge_agg`, and the rest of the raster processing functions used downstream of STAC downloads.

docs/docs/api/tile-structure.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ In GeoBrix, a **tile** is not a simple binary column—it's a **structured type*
1818

1919
A tile has the following structure:
2020

21-
![GeoBrix tile schema — cellid (bigint, nullable), raster (binary, required), metadata (map of string to string), with non-tessellated vs tessellated examples](../../../resources/images/rasterx-tile-structure.png)
21+
![GeoBrix tile schema — cellid (bigint, nullable), raster (binary, required), metadata (map of string to string), with non-tessellated vs tessellated examples](../../../resources/images/diagrams/rasterx/rasterx-tile-structure.png)
2222

2323
```
2424
struct<

docs/docs/api/vectorx-functions.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import vectorxSqlCode from '!!raw-loader!../../tests/python/api/vectorx_function
1111
import quickstartCode from '!!raw-loader!../../tests/python/quickstart/examples.py';
1212
import Tier from '@site/src/components/Tier';
1313
import { Impl } from '@site/src/components/Tier';
14-
import VectorXIcon from '../../../resources/images/VectorX.png';
14+
import VectorXIcon from '../../../resources/images/brand/VectorX.png';
1515

1616
# <img src={VectorXIcon} alt="VectorX" style={{height: '3em', width: 'auto', verticalAlign: 'middle', marginRight: '0.5rem'}} /> Function Reference
1717

@@ -487,5 +487,6 @@ Run this once before the examples below. It registers VectorX so you can use `st
487487
- [Choosing an Execution Tier](./execution-tiers) — lightweight vs heavyweight comparison
488488
- [PMTiles Function Reference](./pmtiles-functions) — Aggregate MVT tiles into a single `.pmtiles` archive
489489
- [PMTiles Writer](../writers/pmtiles) — DataSource for streaming large pyramids to a single `.pmtiles` file
490+
- [Helios notebooks](../notebooks/helios) — worked end-to-end example: `gbx_st_asmvt` + `gbx_st_asmvt_pyramid` encode San Francisco building footprints into a PMTiles archive (NB01).
490491
- [Benchmarking](./benchmarking) — light-vs-heavy timing methodology
491492
- [API Overview](./overview) — All GeoBrix APIs

0 commit comments

Comments
 (0)