Skip to content

CLI reference

Exact facts for every command: usage, key flags, output formats, and exit codes. For a guided first run, see Getting started; for authoring the YAML inputs, see Missions and vehicles.

All commands run as uv run bvlos-sim <command> from a source checkout, or bvlos-sim <command> from an installed wheel. Input files may be .yaml, .yml, or .json; relative asset paths resolve from the referencing file's directory. Every mission file must declare schema_version: mission.v7 — upgrade older files with migrate.

Exit codes

Code Meaning
0 Success (for estimate/scenario/batch: operational GO, or engineering-only pass)
10 Infeasible, failed, or operational NO-GO
11 Invalid input
12 Unsupported operation
13 Internal or adapter runtime error
14 Cancelled by SIGINT/SIGTERM (console entrypoint only)

A missing or unreadable input file is invalid input (11, with an error envelope) like any other load failure. Only genuine command-line usage errors — an unknown flag or a missing required argument — exit 2 from the argument parser.

Failures are machine-readable on stdout: the estimator commands emit their result envelope with an error status, and the commands without an envelope — migrate included — emit a {"command", "status", "message"} JSON error object. batch, convert and export are the remaining exceptions: they still print load failures as plain text on stderr. estimate reports the failing input path exactly as it was given on the command line, so identical inputs produce an identical envelope from any checkout directory.

Per-command behavior:

Command 0 10 11 12 13 Notes
estimate 11 can also be a computed invalid-input failure.
scenario Every non-passed outcome collapses to 10.
batch 10 if any run is infeasible or NO-GO; 11 if any run fails to load.
sample Never 10: an infeasible Monte Carlo result is in the body, exit 0.
propagate Never 10: same divergence as sample.
size-battery A NO answer (no feasible capacity) is in the body, not 10.
sora 10 for out-of-scope Step 8, GRC > 7, rejected mitigation credit, or an infeasible mission; 12 for an unsupported estimator failure.
validate 10 when an acceptance threshold fails.
calibrate Fitted profile is in the body.
compare 10 drifted/failed; 12 for a contract-only bundle.
convert Missing --vehicle-profile and parse errors are 11; a lossy conversion without --allow-lossy is 12.
migrate Legacy input that cannot be migrated is 11; an unexpected write/adapter failure is 13.
export Mission load / exportability failures are 11.
ingest-log Unknown/oversized logs and missing readers are 11.
sitl Input failures are 11; live adapter/timeout failures are 13.
verify 10 on any checksum mismatch or missing artifact; 11 for an unreadable or invalid bundle.
schema-versions Read-only; always 0.
bump Developer-only release tool; 11 on version drift.

Two rules hold everywhere:

  • The exit verdict never depends on the output format. estimate, scenario, and batch apply the fail-closed operational readiness gate for JSON, Markdown, summary, checklist, profile, sensitivity, GeoJSON, KML, and CSV alike. --engineering-only opts out of the gate for non-operational analysis: on estimate it forgives missing evidence and nothing else — a check that ran and failed, or a non-waivable warning, still exits 10; on scenario and batch it still bypasses the whole verdict. The JSON envelope always records the structured operational_readiness verdict.
  • --output writes are atomic (temp file, then os.replace). An interrupted run never leaves a truncated file.

Preflight validation

estimate, scenario, sample, propagate, batch, sora, convert, export, calibrate, compare and size-battery support --validate-only: load and schema-check all inputs — including referenced mission assets — and exit without running anything. Exit 0 on success, 11 otherwise. ingest-log, migrate, verify-evidence and sitl do not offer it.

uv run bvlos-sim estimate mission.yaml vehicle.yaml --validate-only
# mission: mission.yaml: OK
# vehicle: vehicle.yaml: OK

Add --validate-format json for a machine-readable preflight-validation.v1 envelope with one entry per file; a failure pins the offending file with a stable stage (schema, asset-load, reference) and error code.

estimate

Deterministic mission estimation and static feasibility checks.

Usage: bvlos-sim estimate MISSION VEHICLE [--format FMT] [--output PATH]

