Working with sensitivities¶
The StateSensitivities and
ObservationSensitivities tables hold per-epoch state
transition matrices (and their second-order tensors) for use in
covariance propagation, nonlinearity diagnostics, and Bayesian updates.
Filter to one chain¶
Sensitivity tables hold every (orbit, epoch) row across all
propagated orbits — group with the standard quivr select pattern
before pulling matrices:
result = empyrean.propagate(orbits, epochs)
chain = result.sensitivity.select("orbit_id", "99942")
epoch = empyrean.Epochs.from_mjd([61500.0], scale="tdb")
phi = chain.stms_array()[chain.index_at(epoch)] # (6, 6) Φ matrix
STM convention¶
stms_array() returns cumulative STMs:
at each output epoch , referenced to
the orbit’s initial epoch (the epoch on the input
Orbits row). To get the segment STM between two non-initial
epochs, compose:
i_a, i_b = chain.index_at(t_a), chain.index_at(t_b)
phi_ba = chain.stms_array()[i_b] @ np.linalg.inv(chain.stms_array()[i_a])
Multi-chain methods raise with a hint to filter — calling
stms_array() on a multi-orbit table raises:
ValueError: stms_array() requires a single chain but got 2 unique
orbit_ids: 'A', 'B'. Filter to one chain first:
sens.select("orbit_id", "<orbit_id>").stms_array(...)
Propagating a covariance¶
Map an initial 6×6 covariance through the chain to the output epoch:
epoch_t = empyrean.Epochs.from_mjd([60750.0], scale="tdb")
cov_t, dmu_t = chain.propagate_covariance(cov_0, i=chain.index_at(epoch_t))
order="auto" uses Park-Scheeres second-order Gaussian (Park &
Scheeres 2006) when STTs are available, else the linear map:
The order-2 correction adds the Isserlis term plus the mean shift . See Park & Scheeres (2006), Nonlinear Mapping of Gaussian Statistics: Theory and Applications to Spacecraft Trajectory Design, J. Guidance Control Dyn. 29(6).
Nonlinearity diagnostic¶
For Jet2-propagated chains, chain.kappa(cov_0) returns the per-epoch
local nonlinearity diagnostic — a scalar measure of how much the
second-order STT contribution would shift the propagated mean
relative to the linear map:
kappas = chain.kappa(cov_0) # array of length n_t
The diagnostic is per-row, not a single number — the same orbit can
be linear at the last-observation epoch and strongly non-linear over
a multi-year propagation through a planetary close approach. Use it
qualitatively: large local values flag epochs where the first-order
covariance map is going to disagree with sample-based estimates.
Select the uncertainty method up front in
:class:~empyrean.PropagationConfig for the regime you expect.
Observation-side partials¶
After generate_ephemeris(), the
ObservationSensitivities table carries observation
Jacobians (and Hessians for
SECOND_ORDER):
chain = eph.sensitivity \
.select("orbit_id", "2020 CD3") \
.select("obs_code", "F51")
H = chain.jacobians_array() # (n_t, 6, n_params)
sigma_h, dmu_h = chain.propagate_covariance(cov_0) # returns (cov, mean-shift)
n_params is 6 for state-only DC, 9 when fitting non-grav A1/A2/A3.
The six rows of H — and the six axes of sigma_h — are the
topocentric spherical observable, in this order:
row |
constant |
observable |
unit per input unit |
|---|---|---|---|
0 |
|
range |
AU |
1 |
|
RA |
deg |
2 |
|
Dec |
deg |
3 |
|
range rate |
AU/day |
4 |
|
RA rate |
deg/day |
5 |
|
Dec rate |
deg/day |
Range comes first, so row 0 is not RA. Index with the constants
rather than with literals — a hand-written 0 reads a range partial in
AU where an RA partial in degrees was meant, and the result is finite
and plausible-looking either way. To get the sky-plane 2×2 in deg²,
take the RA/Dec submatrix (multiply the RA row and column by
if you want it in the RA·cos δ convention that on-sky
residuals use):
from empyrean import SENSITIVITY_ROW_DEC, SENSITIVITY_ROW_RA
rows = [SENSITIVITY_ROW_RA, SENSITIVITY_ROW_DEC]
cov_radec_deg2 = sigma_h[i][rows][:, rows] # 2×2, deg²
The RA rate is , not .