Skip to content

feat: tile building footprints once per image layer - #183

Open
Caleb Robinson (calebrob6) wants to merge 2 commits into
mainfrom
calebrob6/feat/layer-footprint-tiles
Open

feat: tile building footprints once per image layer#183
Caleb Robinson (calebrob6) wants to merge 2 commits into
mainfrom
calebrob6/feat/layer-footprint-tiles

Conversation

@calebrob6

Copy link
Copy Markdown
Member

Building footprints get tiled twice for a building-workflow layer, and not at all for a standard one. This tiles them once, when the layer's footprints are cached, and has the embedding job reuse that archive.

What changed

Imagery prep queues a tiling job as soon as a layer's building footprints are cached, for both workflowType values. The archive lands on ImageLayer.footprintPmtilesUrl. The standard labeling workflow does not read it yet — this only produces the artifact.

The embedding job stops tiling. tippecanoe and subprocess drop out of embed_buildings.py entirely; it emits the embeddings GeoJSON, the HFTR feature sidecar and a manifest. The feature sidecar is genuinely per model and is unchanged.

Why the two archives were the same file

Both tippecanoe invocations already used the same layer name (buildings), the same --use-attribute-for-id=id, the same -y id -y overture_id and the same zoom range. embed_buildings reads the layer footprints, does reset_index(drop=True) and keeps every row — including buildings outside the raster, which stay as NaN features — so id = 0..N-1 means the same thing in both. That is the invariant the positional prediction join already depends on. A second embedding model on the same layer produced a second identical archive.

Where the tiling runs

tippecanoe ships only in the training image, via conda-forge. I checked the imageryprep base image (mcr.microsoft.com/azure-functions/python:4-nightly-python3.11-slim) and tippecanoe is not in its apt repos, so tiling inline in the existing imagery-prep job would have meant building it from source in a second image. Instead the work runs as a queued task in the training container through the existing UnifiedRunner.

The queue is footprint-tiles-queue, named for the work rather than any one consumer, since it tiles layers for every workflow.

Enqueuing is best-effort: the tiles are derived data, so an unreachable queue leaves the imagery perfectly usable and the job can be re-requested later without re-running imagery prep.

Knock-on simplifications

With one archive, Model.pmtilesUrl and ArtifactTypes.BUILDING_PMTILES retire, and GetModelArtifact serves footprint_pmtiles as a layer-scoped kind instead of a per-model one. The Interactive Labeler reads that.

modelId is now optional on GetModelArtifact when the kind is layer-scoped and an explicit imageLayerId is given. A standard-workflow layer can have no models at all, which would otherwise have made its own archive unfetchable. Every model-scoped kind still requires it.

The labeler used to refuse to start when a model had no archive. That guard goes with the field, so a failed tile load now raises a clear error rather than warning to the console and leaving an empty map with no buildings to label.

Verification

Check Result
hatch run test:pytest 414 passed, 47 failed
UI tests (node --test) 94 passed
vite build exit 0
ESLint 240 problems, identical to main in the same container
black / isort / flake8 clean

All 47 Python failures also fail on main; the sets were diffed rather than compared by count. Forty-six are the hastegeo[planetary-computer] extra missing from the local conda environment, which CI installs, and the last is the pre-existing test_artifacts.py::test_zip. 22 of the 414 passing tests are new.

Exercised on the local docker stack, tiling two real layers end to end through queue, trigger, processor, runner and tippecanoe:

  • standard-workflow layer: 1496 buildings, Processed in about 40 seconds
  • building-workflow layer: 2798 buildings, same
  • the archive is reachable by imageLayerId alone for both layers, and by modelId for a model on one of them, returning the identical bytes
  • the retired kind=pmtiles now 400s, and the per-model kind=sidecar still returns its 1.4 MB payload
  • inside the rebuilt training image, embed_buildings no longer references write_pmtiles, tippecanoe or subprocess, and the embedding job config no longer names a pmtiles output

The first live run failed usefully: the training image predated the new workflow module, so the task died with ModuleNotFoundError. That is worth knowing for deployment — the training image has to ship with this change, since it carries both the workflow and the new prepare-footprint-tiles shim.

Known limitations

Existing layers have no archive, and existing embedding models point at a per-model one that nothing writes any more. Re-tiling them is out of scope by agreement. The queued job means it can be done later by enqueuing one message per layer, without re-running imagery prep.

A layer whose footprints never cached still cannot be tiled. Imagery prep already flips those layers to FAILED, so the enqueue is skipped.

Bicep app-setting parity is skipped for the new queue, matching the seven existing ones. Config resolves the default and the Functions host creates the queue on first use; editing functions.bicep without regenerating infra/main.json causes drift.

No browser tests. This repo has no Playwright config, so the labeler change was verified through the API, the rendered bundle and lint rather than by loading the page.

Footprint geometry belongs to the image layer: every model trained on a
layer draws the same buildings. The embedding job tiled them anyway, once
per model, and standard layers got no tiles at all.

Imagery prep now queues a tiling job as soon as a layer's footprints are
cached, for both workflow types, and the archive lands on
ImageLayer.footprintPmtilesUrl. The standard labeling workflow does not
read it yet; this only produces the artifact.

The embedding job stops tiling. tippecanoe and subprocess drop out of
embed_buildings entirely and it emits the embeddings GeoJSON, the HFTR
feature sidecar and a manifest. The feature sidecar is genuinely per
model and is unchanged.