Flag Default Description
--format json json, markdown, summary, checklist, profile, sensitivity, ground-risk, geojson, kml
--output, -o stdout Write the artifact to a file
--engineering-only off Exit 0 on computational feasibility despite missing operational evidence; failed checks and non-waivable warnings still exit 10
--fidelity v1 v2 adds turn arcs and fixed-wing circular loiter
--wind-layer ALT:EAST:NORTH altitude-banded wind; repeatable
--max-segment-length-m Sample straight legs at bounded intervals (works in v1 and v2)
--calibration Apply a calibration-profile.v2 artifact
--calibration-traces Source flight-trace.v1 document; repeat for every trace used by --calibration. Without them the profile still applies, but earns no log_calibrated credit
uv run bvlos-sim estimate \
  examples/real_world/alpine_mission.yaml \
  examples/real_world/quadplane_v1.yaml \
  --format checklist

Formats:

  • json — canonical estimator-envelope.v11: provenance, diagnostics, route legs, totals, energy/geofence/landing-zone/resource/link/obstacle/weather/ ground-risk blocks, RTH reserve timeline, and operational_readiness.
  • markdown — human-readable report of the same result.
  • summary — one line: FEASIBLE reserve 279.8 % flight 2m 49s [warnings N] [FAILURE_CODE]. reserve is the margin above (positive) or below (negative) the reserve threshold, as a percentage of that threshold — not battery state of charge.
  • checklist — the pre-flight go/no-go view: one // row per check, Status: GO or Status: NO-GO, and a Blocked by: line naming the missing evidence, failed checks, or blocking warnings. GO requires every check present and passed and no unacknowledged warnings (see Warnings); missing evidence (◌ N/A) is NO-GO, never an implicit pass.
  • profile — per-leg altitude table with terrain elevation and clearance columns when a terrain asset is configured.
  • sensitivity — deterministic reserve sweep over cruise power (--sensitivity-power-steps, default 10,20,30 percent), headwind (--sensitivity-wind-steps, default 1,2,3 m/s), and battery capacity (--sensitivity-battery-steps, default 10,20,30 percent); ROBUST when every variation stays feasible.
  • ground-risk — SORA iGRC table (mission and per-leg) from a population grid and the vehicle's characteristic_dimension_m and max_speed_mps. This is the intrinsic class only; the sora command adds ARC, SAIL, and OSOs.
  • geojson / kml — map-ready route layers (see Map exports).

scenario

Deterministic scenario events (lost link, wind change, landing zone unavailable) and machine-readable assertions over the resulting estimate.

Usage: bvlos-sim scenario SCENARIO [--format FMT] [--output PATH]

Formats: json (scenario-report.v4), markdown, summary, checklist, profile, geojson, kml. Also accepts --engineering-only, and takes --calibration with one --calibration-traces flag per source trace.

uv run bvlos-sim scenario examples/scenarios/pipeline_demo_001_scenario.yaml \
  --format summary
# PASSED 3/3   reserve 279.8 %   flight 2m 49s   warnings 3

PASSED n/total counts assertions; policy <ACTION> appears when a lost-link event fired. PASSED describes assertions, not operational readiness — with warnings or missing evidence the process still exits 10 unless --engineering-only is set. Scenario YAML structure (events, triggers, assertions, lost-link policies) is documented in Missions and vehicles.

batch

Multiple estimate, scenario, or propagate runs from one batch.v1 manifest.

Usage: bvlos-sim batch MANIFEST [--format FMT] [--output-dir DIR]

format_version: "batch.v1"
runs:
  - id: alpine_standard
    mission: ../real_world/alpine_mission.yaml
    vehicle: ../real_world/quadplane_v1.yaml

run_type selects what every run in the manifest does — estimate (the default, and what an unversioned manifest means), scenario, or propagate:

format_version: "batch.v1"
run_type: scenario           # each run points at a scenario file
runs:
  - {id: nominal, scenario: scenarios/nominal.yaml}
  - {id: lost_link, scenario: scenarios/lost_link.yaml}
format_version: "batch.v1"
run_type: propagate          # each run points at a stochastic plan
runs:
  - {id: wind_sweep, plan: stochastic/wind.yaml}

Paths resolve relative to the manifest. The table columns match the run type: estimate shows reserve margin and flight time; scenario shows the assertion count (passed/total); propagate shows the modeled pass rate. Exit is 10 when any estimate run is infeasible/NO-GO or any scenario run fails; propagate runs are diagnostic and never exit 10.

--output-dir writes one file per run id, in that run type's envelope (estimator-envelope.v11, scenario-report.v4, or stochastic-envelope.v2). --format selects json, markdown, or summary for any run type; the route-shaped formats geojson, kml, checklist, and profile are estimate-only.

