This page as Markdown, byte for byte: /analyse.md
Analyse an object: the pinned record, at your own exposure#
POST /objects/{id}/analyse runs named analyses over a canonical risk object and records what they found on that object's provenance. It is a job: the call returns 202 with a job id, you poll GET /jobs/{id}, and the finished job carries the artifacts.
Two analyses today:
analyses name |
What it does |
|---|---|
point_hazard |
Looks the pinned record up at each of the object's own coordinates — the same document [GET /hazard](hazard.md) serves, once per location. |
event_intersection |
Selects the catalogue events that fall in the box the exposure spans — the same document [GET /events/{peril}](events.md) serves, filtered by the same selector a backtest filters with. |
Nothing here is new arithmetic. Both analyses are the routes above aimed at an object instead of at coordinates you typed, which is the point: the numbers on an artifact and the numbers you get by calling those routes yourself are the same numbers, read from the same pinned bytes.
Read the quickstart first for the two values you export (CEDE_BASE_URL, CEDE_API_KEY) and how to get a key from POST /signup.
What it is for#
A price is a number computed from a record on a particular day. Six months later, the question a reader has is not "what is the price" but "what did the data say when this was priced" — and by then the catalogue has a new vintage and the answer is unrecoverable unless somebody wrote it down. This verb writes it down, on the object, in provenance.analysis_runs:
- the run id, and a link to the job holding the full artifacts;
- per artifact, a one-line summary, the assumptions behind it, and the data vintages it was computed from — feed id, version, vintage, and the SHA-256 of the exact bytes.
Provenance is append-only. A second run adds a second entry; nothing already recorded is rewritten, including a run that said something different about the same coordinates.
What the object needs#
| The object must carry | Because |
|---|---|
peril.code |
Every analysis here is a reading of one peril. PATCH /objects/{id} writes it. |
exposure.locations[] with latitude and longitude |
Every analysis here is a reading at a place. Ingest never geocodes an address, so a schedule of addresses reaches this route with nowhere to look. |
| at most 25 located locations | A run embeds a full hazard document per location. Above the cap the run is refused, never trimmed: an artifact that quietly described part of an exposure would be read as describing all of it. |
Refusals are structured and name what would have worked: unknown_analysis (422) lists every analysis that can be asked for, unanalysable_object (422) names the block to write and the route that writes it. Both are in the error reference.
Walk it#
The block below stores a two-location Kanto schedule, runs both analyses over it, reads the artifacts out of the finished job, reads the provenance back off the object, and then asks for an analysis that does not exist so you can see the refusal.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first — see the quickstart}"
# 1 — An object to analyse: two located sites, one peril.
cat > schedule.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "location_schedule",
"currency": "JPY",
"locations": [
{
"ref": "LOC-0001",
"name": "Shinagawa distribution centre",
"latitude": 35.6284,
"longitude": 139.7387,
"geocode": {"resolution": "rooftop", "confidence": 0.96},
"values": {"building": {"amount": 8400000000, "currency": "JPY"}}
},
{
"ref": "LOC-0002",
"name": "Chiba assembly plant",
"latitude": 35.5687,
"longitude": 140.1247,
"geocode": {"resolution": "street", "confidence": 0.83},
"values": {"building": {"amount": 5100000000, "currency": "JPY"}}
}
],
"source_fidelity": {
"unmapped_columns": [],
"guessed_units": [],
"ambiguous_rows": []
}
},
"peril": {"code": "earthquake"}
}
JSON
curl -sS "$CEDE_BASE_URL/objects" \
-H "Authorization: Bearer $CEDE_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @schedule.json > object.json
OBJECT_ID="$(python3 -c 'import json; print(json.load(open("object.json"))["id"])')"
echo "object: $OBJECT_ID"
# 2 — Run both analyses. A job comes back.
curl -sS "$CEDE_BASE_URL/objects/$OBJECT_ID/analyse" \
-H "Authorization: Bearer $CEDE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"analyses": ["point_hazard", "event_intersection"]}' > accepted.json
JOB_ID="$(python3 -c 'import json; print(json.load(open("accepted.json"))["id"])')"
echo "job: $JOB_ID"
# 3 — Poll it to a terminal state.
for _ in $(seq 1 60); do
curl -sS "$CEDE_BASE_URL/jobs/$JOB_ID" \
-H "Authorization: Bearer $CEDE_API_KEY" > job.json
STATUS="$(python3 -c 'import json; print(json.load(open("job.json"))["status"])')"
[ "$STATUS" = "queued" ] || [ "$STATUS" = "running" ] || break
sleep 1
done
echo "status: $STATUS"
test "$STATUS" = "succeeded"
# 4 — The artifacts, each with its assumptions and the vintages behind it.
python3 -c 'import json
run = json.load(open("job.json"))["result"]["analysis"]
print("run: ", run["run_id"], "at", run["completed_at"])
print("analyses: ", ", ".join(run["analyses"]))
for artifact in run["artifacts"]:
print()
print("==", artifact["analysis"])
print(" ", artifact["summary"])
for vintage in artifact["data_vintages"]:
print(" read:", vintage["id"], vintage["version"],
"vintage", vintage["vintage"], vintage["sha256"][:12] + "…")
if artifact["analysis"] == "point_hazard":
for reading in artifact["locations"]:
hazard = reading["hazard"]
print(" %-22s %s,%s -> %s over %s years"
% (reading.get("ref", reading["location_index"]),
reading["point"]["latitude"], reading["point"]["longitude"],
hazard["metrics"]["basis"],
hazard["assumptions"]["period_of_record"]["years"]))
for missed in artifact["not_analysed"]:
print(" not read:", missed["location_index"], missed["reason"])
else:
box = artifact["box"]
print(" box: %s-%sN %s-%sE (exposure extent + %s deg)"
% (box["min_latitude"], box["max_latitude"],
box["min_longitude"], box["max_longitude"],
box["cell_half_width_degrees"]))
print(" events:", artifact["counts"]["intersecting"], "of",
artifact["counts"]["events_in_snapshot"], "in the snapshot")
for event in artifact["catalogue"]["events"][:5]:
print(" ", event["time"], "M" + str(event["magnitude"]),
event["event_id"])
print()
print(run["disclosure"])'
# 5 — The record on the object itself. This is what a reader finds months later.
curl -sS "$CEDE_BASE_URL/objects/$OBJECT_ID" \
-H "Authorization: Bearer $CEDE_API_KEY" > analysed.json
python3 -c 'import json
document = json.load(open("analysed.json"))
print("status:", document["status"])
for entry in document["provenance"]["analysis_runs"]:
print("run", entry["run_id"], "->", entry["links"]["job"])
for artifact in entry["artifacts"]:
print(" ", artifact["analysis"] + ":", artifact["summary"])
print(" method:", artifact["assumptions"]["method"][:90] + "…")
for vintage in artifact["data_vintages"]:
print(" vintage:", vintage["id"], vintage["version"],
vintage["sha256"][:12] + "…")'
# 6 — An analysis that does not exist. The refusal names the whole valid set,
# and this block demands the status rather than just printing it: if the
# route ever answered something else, this example would stop exiting 0.
status=$(curl -sS -o refused.json -w '%{http_code}' \
"$CEDE_BASE_URL/objects/$OBJECT_ID/analyse" \
-H "Authorization: Bearer $CEDE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"analyses": ["ground_motion"]}')
python3 -c 'import json
error = json.load(open("refused.json"))["error"]
print(error["status"], error["code"])
print(error["message"])'
test "$status" = 422
Reading the artifacts#
**point_hazard** holds one entry per location that could be read, each carrying the full hazard document for that coordinate — its metrics, its method, its period of record and the snapshot it came from. Locations that carried coordinates no pinned snapshot covers are listed under not_analysed with the edge they fell outside, rather than dropped: an exposure half of which was read must never read as an exposure that was read.
**event_intersection** holds the box (the exposure's own extent, grown by the half-degree cell a point lookup counts in), the counts, and a page of the catalogue events inside it. The box is not a structure's trigger box: a cat-in-a-box trigger carries its own region on trigger.index, and a backtest of that structure selects against that region instead.
What this is, and is not#
- A measurement, not a view of risk. Every run carries that sentence in its
disclosure. No loading, no premium, no statement about whether the risk should be carried or on what terms. - An intersection is not a loss. An event inside the box says the catalogue put a solution there. What happened to anything at the locations needs a vulnerability model this build does not have — and would card in the registry if it did.
- Nothing reaches the network. Every number is arithmetic over a pinned
snapshot whose bytes are servable in full at
/feeds/{id}/versions/{version}/data, with the digest on the artifact. - Free. Craton bills per object and per model run; this run mints no object
and runs no registry model, and every number in it is already free at
GET /hazardandGET /events/{peril}.GET /usagewill not show it. A cancelled account is still refused, though — the run writes to an object, and every route that writes to an object refuses one.
The published schema is analysis.v0.schema.json, listed in the API contract document with every other response shape; the run travels in the job at result.analysis. Validate against it while holding no Craton code.
Where to go next#
- The point hazard lookup — the same reading, at one coordinate.
- Events: the hazard record behind a result — the same catalogue, addressable event by event.
- A technical price, from nothing — the verb that reads the object this one annotated.