Mission Patterns
This tutorial shows how to use the current Octavian options as building blocks. Start with the quick API when the mission is a standard inertial transfer, relative hop, or chain of relative transfers. Move to the composable API when you need explicit phases, links, custom constraints, finite burns, or an arbitrary burn topology.
Choose the API Layer
Use two_burn_rendezvous when your mission is:
- one transfer from
x0toxf, - optionally preceded by a bounded precoast,
- solved with impulsive departure and arrival maneuvers,
- seeded by Lambert guesses.
Use Mission and Phase directly when you need:
- more than one explicitly named phase,
- a continuous or impulsive link between phases,
- path constraints such as minimum radius,
- terminal orbital-element constraints,
- finite chemical-burn phases,
- J2 perturbations,
- custom objectives or per-phase mesh settings,
- chief-centered relative motion, including exact finite burns and coasts.
Use relative_hop or relative_transfer_chain when the relative mission is:
- a bounded coast followed by a two-impulse hop, or
- multiple two-impulse transfers separated by bounded natural coasts.
Pattern 1: Standard Two-Impulse Transfer
mission = two_burn_rendezvous(
x0,
xf,
mu_m3ps2=MU,
tf_bounds_s=(3_000.0, 7_000.0),
nsegs=60,
lambert_grid_size=60,
nrevs_to_try=(0,),
)
Use this when final time is unknown but bounded. Increase
lambert_grid_size if the transfer has a narrow feasible window. Add
additional revolution counts with nrevs_to_try=(0, 1) when multi-rev
solutions are physically plausible and useful.
Pattern 2: Add a Precoast
mission = two_burn_rendezvous(
x0,
xf,
precoast=True,
t1_bounds_s=(1.0, 1_000.0),
tf_bounds_s=(1_200.0, 12_000.0),
precoast_grid_size=12,
lambert_grid_size=50,
)
precoast=True turns the quick builder into a two-phase mission. The first
phase coasts from the initial state; the second phase performs the transfer.
t1_bounds_s bounds the end of the precoast. precoast_grid_size controls how
many candidate departure times are searched before optimization.
Keep t1_bounds_s strictly positive for solver-backed runs. A zero-duration
precoast is usually a degenerate phase rather than a useful mission design.
Pattern 3: Trade Delta-v Against Time
slow = two_burn_rendezvous(x0, xf, w_time=0.0)
fast = two_burn_rendezvous(x0, xf, w_time=2.0)
The quick API always minimizes total delta-v. w_time adds a final-time term to
the objective. Larger values bias toward shorter transfers, usually at higher
delta-v.
Pattern 4: Explicit Phase Linking
precoast = Phase(
name="precoast",
mode="coast",
spacecraft=spacecraft,
dynamics=dynamics,
tof_bounds_s=(0.0, 6_000.0),
constraints=[constraints.state(x0, where="Front")],
)
transfer = Phase(
name="transfer",
mode="coast",
spacecraft=spacecraft,
dynamics=dynamics,
previous=precoast,
link=links.impulsive(),
tof_bounds_s=(400.0, 60_000.0),
constraints=[constraints.state(xf, where="Back")],
variables=[
variables.ImpulsiveDeltaV(where="Front"),
variables.ImpulsiveDeltaV(where="Back"),
],
)
links.continuous() means the next phase starts with the same position and
velocity as the previous phase ended.
links.impulsive() means position and time remain continuous, but velocity can
jump. Add ImpulsiveDeltaV(where="Front") on the linked phase to expose that
jump as a maneuver and objective term.
Pattern 5: Add Path Constraints
constraints.min_radius(r_min_m, where="Path")
Path constraints apply along a phase, not only at boundaries. Minimum radius is useful for keeping transfer arcs above an altitude floor.
Pattern 6: Target Orbital Elements
constraints.semi_major_axis(target_a_m, where="Back", tol_m=2.0e3)
constraints.eccentricity(target_e, where="Back", tol=5.0e-3)
constraints.inclination_deg(target_inc_deg, where="Back", tol_deg=0.2)
Use orbital-element constraints when the final orbit matters more than a single Cartesian state. A Cartesian final state can still be supplied as a seed anchor for the solver.
Pattern 7: Use J2 Perturbations
dynamics = Dynamics(mu_m3ps2=MU, perturbations=Perturbations(j2=True))
J2 is currently implemented in the composable ASSET backend. Other perturbation flags are reserved for future extensions and fail clearly if requested.
Pattern 8: Finite-Thrust Phases
spacecraft = Spacecraft(
name="Demo spacecraft",
dry_mass_kg=500.0,
thrusters=[
Thruster(
name="main",
thrust_N=2_000.0,
isp_s=320.0,
propellant_mass_kg=50.0,
)
],
)
Powered phases use mode="finite_thrust" and require a thruster with positive
thrust and specific impulse. The chemical-specific compatibility spelling is
mode="chemical_burn". The state includes mass, and the solver tracks
propellant usage through mass depletion.
powered = Phase(
name="injection",
mode="finite_thrust",
spacecraft=spacecraft,
dynamics=dynamics,
)
mission = Mission(
phases=[powered],
objectives=[objectives.minimize_propellant()],
)
A common finite-burn transfer uses:
- departure chemical burn,
- coast,
- arrival chemical burn.
This is a mission pattern, not a compiler restriction. Standalone powered phases and longer powered/coast sequences are supported. Coast phases between powered phases carry mass automatically; all phases in that chain must use the same spacecraft configuration.
Pattern 9: Plot the Result
save_trajectory_html(
sol.result.traj,
"trajectory.html",
maneuvers=sol.result.maneuvers,
title=mission.name,
)
Use maneuver markers for impulsive examples. Use phase_segments for
burn-coast-burn examples so the plot shows which part of the trajectory belongs
to each phase.
Pattern 10: Declare Frame And Scaling
from octavian import Dynamics, SolverScaling
from octavian.coordinates import inertial
dynamics = Dynamics(
mu_m3ps2=MU,
frame=inertial("earth", orientation="ECI"),
scaling=SolverScaling(
length_m=7_000e3,
velocity_mps=7_500.0,
time_s=1_000.0,
mass_kg=500.0,
),
)
Explicit scaling is useful when a problem spans very different distance, time,
or mass scales. It does not change the units of mission inputs or solution
arrays. Leave scaling=None to use Octavian's endpoint-derived defaults. The
result records both declarations:
print(solution.frame)
print(solution.scaling)
Pattern 11: Change The Central Body
from octavian import SUN, two_burn_rendezvous
mission = two_burn_rendezvous(
x0,
xf,
central_body=SUN,
tf_bounds_s=(20.0e6, 30.0e6),
)
The body declaration supplies gravitational parameter, reference radius, J2
coefficient, and inertial frame origin together. Named body constants override
raw mu_m3ps2 values so the configuration cannot silently mix Earth and Sun
properties. For a custom object, construct CelestialBody explicitly.
Pattern 12: Optimize Relative Motion With CWH
dynamics = Dynamics.cwh(
chief_orbit_radius_m=EARTH.mean_radius_m + 400_000.0,
central_body=EARTH,
chief_name="Chief",
reference_length_m=1_000.0,
)
phase = Phase(
mode="relative_coast",
spacecraft=deputy,
dynamics=dynamics,
initial_state=initial_relative_state,
final_state=final_relative_state,
tof_bounds_s=(1_200.0, 2_400.0),
constraints=[
constraints.state(initial_relative_state, where="Front"),
constraints.state(final_relative_state, where="Back"),
],
variables=[
variables.impulsive_delta_v(at="Front"),
variables.impulsive_delta_v(at="Back"),
],
)
Relative positions and velocities use the chief LVLH/RTN frame. Use
relative.inertial_to_relative_state(...) and
relative.relative_to_inertial_state(...) to transform analysis states at a
known chief state. CWH assumes a circular chief and small deputy separation;
use a nonlinear model when those assumptions are not appropriate.
For exact central gravity with perturbations, keep the same public RIC states and use the coupled-ECI formulation:
dynamics = Dynamics.relative(
chief_initial_state_eci=chief_initial_state_eci,
central_body=EARTH,
propagation_mode="coupled_eci",
perturbations=Perturbations(j2=True, sun=True),
)
The compiler uses CWH only to seed the solve. It propagates the chief and deputy as two absolute states and converts them to RIC for constraints and results.
Compose multiple relative phases with previous= exactly as for inertial
missions. The compiler links the formulation's native state, so a coupled ECI
chain preserves both spacecraft states without round-tripping through a
boundary conversion.
When Sun or Moon tables are active, each relative duration contributes to the
cumulative absolute mission-time horizon. The shared table extends through
that absolute upper bound plus third_body_table_margin_s; increase the margin
for unusually aggressive time searches or custom solver behavior.
Numeric ephemeris queries clip solver time roundoff within 1e-8 seconds
of either table endpoint. Queries farther outside coverage still raise an
error; this tolerance does not extend the propagation horizon.
For the common coast–transfer–coast pattern, use the quick builder:
mission = relative_hop(
initial_ric,
target_ric,
chief_initial_state_eci=chief_eci,
departure_coast_time_bounds_s=(120.0, 600.0),
transfer_time_bounds_s=(900.0, 1_800.0),
perturbations=Perturbations(j2=True),
)
String complete transfers together by supplying ordered post-arrival states:
mission = relative_transfer_chain(
initial_ric,
[inspection_point_ric, final_ric],
chief_initial_state_eci=chief_eci,
transfer_time_bounds_s=[(600.0, 1_200.0), (600.0, 1_200.0)],
coast_time_bounds_s=(300.0, 600.0),
)
This example has four impulses. To optimize a three-burn topology, build three linked composable phases and leave the intermediate burn's velocity free while constraining only its RIC position waypoint, as shown in composable example 25.
For exact two-body dynamics with native RIC decision variables, select
propagation_mode="nonlinear_ric" for a circular chief or "coupled_ric" for
a propagated circular/eccentric chief. Then target one component directly:
constraints.ric_state("I", -100.0, where="Back")
For a native D'Amico phase, use
propagation_mode="damico", fix the Front vector with
constraints.relative_orbital_elements(...), and target any individual
element with constraints.relative_orbital_element(...). Time remains free
when tof_bounds_s is a nonzero interval.
Save state, range, and solar-phase histories beside either trajectory plot:
solution.viz().save_diagnostics_html("relative_diagnostics.html")
For analysis-only ROE propagation with the same force-model vocabulary:
from octavian import propagate
history = propagate.relative_elements(
initial_roe,
np.linspace(0.0, 6.0 * 3_600.0, 145),
chief_initial_state_eci=chief_eci,
mu_m3ps2=EARTH.mu_m3ps2,
perturbations=Perturbations(j2=True, sun=True),
initial_epoch="2026-01-01T00:00:00Z",
)
osculating_elements = history.elements
ric_for_plotting = history.ric
Use times ending at zero, such as [-600.0, -300.0, 0.0], to generate a
pre-event coast. Use times starting at zero for a post-event coast.
For differential drag or SRP, give both participants their own cannonball properties and use the exact coupled propagator:
chief_vehicle = Spacecraft(
name="Chief",
dry_mass_kg=300.0,
cannonball=Cannonball(drag_area_m2=2.0, srp_area_m2=3.0),
)
deputy_vehicle = Spacecraft(
name="Deputy",
dry_mass_kg=150.0,
cannonball=Cannonball(drag_area_m2=4.0, srp_area_m2=5.0),
)
forces = Perturbations(j2=True, drag=True, srp=True)
history = propagate_relative_orbital_elements(
initial_roe,
times_s,
chief_initial_state_eci=chief_eci,
mu_m3ps2=EARTH.mu_m3ps2,
perturbations=forces,
initial_epoch="2026-01-01T00:00:00Z",
chief_spacecraft=chief_vehicle,
deputy_spacecraft=deputy_vehicle,
)
The default atmosphere is Earth-only. SRP uses the bundled Earth-centered Sun
BSP and does not enable solar third-body gravity unless sun=True is also set.
See the Cannonball Drag And SRP tutorial for formulas and fidelity limits.
Pattern 13: Add Relative Safety And Lighting Geometry
constraints.keep_out_sphere(
radius_m=75.0,
center_m=[0.0, 0.0, 0.0],
)
constraints.approach_cone(
axis=[0.0, -1.0, 0.0],
half_angle_deg=30.0,
)
constraints.lighting_angle(
sun_direction=[1.0, 0.0, 0.0],
min_angle_deg=85.0,
max_angle_deg=121.0,
)
All vectors and origins use the phase's declared frame. The approach cone is
one-sided, so the opposite direction does not satisfy it. Lighting bounds use
a fixed direction over the phase; transform or update that direction when a
longer arc needs time-varying Sun geometry. Constraint extrema and satisfaction
flags are included in solution.result.info["constraint_report"].
Pattern 14: Compose Relative Finite Burns And Coasts
dynamics = Dynamics.relative(
chief_initial_state_eci=chief_initial_state_eci,
propagation_mode="coupled_eci",
)
departure = Phase(
mode="finite_thrust",
spacecraft=deputy,
dynamics=dynamics,
initial_state=initial_ric,
tof_bounds_s=(50.0, 70.0),
)
coast = Phase(
mode="relative_coast",
spacecraft=deputy,
dynamics=dynamics,
previous=departure,
tof_bounds_s=(250.0, 350.0),
tof_is_relative=True,
)
arrival = Phase(
mode="finite_thrust",
spacecraft=deputy,
dynamics=dynamics,
previous=coast,
final_state=target_ric,
tof_bounds_s=(50.0, 70.0),
tof_is_relative=True,
)
Only the deputy is powered. The chief and deputy both receive their declared
gravity and perturbation accelerations, while the coast keeps deputy mass
constant and continuous between burns. Add
objectives.minimize_propellant() to minimize the powered-chain consumption.
Finite thrust currently requires the exact "coupled_eci" formulation;
"nonlinear_ric", "coupled_ric", and relative-element modes remain
propulsion-free.
Pattern 15: Seed A Low-Thrust Spiral
from octavian import guesses
phase = Phase(
mode="low_thrust",
spacecraft=electric_spacecraft,
dynamics=dynamics,
initial_state=initial_circular_state,
final_state=terminal_radius_anchor,
tof_bounds_s=(14 * 3_600.0, 24 * 3_600.0),
initial_guess=guesses.low_thrust_spiral(
throttle=0.85,
direction="auto",
),
constraints=[
constraints.state(initial_circular_state, where="Front"),
constraints.semi_major_axis(target_radius_m, where="Back", tol_m=10_000.0),
constraints.eccentricity(0.01, where="Back", tol=0.0099),
],
)
The seed estimates tangential spiral delta-v, converts it to burn time through
the rocket equation, and integrates gravity, thrust, and mass at the requested
seed throttle. direction="auto" selects prograde for a larger target radius
and retrograde for a smaller one. The optimizer does not retain this steering
law; the vector throttle at every collocation point remains free.
Use a final Cartesian state only as the target-radius and scaling anchor, then
constrain terminal orbital elements to leave longitude free. This built-in seed
assumes a near-circular, approximately coplanar transfer. Adjust time_scale
or provide a future custom seed for strongly eccentric or plane-changing arcs.
Higher-degree central-body gravity
Use Dynamics(perturbations=Perturbations(spherical_harmonics=gravity)) on
coast or powered phases, where gravity is a SphericalHarmonics model.
The same declaration works for coupled ECI relative missions. Choose
backend="python" for an ASSET expression or backend="cpp" for the optional
native evaluator. See Spherical-Harmonic Gravity.