Only two formats render to stdout: summary (the default run table) and csv (the same table comma-separated, columns matching the run type). Every other format is a per-run file, so it requires --output-dir — asking for one without a directory exits 11 with an error object rather than quietly printing the summary table. When --output-dir is set, stdout keeps the run table and the artifacts go to the directory.

sample

Seeded Monte Carlo parameter sweep over wind, cruise speed, cruise power, and battery capacity. Emits uncertainty-report.v2.

Usage: bvlos-sim sample UNCERTAINTY [--format json|markdown|summary]

uv run bvlos-sim sample \
  examples/uncertainty/pipeline_demo_001_wind_uncertainty.yaml --format summary
# DIAGNOSTIC   modeled_pass 100%   conditional_end_energy p5 811.3 Wh   p50 854.9 Wh   p95 898.2 Wh   time p50 2m 50s   n=200

This is a diagnostic, not a probability: modeled_pass is the fraction of evaluated samples whose deterministic run passed the modeled constraints, and the percentiles are conditional on those passing samples. Read p5 as the pessimistic tail — 95% of modeled-pass samples land with at least that much energy — and plan against it, not the median. The command always exits 0 once the run completes.

propagate

Time-stepped stochastic particle propagation over the mission timeline, with optional GPS/battery sensor models and EKF estimation-error traces. Emits stochastic-envelope.v2. Same formats and diagnostic semantics as sample; the timeline adds a per-step conditional_reserve_violation_rate.

Sample accounting is three-way: sample_count + infeasible_sample_count + failed_sample_count == plan.samples, and spatial_infeasible_count is a subset of the infeasible count. Vehicles with a controller block and non-zero wind_process_noise_std_mps are rejected rather than approximated.

Long sample, propagate, and batch runs can stream progress: --progress-format jsonl emits one JSON object per ~5% of the run to stderr ({"event":"progress","command":"propagate","completed":250,"total":1000,"elapsed_s":75.3}; batch records also carry run_id — the id of the run that just completed — so a worker can attribute stalls); --progress-file PATH writes the same stream to a tailable file. Progress never touches the result envelope or the exit code.

size-battery

Search the minimum battery capacity that makes the mission feasible, including the candidate pack's mass. The vehicle must define energy.battery_excluded_operating_mass_kg and energy.battery_specific_energy_wh_per_kg.

Usage: bvlos-sim size-battery MISSION VEHICLE [--margin N]... [--format FMT]

The search stops at mass.max_takeoff_kg and does not assume feasibility improves monotonically with capacity — heavier packs can become infeasible again — so the report gives a verified feasible interval at 1 Wh resolution. A --margin target above the verified upper bound is reported UNAVAILABLE, never silently substituted. Formats: markdown (default), json (battery-sizing-report.v2), summary.

sora

SORA 2.5 pre-assessment: intrinsic and final GRC, ARC, SAIL, Step 8 adjacent-area and containment requirements, and all 17 Table 14 OSO rows.

Usage: bvlos-sim sora MISSION VEHICLE [--format markdown|json]

The command is strictly evidence-gated: it requires population-grid.v2 population evidence, a complete airspace descriptor, and an explicit sora.ground_risk_footprint (see Missions and vehicles). Applied M1/M2 mitigation declarations earn no credit until an Annex B criteria evaluator exists — the assessment is still produced with the final GRC equal to the intrinsic GRC, each declaration is recorded as credit_rejected_pending_annex_b, and the command exits 10 so the no-credit result stays auditable. It never assesses Annex E containment or OSO compliance and is a planning aid, not an authorization.

The maximum-AGL/1:1-buffer gate takes the conservative minimum terrain over the full declared operational/contingency-volume plus Ground Risk Buffer corridor; centreline-only terrain providers and grids that do not cover the width are refused. If assets.geofences_file supplies ICAO properties.airspace_class labels, a label with explicit altitude/time scope can refute the declared worst case only when it overlaps the modeled corridor and implies a higher initial ARC. A different letter that maps to an equal or lower ARC is not a contradiction. Unbounded/timeless labels stay advisory, and neither a match nor an absence verifies the declaration: the fetch workflow retains keep-out airspace only, SORA 2.5 permits multiple classifications across one operational volume, and no NOTAM/TFR/UAS geo-zone currency source is consulted. The SORA envelope records the geofence input digest.

validate

Compare a predicted mission estimate against an observed flight trace (flight-trace.v1, produced by ingest-log).

Usage: bvlos-sim validate MISSION VEHICLE TRACE [--format markdown|json]

Reports predicted-vs-observed flight time, horizontal distance, mean groundspeed, and landing reserve at mission and per-phase level, each with absolute and percent error. Acceptance thresholds gate the exit code:

Flag Default Gates
--max-time-error-percent 20 Mission-time error
--max-distance-error-percent 10 Horizontal-distance error
--max-speed-error-percent 15 Mean-groundspeed error
--max-reserve-error-percent 10 Landing-reserve error

A failed gate still writes the report and exits 10. A trace whose embedded mission/vehicle hashes do not match the supplied inputs is rejected (11).

calibrate

Fit a calibration profile from observed flights. Kinematics — cruise_speed_mps, climb_rate_mps, descent_rate_mps, max_station_keep_wind_mps — and the phase energy coefficients — hover_power_w, cruise_power_w, climb_power_w, descent_power_w, derived from observed battery_current_a x battery_voltage_v — each with observed range, spread, sample count, and provenance. Parameters with no supporting samples are listed in notes, never fabricated.

Usage: bvlos-sim calibrate VEHICLE TRACE... [--format markdown|json]

Apply the resulting calibration-profile.v2 artifact to estimate or scenario with --calibration PATH plus one repeatable --calibration-traces TRACE per source trace. The command parses and segments those traces again, reproduces the dataset id and source ids, reruns the deterministic fitter, and compares a canonical digest of every parameter record. Only then is a runtime-only proof allowed to clear ENERGY_MODEL_UNCALIBRATED.

Supplying --calibration without the traces is not an error: the fitted coefficients are applied and provenance.calibration_lineage.state reports unverified, so the artifact states that nothing reproduced the profile. The vehicle never reaches log_calibrated that way, so ENERGY_MODEL_UNCALIBRATED keeps blocking the operational verdict until the traces arrive or the code is acknowledged. Traces that do not reproduce the profile are a different matter and remain invalid input. The proof is deliberately absent from serialized vehicle files, so copying calibration_status: log_calibrated and an energy_calibration block does not copy trust.

validate --calibration is intentionally different: it applies an untrusted candidate so an independent held-out trace can assess it, and grants no operational calibration credit.

Only the energy coefficients answer ENERGY_MODEL_UNCALIBRATED, and the profile must at least fit cruise_power_w, the coefficient every moving mission uses or falls back to. A trace without battery telemetry yields no energy fit, and a kinematics-only profile cannot clear that warning or unblock a GO. Legacy calibration-profile.v1 artifacts still parse for inspection and candidate validation. They cannot earn operational credit without source traces that reproduce the current deterministic artifact.

This proves reproducibility and content integrity, not flight authenticity. A normalized trace can itself be fabricated, and its raw_log_sha256 remains an assertion until the raw log bytes are independently supplied and checked.

ingest-log

Normalize an ArduPilot DataFlash text/binary log or PX4 ULog into flight-trace.v1. Requires the optional readers:

uv sync --extra flight-logs
uv run bvlos-sim ingest-log flight.bin \
  --trace-id my-flight-001 \
  --mission mission.yaml --vehicle vehicle.yaml \
  --output my-flight-001_trace.json

Supplying --mission/--vehicle embeds their content hashes, which validate requires. Ingestion snapshots the source bytes before parsing; the hard size ceiling is 64 MiB (--max-size-mib can only lower it).

convert / export

QGroundControl .plan interchange, in both directions:

# .plan -> mission.v7 YAML (--vehicle-profile is required)
uv run bvlos-sim convert plan.plan --vehicle-profile quadplane_v1 -o mission.yaml

# mission.v7 YAML -> .plan
uv run bvlos-sim export mission.yaml -o mission.plan

convert reads the planned home, cruise/hover speeds, and supported mission items (takeoff, VTOL takeoff, waypoint, loiter-time, RTL, land, VTOL land). Altitude frames map onto altitude_reference: frame 0 -> amsl, frame 3 -> relative_home, frame 10 (terrain) -> terrain; mixed frames become per-item overrides.

Conversion is fail-closed: any dropped item (Survey/ComplexItem, an unsupported MAVLink command, an unknown altitude frame) and any populated geoFence or rallyPoints section is a loss. By default every loss is listed on stderr (kind plus item index or section), nothing is written, and the command exits 12. --allow-lossy restores convert-what-we-can behavior: each loss is still reported on stderr and the run ends with a one-line summary — lossy conversion: N item(s) dropped, sections: geoFence, rallyPoints — so lossy imports stay visible in CI logs. --validate-only reports the same losses under the same exit-code contract.

export maps route actions back to MAVLink commands and omits bvlos-sim-specific fields (constraints, assets, policy) with a stderr note. Semantic rewrites — terrain altitudes falling back to frame 3, a fixed-wing takeoff exported as VTOL takeoff — are warned per item and summarised with the same one-line lossy conversion summary, but export keeps exit 0: the .plan format simply cannot represent them. The result round-trips through convert.

migrate

Upgrade an unversioned or mission.v6 file to mission.v7. Only this command treats a missing schema_version as legacy.

uv run bvlos-sim migrate mission.yaml --dry-run    # show versions + diff, write nothing
uv run bvlos-sim migrate mission.yaml --backup     # in-place, writes FILE.bak first
uv run bvlos-sim migrate missions/ --glob "*.yaml" --backup

Migration refuses semantic guesses: SORA 2.0 blocks, applied legacy mitigation declarations, and ambiguous classifications require operator reassessment instead of silent relabeling.

A refusal — an unreadable file, an unsupported schema_version, or a payload needing operator reassessment — exits 11 and prints the same JSON error object as every other command on stdout:

{
  "command": "migrate",
  "status": "error",
  "message": "unsupported mission schema_version 'mission.v5'"
}

An unexpected failure while writing exits 13 with the same shape.

sitl / compare

Build SITL evidence bundles and compare them against deterministic expectations. See SITL for the container setup, the live workflow, and the adapter contract.

# contract-only bundle from an existing scenario
uv run bvlos-sim sitl SCENARIO --output evidence.json

# live run against a running ArduPilot SITL
uv run bvlos-sim sitl SCENARIO --live --host 127.0.0.1 --port 5770 \
  --artifact-dir artifacts/ --output evidence.json

# compare a completed bundle against its embedded expectations
uv run bvlos-sim compare evidence.json --output comparison.json

verify

