Program Listing for File clock_dynamics.cc¶
↰ Return to documentation for file (dynamics/clock_dynamics.cc)
#include "lupnt/dynamics/clock_dynamics.h"
#include <tuple>
#include "lupnt/core/asset_factory.h"
#include "lupnt/core/constants.h"
#include "lupnt/core/definitions.h"
#include "lupnt/devices/clock.h"
#include "lupnt/numerics/math_utils.h"
namespace lupnt {
// ClockDynamics
// A. C. Long, ‘Goddard Enhanced Onboard Navigation System (GEONS) Mathematical
// Specifications’, 2024.
std::tuple<double, double, double> ClockDynamics::GetClockValues(ClockModel clk_model) {
double q1 = 0.0; // [s]
double q2 = 0.0; // [1/s]
double q3 = 0.0; // [1/s^2]
switch (clk_model) {
case ClockModel::OCXO:
// Preliminary Navigation System Design for the First LCRNS
// Satellite Providing Lunar PNT Services
q1 = pow(6.2299445014e-13, 2);
q2 = pow(2.0129544799e-14, 2);
q3 = pow(7.0118586804e-28, 2);
break;
case ClockModel::USO:
// MMS USO
q1 = 1.2e-22;
q2 = 1.58e-26;
q3 = 5.9e-75;
break;
case ClockModel::CSAC:
q1 = 5.8e-22;
q2 = 1.5e-29;
q3 = 1e-45;
break;
case ClockModel::MINI_RAFS:
q1 = pow(1.0e-11, 2);
q2 = pow(1.1e-15, 2);
q3 = pow(7.0118586804e-28, 2);
break;
case ClockModel::RAFS:
// https://www.nasa.gov/wp-content/uploads/2025/08/gps-based-autonomous-navigation-study-for-the-lunar-gateway-1.pdf?emrc=68cc48e722112
q1 = 3.70e-24;
q2 = 1.87e-33;
q3 = 7.56e-59;
break;
case ClockModel::DSAC:
q1 = 4.23e-26;
q2 = 6.19e-38;
q3 = 2.24e-61;
break;
default: LUPNT_CHECK(false, "Unknown clock model", "ClockDynamics");
}
return std::make_tuple(q1, q2, q3);
}
ClockDynamics::ClockDynamics() { model_ = ClockModel::UNDEFINED; }
ClockDynamics::ClockDynamics(Config& config) {
model_ = ClockModel::UNDEFINED;
if (config["model"]) {
model_ = enum_cast<ClockModel>(config["model"].as<std::string>()).value();
}
if (config["clock_bias_unit"]) {
bias_unit_ = enum_cast<ClockBiasUnit>(config["clock_bias_unit"].as<std::string>()).value();
} else if (config["bias_unit"]) {
bias_unit_ = enum_cast<ClockBiasUnit>(config["bias_unit"].as<std::string>()).value();
}
}
double ClockDynamics::SecondsToBiasUnitScale(ClockBiasUnit unit) {
switch (unit) {
case ClockBiasUnit::SECONDS: return 1.0;
case ClockBiasUnit::METERS: return C;
case ClockBiasUnit::KILOMETERS: return C * KM_M;
default: LUPNT_CHECK(false, "Unknown clock bias unit", "ClockDynamics");
}
}
Real ClockDynamics::SecondsToBiasUnits(Real value_s, ClockBiasUnit unit) {
return value_s * SecondsToBiasUnitScale(unit);
}
Real ClockDynamics::BiasUnitsToSeconds(Real value, ClockBiasUnit unit) {
return value / SecondsToBiasUnitScale(unit);
}
std::vector<std::string> ClockDynamics::GetStateUnits(int state_size, ClockBiasUnit unit) {
std::vector<std::string> units;
switch (unit) {
case ClockBiasUnit::SECONDS: units = {"s", "s/s", "s/s^2"}; break;
case ClockBiasUnit::METERS: units = {"m", "m/s", "m/s^2"}; break;
case ClockBiasUnit::KILOMETERS: units = {"km", "km/s", "km/s^2"}; break;
default: LUPNT_CHECK(false, "Unknown clock bias unit", "ClockDynamics");
}
LUPNT_CHECK(state_size == 2 || state_size == 3, "Clock state size must be 2 or 3",
"ClockDynamics");
units.resize(state_size);
return units;
}
Real ClockDynamics::RelativisticRateCorrection(const Vec3& r_centered, const Vec3& v_centered,
Real GM, Real c, Real reference_rate_offset) {
Real r = r_centered.norm();
LUPNT_CHECK(r > 0.0, "Relativistic clock correction requires a nonzero radius",
"ClockDynamics");
Real v2 = v_centered.squaredNorm();
return reference_rate_offset - (GM / r + 0.5 * v2) / (c * c);
}
Mat2 ClockDynamics::TwoStateNoise(ClockModel clk_model, Real dt) {
Real q1, q2, q3;
std::tie(q1, q2, q3) = GetClockValues(clk_model);
Real dt2 = dt * dt;
Real dt3 = dt2 * dt;
Mat2 Q;
Q(0, 0) = q1 * dt + q2 * dt3 / 3.0;
Q(0, 1) = q2 * dt2 / 2.0;
Q(1, 0) = Q(0, 1);
Q(1, 1) = q2 * dt;
return Q;
};
Mat2 ClockDynamics::TwoStateNoise(ClockModel clk_model, Real dt, ClockBiasUnit unit) {
Real scale = SecondsToBiasUnitScale(unit);
return TwoStateNoise(clk_model, dt) * scale * scale;
}
Mat3 ClockDynamics::ThreeStateNoise(ClockModel clk_model, Real dt) {
Real q1, q2, q3;
std::tie(q1, q2, q3) = GetClockValues(clk_model);
Real dt2 = dt * dt;
Real dt3 = dt2 * dt;
Real dt4 = dt3 * dt;
Real dt5 = dt4 * dt;
Mat3 Q;
Q(0, 0) = q1 * dt + q2 * dt3 / 3 + q3 * dt5 / 20.0;
Q(0, 1) = q2 * dt2 / 2.0 + q3 * dt4 / 8.0;
Q(0, 2) = q3 * dt3 / 6.0;
Q(1, 0) = Q(0, 1);
Q(1, 1) = q2 * dt + q3 * dt3 / 3.0;
Q(1, 2) = q3 * dt2 / 2.0;
Q(2, 0) = Q(0, 2);
Q(2, 1) = Q(1, 2);
Q(2, 2) = q3 * dt;
return Q;
};
Mat3 ClockDynamics::ThreeStateNoise(ClockModel clk_model, Real dt, ClockBiasUnit unit) {
Real scale = SecondsToBiasUnitScale(unit);
return ThreeStateNoise(clk_model, dt) * scale * scale;
}
VecX ClockDynamics::GetProcessNoise(Real dt, int state_size) {
MatX Q;
VecX noise(state_size);
if (state_size == 2) {
Q = ClockDynamics::TwoStateNoise(model_, dt, bias_unit_);
noise = SampleMvNormal(Vec2d::Zero(), Q, 1, rng_.get()).transpose();
} else if (state_size == 3) {
Q = ClockDynamics::ThreeStateNoise(model_, dt, bias_unit_);
noise = SampleMvNormal(Vec3d::Zero(), Q, 1, rng_.get()).transpose();
} else {
LUPNT_CHECK(false, fmt::format("Invalid clock state size {}", state_size), "ClockDynamics");
}
return noise;
}
Mat2 ClockDynamics::TwoStatePhi(Real dt) {
Mat2 Phi_clk{{1, dt}, {0, 1}};
return Phi_clk;
}
Mat3 ClockDynamics::ThreeStatePhi(Real dt) {
Mat3 Phi_clk{{1, dt, dt * dt / 2}, {0, 1, dt}, {0, 0, 1}};
return Phi_clk;
}
State ClockDynamics::Propagate(const State& x0, Real t0, Real tf, const State* u) {
return PropagateImpl(x0, t0, tf, u, nullptr, nullptr);
}
State ClockDynamics::Propagate(const State& x0, Real t0, Real tf, const State* u, MatXd* stm) {
return PropagateImpl(x0, t0, tf, u, nullptr, stm);
}
State ClockDynamics::PropagateWithRelativity(const State& x0, Real t0, Real tf,
const ClockRelativityContext& relativity,
const State* u, MatXd* stm) {
return PropagateImpl(x0, t0, tf, u, &relativity, stm);
}
State ClockDynamics::PropagateImpl(const State& x0, Real t0, Real tf, const State* u,
const ClockRelativityContext* relativity, MatXd* stm) {
LUPNT_CHECK(u == nullptr, "ClockDynamics does not support control inputs", "ClockDynamics");
MatXd Phi_clk, Q_clk;
VecX noise(x0.size());
if (x0.size() == 2) {
Phi_clk = TwoStatePhi(tf - t0).cast<double>();
if (model_ != ClockModel::UNDEFINED && add_noise_) {
Q_clk = TwoStateNoise(model_, tf - t0, bias_unit_).cast<double>();
noise = SampleMvNormal(Vec2d::Zero(), Q_clk, 1, rng_.get()).transpose();
}
} else if (x0.size() == 3) {
Phi_clk = ThreeStatePhi(tf - t0).cast<double>();
if (model_ != ClockModel::UNDEFINED && add_noise_) {
Q_clk = ThreeStateNoise(model_, tf - t0, bias_unit_).cast<double>();
noise = SampleMvNormal(Vec3d::Zero(), Q_clk, 1, rng_.get()).transpose();
}
} else {
LUPNT_CHECK(false, fmt::format("Invalid clock state size {}", x0.size()), "ClockDynamics");
}
VecX xf = Phi_clk * x0;
if (relativity != nullptr) {
Real rate0 = RelativisticRateCorrection(relativity->rv0_centered.head<3>(),
relativity->rv0_centered.tail<3>(), relativity->GM,
relativity->c, relativity->reference_rate_offset);
Real ratef = RelativisticRateCorrection(relativity->rvf_centered.head<3>(),
relativity->rvf_centered.tail<3>(), relativity->GM,
relativity->c, relativity->reference_rate_offset);
Real dt = tf - t0;
Real bias_rel_s = 0.5 * (rate0 + ratef) * dt;
xf(0) += SecondsToBiasUnits(bias_rel_s, bias_unit_);
}
if (model_ != ClockModel::UNDEFINED && add_noise_) xf += noise;
State x_state(x0, xf);
x_state.SetUnits(GetStateUnits(x0.size(), bias_unit_));
if (stm != nullptr) *stm = Phi_clk.cast<double>();
return x_state;
}
REGISTER_FACTORY_CLASS(ClockDynamics, ClockDynamics)
// Define the GetRegistry function for this specialization (must come before explicit
// instantiation)
template <> std::unordered_map<std::string, AssetFactory<ClockDynamics, Config&>::Creator>&
AssetFactory<ClockDynamics, Config&>::GetRegistry() {
return Registry();
}
// Explicit template instantiation to ensure single registry across library boundaries
template class AssetFactory<ClockDynamics, Config&>;
} // namespace lupnt