Skip to content

Sidecar API

The Workbench sidecar (src/lunartrace/workbench/server.py) is a Starlette application served by Uvicorn on 127.0.0.1:4318 (--port). The Vite development server proxies /py/* to it and removes the /py prefix, so the paths below are relative to the sidecar root.

The sidecar computes every number it returns through the qualified Python method lunartrace.slope_quality/v1 (METHOD_ID, implementation primary-gdal). The browser computes nothing.

Origin and Host guard. Every route checks the Origin and Host headers. A request without Origin is accepted. An Origin must parse as an http or https origin whose host name is 127.0.0.1, localhost or lunartrace.localhost, on any port, with no user info or path. Look-alike names such as localhost.evil.example are refused. Any other origin receives 403 {"error": "non-local origin"}. The Host header must name the same local hosts; a page that re-binds its own DNS name to 127.0.0.1 therefore receives 403 {"error": "non-local host"}.

Size limits. A request body over 64 KiB (MAX_BODY_BYTES) returns 413. At most 64 thresholds (MAX_TAUS) are accepted per request.

Errors. Failures that are input or method errors return HTTP 422 with {"error": "<message>"}. The 422 class covers RoiError (and its subclass CellNotFound, which is caught earlier on /cell), MethodInputError, KeyError, and ValueError (including JSON decoding errors). Other exceptions are not mapped: they surface as HTTP 500 from Starlette. A failure to start the Lab returns 503 (see POST /lab/open).

Bodies. POST routes accept a JSON object. A body that is not valid JSON, or is not a JSON object, returns 422.

Shared request fields (used by POST /roi/evaluate, POST /figure and POST /lab/open):

FieldTypeRequiredLimitsDefault
ringlist of [lon, lat]yes3 to 512 vertices; each pair has exactly two finite JSON numbers (not booleans or strings); longitude in [−180, 180]; latitude in [−90, 90]; closure implicitnone
tauslist of numbersno1 to 64 entries; each a finite JSON number ≥ 0; list given in order[0.5, 2.0, 25.0]

The ring must project to finite grid coordinates and a valid polygon with positive area, and at least one grid cell centre must lie inside it. Invalid geometry returns 422.

Liveness check.

  • 200 {"ok": true, "method": "lunartrace.slope_quality/v1"}

The loaded Site: provenance and reference geometry.

  • 200
    • provenance (object):
      • source_witness (string): manifest identifier, or "?" if absent;
      • manifest_sha256 (string);
      • inputs (object): keys slope and slope_rms, each {name, sha256};
      • grid (object): shape as [rows, cols], and cell_size_m (number).
    • default_taus (list of numbers): [0.5, 2.0, 25.0].
    • reference_rois (list): each {roi, ring} where roi is the string index ("0", "1", …) and ring is a list of [lon, lat] pairs, with the closing vertex included.
  • 422 or 500 if the Site cannot be loaded (see Errors).

The Site is loaded once per process and cached.

Resolve a geographic pick to its containing grid cell.

Query parameters:

NameTypeRequiredNotes
lonfloatyesAny finite value; projected by Python
latfloatyesFinite, in [−90, 90]
taufloat, repeatablenoEach finite and ≥ 0; absent means the default τ values
ringJSON text: list of [lon, lat]noSame limits as the shared ring. When present, the pick must lie inside it
  • 200
    • row (int), col (int): the containing pixel, from floor of the inverse affine transform;
    • lon (float), lat (float): the cell-centre coordinates;
    • slope (float or null): slope in degrees; null when not finite;
    • err (float or null): slope RMS; null when not finite;
    • included (object): keyed by the τ label formatted with %g (for example "0.5", "2", "25"), each value a boolean for that cell’s inclusion under slope_quality/v1.
  • 404 {"error": "..."} when the pick is outside the Site grid ("location is outside the Site grid"), or outside the supplied ring ("location is outside the selected ROI"). The grid test is applied first.
  • 422 when lon or lat is missing or not a number, when lat is outside [−90, 90] or the coordinates are not finite, when ring is not valid JSON or fails the ring checks, or when a tau is invalid.

Pixel footprints are half-open: top and left edges are included, bottom and right edges excluded. See ROI and brushing.

Evaluate slope_quality/v1 rows over the mask of a ring.

Body: ring (required), taus (optional).

  • 200

    • roi (object):
      • id (string): source:<index> when the ring matches a served source ring, otherwise user:<first 12 hex of ring_sha256>;
      • kind (string): source-ring or user-drawn;
      • source_index (int): present only for source-ring;
      • ring_sha256 (string);
      • ring (list of [lon, lat]): the ring as received;
      • mask_rule (string): cell-centre-inside (user polygon; not the v1 lattice closed rule);
      • cells (int): cells in the mask.
    • method (string): lunartrace.slope_quality/v1;
    • implementation (string): primary-gdal;
    • status (string): METHOD_EXECUTED;
    • admission (string): NOT_ADMITTED;
    • taus (list of float): the validated τ values in the order given;
    • rows (list): one object per τ, with the fields below;
    • provenance: as in GET /site;
    • samples (object): see below.

    Row fields:

    FieldTypeMeaning
    roistringThe ROI label, as in roi.id
    taufloatCutoff
    roi_cellsintCells in the mask
    valid_slope_cellsintCells with slope in [0, 90]
    valid_err_cellsintCells with slope RMS finite and > 0
    joint_cellsintCells valid on both
    included_cellsintJoint cells with slope RMS ≤ τ
    included_fractionfloat or nullincluded_cells / joint_cells; null when joint_cells is 0
    slope_quantilesobject or nullKeys "0.05", "0.25", "0.5", "0.75", "0.95"; inverted-CDF quantiles of included slopes; null when none are included
    slope_meanfloat or nullMean slope of included cells; null when none are included
    slope_histogramlist of 45 intIncluded-cell counts in 2-degree bins over [0, 90]

    samples object:

    FieldTypeMeaning
    cells_in_roiintCells in the mask before subsampling
    strideintceil(cells_in_roi / 50000), minimum 1
    countintSampled cells returned
    columnsobjectColumn arrays, one entry per sampled cell

    columns keys: row, col (int); lon_deg, lat_deg (float, rounded to 7 decimals); slope_deg, slope_rms_deg (float or null); jointly_valid (bool); and one included_tau_<tau> boolean array per τ, with τ formatted %g.

  • 422 when the body is invalid, the ring fails the ring checks, the ring self-intersects and cannot be repaired to a polygon, the ring has zero area, no grid cell centre lies inside it ("ROI contains no grid cell centre (outside the Site grid or too small)"), or a τ is invalid.

Render a deterministic figure for a ring.

Body: ring (required), taus (optional), kind (optional), format (optional), tau (optional).

FieldTypeValuesDefault
kindstringhistogram, inclusion, maphistogram
formatstringsvg, png, pdfsvg
taunumberUsed only for map; any number is accepted and used directlyfirst τ

Non-numeric tau values are ignored, and the first τ is used.

  • 200 the figure bytes. The Content-Type is image/svg+xml, image/png or application/pdf. The content-disposition header is inline; filename="lunartrace-<roi id with ':' replaced by '-'>-<kind>.<format>".
  • 422 {"error": "kind must be one of ('histogram', 'inclusion', 'map'), format one of svg/png/pdf"} for an unknown kind or format; and the ring and τ errors listed under POST /roi/evaluate.

Figure content is described in Export.

The setup block and presets used by the Python instrument and the Lab.

  • 200
    • setup (string): the setup block with this sidecar’s manifest and store paths bound;
    • presets (list): each {id, title, summary, code}.

Preset ids: roi-rows, tau-sweep, slope-vs-rms, compare-reference, steep-share, figure.

Create (if needed) and register an editable marimo notebook for a ring, and return its URL.

Body: ring and taus as above, plus:

FieldTypeDefaultNotes
presetslist of string["roi-rows"]Each must be a known preset id, otherwise 422
  • 200 {"url": "<marimo URL>", "file": "roi-<16 hex>.py", "created": <bool>}
    • url has the form http://127.0.0.1:4319/?file=<file>&access_token=<token>. The token is generated per sidecar process.
    • created is true when this request wrote the notebook and false when an identical one existed.
  • 422 for an invalid ring, τ or preset id.
  • 503 if the Lab cannot start: port 4319 is busy, marimo exits during start, or it does not open its port within 30 seconds.

The notebook file name is the first 16 hex digits of the SHA-256 of the whole generated notebook source (ring, τ values, presets and input paths). A changed request therefore gets a new notebook, and an existing file with the same name has the same source, so it is reused without being rewritten.

LimitValueSource
Ring vertices3 to 512MAX_RING_VERTICES
Sampled cells per evaluationat most 50,000 (stride subsample)SAMPLE_LIMIT
Default τ values0.5, 2.0, 25.0DEFAULT_TAUS
Histogram bins45 bins of 2 degrees over [0, 90]HISTOGRAM_EDGES
Lab port4319--lab-port
Sidecar port4318--port
python -m lunartrace.workbench --check # load the Site once, print provenance JSON, exit
python -m lunartrace.workbench --port 4318 # serve (127.0.0.1 only)

Options: --manifest (default research/sources/pgda78-site01.manifest.json), --store (no default; falls back to LUNARTRACE_SOURCE_STORE), --lab-dir (default build/lab), --port, --lab-port.