# Delta-V

# Overview

The system computes Tsiolkovsky rocket-equation delta-V for every stage of a vessel or OAB assembly. Three classes collaborate: `VesselDeltaVComponent` orchestrates the whole pipeline; `DeltaVStageInfo` owns the per-stage simulation; `DeltaVPartInfo` tracks per-part mass and staging metadata.

# Data Model

```javascript
VesselDeltaVComponent
 ├─ PartInfo: DictionaryValueList<IGGuid, DeltaVPartInfo>   (one entry per part)
 ├─ EngineStageSet: DeltaVEngineStageSet
 │    ├─ EngineInfo: List<DeltaVEngineInfo>                 (one entry per engine)
 │    └─ StageInfo: List<DeltaVStageInfo>                   (one per stage number)
 └─ SeparationStageIndexes: List<int>                       (decoupler stage numbers)
```

```javascript
DeltaVPartInfo
 ├─ ActivationStage, DecoupleStage, DecoupleBeforeBurn
 ├─ DryMass, JettisonMass
 └─ StageFuelMass: DictionaryValueList<int, PartStageFuelMass>
      └─ PartStageFuelMass { StartMass, EndMass, Resources }
```

```javascript
DeltaVStageInfo
 ├─ Parts: List<DeltaVPartInfo>        (parts still attached this stage)
 ├─ EnginesActiveInStage / EnginesInStage
 └─ DeltaVCalcs (_subStages): List<DeltaVCalc>
```

# Delta-V Calculations

## Phase 1 — Separation Index Walk (`SetSeparationIndices`)

Called once on construction and on every full recalc. Walks the part tree recursively from the root, propagating `DecoupleStage` downward.

