SolutionCloseness#

class SolutionCloseness#

Bases: object

Statistical distance between orbit determination solutions.

Two OD solutions of the same object rarely agree exactly, and the useful question is not how far apart the estimates are but whether they are consistent: whether the difference is small compared with the uncertainty each solution claims. Comparing state differences alone answers neither — a 10 km difference is nothing for a solution with a 100 km covariance and damning for one with a 1 km covariance.

The measure of Bhaskaran & Rush [1] answers it directly. Around each solution \(S_i = (c_i, P_i)\) grow the \(t\)-sigma ellipsoid

\[(x - c_i)^T P_i^{-1} (x - c_i) = t^2\]

and let both grow together. The closeness \(d(S_1, S_2)\) is the smallest \(t\) at which they first touch. It reads directly as a number of sigma: solutions that touch below \(1\sigma\) overlap comfortably, while a value of several sigma means the two cannot both be right.

The measure is symmetric, scale free, and reduces to the intuitive answers in the simple cases — for equal covariances it is exactly half the Mahalanobis distance between the centres, and for isotropic covariances \(a^2 I\) and \(b^2 I\) it is \(\|c_2 - c_1\| / (a + b)\).

Solutions come either as SolutionOD objects, which are read for their state estimate and covariance at a chosen epoch, or as plain (mean, covariance) pairs.

Methods

between(first, second[, index, covariance, ...])

Closeness between two OD solutions, in sigma.

matrix(solutions, **kwargs)

Pairwise closeness between every solution in a set.

plot_matrix(solutions[, labels, scheme, ...])

Plot the pairwise closeness of a family of solutions.

See also

scarabaeus.SolutionOD

Solution object read by between().

Notes

The tangency point satisfies \(\Lambda_1 (x - c_1) = -\alpha \Lambda_2 (x - c_2)\), so it lies at

\[x(\alpha) = (\Lambda_1 + \alpha \Lambda_2)^{-1} (\Lambda_1 c_1 + \alpha \Lambda_2 c_2)\]

for a single unknown \(\alpha > 0\), fixed by requiring the two Mahalanobis distances at that point to be equal. That difference runs monotonically from negative to positive as \(\alpha\) sweeps \(0 \to \infty\) (the point slides from \(c_1\) to \(c_2\)), so the root is unique and bracketing finds it without a search grid.

Both solutions must be expressed in the same frame and units; nothing here can detect otherwise, and comparing a J2000 solution against a body-fixed one produces a plausible number that means nothing.

References

[1]

Bhaskaran, S., & Rush, J. (2004). Navigation performance measures for orbit determination solution comparison. AIAA/AAS Astrodynamics Specialist Conference, AIAA 2004-4982.

Examples

# two filter solutions of the same arc
result = SolutionCloseness.between(solution_a, solution_b)
print(f"{result['d']:.2f} sigma apart")

# position only, at the first epoch instead of the last
result = SolutionCloseness.between(
    solution_a, solution_b, index=0, states=slice(0, 3)
)

# a whole family at once
D = SolutionCloseness.matrix([sol_a, sol_b, sol_c], labels=["A", "B", "C"])
classmethod between(first, second, index: int = -1, covariance: str = 'est', states=None, max_alpha: float = 1000000000000.0) → dict#

Closeness between two OD solutions, in sigma.

Parameters:
first, secondSolutionOD or tuple

The two solutions. Either SolutionOD objects, or (mean, covariance) pairs with the mean shaped (n,) and the covariance (n, n).

indexint, optional

Epoch row to compare at, for SolutionOD inputs. Defaults to -1, the final estimate; 0 is the first epoch.

covariancestr, optional

Which stored covariance to read: 'est' (default), 'smooth' or 'consider'.

statesslice or sequence of int, optional

Which state components to compare. Defaults to all of them; pass slice(0, 3) to compare position alone, which is often what a navigation team actually cares about.

max_alphafloat, optional

Bracketing limit for the tangency parameter. Only reached for solutions many orders of magnitude apart in scale.

Returns:
dict

d the closeness in sigma, t_sq its square, alpha the tangency parameter, x_tang the touching point, and mahalanobis the pair of distances from each centre to that point — equal to d on both sides, which is the check that the solve converged.

Raises:
SolutionClosenessError

If the shapes disagree, a covariance is singular, or the requested history is not stored on the solution.

classmethod matrix(solutions, **kwargs) → ndarray#

Pairwise closeness between every solution in a set.

The off-diagonal entries are what tell a family of solutions apart: a tight block means the solutions agree within their own uncertainty, while an isolated row is a solution that disagrees with all the others.

Parameters:
solutionssequence

Solutions in any form between() accepts.

**kwargs

Passed through to between().

Returns:
np.ndarray

Symmetric (N, N) matrix of closeness values in sigma, zero on the diagonal.

classmethod plot_matrix(solutions, labels=None, scheme: str = 'sigma', title: str = 'OD solution consistency', show_values: bool = True, distances=None, **kwargs)#

Plot the pairwise closeness of a family of solutions.

Parameters:
solutionssequence

Solutions in any form between() accepts.

labelssequence of str, optional

Axis labels; defaults to S1 ... SN.

schemestr, optional

Colour banding: 'sigma' (1/2/3-sigma bands, the default), 'binary' (consistent or not, split at 0.8), or 'continuous' for a smooth colour bar.

titlestr, optional

Figure title.

show_valuesbool, optional

Print each value in its cell. Defaults to True.

distancesnp.ndarray, optional

A previously computed matrix, to avoid recomputing it.

**kwargs

Passed through to matrix().

Returns:
distances, fignp.ndarray and matplotlib.figure.Figure
exception SolutionClosenessError#

Bases: ValueError

Raised when two solutions cannot be compared as posed.