Drone. Formation
(ex_drone v0.3.0)
View Source
Pure geometric formation planners for swarm coordination.
Formations compute target slots in a shared world frame and return
per-drone Drone.Mission scripts that fly each member to its slot.
They do not run control loops, velocity sync, or flocking behaviours
(Separate / Align / Cohere live). Plan-time Separate is enforced via
min_separation_cm.
Supported formations
:front— side-by-side, perpendicular to heading (sweeps / advance):column— nose-to-tail along heading (narrow corridors):vee— V opening opposite travel (geometry only; no aero model):diamond— four-sided closed layout (requires 4 drones):echelon— diagonal stepped rank (side: :left | :right):circle— evenly spaced on a ring (radius_cm):shoulder_pair— alias for:frontwith two members:grid— optional classroom row/column lattice (columns)
Reference frame
Default origin is the centroid of current positions, or a configured
{:xy, x, y}. Optional :leader uses that member's pose at plan time
only — not a live runtime dependency. Yaw 0 means +Y
(same convention as the shared geometry helpers).
Movement strategy
For each drone: rotate to face the target bearing → move forward (split into ≤500 cm segments) → restore original yaw when needed. Horizontal moves shorter than 20 cm are skipped (SDK minimum).
Example
{:ok, missions} =
Drone.Formation.plan(:front, %{
drones: [:a, :b],
positions: %{
a: %{x: 0, y: 0, z: 30, yaw: 0},
b: %{x: 0, y: 0, z: 30, yaw: 0}
},
heading_deg: 0,
spacing_cm: 100,
min_separation_cm: 80,
origin: {:xy, 0, 0}
})
Drone.Mission.run(missions.a, :a)
Summary
Types
Built-in formation identifier.
Error atoms returned by plan/2.
Options for plan/2.
World-frame pose for one drone used as planning input.
Functions
Plans missions that move each drone into the given formation.
Types
@type formation() ::
:front
| :column
| :vee
| :diamond
| :echelon
| :circle
| :shoulder_pair
| :grid
Built-in formation identifier.
| Value | Min drones | Layout |
|---|---|---|
:front | 2 | Side-by-side ⊥ heading |
:column | 2 | Along heading |
:vee | 3 | V behind tip |
:diamond | 4 | N/E/S/W of origin |
:echelon | 2 | Diagonal stepped rank |
:circle | 3 | Ring around origin |
:shoulder_pair | 2 | Alias of :front |
:grid | 2 | Row/column lattice |
Examples
:front
:vee
:echelon
@type plan_error() ::
:separation_violation
| :unsupported_formation
| :too_few_drones
| :too_many_drones
| :duplicate_drones
| :invalid_option
| :leader_unavailable
| :missing_positions
| :unassigned_slot
Error atoms returned by plan/2.
:separation_violation— two planned slots closer thanmin_separation_cm:unsupported_formation— unknown formation atom:too_few_drones— membership below the formation minimum:too_many_drones— membership above a formation maximum (e.g.:diamond):duplicate_drones— repeated names in:drones:invalid_option— bad spacing, columns, side, heading, etc.:leader_unavailable—:leadermissing from:positions:missing_positions— a member lacks:x/:y:unassigned_slot— planner/assembly mismatch (should not occur)
Example
:separation_violation
@type plan_opts() :: %{ optional(:drones) => [atom()], optional(:positions) => %{optional(atom()) => position()}, optional(:heading_deg) => integer(), optional(:spacing_cm) => pos_integer(), optional(:min_separation_cm) => pos_integer(), optional(:origin) => :centroid | {:xy, number(), number()}, optional(:leader) => atom(), optional(:side) => :left | :right, optional(:radius_cm) => pos_integer(), optional(:columns) => pos_integer() }
Options for plan/2.
Fields
| Key | Type | Default | Meaning |
|---|---|---|---|
:drones | [atom()] | required | Ordered member names |
:positions | %{atom() => position()} | required | Current poses |
:heading_deg | integer() | 0 | Direction of motion |
:spacing_cm | pos_integer() | 100 | Neighbor spacing |
:min_separation_cm | pos_integer() | 80 | Reject closer planned slots |
:origin | :centroid | {:xy, number(), number()} | :centroid | Formation origin |
:leader | atom() | none | Plan-time origin = that member's pose |
:side | :left | :right | :right | Echelon side |
:radius_cm | pos_integer() | spacing_cm | Circle radius |
:columns | pos_integer() | ceil(sqrt(n)) | Grid column count |
Example
%{
drones: [:a, :b, :c],
positions: %{
a: %{x: 0, y: 0, yaw: 0},
b: %{x: 0, y: 0, yaw: 0},
c: %{x: 0, y: 0, yaw: 0}
},
heading_deg: 0,
spacing_cm: 100,
min_separation_cm: 80,
origin: :centroid,
side: :left,
radius_cm: 150,
columns: 2
}
@type position() :: %{ :x => number(), :y => number(), optional(:z) => number(), optional(:yaw) => number() }
World-frame pose for one drone used as planning input.
Fields
| Field | Type | Required | Meaning |
|---|---|---|---|
:x | number() | yes | East/west cm in shared world frame |
:y | number() | yes | North/south cm (yaw 0 faces +Y) |
:z | number() | no | Altitude cm (unused by horizontal planners) |
:yaw | number() | no | Heading degrees 0..359 (default 0) |
Example
%{x: -50, y: 0, z: 30, yaw: 90}
Functions
@spec plan(formation(), plan_opts() | keyword()) :: {:ok, %{required(atom()) => Drone.Mission.t()}} | {:error, plan_error()}
Plans missions that move each drone into the given formation.
Pure function: no processes, no I/O. Returns missions; callers execute them
(e.g. via Drone.Swarm.run/2 or Drone.Mission.run/2).
Positions accept numeric :x/:y/:yaw; values are rounded to integers
before path generation. Moves shorter than 20 cm are skipped (SDK minimum);
longer legs are split into segments of at most 500 cm with any remainder
redistributed so the planned slot is reached exactly.
Parameters
formation(formation/0) — which layout to computeopts(plan_opts/0orkeyword()) — drones, positions, spacing, etc.
Returns
{:ok, %{atom() => Drone.Mission.t()}}— one mission per drone{:error, plan_error()}— validation / geometry failure
Examples
{:ok, missions} =
Drone.Formation.plan(:front,
drones: [:left, :right],
positions: %{
left: %{x: 0, y: 0, yaw: 0},
right: %{x: 0, y: 0, yaw: 0}
},
spacing_cm: 100,
origin: {:xy, 0, 0}
)
{:error, :separation_violation} =
Drone.Formation.plan(:front, %{
drones: [:a, :b],
positions: %{a: %{x: 0, y: 0}, b: %{x: 0, y: 0}},
spacing_cm: 40,
min_separation_cm: 80,
origin: {:xy, 0, 0}
})