* **Normal decoupler** (explosive node toward parent): `DecoupleStage = decoupler.ActivationStage`. Children inherit this value. The parent side is jettisoned.
* **"Facing-away" decoupler** (node toward children, #595 fix): the children are the payload — they keep the *pre-decoupler* `sepIndex`. The ancestor subtree (lower stage) is later tagged by `SetLowerStagesDecoupleStage`.
* Disabled staging (`IsActiveInStagingProp = false`): the part joins the parent's stage instead.
* Non-engine parts with `IsActiveInStaging = false` have their `ActivationStage` reassigned to the highest active ancestor stage seen so far (`carriedInverseStage`). Engines are explicitly excluded from this mutation.

## Phase 2 — Part Info Reset (`ResetPartInfo`)

Builds/refreshes `_partInfo` keyed by part GUID. For each part:


1. If already in the dict: refresh `DryMass`, `ActivationStage`, `DecoupleStage`.
2. Otherwise: create a new `DeltaVPartInfo` (which calls `CalculateStagingValues`).

Post-loop corrections propagate `DecoupleStage` through the tree for separators and docking ports using `ProcessDecouplePartInfoChildren`. For **bottom-attached fuel tanks** below an engine, `DecoupleStage` is promoted to match the engine's `StartBurnStage` so the tank is included in the engine's stage mass (not counted as already-decoupled dead weight).

`**CalculateStagingValues**` resolves `ActivationStage` (falls back up the parent chain only when the value is −1), then determines `DecoupleStage` from the nearest separation event at or below that stage. The edge case for stage-0 engines with a facing-away decoupler parent forces `DecoupleStage = -1` so the engine isn't misclassified as decoupled-before-burn.

## Phase 3 — Stage Assembly (`ProcessParts`)

For each `DeltaVStageInfo`, rebuilds `Parts` from `VesselDeltaVComponent.PartInfo`:

* **Include** a part if `part.DecoupleStage ≤ stage` (it is still physically present when this stage fires).
* **Exclude** parts belonging to a different vessel (checked against `_ship.Parts` in OAB or `Vessel.Contains` in flight).

Then calls `ProcessActiveEngines`, which filters `EngineInfo` to produce `EnginesActiveInStage`:

```javascript
include engine if:
  Stage ≤ engine.StartBurnStage          // engine fires this stage or earlier
  AND part is in _partInfoDictionary     // part is still attached
  AND engine.DecoupleStage < Stage       // engine hasn't been jettisoned yet
  AND engine.ModuleEngine.IsActiveInStagingProp  // player hasn't disabled it
  AND (not flight current-stage, OR engine ignited, OR not yet launched)
```

## Phase 4 — Stage Simulation (`SimulateDeltaV`)

The core calculation chain for one stage:

```none
CalculateStartMass()
  └─ CarryOverConsumedFuelMass()     // propagate prior-stage fuel depletion
  └─ GetStageMass()                  // sum DryMass + FuelMass for all Parts
       Parts with DecoupleStage == Stage → DecoupledMass (excluded from StartMass)
  StartMass = StageMass − DecoupledMass
```

```none
SortEnginesByResource()              // group by propellant type
CalcEngineStats()
  └─ CalculateISP()                  // effective ISP = ThrustTotal / Σ(T_i/ISP_i)
  └─ FuelFlow = Thrust / (ISP × g₀) // per engine, three conditions
  └─ CalculateTWR()                  // Thrust / (StartMass × g_body)
ProcessEngineSharedResources()       // distribute shared tank consumption proportionally by flow rate
CalcEngineBurnTimes()                // BurnTime = fuelAvailable / FuelFlow; drop engines with ~0 fuel
ProcessEnginesToSubStages()          // split into sub-stages at each engine burnout boundary
CalcSubStageStartEndMasses()         // chain StartMass/EndMass across sub-stages
CalcSubStageTotals()
  └─ ΔV = |vectoredThrust| × ln(m₀/m₁) × ISP × g₀ / Thrust  (per sub-stage)
CalcStageDeltaVTotal()               // sum sub-stages → DeltaVinVac, DeltaVatASL, DeltaVActual
```

**ISP averaging** (mixed engines): `ISP_eff = ThrustTotal / Σ(Thrust_i / ISP_i)` — the standard thrust-weighted harmonic mean.

**Sub-staging**: engines are sorted by `MinBurnTime`. Each distinct burnout time boundary creates a new `DeltaVCalc` sub-stage. Engines that run longer carry into subsequent sub-stages.

# Update Scheduling (`CheckDirtyAndRunCalcs`)

| Condition | Action |
|-----------|--------|
| `_calcsDirty` | Full `RunCalculations()` (all stages) |
| Flight, engine firing | Full recalc at 1 Hz |
| Flight, coasting | Full recalc at 10 Hz |
| Flight, always | Lightweight `RunFlightSceneCalculations()` at 5 Hz — reruns only the active stage with fresh mass; other stages do a cheap pressure-based lerp (`CalcLerpDeltaV`) |
| OAB, situation change | `RunSimulateStage()` for the requested stage only |

A 1-second debounce (`_vesselEventDelayTime`) absorbs bursts of structural events (part placement, docking, fuel changes) before triggering a full recalc.

# Three-Condition Outputs

Every quantity is computed for three atmospheric conditions, selected at query time:

| Constant | `Vac` | `ASL` | `Actual` |
|----------|-----|-----|--------|
| ISP      | ISP at 0 Pa | ISP at 1 atm | ISP at current pressure |
| Thrust   | Vac thrust | ASL thrust | Current thrust |
| TWR      | Vac thrust / weight | ASL thrust / weight | Actual thrust / weight |
| ΔV       | Tsiolkovsky w/ ISP_vac | Tsiolkovsky w/ ISP_ASL | Tsiolkovsky w/ ISP_actual |

`DeltaVActual` in flight is optionally interpolated as `Lerp(DvVac, DvASL, pressure_ratio)` rather than re-simulated, which is the cheap path in `RunFlightSceneCalculations`.

# Other relevant classes

## Central classes

`**DeltaVEngineInfo**` — Per-engine data node. Holds `ThrustVac/ASL/Actual`, `IspVac/ASL/Actual`, the three `FuelFlow` scalars, `MinBurnTime`, and a back-reference to `DeltaVPropellantInfo`. Its two key methods:

* `CalculateISP(situation)` — reads `atmosphereCurve` at the relevant pressure; in flight, substitutes the live `Engine.RealISPValue` when the engine is actually ignited.
* `CalculateThrustVector(situation)` — calls `CalcThrustActual`, then scales the engine's world-space `ThrustDirRelativePartWorldSpace` direction by each thrust scalar to produce the three `ThrustVector*` vectors used in the vectored ΔV formula.

`**DeltaVPropellantInfo**` — Fuel availability tracker for one propellant type on one engine. Key responsibilities:

* `UpdatePropellantInfo(stage, partCount)` — walks the resource flow graph (`FlowRequestBroker`) to populate `Containers` (the `DeltaVPartInfo → PartStageFuelMass` dict of tanks this engine can pull from). Skips parts already decoupled at this stage.
* `AmountAvailable(stage)` — sums stored units minus already-consumed units across all `Containers`, subtracts `SharedAmount` (this engine's share pre-reserved by `ProcessEngineSharedResources`), then returns the limiting ingredient's units. IntakeAir is excluded from the limit.
* `ConsumeUnits(units)` — simulates fuel draw by writing to `PartStageFuelMass.Resources.UnitsConsumed`, first-come-first-served across containers. This is the mechanism that carries forward depletion across sub-stages.
* `ConsumeSharedUnits(resourceID, units)` — accumulates units into `SharedAmount` so shared-tank consumption is apportioned by `ProcessEngineSharedResources`.

`**DeltaVEngineStageSet**` — Container and lifecycle manager. Owns `List<DeltaVStageInfo>` and `List<DeltaVEngineInfo>`, sorted descending by stage number. `UpdateStageInfo()` calls `Reset()` on existing entries or creates new ones for each stage from 0 to `LastStage`. `RemoveInvalidEngines()` purges engine entries whose part no longer exists in the vessel/ship.

`**DeltaVCalc**` — Plain data bag for one sub-stage. Holds `ActiveEngines`, `StartMass`, `EndMass`, `BurnTime`, the three thrust/ISP/TWR/ΔV/fuel-flow triples, and the vectored thrust directions. No logic of its own — written by `CalcSubStageStartEndMasses` and `CalcSubStageTotals`.

## Interface

`**IDeltaVPart**` — The abstraction boundary between the dV system and the actual part simulation. Exposes `ActivationStage`/`DecoupleStage` (read-write), `DryMass`/`GreenMass`, atmospheric conditions (`StaticPressureAtm`, `AtmDensity`, etc.), `DeltaVContainers`, `DeltaVChildren`, `Parent`, and `FindAttachNodeAttachedPart`. Implemented by both `PartComponent` (flight) and `IObjectAssemblyPart` (OAB).

## Pure utilities

`**DeltaVExtensions**` (static) — Mode queries (`ModeIsFlight`, `ModeIsOAB`) and stage queries (`CurrentStage`, `LastStage`, `CurrentStageActive`, `LastStageWithEngine`). Also provides `GetSwitchedValue<T>` which is the universal ASL/Actual/Vac selector used everywhere.

`**IDeltaVPartExtensions**` (static) — `IsPartDecoupler`, `IsPartEngine`, `IsPartDockingPort`, etc. — all thin wrappers around `TryGetModuleData<TModule, TData>`. Also the `Get(IDeltaVPart)` list lookup.

`**DeltaVSituationOptions**` — A 3-value enum: `SeaLevel`, `Altitude`, `Vaccum` [sic]. The argument passed into every `GetSituation*` accessor.

`**DeltaVEngineResourcePartInfo**` — A tiny struct pairing a `DeltaVPartInfo` with a `FuelMassUsed` float. Appears to be legacy scaffolding; not visibly used in the current simulation path.

# Class relationships at a glance

```none
VesselDeltaVComponent
  ├─ DeltaVEngineStageSet
  │    ├─ List<DeltaVStageInfo>
  │    │    └─ List<DeltaVCalc>  (_subStages)
  │    └─ List<DeltaVEngineInfo>
  │         └─ DeltaVPropellantInfo
  │              └─ Containers: { DeltaVPartInfo → PartStageFuelMass }
  └─ PartInfo: { IGGuid → DeltaVPartInfo }
                └─ StageFuelMass: { stageN → PartStageFuelMass }
```

```none
IDeltaVPart <────────────  implemented by PartComponent (flight)
                                   and IObjectAssemblyPart (OAB)
```