Commit 7319131
authored
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…210 files changed
Lines changed: 36933 additions & 1480 deletions
File tree
- .github/actions/pyrx_build
- docs
- docs
- api
- notebooks
- sample-data
- writers
- scripts
- src/pages
- static/img
- superpowers
- performance
- plans
- specs
- tests/python/api
- notebooks
- examples
- eo-series
- h3-rasterize
- helios
- xview
- tests
- python/geobrix
- src/databricks/labs/gbx
- ds
- pmtiles
- pyrx
- core
- rasterx
- sample
- stac
- vizx
- test
- ds
- pmtiles_light
- pyrx
- sample
- stac
- vizx
- resources/images
- brand
- diagrams
- eo-series
- h3-rasterize
- helios
- rasterx
- vizx
- xview
- generators
- screenshots
- scripts/commands
- src
- main
- resources/com/databricks/labs/gbx
- scala/com/databricks/labs/gbx
- pmtiles
- rasterx
- expressions/web
- operator
- vectorx/mvt
- test/scala/com/databricks/labs/gbx
- pmtiles
- rasterx/expressions/web
- vectorx/mvt
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
| 25 | + | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
91 | | - | |
| 91 | + | |
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| 202 | + | |
| 203 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
182 | 191 | | |
183 | 192 | | |
184 | 193 | | |
185 | | - | |
| 194 | + | |
186 | 195 | | |
187 | 196 | | |
188 | 197 | | |
| 198 | + | |
| 199 | + | |
189 | 200 | | |
190 | 201 | | |
191 | 202 | | |
| |||
230 | 241 | | |
231 | 242 | | |
232 | 243 | | |
| 244 | + | |
233 | 245 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
1556 | 1556 | | |
1557 | 1557 | | |
1558 | 1558 | | |
1559 | | - | |
| 1559 | + | |
1560 | 1560 | | |
1561 | 1561 | | |
1562 | 1562 | | |
| |||
1645 | 1645 | | |
1646 | 1646 | | |
1647 | 1647 | | |
1648 | | - | |
| 1648 | + | |
1649 | 1649 | | |
1650 | 1650 | | |
1651 | 1651 | | |
| |||
1913 | 1913 | | |
1914 | 1914 | | |
1915 | 1915 | | |
1916 | | - | |
| 1916 | + | |
1917 | 1917 | | |
1918 | 1918 | | |
1919 | 1919 | | |
| |||
2049 | 2049 | | |
2050 | 2050 | | |
2051 | 2051 | | |
| 2052 | + | |
2052 | 2053 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
16 | 28 | | |
17 | 29 | | |
18 | 30 | | |
| |||
161 | 173 | | |
162 | 174 | | |
163 | 175 | | |
164 | | - | |
| 176 | + | |
165 | 177 | | |
166 | 178 | | |
167 | 179 | | |
| |||
230 | 242 | | |
231 | 243 | | |
232 | 244 | | |
233 | | - | |
| 245 | + | |
234 | 246 | | |
235 | 247 | | |
236 | 248 | | |
| |||
249 | 261 | | |
250 | 262 | | |
251 | 263 | | |
| 264 | + | |
252 | 265 | | |
253 | 266 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
487 | 487 | | |
488 | 488 | | |
489 | 489 | | |
| 490 | + | |
490 | 491 | | |
491 | 492 | | |
0 commit comments