The two archives were the same file. Both ran tippecanoe with the same
layer name, the same --use-attribute-for-id=id, the same -y id -y
overture_id and the same zoom range, over the same rows in the same order
(embed_buildings reads the layer footprints, reset_index, and keeps every
row including the ones with no valid tokens, which is what the positional
prediction join already depends on). So an embedding layer was paying for
a duplicate.

tippecanoe ships only in the training image, and it is not in the apt
repos of the imageryprep base image, so tiling runs as a queued task in
the training container through the existing UnifiedRunner rather than
inline in imagery prep. The queue is named footprint-tiles-queue rather
than for any one consumer, since it tiles layers for every workflow.

With one archive, Model.pmtilesUrl and ArtifactTypes.BUILDING_PMTILES
retire, GetModelArtifact serves footprint_pmtiles as a layer-scoped kind
instead of a per-model one, and the Interactive Labeler reads that.

Existing layers have no archive and existing embedding models point at a
per-model one that nothing writes any more. Re-tiling them is out of
scope; the queued job means it can be done later by enqueuing one message
per layer, without re-running imagery prep.

The labeler used to refuse to start when a model had no archive. That
guard goes with the field, so a failed tile load now raises a clear error
rather than warning to the console and leaving an empty map with no
buildings to label.
GetModelArtifact required modelId for every kind, including the
layer-scoped footprint tiles. A standard-workflow layer can have no
models at all, which made its own archive unfetchable.

modelId is now optional when the kind is layer-scoped and an explicit
imageLayerId is given. Every model-scoped kind still requires it, and a
request carrying a modelId still resolves through that model's layer, so
the Interactive Labeler is unaffected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Moves building-footprint PMTiles generation from per-model embedding to a shared, image-layer-scoped queued workflow.

Changes:

  • Adds footprint tiling workflow, processor, queue trigger, state, and configuration.
  • Removes PMTiles generation and ownership from embedding models.
  • Updates artifact retrieval and the Interactive Labeler to use layer-scoped tiles.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ui/src/Components/InteractiveLabeler/InteractiveLabeler.jsx Loads shared layer footprint tiles.
local.settings.example.jsonc Documents the new queue setting.
hastelib/tests/workflows/test_prepare_footprint_tiles.py Tests tiling conversion and naming.
hastelib/tests/core/processors/test_imagery_footprint_tiles.py Tests imagery-triggered queueing.
hastelib/tests/core/processors/test_footprint_tiles.py Tests preparation and queue decisions.
hastelib/src/hastegeo/workflows/prepare_footprint_tiles.py Implements PMTiles generation.
hastelib/src/hastegeo/workflows/embed_buildings.py Removes per-model tiling.
hastelib/src/hastegeo/core/processors/imagery.py Queues tiling after imagery preparation.
hastelib/src/hastegeo/core/processors/footprint_tiles.py Adds the tiling state machine.
hastelib/src/hastegeo/core/processors/embedding.py Removes PMTiles embedding outputs.
hastelib/src/hastegeo/core/models/projects.py Moves tile state onto ImageLayer.
hastelib/src/hastegeo/core/config.py Adds artifact, queue, and metadata types.
docker/training/Dockerfile Adds the tiling CLI shim.
docker/docker-compose.yml Configures the local queue.
docker/data-init/upload_data.py Creates the local queue.
api/hastefuncqueues/function_app.py Adds the queue trigger.
api/hastefuncapi/function_app.py Serves layer-scoped footprint tiles.
Suppressed comments (2)

hastelib/src/hastegeo/core/processors/footprint_tiles.py:451

  • This reads the manifest only from the Batch node. Autoscale nodes can be deallocated as soon as the task completes, and AzureBatchRunner.get_filecontent_from_task() then returns no content; unlike ImageryPostProcessor._read_task_output(), there is no fallback to the copy uploaded under the task output prefix, so a successful tiling task is marked failed. Read the uploaded blob copy when the node-local read misses.
        content = self.runner.get_filecontent_from_task(
            job_id=job.jobId,
            task_id=job.taskId,
            filename=MANIFEST_FILENAME,
        )

api/hastefuncqueues/function_app.py:695

  • Swallowing an unexpected exception makes Azure Functions acknowledge and delete the queue message. Transient metadata/storage failures therefore strand the layer without a retry; log the failure and re-raise so the queue runtime can retry and eventually poison the message.
    except Exception as e:
        logger.error(
            "GetPrepareFootprintTilesQueueTrigger: Error processing queue "
            f"message: {e}\n{traceback.format_exc()}",
            stack_info=True,

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +83 to +88
return {
"projectId": project_id,
"imageLayerId": image_layer_id,
"sourceFootprintsUrl": source_footprints_url or "",
"force": bool(force),
}
Comment on lines +608 to +613
enqueue_footprint_tiles(
project_id=self.image_data.projectId,
image_layer_id=self.image_data.imageLayerId,
source_footprints_url=self.image_data.buildingFootprintsUrl,
config=self.config,
)
Comment on lines +669 to +670
output = await asyncio.to_thread(
FootprintTilesPreprocessor(image_layer).process
Comment on lines +837 to +842
console.error("Failed to load the footprint PMTiles archive:", e);
throw new Error(
"The building footprint tiles for this image layer are not ready " +
"yet. They are built once per layer, shortly after the layer " +
"finishes processing. Try again in a few minutes."
);
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.39rc13
  • training_image_tag: 1.0.39rc13
  • imageprep_image_tag: 1.0.39rc13
  • wheel: hastegeo-1.0.39rc13-py3-none-any.whl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants