Missions and vehicles¶
How to author every input file the CLI accepts, and what each field does at
runtime. Start from the minimal examples, add blocks as you need them, and
check your work with --validate-only at any point:
Files may be YAML or JSON. Unknown fields are rejected everywhere except the
documented free-form metadata maps. Some fields are accepted for schema
stability but not consumed by the estimator yet (for example
route[].acceptance_radius_m, route[].loiter_radius_m,
defaults.hover_speed_mps, assets.comms_coverage_file, and
mission.policy.lost_link_policy as a named policy) — never treat them as
enforced. Every YAML example on this page passes --validate-only exactly as
written, raises no warning this page does not explain, and — composed together
over the repository's reference assets — reaches GO. A regression test
(tests/test_docs_examples.py) keeps all three true.
Mission (mission.v7)¶
A minimal valid mission:
schema_version: mission.v7 # required, exactly this value
mission_id: my_survey_001
vehicle_profile: quadplane_v1 # must match the vehicle file's vehicle_id
planned_home: # initial state and RTL target
lat: 52.0
lon: 4.0
altitude_amsl_m: 12.0
defaults:
cruise_speed_mps: 18.0
altitude_reference: relative_home # relative_home | amsl | terrain
route:
- id: takeoff
action: vtol_takeoff
altitude_m: 80.0
- id: wp1
action: waypoint
lat: 52.001
lon: 4.002
altitude_m: 120.0
- id: rtl
action: rtl
Route actions: takeoff, vtol_takeoff, waypoint, loiter_time (needs
loiter_time_s), land, rtl. Each item needs a unique slug id; lat,
lon, altitude_m, and altitude_reference apply per item.
altitude_reference: terrain resolves altitude above ground and requires a
terrain asset.
Older files: unversioned and mission.v6 inputs are rejected by every normal
command — run bvlos-sim migrate mission.yaml --dry-run first.
Constraints¶
constraints:
accepted_warning_codes: [ENERGY_MODEL_PARTIALLY_CALIBRATED, GEOFENCE_EVALUATED_2D_ONLY]
min_landing_reserve_percent: 25.0 # % of battery that must survive landing
require_rth_reserve: true # hard per-leg return-to-home reserve gate
max_wind_mps: 12.0 # sustained-wind limit at every path sample
max_crosswind_mps: 8.0 # cross-track wind limit
min_distance_to_landing_zone_m: 2500.0 # NOTE: this is a MAXIMUM (see below)
min_obstacle_clearance_m: 15.0 # separation buffer around obstacles
min_terrain_clearance_m: 30.0 # sampled terrain clearance (needs terrain asset)
min_distance_to_landing_zone_m is an upper bound
Despite the min_ prefix, this is the maximum tolerated straight-line
distance from any route state to an emergency landing zone. Raising it
makes the check more permissive, not stricter. On the demo route, 500
fails every sampled state, 2500 fails 69 of 166, and 8000 passes. Set it
to the furthest distance you are willing to be from a landing zone, and do
not raise it to "tighten" the constraint. The reserve-based divert check
runs independently of this value and is unaffected by it.
Warnings block the operational GO verdict by default.
accepted_warning_codes is the explicit, reviewable opt-out: codes listed
there (validated against the warning table) still
appear in every artifact — including a checklist Acknowledged warnings row
and the envelope's operational_readiness.acknowledged_warning_codes — but
no longer force NO-GO. Unlisted warnings keep blocking. Because the
acceptance lives in the mission file, it is versioned and reviewed with the
mission, not slipped in on a command line.
Nine codes cannot be accepted
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 missing
entirely, so the same envelope cannot be checked at all.
GROUND_RISK_FOOTPRINT_INVALID reports that the mission's own
sora.ground_risk_footprint fails its 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
reports that every loaded obstacle has zero width with no clearance
configured, so only an exact overflight could register. All are missing
evidence rather than a modeling caveat. Listing any of the nine
is a schema error, because a mission file must not be able to sign off the
aircraft's own envelope or waive the check on evidence it declares itself.
Fix the mission or correct the vehicle profile instead.
All constraints fail closed. max_gust_mps, min_visibility_m, and
max_precipitation_mm_h are accepted, but no built-in provider supplies those
observations yet, so setting them makes the mission INFEASIBLE
(WEATHER_DATA_UNAVAILABLE) rather than silently compliant. The reserve
threshold in Wh is battery_capacity_wh × percent / 100, using
min_landing_reserve_percent when set and the vehicle's
reserve_percent_default otherwise.
Estimation settings¶
estimation:
fidelity: v2 # v1 default; v2 adds turn arcs + fixed-wing loiter
wind_east_mps: 2.0 # constant wind (positive east/north)
wind_north_mps: -1.0
wind_layers: # altitude-banded wind; supersedes scalar wind
- {altitude_m: 0.0, wind_east_mps: 2.0, wind_north_mps: 0.0}
- {altitude_m: 500.0, wind_east_mps: 6.0, wind_north_mps: -1.0}
max_segment_length_m: 500.0 # sample straight legs at bounded intervals
min_groundspeed_mps: 3.0
isa_temperature_offset_c: 0.0 # forecast deviation from the ISA standard day
isa_temperature_offset_c is the forecast air temperature minus the ISA
standard value, in kelvin, and it feeds the air-density scaling of phase power.
It only bites when the vehicle declares energy.reference_density_kgm3 — that
is the density the coefficients were measured at, and without it there is no
ratio to form. Warm air is thinner and costs more power: on the reference
mission, ISA+25 raises the energy budget about 4 % and ISA−25 lowers it about
the same. The default of 0.0 is a standard day, so omitting it changes
nothing.
Wind precedence, strongest first: CLI --wind-layer flags → mission
assets.wind_grid_file → estimation.wind_layers → scalar
wind_east_mps/wind_north_mps. Scenario initial_conditions override
mission estimation for scenario runs.
max_segment_length_m defaults to 500.0 and works in both fidelity modes.
Every straight leg is integrated in sub-segments of at most that length and
sampled at each sub-segment's midpoint, so a leg crossing a wind gradient is
never billed at the wind it departed in. Legs shorter than the interval still
resolve to a single midpoint sample. Lower it for finer integration over long
legs in strongly varying wind; the estimate reports
metadata.applied_default_max_segment_length_m when the default was used.
Assets¶
departure_time: "2026-06-01T14:00:00Z" # UTC; needed for time-windowed geofences
assets:
terrain_file: assets/terrain.yaml
wind_grid_file: assets/wind_grid.yaml
geofences_file: assets/geofences.geojson
landing_zones_file: assets/landing_zones.geojson
obstacles_file: assets/obstacles.geojson
population_grid_file: assets/population.yaml
Relative paths resolve from the mission file's directory. All assets are
offline files — core execution performs no live lookups; data quality and
freshness are the operator's responsibility. Fetch real data with the bundled
scripts (uv sync --extra scripts first):
| Script | Produces | Notes |
|---|---|---|
fetch_all.py <lat> <lon> |
terrain + wind + landing zones | Convenience wrapper over the three below; prints the assets: block; --wind-single-point explicitly selects a constant-field approximation |
fetch_terrain.py |
terrain_file |
SRTM; use --void-policy interpolate over water |
fetch_wind.py <lat_min> <lat_max> <lon_min> <lon_max> [step_deg] |
wind_grid_file |
Open-Meteo forecast bands, metres AMSL; bbox sampling by default, with the requested lattice and returned source-grid-cell centres recorded separately |
fetch_landing_zones.py |
landing_zones_file |
OSM aeroway features via Overpass |
fetch_geofences.py |
geofences_file |
OpenAIP (complete) or Overpass (way-based only) |
fetch_obstacles.py |
obstacles_file |
OSM towers, masts, cranes, and power lines via Overpass; requires --base-altitude-amsl-m because OSM height tags are AGL and properties.height_m must be AMSL |
fetch_population.py |
population_grid_file |
Diagnostic grid only — the sora command needs build_population_grid.py |
fetch_all.py covers three of the seven; geofences, obstacles, and population
are separate runs. fetch_obstacles.py is the one that fills the obstacle
evidence category — see the evidence table.
See examples/real_world/
for a complete pre-fetched area.
Terrain (terrain-grid.v1) — uniform elevation grid, bilinear
interpolation:
origin_lat: 51.990
origin_lon: 3.990
step_lat_deg: 0.001
step_lon_deg: 0.001
elevations_m:
- [10.0, 10.5, 11.0] # one row per latitude step, south to north
Wind grid (wind-grid.v1) — wind as a function of time, altitude, lat,
lon; quadrilinear interpolation. Each axis must be strictly increasing with at
least 2 entries. The grid must cover the mission: a query outside time_s,
lat or lon, or above the top of altitude_m, fails the estimate with
WIND_COVERAGE_MISSING rather than extrapolating from the nearest edge, the
same way terrain and population grids fail closed. Queries below the lowest
altitude band still clamp, because a forecast's lowest band is a height above
ground and every mission starts underneath it:
fetch_wind.py treats Open-Meteo's 10/80/120/180 m levels as AGL at each
sample location. Since the asset has one shared AMSL axis, it interpolates each
profile onto the intersection of the AMSL ranges covered everywhere in the
box, without vertical extrapolation. If terrain relief leaves no common range
(170 m or more for these source levels), the fetch is rejected. Reduce the
operating area, assess distinct terrain bands as separate missions, supply a
wind product already defined on common AMSL levels, or use --single-point
only when a clearly labelled constant-field approximation is appropriate. A
mission accepts one wind-grid file, so separately fetched tiles are not
silently mosaicked. Metadata records both the source AGL levels and the common
AMSL range.
axes:
time_s: [0.0, 600.0]
altitude_m: [0.0, 200.0]
lat: [51.99, 52.00, 52.01]
lon: [3.99, 4.00, 4.01]
values: # values[time][alt][lat][lon] = [east_mps, north_mps]
...
Geofences (geofence-geojson.v1) — GeoJSON Polygon/MultiPolygon
features, coordinates [lon, lat]. properties.kind is forbidden or
required; forbidden boundary contact is a conflict, required zones must cover
the route as a union. Optional properties: floor_m/ceiling_m (AMSL
altitude band; a zone only conflicts when horizontal geometry intersects and
altitude bands overlap) and active_from/active_until/recurrence
(daily/weekdays) for time-windowed zones — evaluated against
departure_time, and treated as always active (with a warning) when it is
missing. Fetched assets may also carry properties.airspace_class, a decoded
ICAO class letter or special-use area label preserved alongside kind. The
operational sora command treats those labels as incomplete, refutation-only
evidence. A label rejects the declared worst case only when explicit
altitude/time bounds overlap the operational/contingency corridor and the
observed class implies a higher initial ARC. Equal/lower-risk, unbounded or
timeless labels remain advisory; no label proves completeness, exclusivity, or
day-of-flight currency. The openAIP fetcher preserves its raw lowerLimit and
upperLimit objects under properties.airspace_class_source_scope but does not
guess at AGL, MSL or flight-level conversion without terrain/pressure context;
those records therefore remain altitude-scope-unknown for this check.
Landing zones (landing-zone-geojson.v1) — Point/Polygon/
MultiPolygon features. properties.altitude_amsl_m gives the surface
elevation; without it, the terrain provider must cover the zone. Reachability
is checked at route endpoints and interior samples (≤ 50 m spacing); divert
distance uses a Dubins turn-and-straight path when heading and turn radius are
known, wind-triangle-integrated, and divert energy includes the terminal
climb/descent. Reachability does not score surface suitability or obstacles.
Obstacles (obstacle-geojson.v1) — Point/LineString/Polygon
features with required properties.height_m (top of obstacle, AMSL) and
optional radius_m/uncertainty_m expanding the separation check. A sampled
route point violates when it is horizontally within
radius_m + min_obstacle_clearance_m + uncertainty_m and clears the top by
less than min_obstacle_clearance_m + uncertainty_m.
Population — the diagnostic grid (population-grid.v2, or unversioned)
feeds estimate --format ground-risk only. The operational sora command
requires population-grid.v2 with provenance metadata; see
SORA evidence.
Link systems¶
link_systems:
- link_id: c2_primary
kind: direct_radio # direct_radio | mesh_network | cellular_lte_5g
# | satellite | starlink | hybrid
required: true
priority: 1
availability: available
max_range_m: 8000.0
Deterministic, offline checks: availability: unavailable makes a link
infeasible; max_range_m is checked against the maximum distance from home at
leg endpoints. When any link is required, at least one required link must be
feasible; the selected link is the feasible one with the lowest priority.
Vehicle (vehicle.v6)¶
vehicle_id: quadplane_v1 # matched against mission.vehicle_profile
vehicle_class: vtol
characteristic_dimension_m: 1.0 # wingspan / rotor diameter / max tip distance
mass:
empty_kg: 8.0
max_takeoff_kg: 12.0
operating_mass_kg: 11.0 # setting this REQUIRES energy.reference_mass_kg
# and energy.reference_density_kgm3 below
performance:
cruise_speed_mps: 18.0
hover_speed_mps: 5.0
max_speed_mps: 25.0 # max possible commanded speed (used for iGRC)
climb_rate_mps: 3.0
descent_rate_mps: 2.0
turn_radius_m: 80.0
max_wind_mps: 10.0 # exceeding it, or omitting it, emits a
# GO-blocking, non-waivable warning
max_crab_angle_deg: 35.0
max_station_keep_wind_mps: 8.0
energy:
battery_capacity_wh: 900.0
reserve_percent_default: 25.0
cruise_power_w: 450.0
hover_power_w: 1200.0 # required for hover-capable loiter
climb_power_w: 1500.0 # falls back to cruise_power_w when omitted
descent_power_w: 900.0 # omitting it bills descent at hover_power_w
# on a hover-capable vehicle, and raises
# HOVER_POWER_USED_AS_DESCENT_POWER
reference_mass_kg: 11.0 # all-up mass the power values were measured at
reference_density_kgm3: 1.225 # air density they were measured at
battery_specific_energy_wh_per_kg: 225.0 # size-battery only; see below
battery_excluded_operating_mass_kg: 8.0 # size-battery only; see below
failsafe:
low_battery_warn_percent: 30
low_battery_abort_percent: 25
emergency_land_percent: 10
capabilities:
hover: true
forward_flight: true
# manufacturer_derived | placeholder_values | log_calibrated.
# placeholder_values, or omitting the field, raises ENERGY_MODEL_UNCALIBRATED;
# manufacturer_derived raises the waivable ENERGY_MODEL_CALIBRATION_SELF_DECLARED.
# Both block GO until acknowledged. A serialized log_calibrated claim is not
# trusted on its own: --calibration plus every --calibration-traces input must
# reproduce the dataset and deterministic fitter output at runtime.
calibration_status: placeholder_values
metadata:
source: null
notes: Replace with manufacturer data or measured logs before real analysis.
calibration_status is a typed top-level field, not free-form metadata. The
estimator reads it, so a profile that never states where its power numbers came
from cannot quietly produce a GO: every energy figure it yields is arithmetic
on invented coefficients, and the verdict says so.
How energy is computed: a leg that climbs or descends while covering ground is
costed by phase time — the vertical part at climb or descent power, the rest at
cruise power — and reports the resulting time-weighted power. Hover loiter uses
hover power. Optional fidelity fields — energy.reference_mass_kg,
energy.reference_density_kgm3, energy.induced_power_mass_exponent (hover and
climb), energy.cruise_power_mass_exponent (forward flight, default 0.5), and
energy.usable_capacity_fraction — add deterministic mass, ISA-density, and
pack-derating scaling; they are closed-form pre-calibration aids, not
a substitute for fitting against your own flight logs with
calibrate.
energy.usable_capacity_fraction is the share of nameplate capacity the pack
is trusted to actually deliver, covering voltage sag and Peukert-style losses.
It is a single float in (0, 1] and defaults to 1.0. It replaced
usable_capacity_curve in vehicle.v6: that field looked like a
state-of-charge curve but the estimator only ever read it at soc = 1.0, which
the ordering validator made its maximum, so every physically natural sag curve
was an exact no-op. A file still carrying the old key is rejected with a
message naming this one. The derating applies to the mission budget, the
reserve threshold, and the RTH and divert budgets alike.
mass.operating_mass_kg needs both reference conditions
mass.operating_mass_kg is the mass the power values get scaled to, and
scaling needs the conditions they were measured at. Setting it while
either energy.reference_mass_kg or energy.reference_density_kgm3 is
missing raises ENERGY_REFERENCE_CONDITIONS_MISSING: the mission runs, the
scaling silently falls back to unadjusted phase power, and the warning
blocks GO. Either set all three, or set none of them — leaving
operating_mass_kg out is a complete, valid profile. reference_mass_kg
on its own is not enough; the density is required too.
energy.battery_specific_energy_wh_per_kg (pack-level Wh/kg) and
energy.battery_excluded_operating_mass_kg (all-up mission mass without the
swappable pack, which must be ≥ mass.empty_kg) are consumed only by
size-battery, which needs both to feed a candidate
pack's mass back into the power model. Omit either and the command exits 11
with Battery sizing requires capacity-mass feedback inputs. They do not
affect estimate. size-battery also refuses any vehicle that declares
resource_systems, because it sizes the legacy vehicle.energy battery only.
Resource systems¶
Optional resource_systems replace the battery-only energy view: the mission
is resource-feasible when at least one configured system is feasible, RTH
demand included, and the selected system is the feasible one with the lowest
priority. Configuring the block is also what supplies the resource evidence
category — without it that row reads N/A and blocks GO.
resource_systems:
- resource_id: fiber-power-primary # required; [A-Za-z0-9][A-Za-z0-9_-]*
kind: external_power # onboard_battery | external_power | hybrid
priority: 0 # lower wins; default 0
continuous_power_w: 2000.0 # REQUIRED for external_power and hybrid
delivery: optical_fiber # generic | tethered | optical_fiber; documentation only
max_tether_length_m: 2500.0 # max horizontal distance from planned_home
max_route_time_s: 900.0
# max_route_distance_m: 20000.0 # total route path distance ceiling
- resource_id: onboard-battery-backup
kind: onboard_battery
priority: 1
battery_capacity_wh: 900.0 # NOTE: battery_capacity_wh, not capacity_wh.
# Omit to inherit vehicle.energy.battery_capacity_wh
reserve_percent: 25.0 # omit to inherit the mission/vehicle reserve policy
resource_id and kind are the only required keys; continuous_power_w is
additionally required for external_power and hybrid and rejected as missing
at schema load otherwise. battery_capacity_wh and reserve_percent apply to
onboard_battery and hybrid only. delivery is recorded for the report and
always ignored by feasibility. fuel, hydrogen, and other are accepted by
the schema but currently unsupported, so a system declaring one of them is
evaluated as infeasible rather than approximated.
When resource_systems is present, result.energy still reports the legacy
battery-only view, and battery-capacity and RTH-reserve gating move to the
resource check — which is why the checklist row changes from RTH reserve to
RTH feasibility for these vehicles.
Community profiles for real aircraft live in
examples/vehicles/community/.
Validate any profile against your own logs before operational use.
Evidence categories and the inputs that satisfy them¶
A NO-GO verdict ends with a Blocked by: line naming categories, not
fields:
Blocked by: missing evidence (geofence, resource, link, obstacle, ground_risk); blocking warnings (ENERGY_MODEL_UNCALIBRATED) — the checklist is fail-closed
Each category is N/A until its input exists. This table maps every category
to the input that fills it. Rows were derived by removing one input at a time
from
examples/missions/pipeline_demo_001_go.yaml
+ examples/vehicles/quadplane_v1_complete.yaml
and reading operational_readiness.missing_evidence back out of the JSON
envelope.
| Category | What fills it | Also needed to actually pass |
|---|---|---|
energy |
always evaluated — vehicle.energy is mandatory |
— |
geofence |
assets.geofences_file |
≥ 1 zone; an empty collection raises GEOFENCE_ZERO_ZONES, and zones that all sit outside the route relevance envelope raise GEOFENCE_COVERAGE_MISSING. Both block GO and neither can be accepted |
landing_zone |
assets.landing_zones_file |
zone surface altitude, from properties.altitude_amsl_m or terrain coverage |
resource |
vehicle resource_systems (≥ 1 entry) |
at least one system feasible, RTH demand included |
link |
mission link_systems (≥ 1 entry) |
required: false on every link still satisfies the category |
obstacle |
assets.obstacles_file, or constraints.min_terrain_clearance_m with assets.terrain_file |
an empty obstacle file, or min_obstacle_clearance_m set with no obstacle file, raises OBSTACLE_ZERO_FEATURES; obstacles that all sit outside the route relevance envelope raise OBSTACLE_COVERAGE_MISSING. Both block GO and neither can be accepted. Terrain clearance alone satisfies the category silently — it proves nothing about masts or towers, so fetch obstacles with fetch_obstacles.py |
weather |
constraints.max_wind_mps or constraints.max_crosswind_mps, and a configured wind source |
the wind source must be non-zero: assets.wind_grid_file, estimation.wind_layers, or non-zero estimation.wind_east_mps/wind_north_mps. A zero constant wind counts as no source |
rth |
planned_home — mandatory, so this is never missing |
every timeline point feasible |
ground_risk |
assets.population_grid_file and vehicle.characteristic_dimension_m |
estimate accepts any loadable grid, including an unversioned diagnostic one; only the sora command demands population-grid.v2 with provenance |
ground_risk_footprint |
sora.ground_risk_footprint with a positive total buffer |
the footprint must survive the same validity rules the sora command applies, or GROUND_RISK_FOOTPRINT_INVALID blocks GO |
Wind assets alone do not satisfy weather
The intuitive guess — supply a wind grid and the weather row lights up — is
wrong, and it is the single most common reason weather stays N/A on an
otherwise complete mission. The check needs a limit to test against as
well as an observation to test: with assets.wind_grid_file but no
max_wind_mps/max_crosswind_mps, weather is missing evidence; with the
constraint but no wind source, it is missing too. Both, or neither.
Missing evidence and a failed check are different states. A category that was
never evaluated is N/A and blocks GO because nothing was proven; a category
that was evaluated and failed reports FAIL with a code. Both give NO-GO and
exit 10. Two opt-outs exist and no others: --engineering-only trades the
whole gate for a computational verdict, and
constraints.accepted_warning_codes waives named warnings
only — never missing evidence. Neither suppresses the structured
operational_readiness verdict in the JSON envelope.
Scenarios (scenario.v1)¶
A scenario wraps a mission/vehicle pair with initial conditions, timeline events, and assertions:
schema_version: scenario.v1
scenario_id: lost-link-demo
mission_file: mission.yaml
vehicle_file: vehicle.yaml
initial_conditions:
wind_east_mps: 3.0
lost_link_policy:
action: rtl # rtl | land | loiter | divert
loiter_s: 30.0 # loiter before acting
# divert_target_id: zone1 # required when action is divert
events:
- event_id: link-loss-mid
kind: lost_link # lost_link | observe | wind_change | landing_zone_unavailable
trigger: at_route_item # at_mission_start | at_route_item | at_elapsed_time | at_mission_end
trigger_route_item_id: wp1
assertions:
- assertion_id: reserve-ok
kind: field_gt # estimate_succeeds | estimate_fails | field_lt/gt/le/ge/eq
# | policy_action_eq | policy_divert_feasible
field_path: estimate.energy.reserve_at_landing_wh
expected: 100.0
wind_changeevents carry scalar wind orwind_layersand apply from the trigger time onward;landing_zone_unavailableevents needunavailable_zone_idsand re-evaluate reachability from that point.- A
lost_linkevent may carry its ownpolicyblock, overriding the global policy for that event only. Divert outcomes include a Dubins-pathdivert_estimate(distance, time, energy, reserve after divert, feasibility) in the report. field_pathuses dot notation over the estimate result (estimate.status,estimate.energy.reserve_at_landing_wh,estimate.geofence.is_feasible, …). An unrecognized path yields anunsupportedoutcome whose JSON lists all valid paths; paths for unevaluated blocks yieldskipped. Neither fails the scenario by itself.
Uncertainty and stochastic plans¶
uncertainty.v2 (for sample) and stochastic.v2 (for propagate) share
the same five samplable parameters; unset parameters hold their deterministic
value:
schema_version: uncertainty.v2 # or stochastic.v2
uncertainty_id: wind-sweep # propagation_id for stochastic.v2
mission_file: mission.yaml
vehicle_file: vehicle.yaml
samples: 200 # stochastic.v2 max 10 000, plus dt_s and
seed: 42 # wind_process_noise_std_mps: 0.0
parameters:
wind_east_mps: {kind: normal, mean: 0.0, std: 2.0}
wind_north_mps: {kind: normal, mean: 0.0, std: 2.0}
cruise_speed_mps: {kind: uniform, low: 14.0, high: 22.0}
cruise_power_w: {kind: uniform, low: 400.0, high: 500.0}
battery_capacity_wh: {kind: uniform, low: 850.0, high: 950.0}
normal (fields mean, std > 0) is allowed for wind components only;
positive physical parameters require a bounded uniform with low > 0 —
draws are never clipped to an invented floor. Runs are deterministic for a
fixed seed.
SORA evidence¶
The sora command refuses to guess. Beyond a population grid it requires
three mission blocks, all explicit:
airspace:
class: "G"
max_altitude_agl_m: 130.0
operational_and_contingency_volume_assessment_reference: "Airspace study AS-014 rev 2"
worst_case_arc_declared: true
aerodrome_environment: false # mandatory boolean (Annex I definition)
atypical_or_segregated: false # true is rejected without authority evidence
over_urban_area: false
transponder_mandatory_zone: false # mandatory boolean
entirely_above_flight_level_600: false
sora:
version: "2.5" # only supported revision
ground_risk_footprint:
operational_volume_margin_m: 30.0 # route to outer contingency volume
ground_risk_buffer_m: 130.0 # initial 1:1 GRB >= maximum_height_agl_m
maximum_height_agl_m: 130.0 # must cover route AGL + vertical margin
buffer_method: initial_1_to_1
vertical_contingency_margin_m: 10.0
derivation: "Operational volume and GRB study GRB-2026-014"
containment_evidence:
assessment_reference: "Adjacent-area study CONT-2026-004"
average_population_density_ppl_km2: 1200.0
largest_outdoor_assembly: below_40000
sheltering_applicable: true
ground_risk_mitigations:
m1a_sheltering: {applied: false, robustness: none}
m1b_operational_restrictions: {applied: false, robustness: none}
m1c_ground_observation: {applied: false, robustness: none}
m2_impact_reduction: {applied: false, robustness: none}
The population evidence must be population-grid.v2: conservative source-cell
maxima plus metadata with source, population_year,
native_resolution_m/effective_resolution_m,
value_semantics: conservative_cell_maximum, an
authority_assessment_reference, a valid_from/valid_until window
containing departure_time, and a transient-population/assemblies assessment.
WorldPop point samples and legacy grids stay diagnostic-only.
Produce the file from an authority-exported raster with
bvlos_sim/scripts/build_population_grid.py (entry point
bvlos-build-population-grid): it converts an ESRI ASCII grid or a
lat/lon/density CSV using per-cell maxima, refuses uncovered cells, and
requires every metadata field as a flag. The tool guarantees format and
max-pooling only — authority approval and data conservatism remain the
operator's obligations.
Every applied: true mitigation earns no credit until an Annex B criteria
evaluator exists: the assessment still runs with the final GRC equal to the
intrinsic GRC, records each declaration as credit_rejected_pending_annex_b
in the result and the report, and exits 10. Population
density exactly at a band boundary (for example 50,000 ppl/km²) is assigned to
the stricter band. Medium/high containment requires a reference showing the
GRB was fed back through Step 2; Annex E compliance is always
not_assessed.
Contracts and versioning¶
Input schemas (mission.v7, vehicle.v6, scenario.v1, uncertainty.v2,
stochastic.v2, batch.v1, the GeoJSON asset schemas, population-grid.v2)
and output envelopes (estimator-envelope.v11, scenario-report.v4, and the
rest printed by schema-versions) are stable public
contracts. Within a published version, fields are not removed or renamed, enum
and exit-code meanings do not change, and canonical JSON rendering stays
byte-stable — representative outputs are pinned by golden fixtures. When a
change is intentional, the version identifier bumps and fixtures, tests, and
docs move in the same commit (see
CONTRIBUTING).