Re-verify the chain of custody of a sitl-evidence.v1 bundle: recompute the SHA-256 of every referenced artifact file (relative paths resolve against the bundle's directory) and compare against the recorded checksums.

Usage: bvlos-sim verify EVIDENCE.json

One line per artifact — OK, MISMATCH, MISSING, or SKIPPED (no recorded checksum) — then a final verdict. Exit 0 when everything matches, 10 on any mismatch or missing artifact, 11 for an unreadable or invalid bundle.

Provenance and output safety

Three flags exist for evidence hygiene, all default-off so outputs stay byte-identical unless you opt in:

  • --operator-id TEXT (estimate, scenario, sitl) — records the operator identity in the result's free-form metadata map.
  • --generated-at ISO8601|now (same commands) — records a generation timestamp; now resolves to the current UTC time.
  • --no-clobber (every command with --output) — refuse to overwrite an existing output file (exit 11) instead of replacing it, so a re-run cannot silently destroy prior evidence.

schema-versions

Print every supported input and output contract version plus the tool version as canonical JSON, without loading any file. Alias: contracts. Use it to pin compatibility from a backend instead of parsing versions off an envelope.

tool_version — here and in every envelope's provenance — is this checkout's pyproject.toml version when running from source, and the installed distribution metadata otherwise. A vendored deployment (pip install --target, a Lambda layer, a PEX) reports the vendored package's own version, never the host application's.

bump

Developer-only release tool (absent from published wheels): bumps the version in pyproject.toml and rolls CHANGELOG.md. --dry-run previews, --check fails CI when the version is behind the latest v* tag. It never tags, pushes, or publishes.

Map exports

estimate --format geojson|kml (and the same on scenario) emit the computed route as map layers; batch --format geojson|kml with --output-dir writes one file per run.

GeoJSON layers (RFC 7946, coordinates [lon, lat, altitude_m]):

  • route — one LineString per leg with leg_id, action, energy_margin_pct, and RTH reserve margin/color when available.
  • landing_zones — one Point per zone with zone_id and reachable.
  • geofences — one Polygon per zone with kind and conflict.
  • obstacles — configured obstacle geometries with height_m and conflict.

Color thresholds (energy and RTH margin as percent of battery capacity): green above 30, amber 10–30, red below 10. KML uses the same thresholds and opens directly in Google Earth and QGroundControl; GeoJSON opens in QGIS and QGroundControl.

Warnings

Warnings mark conditions that do not make the mission infeasible but block an operational GO (they appear in failed_checks as warnings). The full JSON envelope carries each warning's code, message, and location; the checklist lists the codes on its Warnings row.

Most can be accepted per mission via constraints.accepted_warning_codes (see Missions and vehicles): acknowledged codes stay in every artifact but stop blocking GO; any unlisted warning still blocks.

Nine cannot. MAX_WIND_EXCEEDED and RESERVE_BELOW_FAILSAFE_ABORT_THRESHOLD report that a limit the vehicle profile itself declares was exceeded, and VEHICLE_WIND_ENVELOPE_UNDECLARED reports that the wind limit is absent, so the same envelope cannot be checked at all; GROUND_RISK_FOOTPRINT_INVALID reports that evidence the mission itself declares fails its own invariants. GEOFENCE_ZERO_ZONES, GEOFENCE_COVERAGE_MISSING, OBSTACLE_ZERO_FEATURES and OBSTACLE_COVERAGE_MISSING report that a gated evidence category was configured but consulted nothing, and OBSTACLE_KEEP_OUT_NOT_CONFIGURED says the same a third way — every loaded obstacle has zero width and no clearance is configured, so only an exact overflight could register — an empty collection, or one whose features all sit outside the route relevance envelope, leaves the clearance check indistinguishable from one that was never supplied an asset. Accepting any of the eight would let the file that supplies the evidence waive the check on it. Listing any of them is a schema error rather than a waiver, --engineering-only does not forgive them either, and they are marked non-waivable below.

Code Meaning
MAX_WIND_EXCEEDED Non-waivable. Leg wind exceeds vehicle.performance.max_wind_mps (not enforced as a hard limit).
VEHICLE_WIND_ENVELOPE_UNDECLARED Non-waivable. Wind was evaluated but vehicle.performance.max_wind_mps is absent, so the aircraft's wind envelope is unverifiable rather than satisfied.
RESERVE_BELOW_FAILSAFE_ABORT_THRESHOLD Non-waivable. Predicted landing reserve is below the autopilot abort threshold.
GROUND_RISK_FOOTPRINT_INVALID Non-waivable. The mission's declared sora.ground_risk_footprint is not usable as ground-risk evidence.
RESERVE_BELOW_FAILSAFE_WARN_THRESHOLD Predicted landing reserve is below the low-battery warning threshold.
GEOFENCE_EVALUATED_2D_ONLY At least one zone declares neither floor_m nor ceiling_m, so it is evaluated as an unbounded 2D column. Zones declaring either bound are altitude-constrained and do not raise this.
GEOFENCE_ZERO_ZONES Non-waivable. A geofence file is configured but contains zero zones — the clearance check evaluated no airspace.
GEOFENCE_COVERAGE_MISSING Non-waivable. Zones were loaded, but none lies inside the route relevance envelope — the clearance check evaluated no airspace the mission can reach.
OBSTACLE_ZERO_FEATURES Non-waivable. No obstacle was consulted: either the configured file contains zero obstacles, or min_obstacle_clearance_m is set with no assets.obstacles_file at all.
OBSTACLE_COVERAGE_MISSING Non-waivable. Obstacles were loaded, but none lies inside the route relevance envelope — the clearance check evaluated no vertical structure the mission can reach.
OBSTACLE_KEEP_OUT_NOT_CONFIGURED Non-waivable. Every obstacle has zero radius and uncertainty and min_obstacle_clearance_m is unset, so the keep-out volume has no width.
DEPARTURE_TIME_MISSING A geofence has a time window but the mission has no departure_time; the zone is treated as always active.
DIVERT_ENERGY_TAS_ONLY A scenario divert estimate used TAS without wind correction.
POPULATION_DENSITY_DIMENSION_MISSING Population grid present but the vehicle omits characteristic_dimension_m; iGRC cannot be computed.
GUST_DATA_UNAVAILABLE max_gust_mps is set but no provider supplies gust data.
ENERGY_MODEL_UNCALIBRATED Energy coefficients have no matching runtime refit proof. This includes an absent/placeholder_values status and a serialized log_calibrated + energy_calibration claim loaded without the source traces. Supply a fitted profile with --calibration and every source trace with repeatable --calibration-traces. Declaring manufacturer_derived instead raises the separate waivable ENERGY_MODEL_CALIBRATION_SELF_DECLARED.
HOVER_POWER_USED_AS_DESCENT_POWER A hover-capable vehicle omits vehicle.energy.descent_power_w, so powered descent is billed at the greater of hover_power_w and cruise_power_w. The substitution is conservative — a VTOL descends on its lift rotors, not on a wing, and the greater of the two is taken because a bucket-shaped power curve lets a cruise value calibrated near Vne exceed hover — but the figure is unmeasured. Declare descent_power_w.
ENERGY_MODEL_PARTIALLY_CALIBRATED The calibration profile was verified against its source traces, but it does not fit an energy coefficient this mission bills energy against — the message names which. Those phases are still costed at whatever the vehicle file declares. Fit a profile from a trace covering the missing phase, or accept the code to record that judgement.
ENERGY_MODEL_CALIBRATION_SELF_DECLARED vehicle.calibration_status is manufacturer_derived: an operator attestation that the power coefficients come from published data, which nothing in the run checks against observed flight. Accept the code to put the attestation on record, or fit a profile with calibrate, pass it with --calibration, and supply every source trace with --calibration-traces.
ENERGY_REFERENCE_CONDITIONS_MISSING The vehicle declares operating_mass_kg without reference_mass_kg (or a reference density), so mass/density scaling is inert.
ROUTE_ACTIONS_AFTER_RTL Route items after an RTL are estimated but unreachable.
LOITER_RADIUS_IGNORED loiter_radius_m is ignored; loiter is modeled as station-keep.
LOITER_ASSUMED_ZERO_GROUND_DISTANCE Loiter dwell is modeled with zero ground-path distance.
LOW_GROUNDSPEED_MARGIN Groundspeed within 10% of min_groundspeed_mps.
HIGH_CRAB_MARGIN Crab angle within 10% of max_crab_angle_deg.
HOVER_SPEED_USED_AS_STATION_KEEP_AUTHORITY max_station_keep_wind_mps unset; hover_speed_mps used as fallback.

Evidence relevance

Geofence and obstacle evidence is tested for relevance, not only for presence. The estimator builds a route relevance envelope: a circle centred on the route, with a radius covering the route itself plus a fixed 100 km locality buffer. For obstacles the envelope is widened to at least the declared keep-out width (min_obstacle_clearance_m plus the obstacle's own radius_m and uncertainty_m) so nothing that can conflict is ever outside it.

The buffer is deliberately a constant rather than anything derived from the vehicle. It used to be battery_capacity_wh / cruise_power_w hours at cruise_speed_mps — the aircraft's outer reach — which meant the operator whose evidence was being audited also chose the threshold it was judged by, and it failed both ways: declaring a 3 W cruise power stretched the envelope to 19 440 km and let assets on another continent certify the route, while a small pack shrank it to 5.76 km and rejected a real mast 6.45 km away.

If no loaded zone or obstacle falls inside the envelope, the file is describing somewhere else, and GEOFENCE_COVERAGE_MISSING / OBSTACLE_COVERAGE_MISSING fire. Both are non-waivable, so the repair is to re-fetch for the area actually being flown. One relevant feature is enough — a national fetch that happens to include the route is real evidence. The envelope answers "is this evidence about this mission at all", not "does this file cover the whole route". This mirrors TERRAIN_COVERAGE_MISSING and POPULATION_COVERAGE_MISSING, which likewise compare route geometry against the data's own extent.

Failure codes

A failure stops the estimate and lands in the envelope's failure.code. The accompanying failure.kind selects the exit code — infeasible10, invalid_input11, unsupported12 — and a few codes carry different kinds depending on where they are raised, so read the kind from the envelope rather than assuming it from the code. Unlike a warning, no mission file can accept a failure.

Code Meaning
UNSUPPORTED_ALTITUDE_REFERENCE_TERRAIN altitude_reference: terrain was used with no terrain provider configured.
TERRAIN_COVERAGE_MISSING The terrain grid has no coverage at a sampled route position, a landing-zone surface, or is absent while min_terrain_clearance_m is set.
POPULATION_COVERAGE_MISSING The population grid does not cover the assessed route footprint.
WIND_COVERAGE_MISSING A wind query fell outside the grid's own time_s, lat or lon extent, or above the top of its altitude_m axis. Queries below the lowest altitude band still clamp, because a forecast's lowest band sits above ground level. The context names the axis, the queried value, and the grid's bounds.
SORA_INPUT_UNSUPPORTED The SORA assessment needs an input the mission or vehicle does not supply in a supported form (unsupported version, missing max_speed_mps, invalid assessment buffer).
UNSUPPORTED_LOITER_FOR_VEHICLE_CLASS Station-keep loiter was requested for a vehicle without hover capability.
MISSING_REQUIRED_SPEED_PROFILE No speed source resolves for a leg: no TAS for transit, RTH or divert, no station-keep authority, or no turn_radius_m for fixed-wing loiter.
INVALID_SPEED_PROFILE A resolved speed or angle limit is out of range — non-positive TAS, non-positive min_groundspeed_mps, max_crab_angle_deg outside 0–90.
INVALID_ENERGY_MODEL A vehicle.energy power value is not greater than zero.
INVALID_ENERGY_POLICY A reserve percentage is outside 0–100.
INVALID_MISSION_PROFILE The route cannot be expanded: a missing altitude_m, half-specified lat/lon, loiter_time without loiter_time_s, a capability the vehicle lacks, or a vehicle_profile that does not match the vehicle.
WIND_TRIANGLE_NO_SOLUTION The wind triangle has no solution: wind exceeds the achievable airspeed.
CRAB_ANGLE_LIMIT_EXCEEDED Required crab angle exceeds max_crab_angle_deg.
GROUNDSPEED_NON_POSITIVE The solved groundspeed is zero or negative.
GROUNDSPEED_BELOW_MIN The solved groundspeed is below min_groundspeed_mps.
STATION_KEEP_INFEASIBLE_WIND Loiter dwell wind exceeds the station-keep authority.
INSUFFICIENT_ENERGY The route needs more energy than the pack delivers.
RESERVE_BELOW_THRESHOLD Reserve at landing is below the required threshold.
RTH_RESERVE_BELOW_THRESHOLD Return-to-home reserve drops below the threshold at some point on the route.
MISSING_ENERGY_MODEL A required vehicle.energy field is absent, or an energy estimate a later check needs was never produced.
UNSUPPORTED_PHASE_ENERGY_MODEL A leg phase has no deterministic energy model.
RESOURCE_FEASIBILITY_FAILED No configured resource system can support the mission.
LINK_FEASIBILITY_FAILED No configured link system covers the route.
ROUTE_ENTERS_FORBIDDEN_ZONE The flown path enters a forbidden geofence zone while it is active.
ROUTE_EXITS_REQUIRED_ZONE The flown path leaves a required geofence zone.
ASSET_LOAD_ERROR A referenced asset file could not be read or parsed.
INVALID_GEOMETRY A loaded geometry is invalid, or a path could not be materialized — an unsampleable route, a turn radius with no connected tangent arc, or a timing loop that did not converge.
UNSUPPORTED_GEOMETRY_TYPE A geofence or obstacle geometry type is not supported.
NO_REACHABLE_LANDING_ZONE No landing zone is reachable from an evaluated route state.
LANDING_ZONE_REACHABLE_BUT_BELOW_RESERVE The nearest landing zone is reachable only by spending the landing reserve.
UNSUPPORTED_LANDING_ZONE_GEOMETRY A landing-zone geometry type is not supported.
ALL_LANDING_ZONES_UNAVAILABLE Every landing zone is marked unavailable at an evaluated route state.
UNKNOWN_LANDING_ZONE_REFERENCE A scenario's landing_zone_unavailable names a zone id the mission does not configure.
WIND_LIMIT_EXCEEDED Sustained wind exceeds constraints.max_wind_mps.
GUST_LIMIT_EXCEEDED Gust speed exceeds constraints.max_gust_mps.
CROSSWIND_LIMIT_EXCEEDED Crosswind exceeds constraints.max_crosswind_mps.
WEATHER_DATA_UNAVAILABLE A configured weather minimum needs an observation the active provider does not supply.
OBSTACLE_CLEARANCE_VIOLATED A route segment violates the obstacle keep-out volume.
TERRAIN_CLEARANCE_VIOLATED A route segment violates min_terrain_clearance_m.

Python API

The stable Python surface is bvlos_sim.estimator:

from bvlos_sim.estimator import (
    EstimationOptions, FidelityMode, LayeredWindProvider, WindLayer,
    estimate_mission_distance_time, try_estimate_mission_distance_time,
    run_scenario, run_monte_carlo,
)

result = estimate_mission_distance_time(
    mission, vehicle,
    wind_provider=LayeredWindProvider([
        WindLayer(altitude_m=0.0, wind_east_mps=2.0, wind_north_mps=0.0),
        WindLayer(altitude_m=500.0, wind_east_mps=6.0, wind_north_mps=-1.0),
    ]),
    options=EstimationOptions(fidelity=FidelityMode.V2, max_segment_length_m=500.0),
)

Symbols exported from bvlos_sim.estimator.__all__ are the supported surface; internal module layout is not a contract.