planner

package
v0.36.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package planner implements trajectory prediction (predictor) and stubs for Phase 3 maneuver-library work (hohmann, lambert) that slip past v0.1.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrEquatorialOrbit is retained for callers that want to detect
	// the equatorial case explicitly. v0.8.2.x: PlanInclinationChange
	// no longer returns this — instead it fires at the current state
	// (any point on an equatorial orbit can host a plane-tilt burn).
	ErrEquatorialOrbit  = errors.New("planinclination: source orbit is equatorial — no defined node line")
	ErrHyperbolicOrbit  = errors.New("planinclination: source orbit is hyperbolic / degenerate")
	ErrInclinationRange = errors.New("planinclination: target inclination must be in [0, π] radians")
	ErrInclinationNoOp  = errors.New("planinclination: source already at target inclination")
	ErrNoNodeReachable  = errors.New("planinclination: no future node crossing reachable from current state")
)
View Source
var ErrFiniteBurnDiverged = errors.New("planner: finite-burn iteration did not converge")

ErrFiniteBurnDiverged means IterateForTarget hit maxIter without landing within tolerance, or the numerical derivative collapsed to zero. Callers should fall back to the impulsive guess.

View Source
var ErrInvalidOrbit = errors.New("planner: invalid orbit (r1, r2, mu must be > 0)")

ErrInvalidOrbit is returned when HohmannTransfer is asked to solve for a non-physical input (non-positive radius or mu).

View Source
var ErrNotImplemented = errors.New("planner: not implemented")

ErrNotImplemented is returned by planner entry points that are still stubbed (e.g. Lambert in v0.2).

Functions

func CaptureBurnDeltaV added in v0.3.1

func CaptureBurnDeltaV(vInfinity, muPlanet, rCapture float64) (float64, error)

CaptureBurnDeltaV mirrors EscapeBurnDeltaV for arrival: Δv to drop from a hyperbolic approach (excess speed vInfinity) into a circular orbit of radius rCapture around the destination primary. By symmetry the magnitude equals EscapeBurnDeltaV; provided as a named helper so the transfer-plan layer reads naturally.

func EscapeBurnDeltaV added in v0.3.1

func EscapeBurnDeltaV(vInfinity, muPlanet, rPark float64) (float64, error)

EscapeBurnDeltaV returns the prograde Δv that, applied at periapsis of a circular parking orbit of radius rPark around a primary with gravitational parameter muPlanet, yields a hyperbolic escape trajectory whose excess speed at infinity is vInfinity.

Patched-conic identity (vis-viva at hyperbolic periapsis):

v_peri² = v∞² + 2·µ/r_peri
Δv      = v_peri − v_circ

The result is in m/s (matching the SI used everywhere else in this repo). vInfinity is taken as a magnitude — direction is the caller's concern (typically aligned with the outbound asymptote, which the transfer-plan layer handles via Lambert).

func HohmannTransfer

func HohmannTransfer(r1, r2, mu float64) (dv1, dv2, tTransfer float64, err error)

HohmannTransfer computes the two impulsive burns and transfer time for a circular-to-circular coplanar Hohmann transfer between orbital radii r1 and r2 around a primary with standard gravitational parameter mu. All SI units: r1, r2 in meters, mu in m^3/s^2.

Returned dv1 and dv2 are magnitudes (always ≥ 0). Direction is implicit in r1 vs r2: outbound (r2 > r1) → both burns prograde; inbound → both retrograde. tTransfer is the half-period of the transfer ellipse (time between burn 1 and burn 2).

func IterateForTarget added in v0.6.2

func IterateForTarget(
	init physics.StateVector,
	mu, thrust, isp, initialDvGuess float64,
	direction DirectionFn,
	residual ResidualFn,
	tolerance float64,
	maxIter int,
) (float64, physics.StateVector, error)

