Mission 3D tracks

Requires the viz extra (pip install "longeron[viz]").

The Cesium viewer widget that plays these tracks lives in longeron.widgets.mission3d; this module is the kernel-side track/CZML synthesis.

Mission flight tracks for the CesiumJS globe: synthesis and CZML baking.

Turns MODEL-level mission data into a timestamped geodetic track and animates a drone flying it over a real globe. Two builders produce the same MissionTrack:

  • mission_track() – explicit waypoints (lat, lon, alt[, t]) (degrees, meters above the ellipsoid, seconds). Missing times derive from a cruise speed over the 3D leg lengths. model_waypoints() reads the same tuples off a mission part’s children (attributes named lat/lon/alt, optional t) through the interpreter, so the waypoints can live in the model itself.

  • from_replay() – the state-machine timeline: the interpreter executes the machine through longeron.replay.record_timeline() (the same recorder the diagram replay widget uses) and each leaf state’s activation interval becomes one motion segment. track_from_timeline() is the same synthesis over an EXISTING Timeline, so one recording can feed the diagram replay, the globe, and the time seam’s scrubber without re-simulating (see longeron.widgets.time). The mapping is deliberately simple and name-driven – the point is model-driven animation (the globe shows the ACTUAL executed behavior: state durations, interleavings, reentries), not flight-sim fidelity. A state’s phase comes from substring hints on its name, overridable per state name via phases=:

    ground (idle, standby, parked, …)

    hold the current position; the mission starts at the first waypoint at ground_alt.

    takeoff (takingOff, launch, climb, …)

    vertical climb, in place, to the route altitude at the current route position.

    route (flying, cruise, loiter, hover, survey, …)

    advance along the waypoint polyline; each segment covers a distance proportional to its share of the total route-phase time, so several flying/loiter states spend the one route between them and the drone lands wherever the machine stopped flying.

    landing (landing, descend, …)

    vertical descent, in place, to ground_alt.

    hold (anything unrecognized)

    hover in place.

    Pure event cascades (no clock advance) record in step mode; each step then counts as seconds_per_step seconds of flight – a scalar, or a per-step sequence/mapping when steps take unequal durations (longeron.widgets.time.step_seconds() states the exact ladder).

The trace-to-mission binding can ride the model itself: model_waypoints() reads the route off a mission part’s children, and model_epoch() reads the mission epoch off an attribute typed Time::Iso8601DateTime (vendored; resolves), so the model states WHERE and WHEN the sortie flies. Both fall back honestly: no epoch attribute means the deterministic default epoch.

MissionTrack.to_czml() bakes the track as a CZML document: a grey planned-route polyline, small waypoint pins, and a drone entity that flies the samples with an orange trail, its label following the ACTIVE STATE name through the mission (CZML interval text – the state machine is visibly driving the animation). Pass the drone’s own analysis mesh (mesh=, the dict longeron.analysis.geometry.drone_geometry() and its siblings build) and the ACTUAL airframe geometry flies the route: the mesh exports to a self-contained binary glTF through mesh_to_glb() (in-house, stdlib-only – see longeron.analysis._glb for the exact container), embeds in the CZML as a data: URI (tens of kB for the quad), flies with a MULTIROTOR ATTITUDE, and never shrinks below a legible pixel size however far the camera sits; model_scale blows it up beyond true scale when the route dwarfs the airframe. Without a mesh the drone stays the point entity.

Attitude: a multirotor moves vertically props-up and cruises with a small forward tilt – Cesium’s VelocityOrientationProperty (which points the nose along the velocity vector, so a climb renders the quad VERTICAL) is deliberately NOT used. Instead the track bakes a sampled orientation into the CZML (unitQuaternion keyframes, computed by MissionTrack.attitude()): yaw follows the track heading, pitch is 0 wherever there is no horizontal motion (climb, descent, hover, ground) and the tilt_deg forward tilt while the drone advances along the route, roll stays 0, and orientation changes blend over a few seconds (Cesium slerps between adjacent quaternion samples). A CZML-sampled property is chosen over a front-end CallbackProperty because the module’s whole design bakes the payload kernel-side: the samples are deterministic, testable without a browser, and need no front-end code. The tilt itself should come FROM THE MODEL: model_tilt() evaluates the airframe’s own physics (e.g. the DeepScout MultiRotor’s cruiseTilt: the arccos altitude-hold ceiling at continuous thrust, capped by the operational comfort limit) and feeds mission_track / from_replay tilt_deg=, a plain float override. mission_values() completes the loop: it measures the route’s waypoint legs and evaluates the model’s MissionTime calc at the model’s own achievable cruise speed, producing the scoreboard values= bindings for the mission-time requirement.

The viewer widget itself – mission_viewer(), which plays the baked CZML on a Cesium Viewer – lives in longeron.widgets.mission3d: this module is the kernel-side synthesis, deterministic and testable without a browser. Importing mission_viewer (or the CESIUM_* CDN pins) from here still works but is deprecated.

class longeron.analysis.mission3d.MissionTrack(name, epoch, samples, waypoints, phases, tilt_deg=0.0)[source]

Bases: object

A timestamped geodetic flight path synthesized from the model.

samples are (t, lat, lon, alt) keyframes (seconds past epoch, degrees, meters above the ellipsoid) with strictly increasing times; waypoints is the planned route the samples fly; phases records the motion segments as (t0, t1, phase, qname) – for replay-built tracks qname is the instance-qualified name of the driving leaf state (empty for plain waypoint tracks); tilt_deg is the forward cruise tilt the airframe holds while it moves along the route (degrees nose-down; ideally the MODEL’s own number – see model_tilt()).

property duration: float

Track length in seconds (the last phase end / sample time).

to_czml(*, mesh=None, model_scale=1.0)[source]

The CZML document the widget plays.

Packets: a document packet whose clock spans the mission (CLAMPED, multiplier sized so playback takes ~40 s of wall clock), the planned-route polyline, one pin per waypoint, and the mission-drone entity – sampled positions with linear interpolation, an orange trail, a viewFrom camera offset sized from the route span, and a label whose text follows the active state name through the phases. With mesh (a geometry-module mesh dict) the drone entity is the airframe’s own glTF model – mesh_to_glb() output on a data: URI, nose steered along the track heading with the multirotor attitude (see attitude()), scaled by model_scale and clamped to a legible minimum pixel size – instead of the fallback point.

Return type:

list[dict[str, Any]]

attitude(*, blend_s=3.0)[source]

The orientation keyframes: (t, heading_deg, pitch_deg).

Heading follows the track’s direction of travel (great-circle initial bearing per motion segment, degrees clockwise from true north); hover/vertical segments hold the last heading flown (or face the first leg before anything has moved). Pitch is 0 wherever the drone has no horizontal motion – a multirotor climbs, descends, and hovers props-up – and -tilt_deg (nose-down forward tilt) while it advances along the route. Roll is always 0. Orientation changes blend over blend_s seconds, half on each side of the motion change, clamped to the neighboring segments’ midpoints so keyframe times stay strictly increasing.

Return type:

list[tuple[float, float, float]]

longeron.analysis.mission3d.from_replay(interpreter, state_machine, events=None, *, waypoints, inputs=None, phases=None, ground_alt=0.0, tilt_deg=0.0, seconds_per_step=10.0, epoch=None, name=None)[source]

A track driven by the state machine’s ACTUAL execution.

Simulates state_machine with events (the Interpreter.simulate protocol: event names or (name, payload) tuples, plain numbers advance the clock) via longeron.replay.record_timeline(), then synthesizes the track with track_from_timeline() – see there for waypoints, phases, ground_alt, seconds_per_step, and epoch. tilt_deg is the forward cruise tilt the airframe holds while it advances along the route (degrees nose-down; derive it from the model with model_tilt(), or pass any plain float). name defaults to the machine’s own name. To share one recording across the diagram replay and the globe, record once and call track_from_timeline() instead.

Return type:

MissionTrack

longeron.analysis.mission3d.mesh_to_glb(mesh)[source]

A viewer3d-style mesh dict as one binary glTF 2.0 blob.

mesh is the dict the longeron.analysis.geometry builders produce – parts with name/color/opacity and flat vertices/faces arrays. Every part becomes its own node + mesh + material under a single rotated root (see the module docstring for the exact container and scene shape), so per-part colors and translucency survive the export. Raises AnalysisError on a mesh with no parts, malformed vertex/face arrays, or out-of-range indices.

Return type:

bytes

longeron.analysis.mission3d.mission_track(waypoints, *, speed_mps=12.0, tilt_deg=0.0, epoch=None, name='mission')[source]

A track that flies explicit waypoints, one route phase.

waypoints are (lat, lon, alt) or (lat, lon, alt, t) tuples (degrees, meters above the ellipsoid, seconds past epoch); either every waypoint carries a time or none does, in which case times derive from speed_mps over the 3D leg lengths. tilt_deg is the forward cruise tilt the airframe holds along the route (degrees nose-down; derive it from the model with model_tilt(), or pass any plain float). epoch defaults to a fixed instant so the CZML is deterministic.

Return type:

MissionTrack

longeron.analysis.mission3d.mission_values(interpreter, waypoints, *, ground_alt, assembly='Rotorcraft::QuadCopter', mission_calc='DeepScout::MissionTime', payload_mass=None)[source]

Scoreboard values= bindings for the mission-time requirement.

The kernel measures the GEOMETRY – the waypoint legs’ great-circle lengths, the climb from ground_alt to the first waypoint, and the descent home from the last (the same phase mapping from_replay() flies) – and the MODEL computes the physics: an assembly instance supplies the achievable cruise speed (maxCruiseSpeed at cruiseTilt) and the mission_calc calc def turns distances and speed into minutes. The returned dict (missionMinutes plus the cruiseTiltDeg / cruiseSpeedMps / routeM it derives from) injects straight into scoreboard(model, values=...), exactly like the geometry module’s occlusion measures.

payload_mass overrides the instance’s payloadMass – the one-line what-if: a heavier payload eats the continuous-thrust margin, the tilt ceiling and cruise speed collapse, and the mission budget busts. An airframe that cannot hold altitude at continuous thrust at all (cruise speed 0) reports an INFINITE mission time.

Return type:

dict[str, float]

longeron.analysis.mission3d.model_epoch(interpreter, mission, *, attribute='epoch')[source]

The mission’s stated epoch, read off the model.

Evaluates attribute on mission – an attribute typed Time::Iso8601DateTime (the vendored standard time package’s UTC instant, carried as an ISO 8601 string) – and returns it as an aware UTC datetime, ready for the track builders’ epoch=. Returns None when the mission states no such attribute, so callers fall back to the deterministic default epoch honestly; a stated value that is not ISO 8601 is a loud AnalysisError.

Return type:

datetime | None

longeron.analysis.mission3d.model_tilt(interpreter, assembly, *, attribute='cruiseTilt')[source]

The airframe’s own cruise tilt, degrees, read off the model.

Instantiates assembly and evaluates attribute – for the DeepScout MultiRotor family that is cruiseTilt, the CruiseTilt calc: the altitude-hold ceiling arccos(m g / T) at continuous thrust, capped by the operational comfort limit. Feed the result to mission_track() / from_replay() tilt_deg= so the globe animation flies the MODEL’s physics (any plain float still works as an override).

Return type:

float

longeron.analysis.mission3d.model_waypoints(interpreter, mission, *, lat='lat', lon='lon', alt='alt', time='t')[source]

Waypoints read off the model: mission’s child parts that carry lat/lon attributes, evaluated through the interpreter in declaration order (alt defaults to 0 where absent; an optional time attribute becomes the explicit timestamp).

Return type:

list[tuple[float, ...]]

longeron.analysis.mission3d.track_from_timeline(timeline, *, waypoints, phases=None, ground_alt=0.0, tilt_deg=0.0, seconds_per_step=10.0, epoch=None, name='mission')[source]

A track synthesized from an EXISTING recording.

The synthesis half of from_replay(): it maps every leaf state’s activation interval in timeline (a longeron.replay.record_timeline() product) onto a motion segment along waypoints – see the module docstring for the phase table and phases= for per-state-name overrides. Because the timeline arrives prebuilt, the diagram replay widget and the globe can play the SAME recording (the time seam’s timebase discipline, longeron.widgets.time.Timebase).

waypoints are (lat, lon, alt) tuples (no times – timing comes from the recording); the mission starts at the first waypoint at ground_alt. Timed timelines keep their instants 1:1 (track seconds ARE trace seconds). Step-mode timelines have no time axis, so seconds_per_step states one: a scalar, or a per-step sequence/mapping when steps take unequal durations (longeron.widgets.time.step_seconds()). epoch anchors the track in UTC (model_epoch() reads it off the model; None keeps the deterministic default).

Return type:

MissionTrack