From 13f8756d2e2ab1747b459b636c1ca76225e5fc98 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Mon, 11 May 2026 14:17:14 -0400 Subject: [PATCH 01/12] Add pybind11 eigen/stl includes so Observation.rho_hat is usable from Python Without these, accessing rho_hat / a_vec / d_vec from Python raises "Unable to convert function return value to a Python type" because the binding can't see the Eigen and std::array conversions. This is a minimal enabler (no behavior change); the A/D-vector correctness fix on fix/tangent-basis-vectors is independent and lives on its own branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/detection.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/detection.cpp b/src/lib/detection.cpp index 80e5f2af..df0c0484 100644 --- a/src/lib/detection.cpp +++ b/src/lib/detection.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include namespace py = pybind11; // --- Observation Variant Types --- From c13ccd890274b75d8569b416206395bb3c0aae10 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 16:43:00 -0400 Subject: [PATCH 02/12] Add BK basis primitives for the universal BK fitter. Pure C++/Eigen math layer for the Bernstein-Khushalani parameterization, with the barycenter as the BK coordinate origin and gnomonic projection defining the tangent plane at a fiducial direction n0. No ASSIST, REBOUND, or pybind11 dependencies, so this translation unit can be tested in isolation. New files in src/lib/orbit_fit/: bk_basis.h -- types (BKState, BKFiducial) and function declarations bk_basis.cpp -- implementations: choose_fiducial, bk_to_cartesian, cartesian_to_bk, dcart_dbk (full 6x6 including the bottom-left cross-term block), sigma_gdot_sq The 6x6 Jacobian dcart_dbk has the block structure expected from the math: [ d r / d (alpha,beta,gamma) 0 ] [ d v / d (alpha,beta,gamma) d v / d (adot,bdot,gdot) ] with the top-left and bottom-right 3x3 blocks identical (both built from the (1/gamma)-scaled tangent vectors), and the bottom-left block holding the cross-term contributions through the second derivatives d^2 rho_hat / d (alpha, beta)^2. sigma_gdot_sq returns the bound-orbit energy-prior variance, gamma^2 (2 mu gamma^3 - adot^2 - bdot^2), or +infinity when the right-hand side is non-positive (tangential rates already exceed escape). Returning +infinity yields zero precision in the prior matrix used by the LM step, which is the desired "no prior" behavior. orbit_fit.cpp adds a single line to its unity-build chain: #include "bk_basis.cpp" so the new translation unit compiles into the existing _core module. No pybind11 binding yet -- that comes in a follow-up commit alongside Python-side unit tests for the math primitives. The math derivation, design decisions (barycenter origin, fixed gdot prior, eigendecomp+energy-prior solver, file layout, layered test plan) live in the project memory file bk_everywhere_design.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/orbit_fit/bk_basis.cpp | 213 ++++++++++++++++++++++++++++++++ src/lib/orbit_fit/bk_basis.h | 71 +++++++++++ src/lib/orbit_fit/orbit_fit.cpp | 1 + 3 files changed, 285 insertions(+) create mode 100644 src/lib/orbit_fit/bk_basis.cpp create mode 100644 src/lib/orbit_fit/bk_basis.h diff --git a/src/lib/orbit_fit/bk_basis.cpp b/src/lib/orbit_fit/bk_basis.cpp new file mode 100644 index 00000000..6dda0a29 --- /dev/null +++ b/src/lib/orbit_fit/bk_basis.cpp @@ -0,0 +1,213 @@ +// Bernstein-Khushalani parameterization primitives for the layup-internal +// universal BK fitter (feat/bk-everywhere). +// +// Pure C++/Eigen. No ASSIST, REBOUND, or pybind11 dependencies, so this +// translation unit can be tested in isolation. The design and math +// derivation live in the project memory file bk_everywhere_design.md. + +#include "bk_basis.h" + +#include +#include + +namespace orbit_fit +{ + + namespace + { + // Internal cached quantities at the BK position (alpha, beta). + // + // p = n0 + alpha*a + beta*b + // s_sq = 1 + alpha^2 + beta^2 = |p|^2 + // rho_hat = p / sqrt(s_sq) + // rho_hat_alpha = (a - (a . rho_hat) * rho_hat) / sqrt(s_sq) + // rho_hat_beta = (b - (b . rho_hat) * rho_hat) / sqrt(s_sq) + // + // rho_hat_alpha and rho_hat_beta are the gnomonic-projection tangent + // vectors at rho_hat; they are NOT unit length in general (they + // scale as 1/sqrt(s_sq) times the projection of a/b onto T_{rho_hat}). + struct RhoFrame + { + double s_sq; + double s; + Eigen::Vector3d rho_hat; + Eigen::Vector3d rho_hat_alpha; + Eigen::Vector3d rho_hat_beta; + }; + + RhoFrame compute_rho_frame(double alpha, double beta, const BKFiducial &fid) + { + RhoFrame f; + f.s_sq = 1.0 + alpha * alpha + beta * beta; + f.s = std::sqrt(f.s_sq); + const Eigen::Vector3d p = fid.n0 + alpha * fid.a + beta * fid.b; + f.rho_hat = p / f.s; + const double rho_dot_a = f.rho_hat.dot(fid.a); + const double rho_dot_b = f.rho_hat.dot(fid.b); + f.rho_hat_alpha = (fid.a - rho_dot_a * f.rho_hat) / f.s; + f.rho_hat_beta = (fid.b - rho_dot_b * f.rho_hat) / f.s; + return f; + } + } // namespace + + BKFiducial choose_fiducial(const std::vector &rho_hats) + { + BKFiducial fid; + Eigen::Vector3d mean = Eigen::Vector3d::Zero(); + for (const auto &r : rho_hats) + mean += r; + if (mean.norm() < 1e-12) + { + // Pathological: observation directions cancel out. Pick ICRS x + // as a fallback; the fit is gauge-invariant under fiducial choice + // anyway, so any nonzero direction works. + mean = Eigen::Vector3d::UnitX(); + } + fid.n0 = mean.normalized(); + + // Gram-Schmidt against the ICRS axis least parallel to n0 so we + // don't divide by something tiny. + const Eigen::Vector3d seed = std::abs(fid.n0.z()) < 0.9 + ? Eigen::Vector3d::UnitZ() + : Eigen::Vector3d::UnitX(); + fid.a = (seed - seed.dot(fid.n0) * fid.n0).normalized(); + fid.b = fid.n0.cross(fid.a); + return fid; + } + + Eigen::Matrix bk_to_cartesian( + const BKState &bk, const BKFiducial &fid) + { + const RhoFrame f = compute_rho_frame(bk.alpha, bk.beta, fid); + const double inv_g = 1.0 / bk.gamma; + const double inv_g2 = inv_g * inv_g; + + const Eigen::Vector3d r = inv_g * f.rho_hat; + const Eigen::Vector3d v = inv_g * (bk.adot * f.rho_hat_alpha + bk.bdot * f.rho_hat_beta) + - bk.gdot * inv_g2 * f.rho_hat; + + Eigen::Matrix cart; + cart << r, v; + return cart; + } + + BKState cartesian_to_bk( + const Eigen::Matrix &cart, const BKFiducial &fid) + { + const Eigen::Vector3d r = cart.head<3>(); + const Eigen::Vector3d v = cart.tail<3>(); + + const double r_norm = r.norm(); + const double gamma = 1.0 / r_norm; + const Eigen::Vector3d rho_hat = gamma * r; + + // Gnomonic tangent-plane coordinates of rho_hat at n0. + const double u = rho_hat.dot(fid.n0); + const double alpha = rho_hat.dot(fid.a) / u; + const double beta = rho_hat.dot(fid.b) / u; + + // gdot = d/dt (1/|r|) = -(r . v) / |r|^3 = -gamma^2 * (rho_hat . v) + const double gdot = -gamma * gamma * rho_hat.dot(v); + + // d(rho_hat)/dt = gamma * (v - (v . rho_hat) * rho_hat) -- v's component perp to rho_hat, + // scaled to a sphere tangent vector. alpha-dot, beta-dot come from + // applying the quotient rule to alpha = (rho_hat . a) / (rho_hat . n0) + // and similarly for beta. + const Eigen::Vector3d rho_dot = gamma * (v - v.dot(rho_hat) * rho_hat); + const double rho_dot_n0 = rho_dot.dot(fid.n0); + const double adot = (rho_dot.dot(fid.a) - alpha * rho_dot_n0) / u; + const double bdot = (rho_dot.dot(fid.b) - beta * rho_dot_n0) / u; + + BKState bk; + bk.alpha = alpha; + bk.beta = beta; + bk.gamma = gamma; + bk.adot = adot; + bk.bdot = bdot; + bk.gdot = gdot; + return bk; + } + + Eigen::Matrix dcart_dbk( + const BKState &bk, const BKFiducial &fid) + { + const double alpha = bk.alpha; + const double beta = bk.beta; + const double gamma = bk.gamma; + const double adot = bk.adot; + const double bdot = bk.bdot; + const double gdot = bk.gdot; + + const RhoFrame f = compute_rho_frame(alpha, beta, fid); + const double inv_g = 1.0 / gamma; + const double inv_g2 = inv_g * inv_g; + const double inv_g3 = inv_g2 * inv_g; + const double inv_s2 = 1.0 / f.s_sq; + const double inv_s4 = inv_s2 * inv_s2; + + Eigen::Matrix J = Eigen::Matrix::Zero(); + + // Top-left and bottom-right 3x3 blocks: d(r)/d(alpha,beta,gamma) and + // d(v)/d(adot,bdot,gdot) -- identical shape. + const Eigen::Vector3d dr_dalpha = inv_g * f.rho_hat_alpha; + const Eigen::Vector3d dr_dbeta = inv_g * f.rho_hat_beta; + const Eigen::Vector3d dr_dgamma = -inv_g2 * f.rho_hat; + + J.block<3, 1>(0, 0) = dr_dalpha; + J.block<3, 1>(0, 1) = dr_dbeta; + J.block<3, 1>(0, 2) = dr_dgamma; + + J.block<3, 1>(3, 3) = dr_dalpha; + J.block<3, 1>(3, 4) = dr_dbeta; + J.block<3, 1>(3, 5) = dr_dgamma; + + // Bottom-left 3x3 block: d(v)/d(alpha,beta,gamma). Needs second + // derivatives of rho_hat with respect to (alpha, beta): + // d rho_hat_alpha / d alpha = -(1+beta^2)/s^4 * rho_hat + // - 2 alpha / s^2 * rho_hat_alpha + // d rho_hat_alpha / d beta = (alpha*beta)/s^4 * rho_hat + // - alpha/s^2 * rho_hat_beta + // - beta/s^2 * rho_hat_alpha + // d rho_hat_beta / d beta = -(1+alpha^2)/s^4 * rho_hat + // - 2 beta / s^2 * rho_hat_beta + // and d rho_hat_beta / d alpha == d rho_hat_alpha / d beta by mixed-partial symmetry. + const Eigen::Vector3d d_rha_dalpha = -(1.0 + beta * beta) * inv_s4 * f.rho_hat + - 2.0 * alpha * inv_s2 * f.rho_hat_alpha; + const Eigen::Vector3d d_rha_dbeta = (alpha * beta) * inv_s4 * f.rho_hat + - alpha * inv_s2 * f.rho_hat_beta + - beta * inv_s2 * f.rho_hat_alpha; + const Eigen::Vector3d d_rhb_dbeta = -(1.0 + alpha * alpha) * inv_s4 * f.rho_hat + - 2.0 * beta * inv_s2 * f.rho_hat_beta; + + // d v / d alpha + const Eigen::Vector3d dv_dalpha = inv_g * (adot * d_rha_dalpha + bdot * d_rha_dbeta) + - gdot * inv_g2 * f.rho_hat_alpha; + // d v / d beta + const Eigen::Vector3d dv_dbeta = inv_g * (adot * d_rha_dbeta + bdot * d_rhb_dbeta) + - gdot * inv_g2 * f.rho_hat_beta; + // d v / d gamma + const Eigen::Vector3d dv_dgamma = -inv_g2 * (adot * f.rho_hat_alpha + bdot * f.rho_hat_beta) + + 2.0 * gdot * inv_g3 * f.rho_hat; + + J.block<3, 1>(3, 0) = dv_dalpha; + J.block<3, 1>(3, 1) = dv_dbeta; + J.block<3, 1>(3, 2) = dv_dgamma; + + return J; + } + + double sigma_gdot_sq(const BKState &bk, double mu) + { + const double gamma = bk.gamma; + const double rhs = 2.0 * mu * gamma * gamma * gamma + - bk.adot * bk.adot - bk.bdot * bk.bdot; + if (rhs <= 0.0) + { + // Tangential rates already exceed escape velocity for this + // gamma; the energy bound provides no constraint on gdot. + return std::numeric_limits::infinity(); + } + return gamma * gamma * rhs; + } + +} // namespace orbit_fit diff --git a/src/lib/orbit_fit/bk_basis.h b/src/lib/orbit_fit/bk_basis.h new file mode 100644 index 00000000..634ba61a --- /dev/null +++ b/src/lib/orbit_fit/bk_basis.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +namespace orbit_fit +{ + + // Bernstein-Khushalani parameters at epoch. Origin: barycenter. + // (alpha, beta) are gnomonic tangent-plane coordinates of the + // line-of-sight direction rho_hat at a fiducial direction n0, + // gamma = 1/|r_helio|, and (adot, bdot, gdot) are their time + // derivatives at the epoch. + struct BKState + { + double alpha = 0.0; + double beta = 0.0; + double gamma = 0.0; + double adot = 0.0; + double bdot = 0.0; + double gdot = 0.0; + }; + + // Orthonormal frame defining the BK gnomonic tangent plane. + // {a, b, n0} form a right-handed orthonormal basis; n0 is the + // fiducial line-of-sight, (a, b) span its tangent plane. + struct BKFiducial + { + Eigen::Vector3d n0; + Eigen::Vector3d a; + Eigen::Vector3d b; + }; + + // Choose a fiducial frame from a list of line-of-sight unit vectors. + // n0 := normalize(sum(rho_hats)); (a, b) constructed by Gram-Schmidt + // against ICRS z (or ICRS x if n0 is near the z-axis). This is one + // of many valid choices -- the BK fit is gauge-invariant under any + // rotation of (a, b) about n0. + BKFiducial choose_fiducial(const std::vector &rho_hats); + + // Forward transform: BK -> barycentric Cartesian (position + velocity). + // r_vec = (1/gamma) * rho_hat(alpha, beta) + // v_vec = (1/gamma) [adot * rho_hat_alpha + bdot * rho_hat_beta] + // - (gdot/gamma^2) * rho_hat(alpha, beta) + Eigen::Matrix bk_to_cartesian( + const BKState &bk, const BKFiducial &fid); + + // Inverse transform: barycentric Cartesian -> BK. Well-defined for + // any state with r_vec . n0 > 0 (object on the n0-facing hemisphere) + // and gamma > 0. + BKState cartesian_to_bk( + const Eigen::Matrix &cart, const BKFiducial &fid); + + // 6x6 Jacobian d(r_vec, v_vec) / d(alpha, beta, gamma, adot, bdot, gdot). + // Block structure (each block is 3x3): + // [ d r / d (alpha,beta,gamma) 0 ] + // [ d v / d (alpha,beta,gamma) d v / d (adot,bdot,gdot) ] + // Top-left and bottom-right blocks are identical (both arise from the + // (1/gamma) * tangent-vector structure). + Eigen::Matrix dcart_dbk( + const BKState &bk, const BKFiducial &fid); + + // Variance of the bound-orbit gdot prior: + // sigma_gdot^2 = gamma^2 * (2 * mu * gamma^3 - adot^2 - bdot^2) + // Returns +infinity when the tangential rates already exceed escape + // (the right-hand side would be non-positive), signalling "no prior." + // The caller's precision is 1 / sigma_gdot_sq, so +inf -> 0 precision + // -> no contribution, which is the correct behavior. + double sigma_gdot_sq(const BKState &bk, double mu); + +} // namespace orbit_fit diff --git a/src/lib/orbit_fit/orbit_fit.cpp b/src/lib/orbit_fit/orbit_fit.cpp index 69d682ac..309e7bb6 100644 --- a/src/lib/orbit_fit/orbit_fit.cpp +++ b/src/lib/orbit_fit/orbit_fit.cpp @@ -53,6 +53,7 @@ #include #include "orbit_fit.h" +#include "bk_basis.cpp" #include "../gauss/gauss.cpp" #include "predict.cpp" From d8faf6431954d2fd49587a63379680ea8623e2fd Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 16:56:29 -0400 Subject: [PATCH 03/12] Bind BK basis primitives to Python and add Layer 1 tests. Adds a bk_basis_bindings(py::module&) entry point that exposes BKState, BKFiducial, bk_choose_fiducial, bk_to_cartesian, cartesian_to_bk, dcart_dbk, and sigma_gdot_sq to Python via pybind11 / pybind11/eigen.h, and wires the binding into main.cpp's _core module alongside the existing detection_bindings etc. tests/layup/test_bk_basis.py covers the pure-math invariants (25 tests, all passing): * Round-trip Cartesian <-> BK across mainbelt / NEO / TNO regimes (rtol 1e-12). * Analytic dcart_dbk vs central-difference, per-element relative error < 1e-5 with parameter-scaled epsilon. * Mixed-partial symmetry of the second-derivative cross-terms appearing in the bottom-left block of dcart_dbk (d^2 r / d alpha d beta == d^2 r / d beta d alpha to FD tolerance). * Fiducial-direction gauge invariance: two valid n0 choices recover the same Cartesian orbit through round-trip. * Special-case forms at the fiducial direction alpha = beta = 0: position is (1/gamma) n0, top-left and bottom-right Jacobian blocks are [(1/gamma) a, (1/gamma) b, -(1/gamma^2) n0] as columns, bottom-left block vanishes when rates are zero. * sigma_gdot_sq agreement with the Cartesian energy bound at the parabolic boundary, and +inf return when tangential rates already exceed escape. The energy-bound test caught a real bug in the first cut of sigma_gdot_sq: the formula gamma^2 (2 mu gamma^3 - adot^2 - bdot^2) is only exact at the fiducial direction. Off-fiducial, the gnomonic tangent vectors rho_hat_alpha, rho_hat_beta have magnitudes sqrt((1+beta^2))/s^2 and sqrt((1+alpha^2))/s^2 respectively, and an inner product -alpha*beta/s^4, so the true tangential-velocity term is |adot rho_hat_alpha + bdot rho_hat_beta|^2 = [adot^2 (1+beta^2) - 2 adot bdot alpha beta + bdot^2 (1+alpha^2)] / s^4 which reduces to adot^2 + bdot^2 only at alpha = beta = 0. Fixed in sigma_gdot_sq (and bk_basis.h documentation) to use the exact form, which reproduces the parabolic-boundary condition |v|^2 = 2 mu / |r| to machine precision in the test. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/orbit_fit/bk_basis.cpp | 91 ++++++++++- src/lib/orbit_fit/bk_basis.h | 20 ++- src/main.cpp | 1 + tests/layup/test_bk_basis.py | 275 +++++++++++++++++++++++++++++++++ 4 files changed, 377 insertions(+), 10 deletions(-) create mode 100644 tests/layup/test_bk_basis.py diff --git a/src/lib/orbit_fit/bk_basis.cpp b/src/lib/orbit_fit/bk_basis.cpp index 6dda0a29..b0febb05 100644 --- a/src/lib/orbit_fit/bk_basis.cpp +++ b/src/lib/orbit_fit/bk_basis.cpp @@ -1,14 +1,23 @@ // Bernstein-Khushalani parameterization primitives for the layup-internal // universal BK fitter (feat/bk-everywhere). // -// Pure C++/Eigen. No ASSIST, REBOUND, or pybind11 dependencies, so this -// translation unit can be tested in isolation. The design and math -// derivation live in the project memory file bk_everywhere_design.md. +// The math layer is pure C++/Eigen -- no ASSIST or REBOUND dependencies -- +// so this translation unit can be reasoned about and tested in isolation +// of the dynamics path. pybind11 bindings at the bottom expose the +// primitives to Python so Layer 1 tests (round-trip, finite-difference +// Jacobian, mixed-partial symmetry, etc.) can run via pytest. The design +// and math derivation live in the project memory file +// bk_everywhere_design.md. #include "bk_basis.h" #include #include +#include + +#include +#include +#include namespace orbit_fit { @@ -198,9 +207,30 @@ namespace orbit_fit double sigma_gdot_sq(const BKState &bk, double mu) { + // Bound-orbit constraint: 0.5 |v|^2 <= mu / |r| = mu * gamma. + // Substituting |v|^2 = gdot^2 / gamma^4 + |adot rho_hat_alpha + bdot rho_hat_beta|^2 / gamma^2 + // (cross-terms with rho_hat vanish since rho_hat_alpha, rho_hat_beta are tangent to rho_hat), + // + // gdot^2 <= gamma^2 * (2 mu gamma^3 - |adot rho_hat_alpha + bdot rho_hat_beta|^2) + // + // The tangential term expands via + // |rho_hat_alpha|^2 = (1+beta^2)/s^4, |rho_hat_beta|^2 = (1+alpha^2)/s^4, + // rho_hat_alpha . rho_hat_beta = -alpha*beta/s^4, + // so + // |adot rho_hat_alpha + bdot rho_hat_beta|^2 = + // [adot^2 (1+beta^2) - 2 adot bdot alpha beta + bdot^2 (1+alpha^2)] / s^4 . + // At alpha = beta = 0 (the fiducial direction) this reduces to adot^2 + bdot^2. + const double alpha = bk.alpha; + const double beta = bk.beta; const double gamma = bk.gamma; - const double rhs = 2.0 * mu * gamma * gamma * gamma - - bk.adot * bk.adot - bk.bdot * bk.bdot; + const double adot = bk.adot; + const double bdot = bk.bdot; + const double s_sq = 1.0 + alpha * alpha + beta * beta; + const double s4 = s_sq * s_sq; + const double v_tan_sq = (adot * adot * (1.0 + beta * beta) + - 2.0 * adot * bdot * alpha * beta + + bdot * bdot * (1.0 + alpha * alpha)) / s4; + const double rhs = 2.0 * mu * gamma * gamma * gamma - v_tan_sq; if (rhs <= 0.0) { // Tangential rates already exceed escape velocity for this @@ -210,4 +240,55 @@ namespace orbit_fit return gamma * gamma * rhs; } + static void bk_basis_bindings(pybind11::module &m) + { + namespace py = pybind11; + + py::class_(m, "BKState") + .def(py::init<>()) + .def(py::init([](double alpha, double beta, double gamma, + double adot, double bdot, double gdot) + { + BKState bk; + bk.alpha = alpha; bk.beta = beta; bk.gamma = gamma; + bk.adot = adot; bk.bdot = bdot; bk.gdot = gdot; + return bk; }), + py::arg("alpha") = 0.0, py::arg("beta") = 0.0, py::arg("gamma") = 0.0, + py::arg("adot") = 0.0, py::arg("bdot") = 0.0, py::arg("gdot") = 0.0) + .def_readwrite("alpha", &BKState::alpha) + .def_readwrite("beta", &BKState::beta) + .def_readwrite("gamma", &BKState::gamma) + .def_readwrite("adot", &BKState::adot) + .def_readwrite("bdot", &BKState::bdot) + .def_readwrite("gdot", &BKState::gdot) + .def("__repr__", [](const BKState &b) + { + std::ostringstream s; + s << ""; + return s.str(); }); + + py::class_(m, "BKFiducial") + .def(py::init<>()) + .def_readwrite("n0", &BKFiducial::n0) + .def_readwrite("a", &BKFiducial::a) + .def_readwrite("b", &BKFiducial::b); + + m.def("bk_choose_fiducial", &choose_fiducial, py::arg("rho_hats"), + "Construct a BKFiducial frame from a list of unit line-of-sight vectors."); + m.def("bk_to_cartesian", &bk_to_cartesian, + py::arg("bk"), py::arg("fid"), + "Forward transform: BK state -> 6-vector of barycentric Cartesian (r, v)."); + m.def("cartesian_to_bk", &cartesian_to_bk, + py::arg("cart"), py::arg("fid"), + "Inverse transform: 6-vector barycentric Cartesian -> BK state."); + m.def("dcart_dbk", &dcart_dbk, + py::arg("bk"), py::arg("fid"), + "6x6 Jacobian d(r, v) / d(alpha, beta, gamma, adot, bdot, gdot)."); + m.def("sigma_gdot_sq", &sigma_gdot_sq, + py::arg("bk"), py::arg("mu"), + "Variance of the bound-orbit energy prior on gdot."); + } + } // namespace orbit_fit diff --git a/src/lib/orbit_fit/bk_basis.h b/src/lib/orbit_fit/bk_basis.h index 634ba61a..c0cb263d 100644 --- a/src/lib/orbit_fit/bk_basis.h +++ b/src/lib/orbit_fit/bk_basis.h @@ -61,11 +61,21 @@ namespace orbit_fit const BKState &bk, const BKFiducial &fid); // Variance of the bound-orbit gdot prior: - // sigma_gdot^2 = gamma^2 * (2 * mu * gamma^3 - adot^2 - bdot^2) - // Returns +infinity when the tangential rates already exceed escape - // (the right-hand side would be non-positive), signalling "no prior." - // The caller's precision is 1 / sigma_gdot_sq, so +inf -> 0 precision - // -> no contribution, which is the correct behavior. + // sigma_gdot^2 = gamma^2 * (2 mu gamma^3 - |adot rho_hat_alpha + bdot rho_hat_beta|^2) + // + // The tangential-velocity term in the energy bound depends on the gnomonic + // tangent-vector norms at (alpha, beta), so the exact form expands to + // sigma_gdot^2 = gamma^2 * (2 mu gamma^3 - + // [adot^2 (1+beta^2) + // - 2 adot bdot alpha beta + // + bdot^2 (1+alpha^2)] / s^4) + // where s^2 = 1 + alpha^2 + beta^2. At the fiducial direction (alpha = beta = 0) + // this reduces to the familiar gamma^2 (2 mu gamma^3 - adot^2 - bdot^2). + // + // Returns +infinity when the tangential rates already exceed escape (the + // right-hand side would be non-positive), signalling "no prior." The + // caller's precision is 1 / sigma_gdot_sq, so +inf -> 0 precision -> + // no contribution, which is the correct behavior. double sigma_gdot_sq(const BKState &bk, double mu); } // namespace orbit_fit diff --git a/src/main.cpp b/src/main.cpp index c5a06424..b950669b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,6 +71,7 @@ PYBIND11_MODULE(_core, m) orbit_fit::orbit_fit_result_bindings(m); orbit_fit::predict_bindings(m); orbit_fit::predict_result_bindings(m); + orbit_fit::bk_basis_bindings(m); #ifdef VERSION_INFO m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); diff --git a/tests/layup/test_bk_basis.py b/tests/layup/test_bk_basis.py new file mode 100644 index 00000000..5fec760c --- /dev/null +++ b/tests/layup/test_bk_basis.py @@ -0,0 +1,275 @@ +"""Layer 1 tests for the BK basis primitives. + +These tests exercise the pure-math layer of the universal BK fitter +(`src/lib/orbit_fit/bk_basis.cpp`) via the pybind11 bindings exposed +through `layup.routines`. No ASSIST/REBOUND/ephemeris setup is +required -- the math primitives operate on barycentric Cartesian +states and BK parameters directly. + +Coverage: + * round-trip Cartesian <-> BK transforms + * analytic dcart_dbk vs central-difference finite-differences + * mixed-partial symmetry of the second derivatives that show up + in the bottom-left cross-term block + * fiducial-direction gauge invariance (different choices of n0 + must give the same Cartesian orbit) + * special-case forms at alpha = beta = 0 (the fiducial direction) + * sigma_gdot_sq agreement with the bound-orbit energy bound + computed independently in Cartesian +""" + +from __future__ import annotations + +import numpy as np +import pytest + +from layup.routines import ( + BKState, + BKFiducial, + bk_choose_fiducial, + bk_to_cartesian, + cartesian_to_bk, + dcart_dbk, + sigma_gdot_sq, +) + + +# GM_sun in AU^3 / day^2 (Gaussian gravitational constant squared). +MU_SUN = 0.00029591220828559104 + + +# A spread of test BK states covering mainbelt / NEO / TNO regimes. +# Format: (alpha, beta, gamma, adot, bdot, gdot) with gamma = 1/r_helio in 1/AU. +_BK_CASES = [ + # mainbelt ~3 AU, near-circular + (0.1, -0.05, 1.0 / 3.0, 1e-3, -8e-4, 3e-6), + # NEO ~1.2 AU, modest rates + (-0.2, 0.15, 1.0 / 1.2, 5e-3, 4e-3, -1e-5), + # TNO ~42 AU, slow + (0.02, 0.01, 1.0 / 42.0, 4e-5, -3e-5, 1e-8), + # near the fiducial direction (small alpha, beta) -- a common case + (1e-4, 2e-4, 0.025, 6e-5, 5e-5, -2e-7), + # off the fiducial direction + (0.5, -0.4, 0.05, 2e-4, 1e-4, -3e-8), +] + + +def _make_fiducial(rng: np.random.Generator) -> BKFiducial: + """Pick a reproducible fiducial direction not aligned with an ICRS axis.""" + n0 = rng.normal(size=3) + n0 /= np.linalg.norm(n0) + return bk_choose_fiducial([n0]) + + +def _bk_from_tuple(t): + return BKState(*t) + + +# --------------------------------------------------------------------------- +# Round-trip Cartesian <-> BK +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("case", _BK_CASES) +def test_round_trip_bk_to_cart_to_bk(case): + """BK -> Cartesian -> BK recovers the input to machine precision.""" + rng = np.random.default_rng(seed=12345) + fid = _make_fiducial(rng) + bk = _bk_from_tuple(case) + cart = bk_to_cartesian(bk, fid) + bk_back = cartesian_to_bk(cart, fid) + for name in ("alpha", "beta", "gamma", "adot", "bdot", "gdot"): + original = getattr(bk, name) + recovered = getattr(bk_back, name) + np.testing.assert_allclose(recovered, original, rtol=1e-12, atol=1e-15, + err_msg=f"BK.{name} not recovered through round-trip") + + +@pytest.mark.parametrize("case", _BK_CASES) +def test_round_trip_cart_to_bk_to_cart(case): + """Cartesian -> BK -> Cartesian recovers the input to machine precision.""" + rng = np.random.default_rng(seed=67890) + fid = _make_fiducial(rng) + bk = _bk_from_tuple(case) + cart_in = np.asarray(bk_to_cartesian(bk, fid)).flatten() + bk_round = cartesian_to_bk(cart_in, fid) + cart_out = np.asarray(bk_to_cartesian(bk_round, fid)).flatten() + np.testing.assert_allclose(cart_out, cart_in, rtol=1e-12, atol=1e-15) + + +# --------------------------------------------------------------------------- +# Analytic dcart_dbk vs finite-difference +# --------------------------------------------------------------------------- + +def _bk_perturb(bk: BKState, idx: int, delta: float) -> BKState: + names = ("alpha", "beta", "gamma", "adot", "bdot", "gdot") + vals = [getattr(bk, n) for n in names] + vals[idx] += delta + return BKState(*vals) + + +@pytest.mark.parametrize("case", _BK_CASES) +def test_dcart_dbk_matches_finite_difference(case): + """Analytic 6x6 Jacobian agrees with central-difference per element.""" + rng = np.random.default_rng(seed=11111) + fid = _make_fiducial(rng) + bk = _bk_from_tuple(case) + J_analytic = np.asarray(dcart_dbk(bk, fid)) + + # Step sizes scaled by parameter magnitude so we get a sensible FD + # for both the O(1) (alpha, beta) and O(1e-5) (rates) axes. + param_vals = (bk.alpha, bk.beta, bk.gamma, bk.adot, bk.bdot, bk.gdot) + eps = np.array([max(abs(v), 1.0) * 1e-6 for v in param_vals]) + + J_fd = np.zeros((6, 6)) + for i in range(6): + cart_plus = np.asarray(bk_to_cartesian(_bk_perturb(bk, i, eps[i]), fid)).flatten() + cart_minus = np.asarray(bk_to_cartesian(_bk_perturb(bk, i, -eps[i]), fid)).flatten() + J_fd[:, i] = (cart_plus - cart_minus) / (2.0 * eps[i]) + + # Relative tolerance covering the dynamic range of J entries. + scale = np.maximum(np.abs(J_analytic), np.abs(J_fd)) + scale = np.where(scale > 0, scale, 1.0) + np.testing.assert_array_less( + np.abs(J_analytic - J_fd) / scale, 1e-5, + err_msg="Analytic dcart_dbk disagrees with finite-difference", + ) + + +# --------------------------------------------------------------------------- +# Mixed-partial symmetry of the second-derivative cross-terms +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("case", _BK_CASES) +def test_mixed_partial_symmetry_via_finite_difference(case): + """d(r,v)/(d alpha d beta) -- approached via FD of dcart_dbk -- is symmetric.""" + rng = np.random.default_rng(seed=22222) + fid = _make_fiducial(rng) + bk = _bk_from_tuple(case) + eps_a = max(abs(bk.alpha), 1.0) * 1e-6 + eps_b = max(abs(bk.beta), 1.0) * 1e-6 + + # FD of dcart_dbk's alpha column with respect to beta + J_plus_b = np.asarray(dcart_dbk(_bk_perturb(bk, 1, eps_b), fid)) + J_minus_b = np.asarray(dcart_dbk(_bk_perturb(bk, 1, -eps_b), fid)) + d2r_dadb = (J_plus_b[:, 0] - J_minus_b[:, 0]) / (2.0 * eps_b) + + # FD of dcart_dbk's beta column with respect to alpha + J_plus_a = np.asarray(dcart_dbk(_bk_perturb(bk, 0, eps_a), fid)) + J_minus_a = np.asarray(dcart_dbk(_bk_perturb(bk, 0, -eps_a), fid)) + d2r_dbda = (J_plus_a[:, 1] - J_minus_a[:, 1]) / (2.0 * eps_a) + + np.testing.assert_allclose(d2r_dadb, d2r_dbda, atol=1e-5, rtol=1e-5) + + +# --------------------------------------------------------------------------- +# Fiducial-direction gauge invariance +# --------------------------------------------------------------------------- + +def test_fiducial_gauge_invariance(): + """Two valid n0 choices give the same physical Cartesian orbit.""" + rng = np.random.default_rng(seed=33333) + # An arbitrary Cartesian state (40 AU object, small velocity) + r = np.array([20.0, 30.0, 5.0]) + v = np.array([-2e-4, 1e-4, 5e-5]) + cart = np.concatenate([r, v]) + + # Two different fiducial frames: one nearly aligned with r_hat, one tilted. + r_hat = r / np.linalg.norm(r) + fid_aligned = bk_choose_fiducial([r_hat]) + + tilt = np.array([0.0, 0.0, 1.0]) + fid_tilted = bk_choose_fiducial([r_hat + 0.3 * tilt]) + + bk_aligned = cartesian_to_bk(cart, fid_aligned) + bk_tilted = cartesian_to_bk(cart, fid_tilted) + + cart_back_aligned = np.asarray(bk_to_cartesian(bk_aligned, fid_aligned)).flatten() + cart_back_tilted = np.asarray(bk_to_cartesian(bk_tilted, fid_tilted)).flatten() + + np.testing.assert_allclose(cart_back_aligned, cart, rtol=1e-12, atol=1e-13) + np.testing.assert_allclose(cart_back_tilted, cart, rtol=1e-12, atol=1e-13) + + +# --------------------------------------------------------------------------- +# Special-case forms at alpha = beta = 0 (the fiducial direction) +# --------------------------------------------------------------------------- + +def test_special_case_at_fiducial(): + """At alpha = beta = 0, rho_hat = n0, rho_hat_alpha = a, rho_hat_beta = b.""" + rng = np.random.default_rng(seed=44444) + fid = _make_fiducial(rng) + bk = BKState(alpha=0.0, beta=0.0, gamma=0.05, adot=0.0, bdot=0.0, gdot=0.0) + cart = np.asarray(bk_to_cartesian(bk, fid)).flatten() + + # Position at (alpha, beta) = (0, 0) is exactly (1/gamma) * n0. + expected_r = (1.0 / bk.gamma) * np.asarray(fid.n0) + np.testing.assert_allclose(cart[:3], expected_r, rtol=1e-13, atol=1e-13) + + # With all rates zero, velocity should be zero. + np.testing.assert_allclose(cart[3:], np.zeros(3), atol=1e-15) + + +def test_jacobian_at_fiducial(): + """At the fiducial direction, the Jacobian's top-left 3x3 block is + [a | b | -(1/gamma) * n0] (each as a column).""" + rng = np.random.default_rng(seed=55555) + fid = _make_fiducial(rng) + gamma = 0.025 + bk = BKState(alpha=0.0, beta=0.0, gamma=gamma, adot=0.0, bdot=0.0, gdot=0.0) + J = np.asarray(dcart_dbk(bk, fid)) + + expected_col_alpha = (1.0 / gamma) * np.asarray(fid.a) + expected_col_beta = (1.0 / gamma) * np.asarray(fid.b) + expected_col_gamma = -(1.0 / gamma**2) * np.asarray(fid.n0) + + np.testing.assert_allclose(J[:3, 0], expected_col_alpha, rtol=1e-13, atol=1e-15) + np.testing.assert_allclose(J[:3, 1], expected_col_beta, rtol=1e-13, atol=1e-15) + np.testing.assert_allclose(J[:3, 2], expected_col_gamma, rtol=1e-13, atol=1e-15) + + # Bottom-right block should equal the top-left block (same shape). + np.testing.assert_allclose(J[3:, 3:], J[:3, :3], rtol=1e-13, atol=1e-15) + + # Bottom-left block should be zero when adot = bdot = gdot = 0. + np.testing.assert_allclose(J[3:, :3], np.zeros((3, 3)), atol=1e-15) + + +# --------------------------------------------------------------------------- +# sigma_gdot_sq vs Cartesian-side energy-bound calculation +# --------------------------------------------------------------------------- + +def test_sigma_gdot_sq_consistent_with_energy_bound(): + """sigma_gdot_sq matches the Cartesian-side bound on |gdot|^2 for a bound orbit. + + Derivation: the bound-orbit energy constraint 0.5 |v|^2 <= mu / |r| + rearranges, in BK at fixed (alpha, beta, gamma, adot, bdot), to + gdot^2 <= gamma^2 * (2 * mu * gamma^3 - adot^2 - bdot^2), + which is exactly the formula sigma_gdot_sq returns. + """ + rng = np.random.default_rng(seed=66666) + fid = _make_fiducial(rng) + # Pick (alpha, beta, gamma, adot, bdot) such that the orbit is bound for + # at least some gdot range. + bk = BKState(alpha=0.05, beta=-0.03, gamma=1.0 / 40.0, + adot=4e-5, bdot=-3e-5, gdot=0.0) + sigma_sq = sigma_gdot_sq(bk, MU_SUN) + + # The bound-orbit constraint -> |v|^2 <= 2 * mu * gamma. At the BK state + # with gdot pinned to +sqrt(sigma_sq), the orbit should be exactly at the + # boundary (|v|^2 == 2 * mu * gamma). + assert sigma_sq > 0.0 and np.isfinite(sigma_sq) + bk_at_boundary = BKState(alpha=bk.alpha, beta=bk.beta, gamma=bk.gamma, + adot=bk.adot, bdot=bk.bdot, gdot=np.sqrt(sigma_sq)) + cart = np.asarray(bk_to_cartesian(bk_at_boundary, fid)).flatten() + r_norm = np.linalg.norm(cart[:3]) + v_norm_sq = np.dot(cart[3:], cart[3:]) + energy = 0.5 * v_norm_sq - MU_SUN / r_norm + # At the boundary, total energy = 0 (parabolic orbit). + np.testing.assert_allclose(energy, 0.0, atol=1e-12) + + +def test_sigma_gdot_sq_returns_inf_for_hyperbolic_tangentials(): + """When tangential rates already exceed escape velocity, sigma_gdot_sq + signals 'no prior' by returning +infinity.""" + bk = BKState(alpha=0.0, beta=0.0, gamma=1.0 / 50.0, + adot=1e-3, bdot=1e-3, gdot=0.0) # huge rates at 50 AU + assert np.isinf(sigma_gdot_sq(bk, MU_SUN)) From 418c144e9c682b635a92a2a9b179fa49b51dd770 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 17:12:51 -0400 Subject: [PATCH 04/12] Add universal-BK LM driver run_bk_native_fit. bk_fit.cpp contains the LM driver that performs an orbit fit in the universal Bernstein-Khushalani parameterization on top of layup's existing Cartesian variational machinery. Included from orbit_fit.cpp inside the `namespace orbit_fit` block, after the Cartesian helpers (compute_residuals, create_sequences, get_weight_matrix, converged) and the Observation/FitResult types are in scope, so no forward declarations are needed. The driver structure mirrors run_from_vector_with_initial_guess but operates in BK basis throughout: 1. Pick a fiducial direction from the observations' rho_hat vectors (mean direction, Gram-Schmidt for the orthonormal a, b). 2. Convert the Cartesian seed to BK via cartesian_to_bk. 3. Compute a fixed bound-orbit energy prior precision on gdot from the BK seed (1 / sigma_gdot_sq), zero precision otherwise. 4. LM loop: - convert current BK state to Cartesian -> reb_particle - call compute_residuals to get tangent-plane residuals and Cartesian 6-element partials per observation - chain-rule: B_bk = B_cart * dcart_dbk(current BK, fiducial) - assemble C = B_bk^T W B_bk + lambda I + P_prior, grad = B_bk^T W r + P_prior * p_bk - solve, Marquardt rho-ratio accept/reject, update BK state, check convergence (using the existing `converged` predicate). 5. On convergence: - cov_bk = (B^T W B + P_prior)^-1 (Hessian without lambda) - cov_cart = J cov_bk J^T (J = dcart_dbk at converged BK state) - return FitResult with state = bk_to_cartesian(BK_final) and cov flattened from cov_cart. method = "bk_native". Initial lambda and Marquardt accept threshold match the Cartesian fit at orbit_fit.cpp:553. Early-exit guard: returns a non-success FitResult (flag = 1) without crashing when detections.size() < 3. main.cpp gains an orbit_fit::bk_fit_bindings(m) call alongside the existing orbit_fit bindings, exposing run_bk_native_fit to Python. tests/layup/test_bk_fit.py covers the Layer 2 smoke tests: * binding loads and run_bk_native_fit returns a FitResult * empty-obs path returns flag != 0 without crashing * <3 obs path triggers the early-exit guard Layer 2 convergence tests against synthetic observations from a known orbit (and the Cartesian/BK agreement test on well-arced mainbelt) are next steps -- they need either the predict-path output piped back in or the diagnostic/scan dataset wired up to this branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/orbit_fit/bk_fit.cpp | 230 ++++++++++++++++++++++++++++++++ src/lib/orbit_fit/orbit_fit.cpp | 8 ++ src/main.cpp | 1 + tests/layup/test_bk_fit.py | 83 ++++++++++++ 4 files changed, 322 insertions(+) create mode 100644 src/lib/orbit_fit/bk_fit.cpp create mode 100644 tests/layup/test_bk_fit.py diff --git a/src/lib/orbit_fit/bk_fit.cpp b/src/lib/orbit_fit/bk_fit.cpp new file mode 100644 index 00000000..ef325dbd --- /dev/null +++ b/src/lib/orbit_fit/bk_fit.cpp @@ -0,0 +1,230 @@ +// Levenberg-Marquardt driver for the universal-BK fitter +// (feat/bk-everywhere). Included from orbit_fit.cpp at the bottom of +// its `namespace orbit_fit { ... }` block, so all of layup's existing +// Cartesian-side machinery is in scope without forward declarations: +// +// Observation, FitResult (from detection.cpp / fit_result.cpp) +// residuals, partials (from orbit_fit.h) +// compute_residuals, create_sequences, get_weight_matrix, converged +// (from orbit_fit.cpp) +// bk_basis::* (from bk_basis.cpp -- included earlier in TU) +// +// The driver mirrors `orbit_fit()` and `run_from_vector_with_initial_guess()` +// but works in BK basis throughout: chain-rules the per-observation +// Cartesian partials produced by `compute_residuals` through the 6x6 +// dcart_dbk Jacobian into BK basis, then assembles and solves +// C = B^T W B + lambda I + P_prior in BK coordinates. + +// FitResult run_bk_native_fit +// +// See bk_everywhere_design.md for the algorithmic design. + +FitResult run_bk_native_fit( + struct assist_ephem *ephem, + FitResult initial_guess, + std::vector &detections, + double mu) +{ + FitResult result; + result.method = "bk_native"; + result.flag = 1; + result.epoch = initial_guess.epoch; + result.csq = HUGE_VAL; + result.niter = 0; + result.ndof = (int)(2 * detections.size() - 6); + result.state = initial_guess.state; + result.cov.fill(0.0); + + if (detections.size() < 3) + { + // Not enough observations to constrain a 6-parameter fit. + return result; + } + + // ----- Pick a fiducial direction from the observations ----- + std::vector rho_hats; + rho_hats.reserve(detections.size()); + for (const auto &obs : detections) + { + rho_hats.push_back(obs.rho_hat); + } + const BKFiducial fid = choose_fiducial(rho_hats); + + // ----- Convert the seed to BK ----- + Eigen::Matrix cart_seed; + for (int i = 0; i < 6; i++) cart_seed(i) = initial_guess.state[i]; + BKState bk = cartesian_to_bk(cart_seed, fid); + const BKState bk_seed = bk; + + // ----- Fixed bound-orbit energy prior on gdot ----- + const double sgsq = sigma_gdot_sq(bk_seed, mu); + const double gdot_precision = (std::isfinite(sgsq) && sgsq > 0.0) ? (1.0 / sgsq) : 0.0; + Eigen::Matrix P_prior = Eigen::Matrix::Zero(); + P_prior(5, 5) = gdot_precision; + + // ----- LM workspace ----- + std::vector reverse_in_seq, reverse_out_seq; + std::vector forward_in_seq, forward_out_seq; + std::vector times(detections.size()); + for (size_t i = 0; i < detections.size(); i++) + { + times[i] = detections[i].epoch; + } + create_sequences(times, initial_guess.epoch, + reverse_in_seq, reverse_out_seq, + forward_in_seq, forward_out_seq); + + Eigen::SparseMatrix W = get_weight_matrix(detections); + + std::vector resid_vec(detections.size()); + std::vector partials_vec(detections.size()); + + // ----- LM loop ----- + // Same initial lambda and accept threshold as the Cartesian fit + // (orbit_fit at orbit_fit.cpp L553). + double lambda = (206265.0 * 206265.0) / 1000.0; + const double rho_accept = 0.1; + double chi2_prev = HUGE_VAL; + double chi2_cur = HUGE_VAL; + Eigen::Matrix C_no_lambda; // last accepted Hessian sans Marquardt damping + bool have_accepted_step = false; + + const size_t iter_max = 100; + const double eps = 1e-12; + size_t iters; + for (iters = 0; iters < iter_max; iters++) + { + // --- Build current Cartesian state from BK --- + const Eigen::Matrix cart_now = bk_to_cartesian(bk, fid); + struct reb_particle p0; + p0.x = cart_now(0); p0.y = cart_now(1); p0.z = cart_now(2); + p0.vx = cart_now(3); p0.vy = cart_now(4); p0.vz = cart_now(5); + p0.m = 0.0; p0.r = 0.0; + p0.ax = 0.0; p0.ay = 0.0; p0.az = 0.0; + p0.hash = 0; + + // --- Residuals + Cartesian partials via the existing variational pipeline --- + compute_residuals(ephem, p0, initial_guess.epoch, + detections, + resid_vec, partials_vec, + forward_in_seq, forward_out_seq, + reverse_in_seq, reverse_out_seq); + + // --- Assemble B_cart (2N x 6) and the residual vector --- + const int N = (int)detections.size(); + Eigen::MatrixXd B_cart(2 * N, 6); + Eigen::VectorXd r_vec(2 * N); + for (int i = 0; i < N; i++) + { + for (int j = 0; j < 6; j++) + { + B_cart(2 * i, j) = partials_vec[i].x_partials[j]; + B_cart(2 * i + 1, j) = partials_vec[i].y_partials[j]; + } + r_vec(2 * i) = resid_vec[i].x_resid; + r_vec(2 * i + 1) = resid_vec[i].y_resid; + } + + // --- Chain rule: B_bk = B_cart * dcart_dbk(current BK state) --- + const Eigen::Matrix J = dcart_dbk(bk, fid); + const Eigen::MatrixXd B_bk = B_cart * J; + + // --- Normal equations in BK basis with Marquardt damping + fixed prior --- + const Eigen::MatrixXd Bt = B_bk.transpose(); + const Eigen::MatrixXd BtW = Bt * W; + Eigen::Matrix C_data = BtW * B_bk; // pure data Hessian + Eigen::Matrix C = C_data + + lambda * Eigen::Matrix::Identity() + + P_prior; + + // bk as a 6-vector for the prior-gradient term. The prior mean is zero + // (only gdot is constrained), so grad_prior = P_prior * bk_vec. + Eigen::Matrix bk_vec; + bk_vec << bk.alpha, bk.beta, bk.gamma, bk.adot, bk.bdot, bk.gdot; + Eigen::Matrix grad = BtW * r_vec + P_prior * bk_vec; + + // chi-square including the prior contribution + const double chi2_data = (r_vec.transpose() * W * r_vec)(0); + const double chi2_prior = bk_vec.transpose() * P_prior * bk_vec; + chi2_cur = chi2_data + chi2_prior; + + // --- Solve and Marquardt accept/reject --- + Eigen::Matrix dX = C.colPivHouseholderQr().solve(-grad); + const double rho_num = chi2_prev - chi2_cur; + const double rho_den = (dX.transpose() * (lambda * dX - grad)).norm(); + const double rho = rho_den > 0.0 ? rho_num / rho_den : -1.0; + + if (rho > rho_accept) + { + lambda *= 0.5; + bk.alpha += dX(0); + bk.beta += dX(1); + bk.gamma += dX(2); + bk.adot += dX(3); + bk.bdot += dX(4); + bk.gdot += dX(5); + chi2_prev = chi2_cur; + C_no_lambda = C_data + P_prior; // Hessian for the final covariance + have_accepted_step = true; + } + else + { + lambda *= 2.0; + } + + // --- Convergence (same predicate as the Cartesian fit) --- + const size_t ndof = detections.size() * 2 - 6; + const double thresh = 10.0; + Eigen::MatrixXd dX_mat = dX; + if (converged(dX_mat, eps, chi2_cur, ndof, thresh)) + { + result.flag = 0; + result.csq = chi2_cur; + break; + } + } + + result.niter = (int)iters; + + // ----- Covariance in BK, then transform to Cartesian ----- + if (have_accepted_step) + { + const Eigen::Matrix cov_bk = C_no_lambda.inverse(); + const Eigen::Matrix J = dcart_dbk(bk, fid); + const Eigen::Matrix cov_cart = J * cov_bk * J.transpose(); + for (int i = 0; i < 6; i++) + for (int j = 0; j < 6; j++) + result.cov[i * 6 + j] = cov_cart(i, j); + } + + const size_t ndof = detections.size() * 2 - 6; + const double thresh = 10.0; + if ((result.csq / (double)ndof) > thresh) + { + result.flag = 2; // "converged" but chi2/dof is too large + } + + // ----- Cartesian state of the converged BK fit ----- + const Eigen::Matrix cart_final = bk_to_cartesian(bk, fid); + for (int i = 0; i < 6; i++) + { + result.state[i] = cart_final(i); + } + + return result; +} + +#ifdef Py_PYTHON_H +static void bk_fit_bindings(pybind11::module &m) +{ + namespace py = pybind11; + m.def("run_bk_native_fit", &run_bk_native_fit, + py::arg("ephem"), + py::arg("initial_guess"), + py::arg("detections"), + py::arg("mu"), + "Universal-BK Levenberg-Marquardt orbit fit. Reuses layup's " + "Cartesian variational machinery, with chain-rule + bound-orbit " + "energy prior applied in BK basis."); +} +#endif /* Py_PYTHON_H */ diff --git a/src/lib/orbit_fit/orbit_fit.cpp b/src/lib/orbit_fit/orbit_fit.cpp index 309e7bb6..225cfc30 100644 --- a/src/lib/orbit_fit/orbit_fit.cpp +++ b/src/lib/orbit_fit/orbit_fit.cpp @@ -783,6 +783,14 @@ namespace orbit_fit } +// Universal-BK fit (LM driver in BK basis). Inlined here, inside +// `namespace orbit_fit`, so all of layup's existing Cartesian-fit +// helpers (compute_residuals, create_sequences, get_weight_matrix, +// converged) and the Observation/FitResult types are in scope without +// forward declarations. Math primitives come from bk_basis.cpp, +// included at the top of orbit_fit.cpp. +#include "bk_fit.cpp" + #ifdef Py_PYTHON_H static void orbit_fit_bindings(py::module &m) { diff --git a/src/main.cpp b/src/main.cpp index b950669b..ac9739dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,6 +72,7 @@ PYBIND11_MODULE(_core, m) orbit_fit::predict_bindings(m); orbit_fit::predict_result_bindings(m); orbit_fit::bk_basis_bindings(m); + orbit_fit::bk_fit_bindings(m); #ifdef VERSION_INFO m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); diff --git a/tests/layup/test_bk_fit.py b/tests/layup/test_bk_fit.py new file mode 100644 index 00000000..6f6a2c67 --- /dev/null +++ b/tests/layup/test_bk_fit.py @@ -0,0 +1,83 @@ +"""Layer 2 tests for the universal BK fitter (`run_bk_native_fit`). + +These tests cover the LM driver itself. They reuse the same Gauss IOD + +observation setup as the existing Cartesian fit so the only difference +between the two engines is the parameterization + the energy prior on +gdot, isolating any disagreement to the BK-specific code path. + +Tests skip when the ASSIST ephemeris files aren't available, so CI on +machines without `~/Library/Caches/layup/{linux_p1550p2650.440, +sb441-n16.bsp}` is unaffected. +""" + +from __future__ import annotations + +import os + +import numpy as np +import pytest + +from layup.routines import ( + FitResult, + Observation, + get_ephem, + run_bk_native_fit, +) + +CACHE = os.path.expanduser("~/Library/Caches/layup") +EPHEM_PLANETS = os.path.join(CACHE, "linux_p1550p2650.440") +EPHEM_SMALLBODIES = os.path.join(CACHE, "sb441-n16.bsp") +EPHEM_AVAILABLE = os.path.exists(EPHEM_PLANETS) and os.path.exists(EPHEM_SMALLBODIES) + +# GM_sun in AU^3 / day^2. +MU_SUN = 0.00029591220828559104 + +pytestmark = pytest.mark.skipif( + not EPHEM_AVAILABLE, + reason=f"ASSIST ephemeris missing at {CACHE}; skipping BK-fit Layer 2 tests.", +) + + +# --------------------------------------------------------------------------- +# Tests that don't need real observations -- exercise the API + early-exit +# guards. +# --------------------------------------------------------------------------- + + +def test_run_bk_native_fit_returns_fitresult_for_empty_obs(): + """With zero observations, the fit returns a FitResult with flag != 0 and + does not crash.""" + ephem = get_ephem(CACHE) + ig = FitResult() + ig.state = [40.0, 10.0, 5.0, -8e-4, 9e-4, 1e-4] + ig.epoch = 2460000.5 + result = run_bk_native_fit(ephem, ig, [], MU_SUN) + assert result.method == "bk_native" + assert result.flag != 0 + + +def test_run_bk_native_fit_returns_fitresult_for_too_few_obs(): + """With <3 observations the early-exit guard fires; no crash, flag != 0.""" + ephem = get_ephem(CACHE) + ig = FitResult() + ig.state = [40.0, 10.0, 5.0, -8e-4, 9e-4, 1e-4] + ig.epoch = 2460000.5 + obs = [ + Observation.from_astrometry( + ra=1.57, + dec=0.1, + epoch=2459995.5, + observer_position=[-0.5, 0.8, 0.0], + observer_velocity=[-0.018, -0.009, 0.0], + ), + Observation.from_astrometry( + ra=1.57, + dec=0.1, + epoch=2460005.5, + observer_position=[-0.5, 0.8, 0.0], + observer_velocity=[-0.018, -0.009, 0.0], + ), + ] + result = run_bk_native_fit(ephem, ig, obs, MU_SUN) + assert result.method == "bk_native" + assert result.flag != 0 From dbe3791044e9c8302fe0133ca4c913a6470d2bbc Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 17:16:41 -0400 Subject: [PATCH 05/12] Add Layer 2 convergence tests for run_bk_native_fit. Generate synthetic observations from a known Cartesian state via layup's predict_sequence path (a fixed barycenter observer, so the only dynamical content is the orbit itself), then feed those observations back into both run_bk_native_fit and the existing Cartesian fit. Three new test categories on top of the smoke tests: * test_bk_native_fit_recovers_known_state: with the truth state as the seed, BK converges in essentially one iteration to a fit state matching the truth to rtol=1e-6 and chi2 < 1e-12. Parameterized over a 3 AU mainbelt 60-day arc and a 40 AU TNO 300-day arc. * test_bk_native_fit_recovers_from_perturbed_seed: with the seed perturbed by 0.1% in each component, the LM loop still converges to the truth state (rtol=1e-6) -- exercising the chain-rule Jacobian + Marquardt damping on a non-trivial number of iterations. Same two orbital regimes. * test_bk_and_cartesian_fits_agree: for the well-constrained mainbelt case, run_bk_native_fit and run_from_vector_with_initial_guess converge to states that agree at rtol=1e-6. Establishes the "no regression on the easy case" baseline. All seven tests in tests/layup/test_bk_fit.py pass with ASSIST ephemeris available; skip cleanly without it. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/layup/test_bk_fit.py | 168 +++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/tests/layup/test_bk_fit.py b/tests/layup/test_bk_fit.py index 6f6a2c67..3429c448 100644 --- a/tests/layup/test_bk_fit.py +++ b/tests/layup/test_bk_fit.py @@ -21,7 +21,9 @@ FitResult, Observation, get_ephem, + predict_sequence, run_bk_native_fit, + run_from_vector_with_initial_guess, ) CACHE = os.path.expanduser("~/Library/Caches/layup") @@ -81,3 +83,169 @@ def test_run_bk_native_fit_returns_fitresult_for_too_few_obs(): result = run_bk_native_fit(ephem, ig, obs, MU_SUN) assert result.method == "bk_native" assert result.flag != 0 + + +# --------------------------------------------------------------------------- +# Synthetic-orbit convergence tests. +# +# We pick a known Cartesian state, generate synthetic observations from it +# via the layup C++ predict path, then feed those observations back into +# both run_bk_native_fit and run_from_vector_with_initial_guess and check +# that (a) BK converges, (b) BK recovers the input state, and (c) the BK +# and Cartesian fits agree at convergence. +# --------------------------------------------------------------------------- + + +def _generate_synthetic_observations(ephem, truth_state, truth_epoch, obs_times): + """Generate synthetic Observation objects consistent with `truth_state` + at `truth_epoch`, observed from a fixed point (Sun in barycentric coords) + at each of `obs_times`. + + Returns a list of Observation objects whose epochs and rho_hat directions + match what the truth orbit predicts. + """ + # Use a fixed observer at the solar system barycenter so the only + # dynamical content in the synthetic data is the orbit itself. The + # observer-velocity is zero -- consistent with a barycenter "observer." + observer_position = [0.0, 0.0, 0.0] + observer_velocity = [0.0, 0.0, 0.0] + + # Template observations at the desired times with dummy (ra, dec) + template = [ + Observation.from_astrometry( + ra=0.0, + dec=0.0, + epoch=float(t), + observer_position=observer_position, + observer_velocity=observer_velocity, + ) + for t in obs_times + ] + + # Run predict on each template; the FitResult holds the truth state at epoch. + truth_fit = FitResult() + truth_fit.state = list(map(float, truth_state)) + truth_fit.epoch = float(truth_epoch) + cov = np.zeros((6, 6)) + preds = predict_sequence(ephem, truth_fit, template, cov) + + # Build real Observations from the predicted rho unit vectors. + synth = [] + for t, pr in zip(obs_times, preds): + rho = np.asarray(pr.rho) + # Defensive normalization (predict returns a unit vector already). + rho = rho / np.linalg.norm(rho) + ra = np.arctan2(rho[1], rho[0]) + dec = np.arcsin(np.clip(rho[2], -1.0, 1.0)) + synth.append( + Observation.from_astrometry( + ra=float(ra), + dec=float(dec), + epoch=float(t), + observer_position=observer_position, + observer_velocity=observer_velocity, + ) + ) + return synth + + +def _seed_from_state(state, epoch): + fit = FitResult() + fit.state = list(map(float, state)) + fit.epoch = float(epoch) + return fit + + +@pytest.mark.parametrize( + "name, state, arc_days, nobs", + [ + # ~3 AU mainbelt-ish (well-constrained) + ("mainbelt_3au_60d", [3.0, 0.0, 0.0, 0.0, 0.0102, 0.001], 60.0, 12), + # ~40 AU TNO, longer arc + ("tno_40au_300d", [40.0, 0.0, 5.0, 0.0, 0.00125, 0.0], 300.0, 12), + ], +) +def test_bk_native_fit_recovers_known_state(name, state, arc_days, nobs): + """Synthetic obs from a known state, fitted with BK from a perfect seed, + should recover the input state and produce a tiny chi-square.""" + ephem = get_ephem(CACHE) + truth_epoch = 2460000.5 + obs_times = np.linspace(truth_epoch - 0.5 * arc_days, truth_epoch + 0.5 * arc_days, nobs) + + obs = _generate_synthetic_observations(ephem, state, truth_epoch, obs_times) + seed = _seed_from_state(state, truth_epoch) + + result = run_bk_native_fit(ephem, seed, obs, MU_SUN) + assert result.flag == 0, f"[{name}] BK fit did not converge (flag={result.flag})" + np.testing.assert_allclose( + np.asarray(result.state), + np.asarray(state), + rtol=1e-6, + atol=1e-9, + err_msg=f"[{name}] BK fit did not recover the truth state", + ) + # 2N residuals, 6 free params, noise-free obs -> chi2 essentially zero. + assert result.csq < 1e-12, f"[{name}] BK fit chi-square unexpectedly large: {result.csq}" + + +@pytest.mark.parametrize( + "name, state, arc_days, nobs, rel_perturb", + [ + # Modest perturbation -- exercises the LM loop without falling out of basin. + ("mainbelt_3au_60d_pert", [3.0, 0.0, 0.0, 0.0, 0.0102, 0.001], 60.0, 12, 1e-3), + ("tno_40au_300d_pert", [40.0, 0.0, 5.0, 0.0, 0.00125, 0.0], 300.0, 12, 1e-3), + ], +) +def test_bk_native_fit_recovers_from_perturbed_seed(name, state, arc_days, nobs, rel_perturb): + """With a 0.1%-perturbed seed, the LM loop still converges to the truth state.""" + ephem = get_ephem(CACHE) + truth_epoch = 2460000.5 + obs_times = np.linspace(truth_epoch - 0.5 * arc_days, truth_epoch + 0.5 * arc_days, nobs) + + obs = _generate_synthetic_observations(ephem, state, truth_epoch, obs_times) + + # Perturb each component by rel_perturb * |component| (deterministic, no RNG). + perturbed = np.asarray(state) * (1.0 + rel_perturb) + seed = _seed_from_state(perturbed.tolist(), truth_epoch) + + result = run_bk_native_fit(ephem, seed, obs, MU_SUN) + assert result.flag == 0, f"[{name}] BK fit did not converge (flag={result.flag})" + np.testing.assert_allclose( + np.asarray(result.state), + np.asarray(state), + rtol=1e-6, + atol=1e-9, + err_msg=f"[{name}] BK fit did not recover truth from perturbed seed", + ) + # niter should be > 1 since we actually had to iterate. + assert result.niter >= 1, f"[{name}] niter={result.niter} -- expected at least 1" + + +@pytest.mark.parametrize( + "name, state, arc_days, nobs", + [ + ("mainbelt_3au_60d", [3.0, 0.0, 0.0, 0.0, 0.0102, 0.001], 60.0, 12), + ], +) +def test_bk_and_cartesian_fits_agree(name, state, arc_days, nobs): + """For well-constrained synthetic observations, the BK and Cartesian + engines should converge to states that match to within numerical noise.""" + ephem = get_ephem(CACHE) + truth_epoch = 2460000.5 + obs_times = np.linspace(truth_epoch - 0.5 * arc_days, truth_epoch + 0.5 * arc_days, nobs) + + obs = _generate_synthetic_observations(ephem, state, truth_epoch, obs_times) + seed = _seed_from_state(state, truth_epoch) + + bk_result = run_bk_native_fit(ephem, seed, obs, MU_SUN) + cart_result = run_from_vector_with_initial_guess(ephem, seed, obs) + + assert bk_result.flag == 0, f"[{name}] BK fit failed: {bk_result.flag}" + assert cart_result.flag == 0, f"[{name}] Cartesian fit failed: {cart_result.flag}" + np.testing.assert_allclose( + np.asarray(bk_result.state), + np.asarray(cart_result.state), + rtol=1e-6, + atol=1e-9, + err_msg=f"[{name}] BK and Cartesian fits disagree at convergence", + ) From 1bc63d9e851088228262ec44506ba603f5b5c9c7 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 17:24:13 -0400 Subject: [PATCH 06/12] Add engine='bk_native' dispatch to orbitfit.do_fit. Wires the universal BK fitter (run_bk_native_fit) into layup's Python do_fit pipeline alongside the existing Cartesian fit, so callers can choose the engine per call rather than via the C++ entry point. Changes: - src/layup/orbitfit.py * Import run_bk_native_fit from layup.routines. * Add a module-level _MU_SUN constant (heliocentric GM in AU^3 / day^2) used to construct the BK fixed energy prior. * Add _run_fit(assist_ephem, initial_guess, obs, engine) helper that dispatches to: - run_from_vector_with_initial_guess for engine='cartesian' - run_bk_native_fit (with _MU_SUN) for engine='bk_native' - ValueError otherwise * do_fit gains an `engine='cartesian'` parameter (default preserves the existing behavior). All five run_from_vector_with_initial_guess call sites inside do_fit are now routed through _run_fit so the engine choice propagates uniformly. - tests/layup/test_bk_fit.py * test_run_fit_dispatch_cartesian: _run_fit(..., 'cartesian') matches direct run_from_vector_with_initial_guess on synthetic mainbelt observations. * test_run_fit_dispatch_bk_native: _run_fit(..., 'bk_native') matches direct run_bk_native_fit(ephem, ig, obs, MU_SUN). * test_run_fit_dispatch_unknown_engine_raises: ValueError on an unknown engine name. The 'auto' (distance-dispatched) engine from PR 323 is intentionally not wired up here; when 323 lands first this branch rebases and gains both options. Likewise the 'bk' (liborbfit-backed) engine from PR 323 is independent of this work. All 10 tests in test_bk_fit.py and 25 tests in test_bk_basis.py continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/layup/orbitfit.py | 39 +++++++++++++++++++++++++----- tests/layup/test_bk_fit.py | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/layup/orbitfit.py b/src/layup/orbitfit.py index afab1dbe..3979b994 100644 --- a/src/layup/orbitfit.py +++ b/src/layup/orbitfit.py @@ -15,8 +15,10 @@ Observation, gauss, get_ephem, + run_bk_native_fit, run_from_vector_with_initial_guess, ) + from layup.convert import convert from layup.utilities.astrometric_uncertainty import data_weight_Veres2017 @@ -65,6 +67,25 @@ AU_M = 149597870700 SPEED_OF_LIGHT = 2.99792458e8 * 86400.0 / AU_M +# Heliocentric GM in AU^3 / day^2 (k^2, k = Gaussian gravitational constant). +# Used by the BK-native fit for the bound-orbit energy prior on gdot. +_MU_SUN = 0.00029591220828559104 + + +def _run_fit(assist_ephem, initial_guess, observations, engine): + """Dispatch a single LM fit step to the configured engine. + + Centralizing the dispatch here keeps do_fit's IOD-then-fit pipeline + parameterization-agnostic and lets us add new engines (e.g., a + future distance-dispatched 'auto') with a single edit instead of + threading the choice through every call site. + """ + if engine == "cartesian": + return run_from_vector_with_initial_guess(assist_ephem, initial_guess, observations) + if engine == "bk_native": + return run_bk_native_fit(assist_ephem, initial_guess, observations, _MU_SUN) + raise ValueError(f"Unknown engine {engine!r}; expected one of 'cartesian', 'bk_native'.") + def _get_result_dtypes(primary_id_column_name: str): """Helper function to create the result dtype with the correct primary ID column name.""" @@ -349,7 +370,7 @@ def do_gauss_iod(observations, seq): return solns -def do_fit(observations, seq, cache_dir, iod="gauss"): +def do_fit(observations, seq, cache_dir, iod="gauss", engine="cartesian"): """Carry out an orbit fit to the observations in a series of steps. A list of lists of observation indices specifies the order in which the fit proceeds. @@ -378,6 +399,12 @@ def do_fit(observations, seq, cache_dir, iod="gauss"): iod : str The IOD used to generate an initial guess orbit. Currently supports ['gauss']. Default is 'gauss'. + engine : str + Which LM fitter to dispatch to. Supported: + - 'cartesian' (default): the existing 6D Cartesian-state fit. + - 'bk_native': the universal Bernstein-Khushalani fit + (run_bk_native_fit), with a fixed bound-orbit energy prior + on gdot. Recovers the Cartesian state at the same epoch. Returns ------- @@ -403,16 +430,16 @@ def do_fit(observations, seq, cache_dir, iod="gauss"): # Fit primary interval, starting with gauss solution x = solns[0] obs = [observations[i] for i in seq[0]] - x = run_from_vector_with_initial_guess(assist_ephem, x, obs) + x = _run_fit(assist_ephem, x, obs, engine) if (x.flag != 0) and len(solns) > 1: x = solns[1] obs = [observations[i] for i in seq[0]] - x = run_from_vector_with_initial_guess(assist_ephem, x, obs) + x = _run_fit(assist_ephem, x, obs, engine) elif (x.flag != 0) and len(solns) > 2: x = solns[2] obs = [observations[i] for i in seq[0]] - x = run_from_vector_with_initial_guess(assist_ephem, x, obs) + x = _run_fit(assist_ephem, x, obs, engine) if x.flag != 0: logger.debug(f"Primary interval failed. Total observations: {len(obs)}") x.flag = 3 # caution @@ -420,7 +447,7 @@ def do_fit(observations, seq, cache_dir, iod="gauss"): # Attempt to fit all the data, given the fit of the primary interval obs = observations - x = run_from_vector_with_initial_guess(assist_ephem, x, obs) + x = _run_fit(assist_ephem, x, obs, engine) # If that failed, build up the solution slowly if x.flag != 0: @@ -429,7 +456,7 @@ def do_fit(observations, seq, cache_dir, iod="gauss"): for i, sq in enumerate(seq): obs += [observations[i] for i in sq] print(i, "of", len(seq), obs[0], sq) - x = run_from_vector_with_initial_guess(assist_ephem, x, obs) + x = _run_fit(assist_ephem, x, obs, engine) print("flag:", x.flag) if x.flag != 0: x.flag = 4 diff --git a/tests/layup/test_bk_fit.py b/tests/layup/test_bk_fit.py index 3429c448..f22eb4bb 100644 --- a/tests/layup/test_bk_fit.py +++ b/tests/layup/test_bk_fit.py @@ -249,3 +249,52 @@ def test_bk_and_cartesian_fits_agree(name, state, arc_days, nobs): atol=1e-9, err_msg=f"[{name}] BK and Cartesian fits disagree at convergence", ) + + +# --------------------------------------------------------------------------- +# Engine dispatch through orbitfit._run_fit +# --------------------------------------------------------------------------- + + +def test_run_fit_dispatch_cartesian(): + """orbitfit._run_fit(engine='cartesian') matches direct + run_from_vector_with_initial_guess.""" + from layup.orbitfit import _run_fit + + ephem = get_ephem(CACHE) + state = [3.0, 0.0, 0.0, 0.0, 0.0102, 0.001] + epoch = 2460000.5 + obs = _generate_synthetic_observations(ephem, state, epoch, np.linspace(epoch - 30, epoch + 30, 12)) + seed = _seed_from_state(state, epoch) + + via_dispatch = _run_fit(ephem, seed, obs, "cartesian") + direct = run_from_vector_with_initial_guess(ephem, seed, obs) + np.testing.assert_array_equal(via_dispatch.state, direct.state) + assert via_dispatch.method == direct.method + + +def test_run_fit_dispatch_bk_native(): + """orbitfit._run_fit(engine='bk_native') matches direct + run_bk_native_fit with MU_SUN.""" + from layup.orbitfit import _MU_SUN, _run_fit + + ephem = get_ephem(CACHE) + state = [3.0, 0.0, 0.0, 0.0, 0.0102, 0.001] + epoch = 2460000.5 + obs = _generate_synthetic_observations(ephem, state, epoch, np.linspace(epoch - 30, epoch + 30, 12)) + seed = _seed_from_state(state, epoch) + + via_dispatch = _run_fit(ephem, seed, obs, "bk_native") + direct = run_bk_native_fit(ephem, seed, obs, _MU_SUN) + np.testing.assert_array_equal(via_dispatch.state, direct.state) + assert via_dispatch.method == "bk_native" + + +def test_run_fit_dispatch_unknown_engine_raises(): + """An unrecognized engine name raises ValueError.""" + from layup.orbitfit import _run_fit + + ephem = get_ephem(CACHE) + seed = _seed_from_state([3.0, 0.0, 0.0, 0.0, 0.01, 0.0], 2460000.5) + with pytest.raises(ValueError, match="Unknown engine"): + _run_fit(ephem, seed, [], "not_an_engine") From 005ed61b4b2d56728079bc81db4631fa4b9dd692 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 17:32:02 -0400 Subject: [PATCH 07/12] Add Layer 3 engine-sweep tests against diagnostic/scan dataset. tests/layup/test_bk_everywhere.py drives both engine='cartesian' and engine='bk_native' against the diagnostic/scan dataset at ~/Dropbox/claude_layup/diagnostic/scan/truth/ -- the same 7-population, 14-arc-length scan that PR 323's auto-dispatch was validated against. Skips when either the ASSIST ephemeris or the diagnostic scan is unavailable, so CI is unaffected. Two test groups: * test_engine_sweep_well_arced_cases: on long-arc cases (30-60d mainbelt + 60d classical TNO), both engines converge near truth (drift < 1% of heliocentric distance) and agree with each other. * test_bk_beats_cartesian_on_short_arc_distant: on distant short-arc cases (70 AU scattered / 42 AU classical at 10-14 day arcs), BK drifts no more than Cartesian from truth AND uses fewer LM iterations. This is the regime BK was designed for, and the diagnostic data shows it strongly: on scattered_70AU_arc_014.00d BK stays 0.02 AU from truth in 6 iterations while Cartesian wanders 4.5 AU over 58 iterations. The module also exposes a sweep_cases_from_diagnostic() helper for ad-hoc engine-sweep harness scripts. All 6 Layer 3 tests pass (in addition to the 25 Layer 1 + 10 Layer 2 tests, for 41 total BK tests on this branch). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/layup/test_bk_everywhere.py | 235 ++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 tests/layup/test_bk_everywhere.py diff --git a/tests/layup/test_bk_everywhere.py b/tests/layup/test_bk_everywhere.py new file mode 100644 index 00000000..e1d20f1c --- /dev/null +++ b/tests/layup/test_bk_everywhere.py @@ -0,0 +1,235 @@ +"""Layer 3 engine-sweep tests for the universal BK fitter. + +Drives both engine='cartesian' and engine='bk_native' against the +diagnostic/scan dataset (outside the repo, at +``~/Dropbox/claude_layup/diagnostic/scan/truth/``) so the design +memory's prediction -- ``bk_native`` matches Cartesian across regimes +and shines on distant short arcs -- can be validated against real +ASSIST-integrated truth. + +These tests skip cleanly when either the ASSIST ephemeris or the +diagnostic scan data is unavailable, so machines without either +setup are unaffected. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +import numpy as np +import pytest + +from layup.orbitfit import _MU_SUN, _run_fit +from layup.routines import ( + FitResult, + Observation, + get_ephem, + run_bk_native_fit, + run_from_vector_with_initial_guess, +) + +# --------------------------------------------------------------------------- +# Environment guards +# --------------------------------------------------------------------------- + +CACHE = os.path.expanduser("~/Library/Caches/layup") +EPHEM_PLANETS = os.path.join(CACHE, "linux_p1550p2650.440") +EPHEM_SMALLBODIES = os.path.join(CACHE, "sb441-n16.bsp") +EPHEM_AVAILABLE = os.path.exists(EPHEM_PLANETS) and os.path.exists(EPHEM_SMALLBODIES) + +DIAGNOSTIC_SCAN = Path("~/Dropbox/claude_layup/diagnostic/scan/truth").expanduser() +DIAGNOSTIC_AVAILABLE = DIAGNOSTIC_SCAN.is_dir() + +pytestmark = pytest.mark.skipif( + not (EPHEM_AVAILABLE and DIAGNOSTIC_AVAILABLE), + reason=( + f"Skipping Layer 3 BK-everywhere tests: " + f"ephem at {CACHE} = {EPHEM_AVAILABLE}, " + f"diagnostic scan at {DIAGNOSTIC_SCAN} = {DIAGNOSTIC_AVAILABLE}." + ), +) + + +# --------------------------------------------------------------------------- +# Helpers for loading and converting diagnostic-scan cases +# --------------------------------------------------------------------------- + + +def _load_case(name: str) -> dict: + """Load a diagnostic-scan case by stem (e.g., 'classical_42AU_arc_007.00d').""" + with open(DIAGNOSTIC_SCAN / f"{name}.json") as f: + return json.load(f) + + +def _build_observations(case: dict) -> list: + """Convert a case's observation list into layup Observation objects.""" + obs_list = [] + sigma_arcsec = float(case["sigma_arcsec"]) + sigma_rad = sigma_arcsec * np.pi / (180.0 * 3600.0) + for o in case["observations"]: + # observer_state_AU is position-only; we fill velocity with zeros. + # Velocity is only used for second-order corrections (parallax, light- + # time second derivative) and the design here doesn't depend on it. + pos = list(o["observer_state_AU"]) + vel = [0.0, 0.0, 0.0] + obs_list.append( + Observation.from_astrometry( + ra=np.deg2rad(o["ra"]), + dec=np.deg2rad(o["dec"]), + epoch=float(o["jd_tdb"]), + observer_position=pos, + observer_velocity=vel, + ) + ) + # Per-observation astrometric uncertainty (in radians, matching sigma_arcsec). + obs_list[-1].ra_unc = sigma_rad + obs_list[-1].dec_unc = sigma_rad + return obs_list + + +def _truth_seed(case: dict) -> FitResult: + """Return a FitResult populated with the case's truth state at epoch.""" + fit = FitResult() + fit.state = list(map(float, case["truth_state_at_epoch"])) + fit.epoch = float(case["epoch_jd_tdb"]) + return fit + + +def _r_helio_AU(state) -> float: + return float(np.linalg.norm(state[:3])) + + +# --------------------------------------------------------------------------- +# Engine-sweep tests +# --------------------------------------------------------------------------- + + +# Well-arced cases. With ~30-60 day arcs of mainbelt or TNO objects, the +# data alone is enough to constrain the orbit; both engines should reach a +# state within sub-AU of truth and agree with each other. +WELL_ARCED_CASES = [ + "mainbelt_2.5AU_arc_030.00d", + "mainbelt_3.5AU_arc_060.00d", + "classical_42AU_arc_060.00d", +] + + +@pytest.mark.parametrize("case_name", WELL_ARCED_CASES) +def test_engine_sweep_well_arced_cases(case_name): + """With the truth state as the LM seed on a well-constrained arc, both + engines should converge near truth and agree with each other.""" + ephem = get_ephem(CACHE) + case = _load_case(case_name) + obs = _build_observations(case) + seed = _truth_seed(case) + truth = np.asarray(case["truth_state_at_epoch"]) + r_helio = _r_helio_AU(truth) + tol = 0.01 * r_helio # ~1% of heliocentric distance + + cart_res = _run_fit(ephem, seed, obs, "cartesian") + bk_res = _run_fit(ephem, seed, obs, "bk_native") + + assert cart_res.flag == 0, f"[{case_name}] Cartesian flag={cart_res.flag}" + assert bk_res.flag == 0, f"[{case_name}] BK flag={bk_res.flag}" + + cart_drift = np.linalg.norm(np.asarray(cart_res.state)[:3] - truth[:3]) + bk_drift = np.linalg.norm(np.asarray(bk_res.state)[:3] - truth[:3]) + assert cart_drift < tol, f"[{case_name}] Cartesian drift {cart_drift:.4f} > tol {tol:.4f}" + assert bk_drift < tol, f"[{case_name}] BK drift {bk_drift:.4f} > tol {tol:.4f}" + + # Engine agreement: BK and Cartesian should converge to nearly the same point. + state_disagreement = np.linalg.norm(np.asarray(bk_res.state)[:3] - np.asarray(cart_res.state)[:3]) + assert state_disagreement < tol, f"[{case_name}] BK and Cartesian disagree by {state_disagreement:.4f} AU" + + +# Short-arc / distant cases. These are the cases that motivated BK in the +# first place: the line-of-sight direction is poorly constrained, so the +# Cartesian fit's LM step can walk significantly along that direction. We +# test that BK does at least as well as Cartesian in this regime, AND that +# BK uses substantially fewer LM iterations (the BK basis is better +# conditioned, so the Marquardt damping doesn't need to fight as hard). +SHORT_ARC_DISTANT_CASES = [ + "scattered_70AU_arc_014.00d", + "classical_42AU_arc_010.00d", +] + + +@pytest.mark.parametrize("case_name", SHORT_ARC_DISTANT_CASES) +def test_bk_beats_cartesian_on_short_arc_distant(case_name): + """In the distant short-arc regime where the line-of-sight is poorly + constrained, BK should drift no more than Cartesian from truth and use + substantially fewer LM iterations.""" + ephem = get_ephem(CACHE) + case = _load_case(case_name) + obs = _build_observations(case) + seed = _truth_seed(case) + truth = np.asarray(case["truth_state_at_epoch"]) + + cart_res = _run_fit(ephem, seed, obs, "cartesian") + bk_res = _run_fit(ephem, seed, obs, "bk_native") + + assert cart_res.flag == 0, f"[{case_name}] Cartesian flag={cart_res.flag}" + assert bk_res.flag == 0, f"[{case_name}] BK flag={bk_res.flag}" + + cart_drift = np.linalg.norm(np.asarray(cart_res.state)[:3] - truth[:3]) + bk_drift = np.linalg.norm(np.asarray(bk_res.state)[:3] - truth[:3]) + + # BK should drift no more than Cartesian. (In practice it's often + # *much* less -- e.g. on scattered_70AU_arc_014.00d BK stays ~0.02 AU + # from truth while Cartesian wanders ~4.5 AU.) + assert ( + bk_drift <= cart_drift + 1e-6 + ), f"[{case_name}] BK drift {bk_drift:.4f} > Cartesian drift {cart_drift:.4f}" + + # BK should use fewer LM iterations than Cartesian: the BK basis is + # naturally better-conditioned than the Cartesian state at epoch for + # short-arc distant targets, so the LM step direction is healthier. + assert ( + bk_res.niter < cart_res.niter + ), f"[{case_name}] BK niter={bk_res.niter} not < Cartesian niter={cart_res.niter}" + + +def test_engine_sweep_produces_method_strings(): + """Sanity: each engine populates FitResult.method with its tag, so a + downstream sweep harness can tell which engine produced each fit.""" + ephem = get_ephem(CACHE) + case = _load_case("classical_42AU_arc_060.00d") + obs = _build_observations(case) + seed = _truth_seed(case) + + cart_res = _run_fit(ephem, seed, obs, "cartesian") + bk_res = _run_fit(ephem, seed, obs, "bk_native") + assert cart_res.method == "orbit_fit" + assert bk_res.method == "bk_native" + + +# --------------------------------------------------------------------------- +# Diagnostic helper (not a test) -- used by sweep harness scripts. +# --------------------------------------------------------------------------- + + +def sweep_cases_from_diagnostic(case_names=None) -> list: + """Return a list of (case_name, cartesian FitResult, bk_native FitResult) + tuples for the requested case names (or all 98 cases if None). + + Intended for ad-hoc use from a sweep script that produces tables; not + invoked by pytest collection. + """ + if case_names is None: + case_names = sorted(p.stem for p in DIAGNOSTIC_SCAN.glob("*.json")) + ephem = get_ephem(CACHE) + rows = [] + for name in case_names: + case = _load_case(name) + obs = _build_observations(case) + seed = _truth_seed(case) + rows.append( + ( + name, + _run_fit(ephem, seed, obs, "cartesian"), + _run_fit(ephem, seed, obs, "bk_native"), + ) + ) + return rows From b8dd571aefb2a57952f3a4bfe47d9995fd0a3798 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 15 May 2026 17:48:22 -0400 Subject: [PATCH 08/12] Add tools/bk_engine_sweep.py: full engine-sweep harness. A runnable CLI script that drives both engine='cartesian' and engine='bk_native' across an entire diagnostic-scan directory, writes per-case metrics to CSV, and prints a population-level summary (BK wins / Cartesian wins / per-engine failures / mean iteration counts, plus median+mean drift and iteration ratios). Usage: python tools/bk_engine_sweep.py --scan-dir --output Defaults discover the project's diagnostic scan at ~/Dropbox/claude_layup/diagnostic/scan/truth and the layup ephemeris cache at ~/Library/Caches/layup. Both are overrideable via flags, so anyone with a compatible truth dataset can reproduce. Running on the 98-case scan (truth state as LM seed, sigma_arcsec=0.1): Population n BK win Cart win cart fail bk fail both fail ------------------------------------------------------------------------------- centaur_15AU 14 13 0 0 0 1 centaur_25AU 14 9 2 2 0 1 classical_42AU 14 10 3 0 0 1 mainbelt_2.5AU 14 10 3 0 0 1 mainbelt_3.5AU 14 12 1 0 0 1 scattered_70AU 14 7 2 4 0 1 sednoid_80AU 14 6 1 6 0 1 ------------------------------------------------------------------------------- TOTAL 98 67 12 12 0 7 Across 79 cases where both engines succeed: drift ratio (BK / Cart): median=0.560, mean=187.199 iter ratio (BK / Cart): median=0.386, mean=0.524 Headline: BK never fails when Cartesian succeeds (0 / 98), succeeds in 12 cases where Cartesian flag=2's out, and on the typical case is ~2x closer to truth in ~40% the iterations. The mean drift ratio of 187 is inflated by the 70-80 AU short-arc cases where Cartesian wanders 5-13 AU while BK stays put. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/bk_engine_sweep.py | 350 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 tools/bk_engine_sweep.py diff --git a/tools/bk_engine_sweep.py b/tools/bk_engine_sweep.py new file mode 100644 index 00000000..e94ee26c --- /dev/null +++ b/tools/bk_engine_sweep.py @@ -0,0 +1,350 @@ +"""Engine-sweep harness: run both engine='cartesian' and engine='bk_native' +on every case in a diagnostic scan dataset, write a CSV with per-case +metrics, and print a population-level summary table. + +Each case is seeded with the truth state at epoch, so this is a "given +a perfect starting point, how does each LM behave on the data" comparison +rather than an IOD-recovery test. The data themselves carry the diagnostic +scan's pre-baked Gaussian noise (sigma_arcsec is read from each JSON). + +Expected input format: a directory containing one .json file per case, +each with at least the following keys:: + + { + "population": "...", + "arc_length_days": ..., + "epoch_jd_tdb": ..., + "sigma_arcsec": ..., + "truth_state_at_epoch": [x, y, z, vx, vy, vz], + "observations": [ + {"ra": , "dec": , "jd_tdb": ..., + "observer_state_AU": [x, y, z], ...}, + ... + ] + } + +The diagnostic-scan dataset shipping with the project lives at +``~/Dropbox/claude_layup/diagnostic/scan/truth/`` (98 cases, 7 +populations x 14 arc lengths). + +Usage:: + + python tools/bk_engine_sweep.py --scan-dir ~/path/to/truth/ \\ + --cache-dir ~/Library/Caches/layup \\ + --output bk_engine_sweep.csv + +Defaults: + --scan-dir ~/Dropbox/claude_layup/diagnostic/scan/truth + --cache-dir ~/Library/Caches/layup + --output bk_engine_sweep.csv (in the cwd) +""" + +from __future__ import annotations + +import argparse +import csv +import json +import statistics +import sys +from pathlib import Path +from typing import List + +import numpy as np + +from layup.orbitfit import _MU_SUN +from layup.routines import ( + FitResult, + Observation, + get_ephem, + run_bk_native_fit, + run_from_vector_with_initial_guess, +) + +DEFAULT_SCAN_DIR = "~/Dropbox/claude_layup/diagnostic/scan/truth" +DEFAULT_CACHE_DIR = "~/Library/Caches/layup" +DEFAULT_OUTPUT = "bk_engine_sweep.csv" + + +# ---------------------------------------------------------------------- +# Case loading / observation construction +# ---------------------------------------------------------------------- + + +def load_case(path: Path) -> dict: + with open(path) as f: + return json.load(f) + + +def build_observations(case: dict) -> list: + """Construct layup Observations from a case dict's observation list. + + `observer_state_AU` is treated as position-only (velocity zero); the + layup fit pipeline only uses observer velocity for second-order + corrections that don't affect the chain-rule comparison here. + """ + sigma_arcsec = float(case["sigma_arcsec"]) + sigma_rad = sigma_arcsec * np.pi / (180.0 * 3600.0) + obs_list = [] + for o in case["observations"]: + pos = list(o["observer_state_AU"]) + vel = [0.0, 0.0, 0.0] + obs = Observation.from_astrometry( + ra=np.deg2rad(o["ra"]), + dec=np.deg2rad(o["dec"]), + epoch=float(o["jd_tdb"]), + observer_position=pos, + observer_velocity=vel, + ) + obs.ra_unc = sigma_rad + obs.dec_unc = sigma_rad + obs_list.append(obs) + return obs_list + + +def truth_seed(case: dict) -> FitResult: + fit = FitResult() + fit.state = list(map(float, case["truth_state_at_epoch"])) + fit.epoch = float(case["epoch_jd_tdb"]) + return fit + + +# ---------------------------------------------------------------------- +# Per-case sweep +# ---------------------------------------------------------------------- + + +def sweep_one(ephem, case_path: Path) -> dict: + case = load_case(case_path) + obs = build_observations(case) + seed = truth_seed(case) + truth = np.asarray(case["truth_state_at_epoch"]) + + cart = run_from_vector_with_initial_guess(ephem, seed, obs) + bk = run_bk_native_fit(ephem, seed, obs, _MU_SUN) + + r_helio = float(np.linalg.norm(truth[:3])) + cart_drift = float(np.linalg.norm(np.asarray(cart.state)[:3] - truth[:3])) + bk_drift = float(np.linalg.norm(np.asarray(bk.state)[:3] - truth[:3])) + + return { + "case": case_path.stem, + "population": case["population"], + "arc_days": float(case["arc_length_days"]), + "n_obs": len(case["observations"]), + "r_helio_AU": r_helio, + "cart_flag": int(cart.flag), + "cart_niter": int(cart.niter), + "cart_csq": float(cart.csq), + "cart_drift_AU": cart_drift, + "bk_flag": int(bk.flag), + "bk_niter": int(bk.niter), + "bk_csq": float(bk.csq), + "bk_drift_AU": bk_drift, + } + + +# ---------------------------------------------------------------------- +# Reporting +# ---------------------------------------------------------------------- + + +def write_csv(rows: List[dict], path: Path) -> None: + if not rows: + return + fieldnames = list(rows[0].keys()) + with open(path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + writer.writerow(row) + print(f"Wrote {len(rows)} rows to {path}") + + +def _fmt_drift(d: float) -> str: + if d < 1e-6: + return f"{d:.2e}" + return f"{d:.4f}" + + +def print_summary(rows: List[dict]) -> None: + print() + print("=" * 96) + print( + f"{'Case':<40s} {'r_helio':>8s} {'arc':>6s} " + f"{'cart_drift':>12s} {'bk_drift':>12s} {'cart_it':>7s} {'bk_it':>5s}" + ) + print("=" * 96) + pop_summary: dict = {} + for row in rows: + pop = row["population"] + s = pop_summary.setdefault( + pop, + { + "n": 0, + "bk_better": 0, + "cart_better": 0, + "tie": 0, + "both_failed": 0, + "only_cart_failed": 0, + "only_bk_failed": 0, + "cart_total_iter": 0, + "bk_total_iter": 0, + }, + ) + s["n"] += 1 + cf = row["cart_flag"] + bf = row["bk_flag"] + if cf != 0 and bf != 0: + s["both_failed"] += 1 + elif cf != 0: + s["only_cart_failed"] += 1 + elif bf != 0: + s["only_bk_failed"] += 1 + elif row["bk_drift_AU"] < row["cart_drift_AU"]: + s["bk_better"] += 1 + elif row["cart_drift_AU"] < row["bk_drift_AU"]: + s["cart_better"] += 1 + else: + s["tie"] += 1 + if cf == 0: + s["cart_total_iter"] += row["cart_niter"] + if bf == 0: + s["bk_total_iter"] += row["bk_niter"] + + print( + f"{row['case']:<40s} {row['r_helio_AU']:>8.2f} " + f"{row['arc_days']:>6.2f} " + f"{_fmt_drift(row['cart_drift_AU']):>12s} " + f"{_fmt_drift(row['bk_drift_AU']):>12s} " + f"{row['cart_niter']:>7d} {row['bk_niter']:>5d}" + ) + + print() + print("=" * 102) + print(f"Per-population summary ({len(rows)} cases total)") + print("=" * 102) + header = ( + f"{'Population':<20s} {'n':>3s} {'BK win':>7s} {'Cart win':>9s} " + f"{'cart fail':>10s} {'bk fail':>8s} {'both fail':>10s} " + f"{'mean cart it':>13s} {'mean bk it':>11s}" + ) + print(header) + print("-" * len(header)) + total = { + "n": 0, + "bk_better": 0, + "cart_better": 0, + "tie": 0, + "only_cart_failed": 0, + "only_bk_failed": 0, + "both_failed": 0, + "cart_total_iter": 0, + "bk_total_iter": 0, + } + for pop in sorted(pop_summary): + s = pop_summary[pop] + for k in total: + total[k] += s[k] + cart_succ = max(s["n"] - s["only_cart_failed"] - s["both_failed"], 0) + bk_succ = max(s["n"] - s["only_bk_failed"] - s["both_failed"], 0) + cart_mean_it = s["cart_total_iter"] / cart_succ if cart_succ else float("nan") + bk_mean_it = s["bk_total_iter"] / bk_succ if bk_succ else float("nan") + print( + f"{pop:<20s} {s['n']:>3d} {s['bk_better']:>7d} " + f"{s['cart_better']:>9d} {s['only_cart_failed']:>10d} " + f"{s['only_bk_failed']:>8d} {s['both_failed']:>10d} " + f"{cart_mean_it:>13.1f} {bk_mean_it:>11.1f}" + ) + print("-" * len(header)) + cart_succ = max(total["n"] - total["only_cart_failed"] - total["both_failed"], 0) + bk_succ = max(total["n"] - total["only_bk_failed"] - total["both_failed"], 0) + cart_mean = total["cart_total_iter"] / cart_succ if cart_succ else float("nan") + bk_mean = total["bk_total_iter"] / bk_succ if bk_succ else float("nan") + print( + f"{'TOTAL':<20s} {total['n']:>3d} {total['bk_better']:>7d} " + f"{total['cart_better']:>9d} {total['only_cart_failed']:>10d} " + f"{total['only_bk_failed']:>8d} {total['both_failed']:>10d} " + f"{cart_mean:>13.1f} {bk_mean:>11.1f}" + ) + + # Drift / iter ratios across cases where both engines succeed. + both_ok = [r for r in rows if r["cart_flag"] == 0 and r["bk_flag"] == 0] + if both_ok: + ratios = [r["bk_drift_AU"] / r["cart_drift_AU"] for r in both_ok if r["cart_drift_AU"] > 1e-9] + iter_ratios = [r["bk_niter"] / max(r["cart_niter"], 1) for r in both_ok] + print() + print(f"Across {len(both_ok)} cases where both engines succeed:") + if ratios: + print( + f" drift ratio (BK / Cart): median={statistics.median(ratios):.3f}, " + f"mean={statistics.mean(ratios):.3f}" + ) + print( + f" iter ratio (BK / Cart): median={statistics.median(iter_ratios):.3f}, " + f"mean={statistics.mean(iter_ratios):.3f}" + ) + + +# ---------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------- + + +def parse_args(argv: List[str]) -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + p.add_argument( + "--scan-dir", + default=DEFAULT_SCAN_DIR, + help=f"Directory of diagnostic-scan .json cases (default: {DEFAULT_SCAN_DIR})", + ) + p.add_argument( + "--cache-dir", + default=DEFAULT_CACHE_DIR, + help=f"layup ephemeris cache directory (default: {DEFAULT_CACHE_DIR})", + ) + p.add_argument( + "--output", + default=DEFAULT_OUTPUT, + help=f"Output CSV path (default: {DEFAULT_OUTPUT})", + ) + return p.parse_args(argv) + + +def main(argv: List[str] | None = None) -> int: + args = parse_args(argv or sys.argv[1:]) + scan_dir = Path(args.scan_dir).expanduser() + cache_dir = Path(args.cache_dir).expanduser() + output = Path(args.output).expanduser() + + if not scan_dir.is_dir(): + print(f"ERROR: diagnostic scan not found at {scan_dir}", file=sys.stderr) + return 1 + if not cache_dir.is_dir(): + print(f"ERROR: ephem cache not found at {cache_dir}", file=sys.stderr) + return 1 + + ephem = get_ephem(str(cache_dir)) + case_paths = sorted(scan_dir.glob("*.json")) + print(f"Running engine sweep on {len(case_paths)} cases from {scan_dir}...") + + rows = [] + for i, path in enumerate(case_paths, start=1): + try: + row = sweep_one(ephem, path) + except Exception as exc: # noqa: BLE001 -- want full coverage + print( + f" [{i}/{len(case_paths)}] {path.stem}: raised " f"{type(exc).__name__}: {exc}", + file=sys.stderr, + ) + continue + rows.append(row) + if i % 10 == 0: + print(f" ...{i}/{len(case_paths)} done") + + write_csv(rows, output) + print_summary(rows) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 89688a504a7c5a151368a6da63a0f1b319ec4df0 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Sat, 16 May 2026 20:58:58 -0400 Subject: [PATCH 09/12] Trigger CI rebuild against assist 1.2.3 From ac6876aac3599a1d56c0c9490d7dd07e1943f72e Mon Sep 17 00:00:00 2001 From: matthewholman Date: Sat, 16 May 2026 21:15:40 -0400 Subject: [PATCH 10/12] Apply black formatting to test_bk_basis.py. Pure whitespace -- two blank-line adjustments black wants for PEP 8 spacing. No test changes; 25 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/layup/test_bk_basis.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/layup/test_bk_basis.py b/tests/layup/test_bk_basis.py index 5fec760c..991678db 100644 --- a/tests/layup/test_bk_basis.py +++ b/tests/layup/test_bk_basis.py @@ -33,7 +33,6 @@ sigma_gdot_sq, ) - # GM_sun in AU^3 / day^2 (Gaussian gravitational constant squared). MU_SUN = 0.00029591220828559104 @@ -69,6 +68,7 @@ def _bk_from_tuple(t): # Round-trip Cartesian <-> BK # --------------------------------------------------------------------------- + @pytest.mark.parametrize("case", _BK_CASES) def test_round_trip_bk_to_cart_to_bk(case): """BK -> Cartesian -> BK recovers the input to machine precision.""" @@ -80,8 +80,9 @@ def test_round_trip_bk_to_cart_to_bk(case): for name in ("alpha", "beta", "gamma", "adot", "bdot", "gdot"): original = getattr(bk, name) recovered = getattr(bk_back, name) - np.testing.assert_allclose(recovered, original, rtol=1e-12, atol=1e-15, - err_msg=f"BK.{name} not recovered through round-trip") + np.testing.assert_allclose( + recovered, original, rtol=1e-12, atol=1e-15, err_msg=f"BK.{name} not recovered through round-trip" + ) @pytest.mark.parametrize("case", _BK_CASES) @@ -100,6 +101,7 @@ def test_round_trip_cart_to_bk_to_cart(case): # Analytic dcart_dbk vs finite-difference # --------------------------------------------------------------------------- + def _bk_perturb(bk: BKState, idx: int, delta: float) -> BKState: names = ("alpha", "beta", "gamma", "adot", "bdot", "gdot") vals = [getattr(bk, n) for n in names] @@ -130,7 +132,8 @@ def test_dcart_dbk_matches_finite_difference(case): scale = np.maximum(np.abs(J_analytic), np.abs(J_fd)) scale = np.where(scale > 0, scale, 1.0) np.testing.assert_array_less( - np.abs(J_analytic - J_fd) / scale, 1e-5, + np.abs(J_analytic - J_fd) / scale, + 1e-5, err_msg="Analytic dcart_dbk disagrees with finite-difference", ) @@ -139,6 +142,7 @@ def test_dcart_dbk_matches_finite_difference(case): # Mixed-partial symmetry of the second-derivative cross-terms # --------------------------------------------------------------------------- + @pytest.mark.parametrize("case", _BK_CASES) def test_mixed_partial_symmetry_via_finite_difference(case): """d(r,v)/(d alpha d beta) -- approached via FD of dcart_dbk -- is symmetric.""" @@ -165,6 +169,7 @@ def test_mixed_partial_symmetry_via_finite_difference(case): # Fiducial-direction gauge invariance # --------------------------------------------------------------------------- + def test_fiducial_gauge_invariance(): """Two valid n0 choices give the same physical Cartesian orbit.""" rng = np.random.default_rng(seed=33333) @@ -194,6 +199,7 @@ def test_fiducial_gauge_invariance(): # Special-case forms at alpha = beta = 0 (the fiducial direction) # --------------------------------------------------------------------------- + def test_special_case_at_fiducial(): """At alpha = beta = 0, rho_hat = n0, rho_hat_alpha = a, rho_hat_beta = b.""" rng = np.random.default_rng(seed=44444) @@ -237,6 +243,7 @@ def test_jacobian_at_fiducial(): # sigma_gdot_sq vs Cartesian-side energy-bound calculation # --------------------------------------------------------------------------- + def test_sigma_gdot_sq_consistent_with_energy_bound(): """sigma_gdot_sq matches the Cartesian-side bound on |gdot|^2 for a bound orbit. @@ -249,16 +256,16 @@ def test_sigma_gdot_sq_consistent_with_energy_bound(): fid = _make_fiducial(rng) # Pick (alpha, beta, gamma, adot, bdot) such that the orbit is bound for # at least some gdot range. - bk = BKState(alpha=0.05, beta=-0.03, gamma=1.0 / 40.0, - adot=4e-5, bdot=-3e-5, gdot=0.0) + bk = BKState(alpha=0.05, beta=-0.03, gamma=1.0 / 40.0, adot=4e-5, bdot=-3e-5, gdot=0.0) sigma_sq = sigma_gdot_sq(bk, MU_SUN) # The bound-orbit constraint -> |v|^2 <= 2 * mu * gamma. At the BK state # with gdot pinned to +sqrt(sigma_sq), the orbit should be exactly at the # boundary (|v|^2 == 2 * mu * gamma). assert sigma_sq > 0.0 and np.isfinite(sigma_sq) - bk_at_boundary = BKState(alpha=bk.alpha, beta=bk.beta, gamma=bk.gamma, - adot=bk.adot, bdot=bk.bdot, gdot=np.sqrt(sigma_sq)) + bk_at_boundary = BKState( + alpha=bk.alpha, beta=bk.beta, gamma=bk.gamma, adot=bk.adot, bdot=bk.bdot, gdot=np.sqrt(sigma_sq) + ) cart = np.asarray(bk_to_cartesian(bk_at_boundary, fid)).flatten() r_norm = np.linalg.norm(cart[:3]) v_norm_sq = np.dot(cart[3:], cart[3:]) @@ -270,6 +277,5 @@ def test_sigma_gdot_sq_consistent_with_energy_bound(): def test_sigma_gdot_sq_returns_inf_for_hyperbolic_tangentials(): """When tangential rates already exceed escape velocity, sigma_gdot_sq signals 'no prior' by returning +infinity.""" - bk = BKState(alpha=0.0, beta=0.0, gamma=1.0 / 50.0, - adot=1e-3, bdot=1e-3, gdot=0.0) # huge rates at 50 AU + bk = BKState(alpha=0.0, beta=0.0, gamma=1.0 / 50.0, adot=1e-3, bdot=1e-3, gdot=0.0) # huge rates at 50 AU assert np.isinf(sigma_gdot_sq(bk, MU_SUN)) From 2b37e9372df3e5e319a9adc6f5e1707a3bf4118e Mon Sep 17 00:00:00 2001 From: matthewholman Date: Sun, 17 May 2026 12:44:33 -0400 Subject: [PATCH 11/12] Expose --engine flag on the layup orbitfit CLI. PR 326 added the engine= parameter to do_fit, but the layup-orbitfit command-line entry point didn't forward it -- so end users running `layup orbitfit input.csv ADES_csv` always got the Cartesian engine. This wires --engine end-to-end: CLI argparse (--engine {cartesian, bk_native}, default 'cartesian') -> orbitfit_cli reads cli_args.engine -> orbitfit() forwards engine through to process_data_by_id's kwargs -> _orbitfit() receives engine and passes to do_fit() -> do_fit (already engine-aware from PR 326) dispatches via _run_fit() to either run_from_vector_with_initial_guess or run_bk_native_fit. argparse's choices=["cartesian", "bk_native"] gives users an early error on a typo'd engine name; the Python-level _run_fit dispatch also raises ValueError if a caller passes an unknown engine directly (existing behavior). Tests: - test_orbit_fit_cli_raises_with_unknown_engine: parallel to the existing unknown-iod test; FakeCliArgs.engine='not_an_engine' triggers a ValueError from the dispatch. - The existing test_orbit_fit_cli_raises_with_unknown_iod gets a FakeCliArgs.engine='cartesian' field so it continues to pass after orbitfit_cli starts reading that attribute. Confirmed `layup orbitfit --help` shows the new flag with usage `--engine {cartesian,bk_native}` and a description of the tradeoff. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/layup/orbitfit.py | 14 +++++++++++++- src/layup_cmdline/orbitfit.py | 13 +++++++++++++ tests/layup/test_orbit_fit.py | 36 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/layup/orbitfit.py b/src/layup/orbitfit.py index 3979b994..420874b2 100644 --- a/src/layup/orbitfit.py +++ b/src/layup/orbitfit.py @@ -485,6 +485,7 @@ def _orbitfit( sort_array: bool = True, weight_data: bool = False, iod: str = "gauss", + engine: str = "cartesian", ): """This function will contain all of the calls to the c++ code that will calculate an orbit given a set of observations. Note that all observations @@ -617,7 +618,13 @@ def _orbitfit( # Perform the orbit fitting if initial_guess is None or initial_guess["flag"] != 0: if iod.lower() in ["gauss"]: - res = do_fit(observations=observations, seq=sequence, cache_dir=kernels_loc, iod=iod.lower()) + res = do_fit( + observations=observations, + seq=sequence, + cache_dir=kernels_loc, + iod=iod.lower(), + engine=engine, + ) else: res = do_other_fit(iod=iod.lower()) else: @@ -658,6 +665,7 @@ def orbitfit( debias=False, weight_data=False, iod="gauss", + engine="cartesian", ): """This is the function that you would call interactively. i.e. from a notebook @@ -707,6 +715,7 @@ def orbitfit( bias_dict=bias_dict, weight_data=weight_data, iod=iod, + engine=engine, ) @@ -746,6 +755,7 @@ def orbitfit_cli( weight_data = cli_args.weight_data output_orbit_format = cli_args.output_orbit_format iod = cli_args.iod + engine = getattr(cli_args, "engine", "cartesian") else: cache_dir = None debias = False @@ -753,6 +763,7 @@ def orbitfit_cli( weight_data = False output_orbit_format = "COM" # Default output orbit format. iod = "gauss" + engine = "cartesian" _primary_id_column_name = cli_args.primary_id_column_name @@ -863,6 +874,7 @@ def orbitfit_cli( debias=debias, weight_data=weight_data, iod=iod, + engine=engine, ) # Convert the fit_orbits to the preferred output format diff --git a/src/layup_cmdline/orbitfit.py b/src/layup_cmdline/orbitfit.py index c1b79b14..99c319de 100644 --- a/src/layup_cmdline/orbitfit.py +++ b/src/layup_cmdline/orbitfit.py @@ -78,6 +78,19 @@ def main(): default="gauss", required=False, ) + optional.add_argument( + "--engine", + help=( + "LM fitter to use after IOD: 'cartesian' (default; classic " + "barycentric-Cartesian LM) or 'bk_native' (universal " + "Bernstein-Khushalani fit with energy prior, better-conditioned " + "for distant short-arc targets and at least as good elsewhere)." + ), + dest="engine", + choices=["cartesian", "bk_native"], + default="cartesian", + required=False, + ) optional.add_argument( "-o", "--output", diff --git a/tests/layup/test_orbit_fit.py b/tests/layup/test_orbit_fit.py index 733375dc..41924ef1 100644 --- a/tests/layup/test_orbit_fit.py +++ b/tests/layup/test_orbit_fit.py @@ -249,6 +249,7 @@ def __init__(self, g=None): self.g = g # Command line argument for initial guesses file self.output_orbit_format = ("BCART_EQ",) self.iod = "bad_iod" + self.engine = "cartesian" with pytest.raises(ValueError) as e: # Now run the orbit_fit cli with overwrite set to True @@ -263,3 +264,38 @@ def __init__(self, g=None): ) assert "The IOD, bad_iod is not supported" in str(e.value) + + +def test_orbit_fit_cli_raises_with_unknown_engine(tmpdir): + """The CLI's --engine flag is validated by argparse choices, but the + Python-level orbitfit_cli also accepts a cli_args.engine attribute; + if a caller passes an unrecognized engine value, the dispatch in + _run_fit raises ValueError at fit time.""" + os.chdir(tmpdir) + guess_file_stem = "test_guess" + test_input_filepath = get_test_filepath("4_random_mpc_ADES_provIDs_no_sats.csv") + + class FakeCliArgs: + def __init__(self, g=None): + self.ar_data_file_path = None + self.primary_id_column_name = "provID" + self.separate_flagged = False + self.force = False + self.debias = False + self.weight_data = False + self.g = g + self.output_orbit_format = ("BCART_EQ",) + self.iod = "gauss" + self.engine = "not_an_engine" + + with pytest.raises(ValueError) as e: + orbitfit_cli( + input=test_input_filepath, + input_file_format="ADES_csv", + output_file_stem=guess_file_stem, + output_file_format="csv", + chunk_size=10_000, + num_workers=1, + cli_args=FakeCliArgs(), + ) + assert "Unknown engine" in str(e.value) From c4bf9b599671ab115abc89956f5ac1d3ec8662a9 Mon Sep 17 00:00:00 2001 From: matthewholman Date: Fri, 19 Jun 2026 19:55:54 -0400 Subject: [PATCH 12/12] Un-skip the BK test suites on CI (cross-platform guards + in-repo fixture) Address Little-Ryugu's review on #327: test_bk_everywhere.py (and test_bk_fit.py) skipped on GitHub CI and on other contributors' machines because they hardcoded the macOS ASSIST cache path (~/Library/Caches/layup) and, for the Layer-3 sweep, a personal absolute truth-set path (~/Dropbox/claude_layup/diagnostic/scan/truth). Port the same fix already on #326/#331 onto this branch: - Add tests/layup/_bk_guards.py: resolves the ephemeris via pooch.os_cache (correct on macOS and Linux) and loads the diagnostic-scan truth set. - Ship the truth set in-repo as the consolidated, minified tests/data/bk_scan_truth.json (98 cases, 135 KB, one file). - Re-point test_bk_everywhere.py and test_bk_fit.py at those guards; test bodies are unchanged. Now only requires_ephem skips, on machines that haven't run `layup bootstrap`. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/data/bk_scan_truth.json | 1 + tests/layup/_bk_guards.py | 83 +++++++++++++++++++++++++++++++ tests/layup/test_bk_everywhere.py | 60 ++++++++++------------ tests/layup/test_bk_fit.py | 22 ++++---- 4 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 tests/data/bk_scan_truth.json create mode 100644 tests/layup/_bk_guards.py diff --git a/tests/data/bk_scan_truth.json b/tests/data/bk_scan_truth.json new file mode 100644 index 00000000..c42f4bf8 --- /dev/null +++ b/tests/data/bk_scan_truth.json @@ -0,0 +1 @@ +{"centaur_15AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4491994467835034,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65479932865384},{"dec":-1.4490699743089461,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":356.6554096049876},{"dec":-1.4488991103787023,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":356.6559600919128}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492245996645683,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.6547990635539},{"dec":-1.4490954532847469,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65544199657904},{"dec":-1.4489909838622979,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":356.65558721691764},{"dec":-1.4488485963285653,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":356.65622243155264},{"dec":-1.4488263299380204,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":356.6562939012036},{"dec":-1.4487274657063738,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":356.6569064593827},{"dec":-1.448596578639507,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":356.65705514148965},{"dec":-1.44840183353117,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":356.65779845486946}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492034666006814,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65481740912867},{"dec":-1.449043352198614,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.6554539951974},{"dec":-1.4482813258802087,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":356.65862857309526},{"dec":-1.448102067491321,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":356.65919253545405},{"dec":-1.4472953454787187,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":356.66248436332637},{"dec":-1.4471203750868051,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":356.66313518860665},{"dec":-1.44634996386402,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":356.66643735062615},{"dec":-1.4453705361506075,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":356.67041901529717}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492861793374936,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.6547759018168},{"dec":-1.449059948407453,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65538378667765},{"dec":-1.4473528163986793,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":356.66247035786336},{"dec":-1.4471529939572425,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":356.66311027624033},{"dec":-1.4453836009189962,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":356.6703726110472},{"dec":-1.4452129041333377,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":356.6710851009435},{"dec":-1.4434856198689834,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":356.6782899231551},{"dec":-1.4414852964257927,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":356.6859252217941}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4491967112297617,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65478988175556},{"dec":-1.4490197332968968,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.6554370229746},{"dec":-1.4454777631830482,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":356.67041779142363},{"dec":-1.4452948384331648,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":356.6710713997522},{"dec":-1.4415153485256067,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":356.6859553132384},{"dec":-1.4413058288962324,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":356.6866504402725},{"dec":-1.4374882638909958,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":356.7020987849885},{"dec":-1.433323522807865,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":356.71809557448836}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492248032002055,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65479159248184},{"dec":-1.4490153985183376,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65536083657486},{"dec":-1.4434419810630514,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":356.6782599671636},{"dec":-1.4433136806638989,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":356.67890960268306},{"dec":-1.4374796373543128,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":356.70206243343165},{"dec":-1.437308590910806,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":356.70278426559497},{"dec":-1.43125662307515,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":356.7261887084389},{"dec":-1.424800844576866,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":356.7511486250621}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4491925317226129,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65482435771526},{"dec":-1.4490703416375883,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.6554281867781},{"dec":-1.4395004556730655,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":356.6938504761679},{"dec":-1.4393023076599085,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":356.6945388220965},{"dec":-1.429117051980451,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":356.73470823060484},{"dec":-1.4289533716437852,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":356.7353661551484},{"dec":-1.418090405210537,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":356.7767849782202},{"dec":-1.4064130164697244,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":356.8200789192187}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4491885291672848,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.6547651655003},{"dec":-1.4490511193200593,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65541525936646},{"dec":-1.4354525637569573,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":356.7101612774742},{"dec":-1.4352474749989401,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":356.71084632472747},{"dec":-1.4203805992891245,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":356.7682008213996},{"dec":-1.4201678331825545,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":356.7689231990725},{"dec":-1.4040025711693231,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":356.82892526877583},{"dec":-1.3863907778860225,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":356.89277746106546}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492084669745133,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65481304274635},{"dec":-1.4490335782263233,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65541593232666},{"dec":-1.429130668991407,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":356.7347135347897},{"dec":-1.4288914445716778,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":356.73537371781447},{"dec":-1.4063846361008625,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":356.82007699277443},{"dec":-1.406238523531502,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":356.8208787583976},{"dec":-1.3810692306678902,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":356.91172305962385},{"dec":-1.3532932295096194,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":357.0085741274295}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492001647550758,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65478065921616},{"dec":-1.4490774837122706,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65542725512836},{"dec":-1.4203946592707324,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":356.76817040176434},{"dec":-1.420155808867114,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":356.768934824782},{"dec":-1.3863243126550906,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":356.8928165866134},{"dec":-1.3861479999080744,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":356.8934760672442},{"dec":-1.3473979193027623,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":357.028838890101},{"dec":-1.3035951639416203,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":357.1753948196305}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492312275446255,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65478353154055},{"dec":-1.4490428583228128,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65543842481713},{"dec":-1.4039644219887777,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":356.8290306207063},{"dec":-1.403807987670057,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":356.82974235033595},{"dec":-1.347416692090215,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":357.0289202224693},{"dec":-1.3471241032187369,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":357.02972426340614},{"dec":-1.2798993667243783,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":357.252773117022},{"dec":-1.202393038508383,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":357.4992047287229}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.449203229604024,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.6547866015496},{"dec":-1.449064585098429,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65544031407063},{"dec":-1.3810868101587428,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":356.9116812039396},{"dec":-1.3808698476112065,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":356.9124740682816},{"dec":-1.2902405525046146,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":357.2191505258178},{"dec":-1.2899092723637045,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":357.2200895709717},{"dec":-1.1785059785099672,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":357.5738341795043},{"dec":-1.0480824197123275,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":357.9700210361826}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.4492028522259333,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.6547375704873},{"dec":-1.4490614152633325,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.6554287173715},{"dec":-1.3383733924500556,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":357.0592387188013},{"dec":-1.338138805810856,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":357.06006104585884},{"dec":-1.1785430450628327,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":357.5737440604094},{"dec":-1.1781284836672135,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":357.57487044077067},{"dec":-0.9764673449879517,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":358.1826374039707},{"dec":-0.7395173511688325,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":358.8700199022821}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_15AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-1.449197319171876,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":356.65480563991423},{"dec":-1.4490645637866633,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":356.65539626516704},{"dec":-1.2901963444330176,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":357.2192244807384},{"dec":-1.2899761460619465,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":357.22010562426397},{"dec":-1.0480531817572025,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":357.96993815793263},{"dec":-1.0476745129343714,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":357.9711506510749},{"dec":-0.7395322334172404,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":358.86992161587693},{"dec":-0.38394972919786463,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":359.87654630727417}],"sigma_arcsec":0.1,"truth_state_at_epoch":[15.0,0.0,0.0,0.0,0.004426093043621625,0.00038723296502784523]},"centaur_25AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.874786173461337,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98198401599035},{"dec":-0.8747101358353822,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":357.98230133437477},{"dec":-0.8746244071139716,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":357.98255082284805}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8748208527770812,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.9819638112425},{"dec":-0.8746834482205772,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9822754383748},{"dec":-0.8746519254011872,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":357.98235224351225},{"dec":-0.8744914269049031,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":357.98262453612864},{"dec":-0.8745312402311659,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":357.9826852227653},{"dec":-0.8744243535000226,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":357.9830260413643},{"dec":-0.8743882674705383,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":357.9830708217385},{"dec":-0.8742806953672266,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":357.9834252894782}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747575487045643,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98196983171414},{"dec":-0.8746582058297164,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.98226648939874},{"dec":-0.8741595921718829,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":357.98382010784604},{"dec":-0.8740654285690528,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":357.98411559851564},{"dec":-0.873490603717691,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":357.98572777468024},{"dec":-0.873387383341721,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":357.9861084546938},{"dec":-0.8728099173536813,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":357.9876518704468},{"dec":-0.8720630699803124,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":357.98968400625245}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747641851905898,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.9819638053939},{"dec":-0.8747405655592477,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9823122909708},{"dec":-0.8734668779703675,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":357.9857091641343},{"dec":-0.8733237237866421,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":357.9860303003357},{"dec":-0.8721563188439553,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":357.98967568445545},{"dec":-0.8720617718118138,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":357.9900267051297},{"dec":-0.8707671533753925,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":357.9936141263177},{"dec":-0.8694284502563409,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":357.9973578176365}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8748219713134785,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98197118798134},{"dec":-0.874747444056253,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.98229316758875},{"dec":-0.8721201959227083,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":357.98973469608313},{"dec":-0.8720060578982992,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":357.9900226967775},{"dec":-0.8693875257949658,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":357.99734155591534},{"dec":-0.8692348354694839,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":357.9977187987768},{"dec":-0.866559155505919,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":358.00540965468144},{"dec":-0.8636849164823314,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":358.0134041053939}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747770424084984,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98197519047693},{"dec":-0.8747046460204796,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9823076499564},{"dec":-0.8707614863428481,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":357.99359379606204},{"dec":-0.8706754914090846,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":357.9939284397321},{"dec":-0.8665107343014136,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":358.0054670490115},{"dec":-0.8664674973857579,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":358.00566534079974},{"dec":-0.8622191848867384,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":358.0174750385381},{"dec":-0.8577304433664532,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":358.03005855301734}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747748511391348,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.9819523531676},{"dec":-0.8746870574690113,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9822956677991},{"dec":-0.8680040135509435,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":358.001351094063},{"dec":-0.8678986431123116,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":358.00163185796634},{"dec":-0.8607669949502277,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":358.02178557579725},{"dec":-0.8606651011578776,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":358.02209637839434},{"dec":-0.8530801781831842,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":358.04297984503245},{"dec":-0.8450529073950087,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":358.06505809417115}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747853092994988,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98194871514585},{"dec":-0.8746969769817752,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9823117901796},{"dec":-0.8651589631797737,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":358.00948215689715},{"dec":-0.8650235656807058,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":358.0098004795166},{"dec":-0.8546688899099869,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":358.03868372943737},{"dec":-0.8545197245091061,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":358.03905659074735},{"dec":-0.8434104448442853,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":358.06959177407725},{"dec":-0.8313579856105785,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":358.1024193356789}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8748493054762905,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98197209334506},{"dec":-0.8747293300926957,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.98228420600884},{"dec":-0.8607226372946909,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":358.02179432496735},{"dec":-0.8606073909692422,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":358.0220909373294},{"dec":-0.8450396822214112,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":358.0650504687225},{"dec":-0.8449452544742228,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":358.06541833199293},{"dec":-0.827786953144723,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":358.1122451995717},{"dec":-0.8089281226260455,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":358.1628477333548}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8748511748394089,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98194991700996},{"dec":-0.8746610465824305,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9822617828322},{"dec":-0.8547015438772365,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":358.03871465046075},{"dec":-0.8545458730955063,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":358.0390327807575},{"dec":-0.8313540326870971,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":358.10245548404185},{"dec":-0.8312412283449692,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":358.1028267201838},{"dec":-0.8049504814259625,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":358.1735133988831},{"dec":-0.7755360798892112,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":358.2512359106914}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747410789259804,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.9819741398437},{"dec":-0.8746970806997113,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9822903486657},{"dec":-0.8433647233615233,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":358.0696163302104},{"dec":-0.8432287562939434,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":358.06995566290374},{"dec":-0.8049197787737068,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":358.1734862772192},{"dec":-0.8048636693465684,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":358.17398060466377},{"dec":-0.7597789531031064,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":358.2927987098694},{"dec":-0.7081548747273001,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":358.42654613420905}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8747906347032446,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.9819410784857},{"dec":-0.874714693490251,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9822651527055},{"dec":-0.8277326182873824,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":358.1122697100774},{"dec":-0.8276158545492038,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":358.1126565021686},{"dec":-0.7665937020814019,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":358.27468150481536},{"dec":-0.7664239915838745,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":358.275226021341},{"dec":-0.6923199397361384,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":358.46747434774284},{"dec":-0.6061063864428907,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":358.68720607516985}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8748307803221088,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.98193965945205},{"dec":-0.8747287178974625,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.9822376428296},{"dec":-0.7989092325577206,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":358.1895475599366},{"dec":-0.7987115652031994,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":358.18996931828144},{"dec":-0.6922074066207771,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":358.46749413883504},{"dec":-0.6920813812786796,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":358.4680739404266},{"dec":-0.5589578056667897,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":358.8066616075974},{"dec":-0.403094108424581,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.1973028048615}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"centaur_25AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.8748198637727782,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":357.9819852289455},{"dec":-0.8747394118849842,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":357.98228948904205},{"dec":-0.7665741620877011,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":358.27474504245384},{"dec":-0.7663983072860837,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":358.2751982950105},{"dec":-0.6061100836910426,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":358.6872359696384},{"dec":-0.605860180443529,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":358.68796214013486},{"dec":-0.4031542893530919,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.19732596305374},{"dec":-0.16940281161493287,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":359.77782469302633}],"sigma_arcsec":0.1,"truth_state_at_epoch":[25.0,0.0,0.0,0.0,0.003323750768260684,0.0008905963341977738]},"classical_42AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222835341925041,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7948778099288},{"dec":-0.5222065688544107,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":358.7950003868874},{"dec":-0.5222035424479406,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":358.7951569877835}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222759860524708,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7949043573159},{"dec":-0.5222055242760337,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.7950413684494},{"dec":-0.522194667033576,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":358.79507892631125},{"dec":-0.5221925769533508,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":358.79528151029416},{"dec":-0.5221583134934393,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":358.79528250452114},{"dec":-0.5221419490738787,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":358.795386570398},{"dec":-0.5220884402720657,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":358.79546774079864},{"dec":-0.5220998987902438,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":358.7956653052702}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5221815481419912,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7948709206377},{"dec":-0.5222039975376492,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.7950726547113},{"dec":-0.5220562745693339,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":358.79585049254183},{"dec":-0.5219144283955119,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":358.7960241207339},{"dec":-0.5217839403268612,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":358.7968190048395},{"dec":-0.5217209333023893,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":358.7969771522432},{"dec":-0.5215657249831817,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":358.797830364859},{"dec":-0.5212938114304484,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":358.79883887145587}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222422209683506,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.794876382898},{"dec":-0.5222578709697756,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79501380764407},{"dec":-0.5218043217567093,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":358.7968187234965},{"dec":-0.5216889751245878,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":358.7969861328888},{"dec":-0.5212752665442165,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":358.798832169472},{"dec":-0.521269232267307,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":358.7989988657898},{"dec":-0.5208048890364062,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":358.80084862359934},{"dec":-0.5202718610214933,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":358.80281311811893}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5223009221041851,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7949187730871},{"dec":-0.5222168885782577,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79505208650573},{"dec":-0.521251518497297,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":358.798833100327},{"dec":-0.5212529767256177,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":358.799035734908},{"dec":-0.5202293100590244,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":358.80278724142005},{"dec":-0.5202312662310262,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":358.80296205043527},{"dec":-0.5191633440770415,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":358.8069502361372},{"dec":-0.5181147510237726,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":358.8110149012976}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.522314081474966,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.79490097670697},{"dec":-0.5222407048768147,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79503057665335},{"dec":-0.5207255543756142,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":358.8008422180214},{"dec":-0.5207084966343503,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":358.80102203360957},{"dec":-0.5191920716592205,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":358.80692769489497},{"dec":-0.5191581367181656,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":358.8070990923723},{"dec":-0.5174518440322177,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":358.81313566599886},{"dec":-0.5157196924771807,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":358.81964541026144}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222571024825375,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.79495960929756},{"dec":-0.522238051444901,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.7950323530385},{"dec":-0.5197015568312762,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":358.80478652522794},{"dec":-0.5196963990759756,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":358.8049865892983},{"dec":-0.5168961415900294,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":358.8153354526703},{"dec":-0.5168573410213435,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":358.81555948086776},{"dec":-0.5138407554253427,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":358.8264476829125},{"dec":-0.5105873047916939,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":358.83796854959775}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.522278491392362,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7949097465001},{"dec":-0.5222348060780835,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79503002988184},{"dec":-0.5186518392113797,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":358.8090272288636},{"dec":-0.5185791590547272,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":358.80917977818024},{"dec":-0.5144903432590294,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":358.824112761868},{"dec":-0.514459934396411,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":358.8243102445686},{"dec":-0.5099539108225501,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":358.8403801116191},{"dec":-0.5048059585958776,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":358.8576824668562}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222532797267256,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7949131042459},{"dec":-0.5221967732059161,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.7950403179311},{"dec":-0.5169629233933827,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":358.8153020021032},{"dec":-0.5168761692338968,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":358.8155696260752},{"dec":-0.5105914367750709,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":358.83798469410715},{"dec":-0.5105316496651225,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":358.8381349796218},{"dec":-0.5032268520173567,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":358.86289760042456},{"dec":-0.49498168873996473,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":358.8900305503263}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222752498261891,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.7949472459421},{"dec":-0.5222420907072172,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79503226038787},{"dec":-0.5145182908484071,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":358.82412628025395},{"dec":-0.5144782304312626,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":358.82430284651},{"dec":-0.5048805134703334,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":358.8577469160488},{"dec":-0.5047915641058671,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":358.8579319753683},{"dec":-0.4932081127428609,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":358.8957697009003},{"dec":-0.47979994302855344,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":358.9378402002473}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222873238010615,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.794881486574},{"dec":-0.5221910869127447,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.7950461494135},{"dec":-0.5099250652690928,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":358.8403317493266},{"dec":-0.509842657622993,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":358.8405404054184},{"dec":-0.4932920382570381,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":358.8957850522315},{"dec":-0.49317383204678356,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":358.8959680081346},{"dec":-0.4724076704999552,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":358.96056772794145},{"dec":-0.44765596982057687,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":359.03439727097754}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222270934626748,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.79493617977266},{"dec":-0.5221886767244027,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79506004259247},{"dec":-0.5033482254232818,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":358.8628726411151},{"dec":-0.5032380516802768,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":358.8631419865645},{"dec":-0.4755590662905623,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":358.950681073653},{"dec":-0.4755109614699104,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":358.95090642243014},{"dec":-0.4398476776213122,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":359.0571644665036},{"dec":-0.3966498229626184,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":359.1804611783433}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5223465910064444,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.79483897213646},{"dec":-0.522253946212452,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.79501518953987},{"dec":-0.490471701519477,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":358.9043261692855},{"dec":-0.4904169799329148,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":358.9045999382035},{"dec":-0.4398487512175374,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":359.05713170280586},{"dec":-0.4397355288308645,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":359.0575118442809},{"dec":-0.37255058600619556,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":359.24808497926387},{"dec":-0.2911281853613487,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.4712406410399}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"classical_42AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.5222819696476402,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":358.79494428773967},{"dec":-0.5222093000190892,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":358.7950310498496},{"dec":-0.4756522867287151,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":358.95073968279644},{"dec":-0.4755136671978817,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":358.9509507928889},{"dec":-0.3966634362382868,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":359.1805414370173},{"dec":-0.3965009136847913,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":359.1807995495642},{"dec":-0.29114389802182644,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.4712276766829},{"dec":-0.16628710672140978,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":359.8063795315549}],"sigma_arcsec":0.1,"truth_state_at_epoch":[42.0,0.0,0.0,0.0,0.002583425287844363,9.021519897596501e-05]},"mainbelt_2.5AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772723005817182,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65287476893104},{"dec":-7.771237097832993,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":341.6602466087987},{"dec":-7.769733945984376,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":341.6676040360999}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.7728224291042896,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528328540995},{"dec":-7.771187737120329,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66049659472395},{"dec":-7.770918555637785,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":341.6620552185882},{"dec":-7.769285282753616,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":341.6696892691245},{"dec":-7.768987999939121,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":341.67133726578544},{"dec":-7.767386418148543,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":341.6790240751655},{"dec":-7.767035461961152,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":341.68057791364345},{"dec":-7.765228728528702,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":341.68988592597765}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772776210690958,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528681923562},{"dec":-7.771200349418955,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.660559708014},{"dec":-7.763263894898378,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":341.6991674099827},{"dec":-7.761736229690249,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":341.70692565698226},{"dec":-7.753693381108238,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":341.74594073686234},{"dec":-7.752174588823148,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":341.75382748156807},{"dec":-7.744257322213001,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":341.7930687229422},{"dec":-7.734732877188163,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":341.8402261770285}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772752747625068,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528387309267},{"dec":-7.771194550824114,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66053794473925},{"dec":-7.753693432151831,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":341.7459337899451},{"dec":-7.75217469645121,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":341.753851312245},{"dec":-7.734757332119486,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":341.8402483831299},{"dec":-7.733134972962568,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":341.84798273562313},{"dec":-7.715772765686228,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":341.9334197914892},{"dec":-7.6965540844279445,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":342.0257328065692}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772819875169356,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65282301632266},{"dec":-7.771169305604323,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.6605707356943},{"dec":-7.734723750249554,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":341.8402261235071},{"dec":-7.733139936980678,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":341.8479965029455},{"dec":-7.6965362682701555,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":342.0257205796326},{"dec":-7.694941272839248,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":342.0334016563311},{"dec":-7.657666233699548,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":342.21380681288264},{"dec":-7.618705816815474,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":342.4001011299751}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772784089890341,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528332558221},{"dec":-7.771215463763133,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66054743426895},{"dec":-7.715754675918188,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":341.93337864856716},{"dec":-7.7142209890367415,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":341.94107395848926},{"dec":-7.657742755029036,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":342.2137991543739},{"dec":-7.6560451768975595,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":342.22168152343136},{"dec":-7.5988536827541955,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":342.4939624144089},{"dec":-7.5393756472576055,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":342.77584650267426}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772809025463446,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528182203641},{"dec":-7.77121367652591,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66054672487013},{"dec":-7.677084794805143,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":342.1191563426094},{"dec":-7.675497688781388,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":342.1271113847905},{"dec":-7.579166352592143,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":342.5889135821427},{"dec":-7.577446437155577,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":342.5967078240077},{"dec":-7.478858734102109,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":343.05976350848334},{"dec":-7.376064984664029,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":343.53175332787833}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772733708853695,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65286777687794},{"dec":-7.771244911606277,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.6605273898917},{"dec":-7.638322144289141,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":342.30733248315886},{"dec":-7.636713036902887,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":342.31508874486957},{"dec":-7.498925965243071,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":342.96542080831534},{"dec":-7.497330933997301,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":342.97333693535796},{"dec":-7.3551010618481145,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":343.62668996252387},{"dec":-7.206803547231072,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":344.2931833293291}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772819679795063,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65288551055426},{"dec":-7.771179987882324,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66055713992546},{"dec":-7.579132213373805,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":342.58893457397},{"dec":-7.577487561022347,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":342.596749647491},{"dec":-7.376054880379327,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":343.5318271880431},{"dec":-7.374265693363925,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":343.5396084226691},{"dec":-7.163482240648249,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":344.48550931563335},{"dec":-6.9422377542966895,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":345.44518881323665}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.7727411924437675,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528526391097},{"dec":-7.771262397565698,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66058837402977},{"dec":-7.4989811992840245,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":342.96548576690583},{"dec":-7.497285227022122,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":342.9733598785276},{"dec":-7.20676586599618,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":344.29318266369825},{"dec":-7.205034096318268,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":344.3010570045145},{"dec":-6.896784900432848,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":345.63943832030736},{"dec":-6.570495267892216,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":346.99835157664603}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.7728238498542055,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65283416809723},{"dec":-7.771206612986307,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.6604656040934},{"dec":-7.355108969793258,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":343.62672056409536},{"dec":-7.353295258605086,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":343.6347276686986},{"dec":-6.896778185109248,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":345.63935131378975},{"dec":-6.894834372437299,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":345.64755412358164},{"dec":-6.40152600365829,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":347.68455394202715},{"dec":-5.872973320017611,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":349.7582166772284}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772806819316846,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65286843411747},{"dec":-7.7711831255161385,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66051948252334},{"dec":-7.163480584486581,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":344.4855219978927},{"dec":-7.161689797676771,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":344.49352588512215},{"dec":-6.474350045358723,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":347.38951917126036},{"dec":-6.4723484094427794,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":347.39761632267863},{"dec":-5.7164301212101565,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":350.3570009681876},{"dec":-4.900876174034555,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":353.3732675871211}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772776598773812,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.6528661945714},{"dec":-7.77121724140542,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.66058704026216},{"dec":-6.828153063996959,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":345.92868786625036},{"dec":-6.82622806298369,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":345.9368563072905},{"dec":-5.716440366826005,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":350.35695138491906},{"dec":-5.714237849654136,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":350.3653117089695},{"dec":-4.474394923734915,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":354.9000826817164},{"dec":-3.134106007186191,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.5379155677429}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_2.5AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-7.772785047475494,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":341.65285466635845},{"dec":-7.771197377723921,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":341.6605061794052},{"dec":-6.474346555695651,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":347.38951578484074},{"dec":-6.4723036890053525,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":347.3976086214827},{"dec":-4.9008731382285555,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":353.37324211904865},{"dec":-4.898539455072463,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":353.38171952452603},{"dec":-3.134057809287692,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.53796372328935},{"dec":-1.2525373902685575,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":5.836953653184908}],"sigma_arcsec":0.1,"truth_state_at_epoch":[2.5,0.0,0.0,0.0,0.010838598315238193,0.0009482544810945209]},"mainbelt_3.5AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.80973402683022,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.430142054162},{"dec":-5.808504376701544,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":346.4348163806678},{"dec":-5.807225891905797,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":346.4394423245684}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809771841675749,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301702825588},{"dec":-5.808426298468511,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.43503032567696},{"dec":-5.808143667341637,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":346.4359726349663},{"dec":-5.80684877909019,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":346.44081450928275},{"dec":-5.8065751172112225,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":346.4418960395871},{"dec":-5.805305237828251,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":346.44666686665},{"dec":-5.80497564594966,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":346.4476087861698},{"dec":-5.803444817889149,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":346.4535477383929}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809730853720785,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.43013428262736},{"dec":-5.808416155513329,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4349923475687},{"dec":-5.801785032824691,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":346.4593580586127},{"dec":-5.800501276666583,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":346.4643007410199},{"dec":-5.793901328490568,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":346.4888683582073},{"dec":-5.792596808174629,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":346.4938680483411},{"dec":-5.785952970413068,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":346.51874627560204},{"dec":-5.778028131691938,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":346.548676449371}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809720647574021,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4302440362718},{"dec":-5.808446961530618,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4350023470445},{"dec":-5.793877960383535,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":346.48886213946935},{"dec":-5.792559602904056,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":346.493896964381},{"dec":-5.778046412767268,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":346.54862870420095},{"dec":-5.7766683181545435,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":346.5535961890664},{"dec":-5.762178198209103,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":346.60769176145186},{"dec":-5.746132302160718,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":346.66611403587893}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809783317213177,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301777339298},{"dec":-5.808414777182311,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.434997716397},{"dec":-5.778033242969375,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":346.5486559233234},{"dec":-5.776687329515248,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":346.55357487771954},{"dec":-5.746146362455937,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":346.66611582961286},{"dec":-5.744773658993336,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":346.67099630859354},{"dec":-5.713782053401671,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":346.785554048549},{"dec":-5.681299524816053,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":346.9040337605841}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809777604046456,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301411693513},{"dec":-5.808399738018769,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4349942491998},{"dec":-5.762120009598839,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":346.60771101807677},{"dec":-5.760725151982571,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":346.61257239852966},{"dec":-5.713755564554572,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":346.785590444237},{"dec":-5.712427165778105,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":346.7905786640095},{"dec":-5.664771983312735,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":346.96384926269013},{"dec":-5.615224421649107,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":347.1439684718096}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809705431794237,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301790846528},{"dec":-5.808470340556485,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4350593780941},{"dec":-5.729947268269504,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":346.7253467614998},{"dec":-5.728545478101568,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":346.7303631260556},{"dec":-5.648295830942379,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":347.0245374827741},{"dec":-5.646917692452126,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":347.02955535322843},{"dec":-5.5649232589306195,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":347.3260762089249},{"dec":-5.479611428003151,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":347.6297842327303}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809798187078631,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301286227301},{"dec":-5.8084259936611575,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4350474453039},{"dec":-5.6976710529109695,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":346.84516870472146},{"dec":-5.696260751381871,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":346.8501087448191},{"dec":-5.5816461371686215,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":347.26551899710546},{"dec":-5.580274782983786,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":347.2706047958589},{"dec":-5.462260859148952,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":347.69106979297527},{"dec":-5.339387860617576,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":348.1231432047927}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.8097236110006865,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.43015210648946},{"dec":-5.80842836800647,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.43503374189265},{"dec":-5.648294506233027,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":347.0245815239332},{"dec":-5.646941415638776,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":347.0295690690893},{"dec":-5.479598160904689,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":347.6297585405841},{"dec":-5.478152166800001,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":347.634806831993},{"dec":-5.303647560644616,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":348.24853222100904},{"dec":-5.120781759970185,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":348.8769673280499}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809719240459001,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.43013441278134},{"dec":-5.808441293358627,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.43501071530426},{"dec":-5.581681910224271,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":347.26548800502786},{"dec":-5.580315470645882,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":347.27054335847714},{"dec":-5.339422231599084,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":348.1231641399523},{"dec":-5.337912729133985,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":348.1282801883978},{"dec":-5.08336785586167,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":349.0049974246145},{"dec":-4.814544732138694,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":349.9062598402762}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809745236122292,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.43017637072063},{"dec":-5.808412465281192,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4350262257005},{"dec":-5.46225596965262,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":347.6910769084945},{"dec":-5.460826306020038,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":347.69622096000444},{"dec":-5.083345110661473,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":349.00499927180664},{"dec":-5.081726911528345,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":349.01037460611036},{"dec":-4.675624065718288,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":350.3654122618576},{"dec":-4.241838773371332,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":351.7675014917792}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809804130251565,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4302048628141},{"dec":-5.808443842411137,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.43503170885054},{"dec":-5.303576417223256,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":348.2484497331828},{"dec":-5.302057230693951,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":348.25368789874784},{"dec":-4.735498207235814,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":350.16763720539524},{"dec":-4.733820318492799,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":350.1730396384545},{"dec":-4.113461396534389,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":352.1762493108192},{"dec":-3.4454146649551918,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":354.2578475076669}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809745300037712,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301536356699},{"dec":-5.808423005151617,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.4350382263413},{"dec":-5.026802753370175,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":349.19580222988094},{"dec":-5.025168151576477,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":349.20121415858614},{"dec":-4.113427219321398,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":352.1762212326558},{"dec":-4.111670568534935,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":352.181930295723},{"dec":-3.0963260470849723,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":355.3246226325214},{"dec":-1.9982916455205335,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":358.6080365708351}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"mainbelt_3.5AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-5.809724649835228,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":346.4301815912377},{"dec":-5.808428700471325,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":346.43500289671294},{"dec":-4.73550406567438,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":350.16761913409886},{"dec":-4.7338674081932774,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":350.17300033547497},{"dec":-3.445494213984082,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":354.2578180356806},{"dec":-3.443579094833915,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":354.26366412952547},{"dec":-1.9982750681022632,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":358.60805106107136},{"dec":-0.4503882270659273,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":3.1422806062939608}],"sigma_arcsec":0.1,"truth_state_at_epoch":[3.5,0.0,0.0,0.0,0.009057276904453277,0.0015970422900027583]},"scattered_70AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.31416665576486025,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.27560410891317},{"dec":-0.31412820127242874,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":359.275682285656},{"dec":-0.31401713066078846,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":359.27579792693587}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141477004540293,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.27556206377614},{"dec":-0.31407576819615,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27565681304526},{"dec":-0.3140881863069613,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":359.27568560779406},{"dec":-0.31407642804417346,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":359.2757787997065},{"dec":-0.3140312389804962,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":359.2757956711784},{"dec":-0.31398833848365093,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":359.27588759052986},{"dec":-0.31403889172323707,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":359.2758629368941},{"dec":-0.3139892637479526,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":359.2759977326099}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.31412777272591963,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.27561499827715},{"dec":-0.31408458470244227,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27569289251187},{"dec":-0.31396996175214026,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":359.27605501893146},{"dec":-0.31390146994899853,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":359.2761987643257},{"dec":-0.3137625432745036,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":359.276586142032},{"dec":-0.31369399257827013,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":359.2766693437499},{"dec":-0.3135698904377588,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":359.27701000274885},{"dec":-0.3133501730296445,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":359.2775743762114}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.31412180701348275,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2755980378395},{"dec":-0.31414197737919847,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.2756826993954},{"dec":-0.3136974363846674,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":359.27656942212036},{"dec":-0.31372756919866146,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":359.2766478437663},{"dec":-0.31330340226679637,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":359.27760181866137},{"dec":-0.3133338233181647,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":359.27764493540474},{"dec":-0.31292755972138897,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":359.2786553942491},{"dec":-0.3124862975926942,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":359.2795457081777}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141520265256171,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2756057032906},{"dec":-0.3140582124554518,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.2757059760667},{"dec":-0.31331087157806015,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":359.277616798272},{"dec":-0.3133427011011166,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":359.2776724924843},{"dec":-0.31248391388417185,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":359.27959328346714},{"dec":-0.3125297175059309,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":359.27967892797443},{"dec":-0.3116474256650217,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":359.28167575172176},{"dec":-0.3107831710594089,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":359.28374782193254}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141190727822405,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2756276711746},{"dec":-0.3140984290407978,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27568590348096},{"dec":-0.31294742460399216,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":359.278563202185},{"dec":-0.3129049321660565,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":359.2786859789732},{"dec":-0.3116276511622015,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":359.28161252423524},{"dec":-0.31162720196185834,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":359.2817372674082},{"dec":-0.3103208359303328,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":359.2848336137388},{"dec":-0.3090186343715498,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":359.28812532721395}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141594819609151,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.27563852879285},{"dec":-0.3140564575813689,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27571105961357},{"dec":-0.31214541964238884,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":359.280587087528},{"dec":-0.3120661163444638,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":359.28066778637077},{"dec":-0.3099371686727686,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":359.2859084677841},{"dec":-0.30981817768136644,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":359.2860364065883},{"dec":-0.3075941835770901,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":359.29161209576733},{"dec":-0.305102121780404,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":359.29759693439064}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.31415645709341816,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.275572785488},{"dec":-0.31407557348312476,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.2757245499723},{"dec":-0.31121852759827534,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":359.28272087206363},{"dec":-0.3112352372168438,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":359.28278600372516},{"dec":-0.30804150643011413,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":359.29044100076203},{"dec":-0.30803799022048206,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":359.2905280573945},{"dec":-0.3045674461951718,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":359.298837272592},{"dec":-0.3007729804116239,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":359.3079103075828}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141163622249574,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.27560847072147},{"dec":-0.31409929925962904,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.2756928267179},{"dec":-0.309859620649477,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":359.2859299229095},{"dec":-0.30982446037204947,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":359.28604139425136},{"dec":-0.3050607423490059,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":359.2975734089818},{"dec":-0.30509028757933815,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":359.29768451378106},{"dec":-0.2996926874818336,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":359.3106777790056},{"dec":-0.2936938530076774,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":359.325061020441}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141275106765839,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2755979052958},{"dec":-0.31407821911487727,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27566689204536},{"dec":-0.308076653181925,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":359.2904574279796},{"dec":-0.3080208017215431,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":359.290549781616},{"dec":-0.3008154388810224,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":359.30794095058627},{"dec":-0.3007773965308239,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":359.3080080991129},{"dec":-0.2924607481302842,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":359.32809054815385},{"dec":-0.2829345765963122,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":359.35083830282434}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.31409686244187635,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2756526492613},{"dec":-0.3140395609030631,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27570051664344},{"dec":-0.3046142300342881,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":359.2988575114579},{"dec":-0.3045317551847999,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":359.29890922110576},{"dec":-0.2924275147438783,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":359.32812352357945},{"dec":-0.29235587173917377,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":359.328238960248},{"dec":-0.27777349583329447,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":359.3631944382547},{"dec":-0.2606890229233429,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":359.4037499585847}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.31412363377228447,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2755782746166},{"dec":-0.3141341200552927,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27565724046644},{"dec":-0.29972298156183885,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":359.3106945354234},{"dec":-0.29963099558412476,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":359.3107939464704},{"dec":-0.28000893433795865,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":359.35779912696864},{"dec":-0.2799433160966835,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":359.3579639279518},{"dec":-0.25540699426825886,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":359.41639380831697},{"dec":-0.2261913443975006,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":359.4853469779265}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141150559214514,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.27559051410714},{"dec":-0.31407899465653843,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27571459718905},{"dec":-0.2904678907070501,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":359.3327771669316},{"dec":-0.29045232554496614,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":359.33293332834177},{"dec":-0.25535003883878077,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":359.41640769892666},{"dec":-0.2552344772176772,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":359.4165227270292},{"dec":-0.21008201676770175,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":359.5235216733721},{"dec":-0.15603623515955195,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.6504848230366}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"scattered_70AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.3141137140579034,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.2756409278886},{"dec":-0.3141532709290995,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.27566335013364},{"dec":-0.280020971106052,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":359.35780797059556},{"dec":-0.2799829764387481,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":359.3579526622727},{"dec":-0.22619082617207567,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":359.4853488981404},{"dec":-0.22613333748927839,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":359.4855191412246},{"dec":-0.1561147098262269,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.65050068109474},{"dec":-0.07403727211517153,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":359.8432199081735}],"sigma_arcsec":0.1,"truth_state_at_epoch":[70.0,0.0,0.0,0.0,0.0017675618196982938,0.0006433398895955829]},"sednoid_80AU_arc_000.04d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750325555385689,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.36572636593354},{"dec":-0.2750079865979501,"jd_tdb":2460310.5208007395,"observer_state_AU":[-0.17414792183642253,0.8864887727467927,0.3844784040528045],"ra":359.36587481205595},{"dec":-0.27496907787488645,"jd_tdb":2460310.5408007395,"observer_state_AU":[-0.1744954714744205,0.8864377022290952,0.38445473903121385],"ra":359.36589616811716}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_000.10d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.27504432778491394,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.36574785655097},{"dec":-0.27503673313370386,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3658061550857},{"dec":-0.27498938064723316,"jd_tdb":2460310.5258007394,"observer_state_AU":[-0.17423476868369125,0.8864760502509452,0.3844724921841926],"ra":359.36584391492966},{"dec":-0.27495735013555656,"jd_tdb":2460310.546634073,"observer_state_AU":[-0.17459691845387337,0.8864227123313079,0.38444782791222387],"ra":359.3658722854397},{"dec":-0.2749550220082325,"jd_tdb":2460310.5508007393,"observer_state_AU":[-0.17466940092204172,0.8864119777793248,0.38444288895703327],"ra":359.365901863766},{"dec":-0.27490212220103855,"jd_tdb":2460310.571634073,"observer_state_AU":[-0.17503204931892713,0.886357944892065,0.3844181636139291],"ra":359.36600899355363},{"dec":-0.2749200164029688,"jd_tdb":2460310.5758007397,"observer_state_AU":[-0.17510462258296466,0.886347063346462,0.3844132124237931],"ra":359.3659805606639},{"dec":-0.2748871650875036,"jd_tdb":2460310.6008007396,"observer_state_AU":[-0.1755403208902572,0.8862812178397388,0.38438346233059384],"ra":359.3661240054032}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_000.50d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.27498940719677983,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3656995839091},{"dec":-0.2750081309061167,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.36576472395575},{"dec":-0.27484388397642084,"jd_tdb":2460310.6258007395,"observer_state_AU":[-0.1759763705069549,0.886214370920852,0.38435363839207454],"ra":359.3661688268602},{"dec":-0.2748303327755003,"jd_tdb":2460310.646634073,"observer_state_AU":[-0.17633991648958763,0.8861578635983237,0.384328728478743],"ra":359.36628812892855},{"dec":-0.27465845735157546,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":359.36667287150607},{"dec":-0.27460927547981434,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":359.3667259498513},{"dec":-0.27448117466905625,"jd_tdb":2460310.8758007395,"observer_state_AU":[-0.18032623782256,0.8854906730193006,0.3840512688442238],"ra":359.36712301805454},{"dec":-0.2742912226160445,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":359.36756315760255}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_001.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.27500118783553024,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657153407344},{"dec":-0.27500735059397174,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3658311897585},{"dec":-0.2746110834498953,"jd_tdb":2460310.7508007395,"observer_state_AU":[-0.17815702768397493,0.8858641435928897,0.3842033999032811],"ra":359.36667381345165},{"dec":-0.2746738489107608,"jd_tdb":2460310.771634073,"observer_state_AU":[-0.17851976101616254,0.88580326805312,0.3841781770863005],"ra":359.3667376034052},{"dec":-0.2742671697356571,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":359.3676157582778},{"dec":-0.2742223111976171,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":359.36776160553114},{"dec":-0.2739149330161638,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":359.3686227420215},{"dec":-0.27349292251853136,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":359.3695185190183}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_002.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.27501687967625527,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657082547353},{"dec":-0.27496241114753023,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.36582486354195},{"dec":-0.27433081171895185,"jd_tdb":2460311.0008007395,"observer_state_AU":[-0.18247370969968194,0.8851070739923574,0.38389722138552784],"ra":359.3676455628567},{"dec":-0.27425789515511456,"jd_tdb":2460311.021634073,"observer_state_AU":[-0.18282945945171236,0.8850433620264009,0.38387136037429487],"ra":359.36769185267934},{"dec":-0.27353154384247175,"jd_tdb":2460311.5008007395,"observer_state_AU":[-0.19100509556680842,0.8836786156367427,0.3832622397123696],"ra":359.3695033768208},{"dec":-0.2735366653051285,"jd_tdb":2460311.521634073,"observer_state_AU":[-0.1913656329194933,0.8836200894019831,0.3832351465173154],"ra":359.3695305102472},{"dec":-0.27274849642994226,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":359.3714163222615},{"dec":-0.27187779192715,"jd_tdb":2460312.5008007395,"observer_state_AU":[-0.20815383864423578,0.8805422347617263,0.38190306415804204],"ra":359.37342162265696}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_003.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.27498331581396085,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.36576663625436},{"dec":-0.27498822506058784,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3657874875643},{"dec":-0.2739150207489292,"jd_tdb":2460311.2508007395,"observer_state_AU":[-0.18672267081256524,0.8843693343173894,0.38358342195575945],"ra":359.36854358333665},{"dec":-0.27382324569900024,"jd_tdb":2460311.271634073,"observer_state_AU":[-0.1870767712918262,0.8843109028112077,0.38355693520353223],"ra":359.368692055004},{"dec":-0.2727698995727164,"jd_tdb":2460312.0008007395,"observer_state_AU":[-0.19965024119155023,0.8821073175675992,0.3825977081026224],"ra":359.371506411496},{"dec":-0.2727420748101034,"jd_tdb":2460312.021634073,"observer_state_AU":[-0.20000476366137085,0.8820379102526436,0.38256935960568583],"ra":359.37153057030156},{"dec":-0.2715094406979133,"jd_tdb":2460312.7508007395,"observer_state_AU":[-0.21248240142950636,0.8797274962416777,0.38154476426703343],"ra":359.37446270941274},{"dec":-0.2701851377176728,"jd_tdb":2460313.5008007395,"observer_state_AU":[-0.22524175729248222,0.8771309459139282,0.3804247739881352],"ra":359.3776044488242}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_005.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.27505411962454956,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657092815851},{"dec":-0.275005252563883,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3657427006209},{"dec":-0.2731154224980483,"jd_tdb":2460311.7508007395,"observer_state_AU":[-0.19534814691918537,0.8829335429478802,0.3829337452978234],"ra":359.3704421768371},{"dec":-0.2730538920848433,"jd_tdb":2460311.771634073,"observer_state_AU":[-0.19570970350702085,0.8828668530117665,0.38290603370687193],"ra":359.37054904528213},{"dec":-0.27105221077905783,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":359.3755573795213},{"dec":-0.27103564905384514,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":359.375631903076},{"dec":-0.26894192827356306,"jd_tdb":2460314.2508007395,"observer_state_AU":[-0.23802560396754527,0.8743415654232194,0.37923810101558697],"ra":359.38086136930053},{"dec":-0.2665820079059684,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":359.386489093437}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_007.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750368707077377,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657341249335},{"dec":-0.27497921683518833,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.36578882446014},{"dec":-0.27230062958214507,"jd_tdb":2460312.2508007395,"observer_state_AU":[-0.20388492337631964,0.8813015660728668,0.38225406780977467],"ra":359.3724268774463},{"dec":-0.27227991784227973,"jd_tdb":2460312.271634073,"observer_state_AU":[-0.20423788480666458,0.8812374652926601,0.38222509522638753],"ra":359.3725744924907},{"dec":-0.2693095060957842,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":359.3797942320716},{"dec":-0.2693318177151548,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":359.37986998182697},{"dec":-0.26610898764800117,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":359.3876663141028},{"dec":-0.26259127261369636,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":359.39611763281306}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_010.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750022444025027,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.36578103235433},{"dec":-0.2749883668002561,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.36583297695614},{"dec":-0.2710199892239614,"jd_tdb":2460313.0008007395,"observer_state_AU":[-0.21676858138129576,0.8788323030341431,0.3811789369289377],"ra":359.37553692891805},{"dec":-0.2710440184668935,"jd_tdb":2460313.021634073,"observer_state_AU":[-0.21712176705432482,0.8787572161964935,0.3811481067514695],"ra":359.3756631988088},{"dec":-0.26658177596853183,"jd_tdb":2460315.5008007395,"observer_state_AU":[-0.2592137639213401,0.8694868754354372,0.3771121336327481],"ra":359.3865051948589},{"dec":-0.2665969378654359,"jd_tdb":2460315.521634073,"observer_state_AU":[-0.25956905103674294,0.8694053164646244,0.3770751428924803],"ra":359.38656301523525},{"dec":-0.2615925757662433,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":359.3987065478077},{"dec":-0.2560760035418562,"jd_tdb":2460320.5008007395,"observer_state_AU":[-0.3427013208766776,0.8456328239459369,0.3667729122602299],"ra":359.4120111947906}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_014.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750237978887127,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.365728258684},{"dec":-0.2749672105099736,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3658261198395},{"dec":-0.2693293685465146,"jd_tdb":2460314.0008007395,"observer_state_AU":[-0.23382341792314665,0.8752827740863148,0.3796412027203434],"ra":359.37976445099116},{"dec":-0.26933167164140914,"jd_tdb":2460314.021634073,"observer_state_AU":[-0.23417515657864443,0.8752020252807267,0.3796078973680679],"ra":359.37988178519436},{"dec":-0.2626299706883544,"jd_tdb":2460317.5008007395,"observer_state_AU":[-0.29287800934464864,0.8607537938057295,0.3733272701162683],"ra":359.396166099961},{"dec":-0.26256257194022653,"jd_tdb":2460317.521634073,"observer_state_AU":[-0.2932299907311769,0.8606608316112794,0.37328537863429057],"ra":359.39618061309926},{"dec":-0.2549025847717551,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":359.41485482224607},{"dec":-0.24620559655278865,"jd_tdb":2460324.5008007395,"observer_state_AU":[-0.4076432527166463,0.8217658446935986,0.35642516077625447],"ra":359.43579328242544}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_021.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750324264053586,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657487422675},{"dec":-0.27499007275579457,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3657420239126},{"dec":-0.266097282357541,"jd_tdb":2460315.7508007395,"observer_state_AU":[-0.26349075889060436,0.8684643368610818,0.3766649013913776],"ra":359.3876395212009},{"dec":-0.2660393236584532,"jd_tdb":2460315.771634073,"observer_state_AU":[-0.263846487952124,0.8683745574855772,0.37662729818059765],"ra":359.38775288535464},{"dec":-0.2548862267770107,"jd_tdb":2460321.0008007395,"observer_state_AU":[-0.35097472893439485,0.8428322976309781,0.36557929538773065],"ra":359.4148835585838},{"dec":-0.2548742110096855,"jd_tdb":2460321.021634073,"observer_state_AU":[-0.3513131834618763,0.8427125884492802,0.36552892428495937],"ra":359.41493297183825},{"dec":-0.24147881817652886,"jd_tdb":2460326.2508007395,"observer_state_AU":[-0.43544143447235506,0.8099708877846534,0.35133343204021644],"ra":359.44712152450364},{"dec":-0.22596104957246965,"jd_tdb":2460331.5008007395,"observer_state_AU":[-0.5162613446618664,0.7702120990013835,0.33407198528566023],"ra":359.4842553401821}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_030.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750404302945244,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657220319938},{"dec":-0.2750254357503898,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3658260339977},{"dec":-0.26160537248557525,"jd_tdb":2460318.0008007395,"observer_state_AU":[-0.30130016506949203,0.8583575289508647,0.3723078214617241],"ra":359.3986912553421},{"dec":-0.2615043926442671,"jd_tdb":2460318.021634073,"observer_state_AU":[-0.30164499731122296,0.8582543469042678,0.37226469684947255],"ra":359.3987624481311},{"dec":-0.24351177190933868,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":359.4421857917732},{"dec":-0.24351501410390994,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":359.44229348750594},{"dec":-0.22116538735295083,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":359.49577348899595},{"dec":-0.19475145038489294,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":359.55841654885165}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_045.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750461632295805,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.36578788067646},{"dec":-0.2749681059701787,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.3657942640099},{"dec":-0.25306803048152554,"jd_tdb":2460321.7508007395,"observer_state_AU":[-0.3632438797757617,0.8386290930622567,0.3637348462989859],"ra":359.4191093396555},{"dec":-0.25301937273241354,"jd_tdb":2460321.771634073,"observer_state_AU":[-0.3635874179160107,0.8385054306558842,0.363682701284376],"ra":359.41920846060356},{"dec":-0.2211214006922055,"jd_tdb":2460333.0008007395,"observer_state_AU":[-0.538625695623857,0.7575551491024204,0.32860981598931105],"ra":359.4957475075235},{"dec":-0.22104477431905292,"jd_tdb":2460333.021634073,"observer_state_AU":[-0.5389291227443627,0.7573736384636567,0.32853232364332935],"ra":359.49594721702687},{"dec":-0.18021916743326977,"jd_tdb":2460344.2508007395,"observer_state_AU":[-0.6931833514475642,0.6468799182387056,0.28063348363417956],"ra":359.5929939708231},{"dec":-0.13165250414505159,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.7077193030425}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]},"sednoid_80AU_arc_060.00d":{"epoch_jd_tdb":2460310.5008007395,"observations":[{"dec":-0.2750237632577878,"jd_tdb":2460310.5008007395,"observer_state_AU":[-0.17380082324502685,0.8865393864489687,0.38450202232458125],"ra":359.3657473308006},{"dec":-0.2749894478123713,"jd_tdb":2460310.521634073,"observer_state_AU":[-0.1741623943719456,0.8864866543400558,0.3844774189441282],"ra":359.36583142013086},{"dec":-0.24352943281938105,"jd_tdb":2460325.5008007395,"observer_state_AU":[-0.4235767191218686,0.8151511369787539,0.35355700878988844],"ra":359.44227269126424},{"dec":-0.24345534519852796,"jd_tdb":2460325.521634073,"observer_state_AU":[-0.4239108550289707,0.8150138666079211,0.35349608665266696],"ra":359.4423337819677},{"dec":-0.19477761458120132,"jd_tdb":2460340.5008007395,"observer_state_AU":[-0.6443367423624429,0.6868790159045345,0.29794909600233527],"ra":359.5583922179036},{"dec":-0.19472102877581876,"jd_tdb":2460340.521634073,"observer_state_AU":[-0.6446202715839477,0.6866674602771821,0.2978564427323442],"ra":359.5586370563665},{"dec":-0.1316501911946113,"jd_tdb":2460355.5008007395,"observer_state_AU":[-0.8211623920666156,0.5108222891127121,0.22162901384891132],"ra":359.7077624086179},{"dec":-0.05806154214639713,"jd_tdb":2460370.5008007395,"observer_state_AU":[-0.941999977669904,0.29948928089926374,0.13002037136083922],"ra":359.8813019405243}],"sigma_arcsec":0.1,"truth_state_at_epoch":[80.0,0.0,0.0,0.0,0.002212976121950814,0.0008054574375319498]}} diff --git a/tests/layup/_bk_guards.py b/tests/layup/_bk_guards.py new file mode 100644 index 00000000..c1da20df --- /dev/null +++ b/tests/layup/_bk_guards.py @@ -0,0 +1,83 @@ +"""Shared environment guards + data loaders for the BK engine test suites. + +Centralizes two things that the BK tests (Layers 2 and 3, plus the +BK-IOD and iod='auto' suites that stack on top) used to each hardcode: + +1. **The ASSIST ephemeris location.** Layup resolves its cache with + ``pooch.os_cache("layup")`` -- ``~/Library/Caches/layup`` on macOS, + ``~/.cache/layup`` on Linux. The tests previously hardcoded the + macOS path, so on the Linux CI legs ``EPHEM_AVAILABLE`` was always + False and every BK suite skipped even though ``layup bootstrap`` had + downloaded the files. Deriving the path from ``pooch`` the same way + the library does keeps the guard correct on every platform. + +2. **The diagnostic-scan truth set.** These ASSIST-integrated truth + cases used to live at a personal absolute path + (``~/Dropbox/claude_layup/diagnostic/scan/truth``) that existed on no + CI runner and no other contributor's machine, so the Layer-3 suites + skipped everywhere. The set is small (98 cases) and now ships in-repo + as a single consolidated, minified file ``tests/data/bk_scan_truth.json`` + (keyed by case stem) carrying only the fields the tests read. +""" + +from __future__ import annotations + +import functools +import json +from pathlib import Path + +import pooch +import pytest + +# --------------------------------------------------------------------------- +# ASSIST ephemeris guard +# --------------------------------------------------------------------------- + +EPHEM_CACHE = Path(pooch.os_cache("layup")) +EPHEM_PLANETS = EPHEM_CACHE / "linux_p1550p2650.440" +EPHEM_SMALLBODIES = EPHEM_CACHE / "sb441-n16.bsp" +EPHEM_AVAILABLE = EPHEM_PLANETS.exists() and EPHEM_SMALLBODIES.exists() + +requires_ephem = pytest.mark.skipif( + not EPHEM_AVAILABLE, + reason=( + f"ASSIST ephemeris missing at {EPHEM_CACHE} " + f"(expected {EPHEM_PLANETS.name} + {EPHEM_SMALLBODIES.name}); " + "run `layup bootstrap` to download it." + ), +) + +# --------------------------------------------------------------------------- +# Diagnostic-scan truth set (shipped in-repo) +# --------------------------------------------------------------------------- + +# The 98 cases ship as a single consolidated JSON file keyed by case stem +# rather than 98 separate files. Each case keeps only the fields the BK test +# suites actually read -- sigma_arcsec, epoch_jd_tdb, truth_state_at_epoch and +# per-observation {ra, dec, jd_tdb, observer_state_AU} -- which both shrinks +# the fixture (~600 KB -> ~135 KB) and keeps the repo to one data file. +DIAGNOSTIC_SCAN = Path(__file__).resolve().parents[1] / "data" / "bk_scan_truth.json" +DIAGNOSTIC_AVAILABLE = DIAGNOSTIC_SCAN.is_file() + + +@functools.lru_cache(maxsize=1) +def _diagnostic_cases() -> dict: + """Load and cache the consolidated diagnostic-scan truth set.""" + with open(DIAGNOSTIC_SCAN) as f: + return json.load(f) + + +requires_diagnostic = pytest.mark.skipif( + not DIAGNOSTIC_AVAILABLE, + reason=f"Diagnostic-scan truth set missing at {DIAGNOSTIC_SCAN}.", +) + + +def load_diagnostic_case(name: str) -> dict: + """Load a diagnostic-scan case by stem (e.g. 'classical_42AU_arc_007.00d').""" + return _diagnostic_cases()[name] + + +def diagnostic_case_names() -> list[str]: + """Sorted stems of every diagnostic-scan case shipped in-repo.""" + return sorted(_diagnostic_cases()) diff --git a/tests/layup/test_bk_everywhere.py b/tests/layup/test_bk_everywhere.py index e1d20f1c..85ad994e 100644 --- a/tests/layup/test_bk_everywhere.py +++ b/tests/layup/test_bk_everywhere.py @@ -1,23 +1,18 @@ """Layer 3 engine-sweep tests for the universal BK fitter. Drives both engine='cartesian' and engine='bk_native' against the -diagnostic/scan dataset (outside the repo, at -``~/Dropbox/claude_layup/diagnostic/scan/truth/``) so the design -memory's prediction -- ``bk_native`` matches Cartesian across regimes -and shines on distant short arcs -- can be validated against real -ASSIST-integrated truth. - -These tests skip cleanly when either the ASSIST ephemeris or the -diagnostic scan data is unavailable, so machines without either -setup are unaffected. +diagnostic-scan truth set (shipped in-repo as the consolidated +``tests/data/bk_scan_truth.json``) so the design memory's prediction -- +``bk_native`` matches Cartesian across regimes and shines on distant +short arcs -- can be validated against real ASSIST-integrated truth. + +These tests skip cleanly when the ASSIST ephemeris is unavailable +(see `_bk_guards`), so machines that haven't run `layup bootstrap` +are unaffected. """ from __future__ import annotations -import json -import os -from pathlib import Path - import numpy as np import pytest @@ -30,37 +25,32 @@ run_from_vector_with_initial_guess, ) +from _bk_guards import ( + EPHEM_CACHE, + diagnostic_case_names, + load_diagnostic_case, + requires_diagnostic, + requires_ephem, +) + # --------------------------------------------------------------------------- # Environment guards # --------------------------------------------------------------------------- -CACHE = os.path.expanduser("~/Library/Caches/layup") -EPHEM_PLANETS = os.path.join(CACHE, "linux_p1550p2650.440") -EPHEM_SMALLBODIES = os.path.join(CACHE, "sb441-n16.bsp") -EPHEM_AVAILABLE = os.path.exists(EPHEM_PLANETS) and os.path.exists(EPHEM_SMALLBODIES) - -DIAGNOSTIC_SCAN = Path("~/Dropbox/claude_layup/diagnostic/scan/truth").expanduser() -DIAGNOSTIC_AVAILABLE = DIAGNOSTIC_SCAN.is_dir() - -pytestmark = pytest.mark.skipif( - not (EPHEM_AVAILABLE and DIAGNOSTIC_AVAILABLE), - reason=( - f"Skipping Layer 3 BK-everywhere tests: " - f"ephem at {CACHE} = {EPHEM_AVAILABLE}, " - f"diagnostic scan at {DIAGNOSTIC_SCAN} = {DIAGNOSTIC_AVAILABLE}." - ), -) +# Directory passed to get_ephem(); str() preserves the pre-refactor type. +CACHE = str(EPHEM_CACHE) + +# Both gates apply: the fits need the ASSIST ephemeris, the assertions need +# the diagnostic-scan truth set. The latter now ships in-repo, so in practice +# only `requires_ephem` skips (on machines without `layup bootstrap`). +pytestmark = [requires_ephem, requires_diagnostic] # --------------------------------------------------------------------------- # Helpers for loading and converting diagnostic-scan cases # --------------------------------------------------------------------------- - -def _load_case(name: str) -> dict: - """Load a diagnostic-scan case by stem (e.g., 'classical_42AU_arc_007.00d').""" - with open(DIAGNOSTIC_SCAN / f"{name}.json") as f: - return json.load(f) +_load_case = load_diagnostic_case def _build_observations(case: dict) -> list: @@ -218,7 +208,7 @@ def sweep_cases_from_diagnostic(case_names=None) -> list: invoked by pytest collection. """ if case_names is None: - case_names = sorted(p.stem for p in DIAGNOSTIC_SCAN.glob("*.json")) + case_names = diagnostic_case_names() ephem = get_ephem(CACHE) rows = [] for name in case_names: diff --git a/tests/layup/test_bk_fit.py b/tests/layup/test_bk_fit.py index f22eb4bb..409fe210 100644 --- a/tests/layup/test_bk_fit.py +++ b/tests/layup/test_bk_fit.py @@ -6,14 +6,14 @@ gdot, isolating any disagreement to the BK-specific code path. Tests skip when the ASSIST ephemeris files aren't available, so CI on -machines without `~/Library/Caches/layup/{linux_p1550p2650.440, -sb441-n16.bsp}` is unaffected. +machines that haven't run `layup bootstrap` is unaffected. The +ephemeris location is resolved the same way the library resolves it +(`pooch.os_cache`), so the guard is correct on macOS and Linux alike -- +see `_bk_guards`. """ from __future__ import annotations -import os - import numpy as np import pytest @@ -26,18 +26,16 @@ run_from_vector_with_initial_guess, ) -CACHE = os.path.expanduser("~/Library/Caches/layup") -EPHEM_PLANETS = os.path.join(CACHE, "linux_p1550p2650.440") -EPHEM_SMALLBODIES = os.path.join(CACHE, "sb441-n16.bsp") -EPHEM_AVAILABLE = os.path.exists(EPHEM_PLANETS) and os.path.exists(EPHEM_SMALLBODIES) +from _bk_guards import EPHEM_CACHE, requires_ephem + +# Directory passed to get_ephem(); the library joins the ephemeris filenames +# onto it. str() preserves the original (pre-refactor) argument type. +CACHE = str(EPHEM_CACHE) # GM_sun in AU^3 / day^2. MU_SUN = 0.00029591220828559104 -pytestmark = pytest.mark.skipif( - not EPHEM_AVAILABLE, - reason=f"ASSIST ephemeris missing at {CACHE}; skipping BK-fit Layer 2 tests.", -) +pytestmark = requires_ephem # ---------------------------------------------------------------------------