IterateForTarget Newton-iterates the commanded Δv until the residual function evaluates within `tolerance` (in residual units — for TargetApoapsis that's metres). Each iteration runs SimulateFiniteBurn twice: once at the current guess and once at a nudged guess to estimate dResidual/dDv numerically.

Returns the converged commandedDv and the post-burn state. Errors out with ErrFiniteBurnDiverged after maxIter without convergence — callers should fall back to the impulsive guess in that case (still better than no plan).

Use case: v0.5.10's S-IVB-1 default vessel makes finite-burn gravity-rotation loss < 0.1% on the Earth → Luna profile, so the impulsive Hohmann is "good enough" out of the box. v0.6.2 ships this iterator for low-TWR loadouts (revived ICPS, future ion stages) where the impulsive guess can mis-deliver apoapsis by 20 %+.

func LambertSolve

func LambertSolve(r1, r2 orbital.Vec3, dt, mu float64, retrograde bool) (v1, v2 orbital.Vec3, err error)

LambertSolve is the single-revolution (N=0) entry point to the Lambert solver. Set retrograde=false for the standard prograde transfer (anticlockwise as viewed from +z); retrograde=true selects the clockwise branch — useful for matching a target's orbital direction when the system happens to spin retrograde, or for exploring multi-rev porkchop alternatives. v0.7.5+.

func LambertSolveRev added in v0.3.3

func LambertSolveRev(r1, r2 orbital.Vec3, dt, mu float64, nRev int, retrograde, longBranch bool) (v1, v2 orbital.Vec3, err error)

LambertSolveRev solves Lambert's problem for an N-revolution transfer: given two position vectors, a time of flight, and a revolution count, find the velocity vectors that connect them on a Keplerian orbit completing exactly N full revs before reaching r2.

Algorithm: Curtis "Orbital Mechanics for Engineering Students" Algorithm 5.2 — universal-variables formulation, Newton-Raphson on z. For N-rev transfers the lower bound on z shifts to (2πN)² (each rev contributes (2π)² to the universal-variable domain); the bracket sweep starts just past that lower bound.

At N ≥ 1 there are typically two time-of-flight solutions per N — a "short" branch (lower z, more eccentric ellipse) and a "long" branch (higher z) flanking the minimum-energy critical z. longBranch=false returns the short branch (the default; v0.10.5 and earlier behavior); longBranch=true seeds Newton from just below the upper bound so it converges onto the long-branch root instead. For N = 0 the flag is ignored (single branch).

retrograde flips the transfer-angle convention: prograde takes the short way when (r1 × r2)·ẑ ≥ 0 and the long way otherwise; retrograde reverses the rule. v0.7.5+.

func NextClosestApproach added in v0.9.3

func NextClosestApproach(
	stateA, stateB orbital.Vec3State,
	primary bodies.CelestialBody,
	mu, horizon float64,
) (t, dist float64, vRel orbital.Vec3, err error)

NextClosestApproach finds the next time-to-encounter between two craft along their predicted segments. Both states must be in the SAME frame (typically the active craft's primary-relative frame — world.TargetStateRelativeToActivePrimary handles cross-primary conversion before this is called). The function only handles same-primary rendezvous; cross-SOI tooling is out of scope for v0.9.3 (matches the slice's manual-loop scenario in LEO).

Algorithm: forward-propagate both craft via Verlet at intervals of roughly period/50 over `horizon` seconds, track minimum |rA - rB|, then **parabolically refine** the minimum from its two bracketing samples and re-propagate to that sub-grid time for the reported distance + relative velocity.

The refinement is not cosmetic. The HUD recomputes this every frame from live, slightly-noisy integrated state. Without refinement the answer is snapped to the ~period/50 grid (~111 s for LEO), so as the true minimum drifts across a grid boundary between frames the reported time jumps by a whole grid step and the distance pops to a different sample — the readout looks erratic even though the physics is smooth, making it impossible to judge whether the approach needs adjusting. The parabolic vertex is continuous in the inputs, so the readout is now stable frame-to-frame and reports the true sub-grid minimum, not the nearest sample.

Returns:

  • t: seconds from now until closest approach (0 if "now").
  • dist: distance at closest approach (meters).
  • vRel: vA − vB at closest approach (m/s vector — magnitude is the |v_rel| HUD readout).
  • err: non-nil for invalid inputs (non-positive mu / horizon).

v0.9.3+.

func NextClosestApproachPositions added in v0.36.0

func NextClosestApproachPositions(
	stateA, stateB orbital.Vec3State,
	mu, horizon float64,
) (t, dist float64, posA, posB orbital.Vec3, err error)

NextClosestApproachPositions is NextClosestApproach plus the two craft's positions at the refined closest-approach time — the map's ✕ marker (ADR 0020 / #346) plots both ends of the encounter there. The positions come from the exact same re-propagation that produces `dist`, so a marker at posA/posB is always exactly `dist` apart — there is no separate position pass to drift out of sync with the scalar HUD readout.

func PorkchopGrid added in v0.3.3

func PorkchopGrid(
	muSun float64,
	depState, arrState EphemerisFn,
	epoch0 float64,
	depDays, tofDays []float64,
	muDep, rPark float64,
	muArr, rCapture float64,
	retrograde bool,
	nRev int,
	longBranch bool,
) [][]float64

PorkchopGrid evaluates a grid of Lambert transfers and returns per- cell total Δv (departure + arrival, m/s). NaN marks cells where the Lambert solver failed to converge — the TUI can render those as "impossible" pixels.

The returned slice is indexed [tofIdx][depIdx] so rendering row-by- row in the TUI naturally walks TOF vertically and departure day horizontally.

  • epoch0: sim-time in seconds at which depDays[0] is measured. The ephemeris is sampled at epoch0 + depDays[i]*86400 for departure and epoch0 + (depDays[i]+tofDays[j])*86400 for arrival.
  • depState, arrState: body ephemerides (heliocentric r, v).
  • muSun: gravitational parameter of the system primary.
  • muDep, rPark: departure body μ + parking-orbit radius (for departure Δv via the patched-conic identity).
  • muArr, rCapture: arrival body μ + capture-orbit radius.
  • retrograde: forwarded to LambertSolve to select the prograde or retrograde transfer branch. The TUI surfaces prograde today; retrograde unblocks multi-rev porkchop work in v0.8+. v0.7.5+.
  • nRev / longBranch: forwarded to LambertSolveRev. nRev=0 + longBranch=false is the legacy single-rev short-only path (byte-identical to pre-v0.10.5). nRev≥1 scores N-revolution transfers; longBranch picks the higher-z (long) root of the two-branch N-rev solution. v0.10.5+.

func PorkchopMinCell added in v0.3.3

func PorkchopMinCell(grid [][]float64) (depIdx, tofIdx int, total float64, ok bool)

PorkchopMinCell scans a grid and returns the (depIdx, tofIdx, total) of the lowest-Δv non-NaN cell. ok=false if the entire grid is NaN.

func Predict

func Predict(start physics.StateVector, mu, totalSeconds float64, samples int) []orbital.Vec3

Predict forward-integrates a shadow StateVector using Verlet, returning a slice of inertial (primary-relative) positions sampled at regular intervals. Used by the maneuver screen for its live preview line.

- start: initial state (post-burn). - mu: gravitational parameter of the primary. - totalSeconds: total sim-time horizon. - samples: number of points to return (inclusive of start).

func SimulateFiniteBurn added in v0.6.2

func SimulateFiniteBurn(
	init physics.StateVector,
	mu, thrust, isp, commandedDv float64,
	direction DirectionFn,
) physics.StateVector

SimulateFiniteBurn forward-integrates a finite burn delivering the commanded Δv (m/s) at constant thrust, returning the post-burn state. Mass is reduced via the Tsiolkovsky rocket equation; duration follows from `dt = m0/mdot · (1 − exp(−Δv / (Isp·g0)))`.

The integration uses physics.StepRK4 with an accel closure that adds thrust along direction(r, v) on top of two-body gravity. Mass is snapshotted per sub-step (linear within each step) — at 200 sub-steps the per-step mass error is < 0.5%, well below other modelling errors. Sufficient for a planner-side predictor; the in-flight integrator does the same trick at finer granularity.

Returns init unchanged when any input is non-positive (degenerate) or when commandedDv ≤ 0.

Types

type AxisLabel added in v0.10.2

type AxisLabel int

AxisLabel identifies which of the eight velocity-frame burn axes RecommendRendezvousNudge picked. The sim layer (which does have the spacecraft package in scope) maps this to a spacecraft.BurnMode before plant. Kept here as a planner-local enum so this file stays dependency-clean (planner is a sibling of spacecraft — neither imports the other; see CLAUDE.md "Architecture").

Order matches spacecraft.AllBurnModes for the eight non-position modes. The two position-relative modes (BurnTarget / BurnAntiTarget) are intentionally excluded — Lambert's Δv is a velocity correction; a position-axis pick would be physically unjustified. v0.10.2+.

const (
	AxisPrograde AxisLabel = iota
	AxisRetrograde
	AxisNormalPlus
	AxisNormalMinus
	AxisRadialOut
	AxisRadialIn
	AxisTargetPrograde
	AxisTargetRetrograde
)

func (AxisLabel) String added in v0.10.2

func (a AxisLabel) String() string

String labels match the spacecraft.BurnMode.String() canonical names so HUD callers don't need a parallel naming table.

type DirectionFn added in v0.6.2

type DirectionFn func(r, v orbital.Vec3) orbital.Vec3

DirectionFn returns the unit vector a burn-mode pushes the craft along, given a (r, v) state. Callers wrap their existing direction helpers (e.g. spacecraft.DirectionUnit applied to a fixed BurnMode) into this signature so the planner never needs to know what "prograde" means structurally.

type EphemerisFn added in v0.3.3

type EphemerisFn func(epoch float64) (r, v orbital.Vec3)

EphemerisFn returns the heliocentric (system-primary-centered) position and velocity of a body at the given sim-time epoch, in SI units (m, m/s). The planner package doesn't know about bodies/orbital elements — callers (typically sim.World) adapt their Kepler/ calculator machinery into this function type.

type InclinationPlan added in v0.7.4

type InclinationPlan struct {
	PrimaryID  string
	DV         float64       // m/s, magnitude
	OffsetTime time.Duration // wall delay from "now" until burn fires
	NormalSign int           // +1 → rotate toward +ĥ, -1 → toward −ĥ
	// PlaneChangeRad is the signed orbital-plane rotation angle (rad):
	// +θ rotates the plane toward +ĥ, −θ toward −ĥ. The sim layer
	// stores this on the BurnPlaneChange node; the burn rotates the
	// horizontal velocity through θ about the radial axis, preserving
	// |v| — unlike a pure orbit-normal burn, which would only add
	// speed. |θ| = |Δi|; its sign equals NormalSign.
	PlaneChangeRad float64
	AtAN           bool
}

InclinationPlan describes a single normal-burn that rotates the craft's orbital plane around the line of nodes. PlanInclinationChange returns one of these; the sim layer adapts it into a BurnPlaneChange ManeuverNode carrying PlaneChangeRad. We don't reuse TransferPlan because that type encodes mode via the boolean IsRetrograde — a plane-rotation burn doesn't fit cleanly through a prograde/retrograde flag.

AtAN is set true when the planner picked the ascending node, false for descending. Diagnostic only — the integrator doesn't care.

func PlanInclinationChange added in v0.7.4

func PlanInclinationChange(state orbital.Vec3State, mu, targetIncl float64, primaryID string) (InclinationPlan, error)

PlanInclinationChange constructs a single-burn plane rotation that fires at the next ascending or descending node (whichever comes sooner) and rotates the orbit's inclination to targetIncl (radians, in [0, π]). The longitude of ascending node Ω is preserved — pure inclination change, no plane shift.

Δv magnitude: 2 · v_horizontal · sin(|Δi|/2), where v_horizontal is the velocity component perpendicular to the position vector at the chosen node (= h/r). For circular orbits v_horizontal = v; for eccentric orbits at the node it's v · cos(γ) where γ is the flight-path angle. Using v_horizontal (rather than |v|) keeps the formula exact for eccentric orbits — only the in-plane perpendicular component contributes to plane rotation.

Direction: the burn is along ±h (orbit normal). At the ascending node, +h increases inclination; at the descending node, +h decreases it (h_z gains/loses sign based on which side of the equator the velocity is currently pushing). NormalSign records the chosen side.

Errors:

  • ErrEquatorialOrbit when |i| or |π−i| < 1 mrad — line of nodes undefined, no AN/DN to fire at.
  • ErrHyperbolicOrbit when e ≥ 1 or a ≤ 0.
  • ErrInclinationRange when targetIncl is outside [0, π].
  • ErrInclinationNoOp when |Δi| < 1 µrad.
  • ErrNoNodeReachable when both TimeToNodeCrossing calls return -1 (defensive — should be unreachable when the elements check passes).

type RendezvousAdvisory added in v0.10.2

type RendezvousAdvisory struct {
	Ok       bool
	DV       float64      // scalar Δv along AxisUnit, m/s
	Axis     AxisLabel    // discrete pick from the eight velocity-frame axes
	AxisUnit orbital.Vec3 // unit vector for the recommended axis (in same frame as stateA)

	CurrentCA    float64 // m — what the player would get with no burn
	AchievableCA float64 // m — what the recommended burn delivers
	TArrival     float64 // s — time-to-CA from now after the burn

	// ArrivalSpeed is |v_rel| AT the achieved closest approach (post-burn),
	// m/s — populated only on Ok=true (ADR 0039 S1). Pure information, no
	// gate: it sizes the hand-flown part of the job ("CA 9 km, arriving
	// ~540 m/s") rather than judging it. #290 found this invisible at plan
	// time: a K-plant that read as a clean success in fact arrived at
	// 95.4 m/s, 4.6 under the lock gate, with no readout anywhere warning
	// the player before they committed to it.
	ArrivalSpeed float64

	LambertIdealDV float64 // m/s — |full Lambert ΔV| (always ≥ DV; gap shows projection loss)

	Reason string // populated when Ok=false: "no improvement available" | "no lambert convergence" | "degenerate axes" | "horizon too short" | "burn too large — use H/I/m" | "burn drops periapsis unsafely"
}

RendezvousAdvisory is the result of a single-burn nudge recommendation. v0.10.2+.

Ok=true: DV / Axis / AxisUnit / AchievableCA / TArrival populated; a plant-side caller can build the maneuver node directly from these fields.

Ok=false: Reason carries a short tag for the gate that fired. The HUD surfaces the "no improvement available" tag specifically; other reasons mean the advisory block is hidden (the existing TARGET HUD readouts already convey state).

func RecommendRendezvousNudge added in v0.10.2

func RecommendRendezvousNudge(
	stateA, stateB orbital.Vec3State,
	primary bodies.CelestialBody,
	mu, horizon, currentCA float64,
) RendezvousAdvisory

RecommendRendezvousNudge picks a single-burn nudge that brings the chaser (stateA) closer to the target (stateB) at a future closest approach, given they share a primary with gravitational parameter mu. currentCA is the no-burn closest-approach distance the caller already has on the HUD (typically from NextClosestApproach); the function uses it for the two-prong improvement floor.

Algorithm (see designdocs/terminal-space-program/v0.10-plan.md §v0.10.2 / plan file):

  1. Scan Lambert intercept solutions at T_k = {0.15, 0.3, 0.5, 0.8, 1.2}·P_B; pick the lookahead that minimises |Δv_full|.
  2. Project Δv_full onto the eight velocity-frame axes; pick the axis with the largest positive projection (scalar Δv ≥ 0).
  3. Re-run NextClosestApproach with the axis-aligned perturbation applied — this is the *honest* post-burn CA, not the Lambert ideal (the projection is lossy by design; the slice's loop is "iterate until DOCK READY").
  4. Two-prong improvement floor: (CA_improvement ≥ 10 %) OR (Δv ≥ 0.5 m/s AND CA_improvement ≥ 100 m absolute). Fails the gate ⇒ Ok=false.
  5. Nudge-scale ceiling (v0.10.3+): bestProj ≤ maxNudgeDV — single- burn recommendations above this aren't "nudges", they're major orbit-shape changes that belong in the manual planner (H / I / m). Without this ceiling the gate would happily plant a 1.7 km/s K-burn whenever it improved CA by ≥10 %, because the Lambert lookahead fan converges on whatever transfer fits T_k even when the orbits are wildly mismatched.
  6. Orbit-safety gate (v0.10.3+): the projection in Step 2 is lossy — the perturbed orbit is NOT the Lambert transfer, just the chaser's orbit + a scalar push in one axis. A large retrograde or radial-in nudge can drop the chaser's periapsis into the atmosphere while still nominally "improving CA." Reject burns that put post-periapsis below primary+50 km or drop it by more than 100 km from pre-burn.

Caller-side gates (no target, target == active, different primaries, already DOCK READY) live in the sim layer; the planner is not exercised on those paths.

type ResidualFn added in v0.6.2

type ResidualFn func(state physics.StateVector, mu float64) float64

ResidualFn returns the signed error between a post-burn state and the planner's target. Positive when the burn under-delivered (e.g. "we want apoapsis higher than this"), negative when overshot. The Newton iteration drives this toward zero.

func TargetApoapsis added in v0.6.2

func TargetApoapsis(targetApoMeters float64) ResidualFn

TargetApoapsis builds a ResidualFn whose zero crossing is the post-burn orbit's apoapsis matching targetApoMeters (distance from primary's centre, not altitude).

func TargetPeriapsis added in v0.6.2

func TargetPeriapsis(targetPeriMeters float64) ResidualFn

TargetPeriapsis is the perigee analogue of TargetApoapsis.

type TransferLeg added in v0.3.1

type TransferLeg int

TransferLeg names which end of a TransferPlan a node belongs to — helpful for HUDs and logging that want to show "departure" vs "arrival" without having to derive it from PrimaryID.

const (
	LegDeparture TransferLeg = iota
	LegArrival
)

type TransferNode added in v0.3.1

type TransferNode struct {
	Leg          TransferLeg
	PrimaryID    string        // body whose frame the burn was planned in
	DV           float64       // m/s, magnitude
	OffsetTime   time.Duration // time after PlanTransfer returns when this fires
	IsRetrograde bool          // true → retrograde mode; false → prograde
	// BurnDir (v0.12.x+) is the full 3D thrust unit direction for a
	// fused-Lambert departure that carries eccentricity + raise + plane
	// change together — a vector no prograde/retrograde flag can express.
	// Non-zero only on the departure leg of PlanIntraPrimaryFused; the
	// sim adapter (transferNodeToManeuver) plants a BurnVector node when
	// set, otherwise it falls back to the IsRetrograde prograde/retro
	// mode. Expressed in the shared primary's (inertially-oriented)
	// frame, so it is fixed in inertial space between plant and fire.
	BurnDir orbital.Vec3
}

TransferNode is a planner-layer description of a single burn that the sim layer will turn into a sim.ManeuverNode. We keep it free of any sim-package dependencies so planner stays a pure math/algorithms surface — sim.PlanTransfer adapts these into sim.ManeuverNodes.

type TransferPlan added in v0.3.1

type TransferPlan struct {
	Departure  TransferNode
	Arrival    TransferNode
	TransferDt time.Duration // coast time (Departure → Arrival)
}

TransferPlan is the two-burn output of an auto-plant transfer. Departure fires at a parking-orbit periapsis around the origin primary; Arrival fires at the destination's SOI/circular-capture radius after the transfer ellipse coast.

func PlanHohmannTransfer added in v0.3.1

func PlanHohmannTransfer(
	muSun float64,
	rDeparture, rArrival float64,
	muDeparture, rPark float64, departureID string,
	muDestination, rCapture float64, destinationID string,
) (TransferPlan, error)

PlanHohmannTransfer constructs a Hohmann-style transfer plan from a circular parking orbit at radius rPark around a departure planet (helios distance rDeparture, gravitational parameter muDeparture) to a circular capture orbit at radius rCapture around a destination planet (helios distance rArrival, gravitational parameter muDestination), all heliocentric distances in the system primary's frame (mu = muSun).

Result Δv magnitudes are the patched-conic Hohmann values:

departure: v∞_dep = sqrt(µ_sun · (2/r_dep − 1/a_t)) − v_dep_orbit
         Δv_dep = EscapeBurnDeltaV(v∞_dep, µ_planet, r_park)
arrival:   v∞_arr = v_arr_orbit − sqrt(µ_sun · (2/r_arr − 1/a_t))
         Δv_arr = CaptureBurnDeltaV(|v∞_arr|, µ_dest, r_capture)

PrimaryIDs are set so the sim layer can render frame-aware glyphs and the planner UI can label the legs. Phasing is *not* accounted for — both burns assume the destination planet is at the right place at the right time, which the v0.3.1 sandbox doesn't enforce. A porkchop-plot screen (deferred to v0.3.2) is the natural next step.

func PlanIntraPrimaryFused added in v0.12.1

func PlanIntraPrimaryFused(
	mu float64,
	rDep, vDep orbital.Vec3,
	rArr, vArr orbital.Vec3,
	tof float64,
	depOffset time.Duration,
	departureID string,
	muTarget, rCapture float64,
	targetID string,
) (TransferPlan, error)

PlanIntraPrimaryFused builds a combined plane-shift + Hohmann transfer for the intra-primary case (craft + target share a primary, e.g. a LEO craft → Luna, both around Earth) using a single-revolution Lambert solve from the craft's actual departure state to the target's actual arrival position. Unlike PlanIntraPrimaryHohmann — which feeds the craft's |R| in as a *circular* parking radius and has no plane-change term — the returned departure velocity v1 connects the two points on a Keplerian arc regardless of nodes, so it inherently carries eccentricity, the apsis raise, AND any plane change together. The departure leg is therefore a full 3D BurnVector (Δv = v1 − vDep), not a prograde scalar. "Eccentric-aware departure" and "plane change" are not separate deliverables here — they fall out of the solve (ADR 0005).

All states are in the shared primary's (inertially-oriented) frame:

  • rDep, vDep: craft state at the departure epoch (now + depOffset).
  • rArr, vArr: target state at the arrival epoch (now + depOffset + tof).

The caller (sim layer) propagates the craft (analytic Kepler) and the target (ephemeris) to those epochs and seeds tof + depOffset from the shared intra-primary phasing (intraPrimaryPhasing).

The arrival leg is the SOI-capture braking burn: the hyperbolic excess |v2 − vArr| converted to a circular-insertion Δv via CaptureBurnDeltaV, planted as a retrograde scalar at the target — matching PlanIntraPrimaryHohmann's arrival. The departure carries the geometry; the arrival just brakes into the target's SOI.

v0.12.x+.

func PlanIntraPrimaryHohmann added in v0.5.7

func PlanIntraPrimaryHohmann(
	mu float64,
	rDeparture, rArrival float64,
	craftAngleNow, targetAngleNow float64,
	minLeadSeconds float64,
	departureID string,
	muTarget, rCapture float64,
	targetID string,
) (TransferPlan, error)

PlanIntraPrimaryHohmann constructs a Hohmann transfer for the case where craft and target both orbit the same primary (e.g. craft in LEO around Earth → Luna also around Earth). Pre-v0.5.7 PlanTransfer assumed craft and target both heliocentric; for moon targets it computed nonsense (Luna's parent-relative semimajor used as a heliocentric distance).

Inputs:

  • mu: GM of the shared primary (e.g. Earth's GM).
  • rDeparture: craft's |R| in primary's frame (e.g. LEO radius ≈ 6571 km for 200 km altitude).
  • rArrival: target's semimajor axis around primary (e.g. Luna's 384 399 km).
  • craftAngleNow / targetAngleNow: current angular positions of craft and target around the shared primary (radians, atan2 of position-vector y, x in the primary's frame). Used for phase- corrected launch-window timing — the departure burn fires when target leads craft by (π − n_target · T_transfer), so craft arrives at apoapsis when target is also there. v0.5.9+.
  • departureID: identifier of the primary (for ManeuverNode PrimaryID labeling).
  • muTarget, rCapture, targetID: target body's GM, capture-orbit radius around target, and ID. Used for the SOI-entry braking burn — closing speed at rendezvous becomes v∞ relative to target, which CaptureBurnDeltaV converts to a circular-orbit insertion Δv.

Departure burn: prograde Δv at periapsis raising apoapsis to rArrival. Arrival burn: capture into target SOI at rArrival.

minLeadSeconds (v0.5.11+): the planner pads the wait time τ by integer synodic periods until τ ≥ minLeadSeconds. Callers pass half the expected burn duration so the live integrator can center the finite burn on the planner's intended firing point without the trigger time falling in the past. Pass 0 to skip padding.

func PlanLambertTransfer added in v0.4.1

func PlanLambertTransfer(
	muSun float64,
	rDep, vDepBody orbital.Vec3,
	rArr, vArrBody orbital.Vec3,
	tof float64,
	muDeparture, rPark float64, departureID string,
	muDestination, rCapture float64, destinationID string,
	depOffset time.Duration,
	retrograde bool,
	nRev int,
	longBranch bool,
) (TransferPlan, error)

PlanLambertTransfer builds a two-burn transfer for an arbitrary (departure-time, time-of-flight) pair using a single-rev Lambert solve for the heliocentric coast. Unlike PlanHohmannTransfer which assumes 180° opposition geometry, this supports off-Hohmann launch windows — the same geometry the porkchop grid scores, so Enter-to- plant from the porkchop cursor is a direct call-through.

Inputs: heliocentric state of departure body at t_dep, heliocentric state of arrival body at t_dep + tof, transfer TOF in seconds, plus the parking / capture orbit parameters used for the patched-conic Δv identity (matching PlanHohmannTransfer + PorkchopGrid).

depOffset is the wall-clock delay from "now" (sim-time at planning) until the departure burn; it becomes the Departure node's OffsetTime. The Arrival node's OffsetTime is depOffset + tof.

Retrograde flags follow the same outbound/inbound rule as PlanHohmannTransfer: outbound (|rArr| > |rDep|) gets a prograde departure + retrograde arrival; inbound flips both. Lambert geometry varies more than Hohmann's 180° opposition, but the radius-based sign captures the common case well enough for the porkchop cursor and we can revisit if off-Hohmann arrivals need a sharper rule.

func PlanMoonEscape added in v0.6.3

func PlanMoonEscape(
	muMoon, muParent float64,
	craftR, craftV orbital.Vec3,
	moonR, moonV orbital.Vec3,
	rSOI, rTargetPeri float64,
	minLeadSeconds float64,
	moonID, parentID string,
) (TransferPlan, error)

PlanMoonEscape constructs a two-impulse Moon Return: a single targeted departure (a full-3D BurnVector) that injects the craft from a moon parking orbit onto a parent-frame transfer whose perigee reaches a chosen target, plus a zero-Δv arrival marker at the SOI-exit moment.

ADR 0013 replaced the v0.6.3 "minimum kiss-the-SOI escape" objective (a prograde impulse sized so the transfer apolune merely touched the moon's SOI, leaving the craft ≈ at rest relative to the moon and inheriting roughly the moon's own parent orbit). The targeted return instead leaves the SOI with a deliberate excess velocity v∞ aimed *retrograde to the moon's orbital motion, in the moon's orbital plane*, sized so the inherited parent-frame orbit drops to a usable perigee — folding the perigee-lowering work into the deep-in-the-well departure (Oberth) rather than paying for it separately later.

Construction (ADR 0013 Decisions C/D):

  • v∞ direction is analytic: −v̂_moon (retrograde, in the moon's orbital plane). Because the target plane is the moon's own plane — true at any departure time — there is no around-parent phasing wait, only a short intra-moon wait for the parking-orbit point where a prograde burn aims v∞ correctly.
  • |v∞| is solved (1-D) so the analytic parent-frame perigee equals rTargetPeri. The arrival inclination is whatever the moon's plane is; only the perigee and the prograde-around-parent sense are controlled (the cheap, controllable goal).
  • The departure is a BurnVector: the escape-hyperbola periapsis velocity (vEsc = √(v∞² + 2µ/r_park)) aimed prograde in the moon's orbital plane at the periapsis direction whose outgoing asymptote is −v̂_moon. Any tilt between the parking orbit and the moon's orbital plane is folded into that one impulse (Δv = v_post − v_park).

The arrival node stays a zero-Δv frame marker at the SOI-exit moment; the player plants their own capture/aerobrake manually (the expensive parent-capture term is theirs to choose — ADR 0013 Decision A).

Inputs:

  • muMoon, muParent: GM of the moon and of its parent.
  • craftR, craftV: craft state in the moon's (inertially-oriented) frame at plant time. |craftR| is the parking radius; the velocity fixes the parking-orbit plane and motion sense.
  • moonR, moonV: the moon's state relative to its parent at departure (from the ephemeris Calculator). Fixes the moon's orbital plane, motion direction, and parent distance.
  • rSOI: the moon's sphere-of-influence radius (arrival-marker timing).
  • rTargetPeri: desired parent-frame perigee (e.g. parent radius + 200 km).
  • minLeadSeconds: pad the departure offset by whole parking-orbit periods until it is ≥ this, so a centered finite burn fits ahead of "now". Pass 0 to skip padding.
  • moonID, parentID: PrimaryIDs so the HUD renders the departure in the moon's frame and the arrival marker in the parent's frame.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL