Skip to content

Tidal Corrections

Earth's oceans and solid body are elastically deformed by the gravitational attraction of the Sun and Moon. This deformation causes time-varying changes to the geopotential, producing small but measurable accelerations on low-Earth orbit satellites. For a 500 km LEO satellite propagated over one orbital period, solid Earth tides shift the position by roughly 1–2 m relative to a tide-free model.

Brahe implements the solid Earth tide, ocean tide, and pole tide models from Chapter 6 of the IERS Conventions (2010), Technical Note 36 (TN36): https://iers-conventions.obspm.fr/content/chapter6/icc6.pdf

Validation Help Desired

Tidal corrections have been implemented based on a best-effort reading of the IERS conventions. However, it is generally a complex model with multiple steps and assumptions, so any experts who could review the implementation and confirm its correctness would be greatly appreciated.

The Three Tide Systems

Geopotential models are published in one of three tide systems, which differ in how the permanent (zero-frequency) part of the tidal potential is handled. This distinction affects only the degree-2 zonal coefficient \(\bar{C}_{20}\).

System \(\bar{C}_{20}\) contains Notes
Mean-tide direct + indirect permanent terms Physically consistent with the mean sea surface; rare for satellite models
Zero-tide indirect term only (elastic deformation) Used by some older models (e.g. EGM96 S11)
Tide-free neither term Conventional choice; used by EGM2008, GGM05S, JGM-3

The direct term is the permanent part of the tide-generating potential itself; the indirect term is Earth's permanent elastic deformation in response to it, given by IERS Eq. 6.14 with the nominal degree-2 Love number \(k_{20} = 0.30190\) (IERS Table 6.3):

\[ \delta\bar{C}_{20}^{\text{direct}} = A_0 H_0 = 4.4228 \times 10^{-8} \times (-0.31460) \approx -1.391 \times 10^{-8} \]
\[ \delta\bar{C}_{20}^{\text{indirect}} = A_0 H_0 k_{20} \approx -4.201 \times 10^{-9} \]

The solid-tide acceleration model (§6.2.1) assumes a conventional tide-free background. When using a model whose \(\bar{C}_{20}\) is in a different system, the permanent term must be removed before adding the time-varying solid-tide corrections, so the two contributions are not double-counted.

Design: Normalize to Tide-Free, Then Layer

Brahe's tide handling follows one mental model: when tides are configured (ForceModelConfig.tides is set), the static gravity field is by default normalized to conventional tide-free — a field with no tidal contributions in \(\bar{C}_{20}\) — so that tidal effects can be explicitly layered back on top. (With no TidesConfiguration at all, the model's \(\bar{C}_{20}\) is left exactly as published.) The solid Earth tide model computes the total tidal contribution, including its time average (the permanent part), so a tide-free background is exactly what it composes with; stacking it on a zero-tide or mean-tide field would count the permanent part twice.

This layering is why Auto always targets tide-free, regardless of which tidal force models are enabled. If you do not want the normalization — for example, you propagate without solid tides and prefer to keep a zero-tide model's \(\bar{C}_{20}\) as the better time-average field — set the permanent handling to Off, or use ConvertTo to select a specific system. Both are valid even when no tidal force model is enabled.

Permanent Tide Configuration

PermanentTideConfig controls how Brahe reconciles the loaded model's \(\bar{C}_{20}\) with the conventional tide-free convention:

Variant Behavior
Auto (default) Reads the tide-system flag stored in the model file and converts \(\bar{C}_{20}\) to conventional tide-free automatically. If the flag is Unknown, no conversion is applied (a warning is emitted).
ConvertTo(system) Forces the model into the specified tide system. Errors at propagator construction if the stored flag is Unknown.
Off Leaves \(\bar{C}_{20}\) untouched. Use when you have pre-corrected the model yourself, or for debugging.

Auto is the right choice for almost all practical cases.

Inconsistent combination: ConvertTo(ZeroTide/MeanTide) + solid tides

Converting the background field to zero-tide or mean-tide while solid Earth tides are enabled double-counts the permanent tide (once in the static \(\bar{C}_{20}\), once in the tide model's time average). Brahe emits a warning for this combination — at propagator construction in Rust, and as a suppressible UserWarning when constructing TidesConfiguration in Python. For gravity models owned by the propagator (GravityModelSource.ModelType) the conversion is still applied as requested, since Step-3-style workflows that pre-subtract the permanent part externally are legitimate.

Shared global gravity models (GravityModelSource.Global) are read-only across every propagator that references them, so their permanent-tide handling is resolved once, when the model is installed as the global — not per propagator. The propagator trusts the global model's tide system as-is and never mutates shared state, so the PermanentTideConfig in a Global-source force model has no effect. Set the tide system on the global model up front, either with the convenience setter set_global_gravity_model_to_tide_system(model, target) or by calling GravityModel.convert_tide_system before set_global_gravity_model:

import brahe as bh

# GGM05S is published in the zero-tide system. Convert it to conventional
# tide-free and install it as the global gravity model in one call.
model = bh.GravityModel.from_model_type(bh.GravityModelType.GGM05S)
assert model.tide_system == bh.GravityModelTideSystem.ZeroTide

bh.set_global_gravity_model_to_tide_system(model, bh.GravityModelTideSystem.TideFree)

assert bh.get_global_gravity_model().tide_system == bh.GravityModelTideSystem.TideFree
use brahe::gravity::{
    GravityModel, GravityModelTideSystem, GravityModelType, get_global_gravity_model,
    set_global_gravity_model_to_tide_system,
};

fn main() {
    // GGM05S is published in the zero-tide system. Convert it to conventional
    // tide-free and install it as the global gravity model in one call.
    let model = GravityModel::from_model_type(&GravityModelType::GGM05S).unwrap();
    assert_eq!(model.tide_system, GravityModelTideSystem::ZeroTide);

    set_global_gravity_model_to_tide_system(model, GravityModelTideSystem::TideFree).unwrap();

    assert_eq!(
        get_global_gravity_model().tide_system,
        GravityModelTideSystem::TideFree
    );
}

Solid Earth Tides

The solid Earth tide model adds time-varying corrections \(\Delta\bar{C}_{nm}\) and \(\Delta\bar{S}_{nm}\) to the static geopotential coefficients at each integration step.

Static Correction (Always On)

When solid tides are enabled, the static correction is always computed. For each tide-raising body (Moon and Sun) at ECEF position \(\mathbf{r}_j\) with distance \(r_j\), geocentric latitude \(\phi_j\), and longitude \(\lambda_j\):

\[ \Delta\bar{C}_{nm} - i\,\Delta\bar{S}_{nm} = \frac{k_{nm}}{2n+1} \sum_j \frac{GM_j}{GM_\oplus} \left(\frac{R_\oplus}{r_j}\right)^{n+1} \bar{P}_{nm}(\sin\phi_j)\, e^{-im\lambda_j} \]

(IERS Eq. 6.6, degrees \(n = 2, 3\))

The complex Love numbers \(k_{nm} = k^{\rm re}_{nm} + i\,k^{\rm im}_{nm}\) are taken from IERS Table 6.3 (anelastic values). The imaginary parts are non-zero only for the degree-2 terms and produce small out-of-phase corrections.

Degree-2 tides also feed back into degree-4 via coupling Love numbers \(k_m^+\) (IERS Eq. 6.7, \(m = 0, 1, 2\)):

\[ \Delta\bar{C}_{4m} - i\,\Delta\bar{S}_{4m} = \frac{k_m^+}{5} \sum_j \frac{GM_j}{GM_\oplus} \left(\frac{R_\oplus}{r_j}\right)^{3} \bar{P}_{2m}(\sin\phi_j)\, e^{-im\lambda_j} \]

The resulting \(\Delta\bar{C}_{nm}/\Delta\bar{S}_{nm}\) corrections (up to degree 4) are folded into the static gravity field's packed coefficient table and evaluated together with it in a single Clenshaw pass per dynamics call (see the fold-in evaluation described below), rather than as a separate acceleration term.

Time-Varying Correction (Optional)

Setting frequency_dependent=True on SolidTideConfig activates the IERS time-varying correction tables (Tables 6.5a/b/c). These corrections account for the frequency dependence of the anelastic Love numbers near tidal resonances, primarily affecting the degree-2 coefficients:

Quantity Equation
\(\Delta\bar{C}_{20}\) Eq. 6.8a (real part, \(m=0\), 21 lines)
\(\Delta\bar{C}_{21}, \Delta\bar{S}_{21}\) Eq. 6.8b (\(m=1\), 48 lines)
\(\Delta\bar{C}_{22}, \Delta\bar{S}_{22}\) Eq. 6.8c (\(m=2\), 2 lines)

Each correction line is computed from Doodson/Delaunay arguments \(\theta_f = m(\theta_G + \pi) - (n_l l + n_{l'} l' + n_F F + n_D D + n_\Omega \Omega)\) (IERS §6.2.1), where \(\theta_G\) is GMST and \(l, l', F, D, \Omega\) are the five Delaunay fundamental arguments evaluated at the current TT epoch.

Time-varying corrections are at the \(10^{-10}\)\(10^{-11}\) level, contributing a sub-millimetre position effect per orbit for LEO satellites. They are recommended for precise orbit determination but can be omitted for most mission-analysis applications.

Solid Earth Pole Tide (Optional)

Setting pole_tide=True on SolidTideConfig adds the solid Earth pole tide (IERS TN36 §6.4), the elastic deformation caused by the centrifugal effect of polar motion. It contributes to \(\bar{C}_{21}\) and \(\bar{S}_{21}\) only:

\[ \Delta\bar{C}_{21} = -1.333\times10^{-9}\,(m_1 + 0.0115\,m_2), \qquad \Delta\bar{S}_{21} = -1.333\times10^{-9}\,(m_2 - 0.0115\,m_1) \]

The wobble parameters \((m_1, m_2)\) are the polar motion coordinates \((x_p, y_p)\) relative to the IERS secular pole \((\bar{x}_s, \bar{y}_s)\):

\[ m_1 = x_p - \bar{x}_s, \qquad m_2 = -(y_p - \bar{y}_s) \]

Brahe evaluates the secular pole with the updated linear model (IERS Conventions §7.1.4, Eq. 21, version 2018/02/01), which supersedes the cubic mean-pole model published in TN36 (2010) and is consistent with ITRF2014:

\[ \bar{x}_s = 55.0 + 1.677\,(t - 2000.0)\ \text{mas}, \qquad \bar{y}_s = 320.5 + 3.460\,(t - 2000.0)\ \text{mas} \]

with \(t\) in Julian years of TT since J2000.0.

The solid Earth pole tide requires initialized global EOP data (for \(x_p\), \(y_p\)); enabling pole_tide without EOP initialized returns an error at propagator construction.

Ocean Tides

Ocean tides redistribute mass as the tidal bulge moves through the world's oceans, producing a geopotential contribution distinct from, and generally smaller than, the solid Earth tide. Brahe implements the FES2004 ocean tide model (IERS TN36 §6.3):

\[ \Delta\bar{C}_{nm} = \sum_f \left[ (C_f^{+} + C_f^{-})\cos\theta_f + (S_f^{+} + S_f^{-})\sin\theta_f \right] \]
\[ \Delta\bar{S}_{nm} = \sum_f \left[ (S_f^{+} - S_f^{-})\cos\theta_f - (C_f^{+} - C_f^{-})\sin\theta_f \right] \]

(the real form of Eq. 6.15, summed over tidal constituents \(f\), exactly as implemented), where \(C_f^{\pm}, S_f^{\pm}\) are the prograde (+) and retrograde (−) fully normalized Stokes-coefficient amplitudes of constituent \(f\), and \(\theta_f\) is its Doodson/Delaunay tidal argument (§6.2.1) — the same argument construction used by the solid-tide time-varying correction.

Coefficient Download

FES2004 coefficients are not bundled with Brahe. The first time a propagator is constructed with ocean tides enabled, Brahe downloads the IERS coefficient file (fes2004_Cnm-Snm.dat, ~3.7 MB) into $BRAHE_CACHE/tides/ and reuses the cached copy on every subsequent call; no network access is needed once the file is cached. Degree-1 rows present in the file are skipped: they represent geocenter motion, not geopotential coefficients about the Earth's center of mass.

Degree, Order, and Admittance

OceanTideConfig.degree / .order truncate the expansion (2–100, default 20). The FES2004 file tabulates 18 main tidal constituents directly; setting include_admittance=True (the default) completes the model with the ~63 secondary constituents of Table 6.7, obtained by linear admittance interpolation between neighboring main waves (Eq. 6.16). The admittance-wave coefficients are constant linear combinations of the main-wave coefficients, computed once when the model is loaded.

Ocean Pole Tide

Setting pole_tide=True on OceanTideConfig adds the dominant \((2,1)\) term of the ocean's self-consistent equilibrium response to polar motion (IERS TN36 §6.5, Eq. 6.24), which captures roughly 90% of the ocean pole tide potential variance:

\[ \Delta\bar{C}_{21} = -2.1778\times10^{-10}\,(m_1 - 0.01724\,m_2), \qquad \Delta\bar{S}_{21} = -1.7232\times10^{-10}\,(m_2 - 0.03365\,m_1) \]

using the same wobble parameters \((m_1, m_2)\) and secular pole as the solid Earth pole tide above. Like the solid pole tide, it requires initialized global EOP data.

Configuring Tides

Loading configurations saved before ocean tides

TidesConfiguration gained an ocean field when ocean tides were added. Configurations serialized before this change deserialize without error, with ocean defaulting to None — ocean tides stay disabled unless the field is set explicitly.

Sun and Moon Ephemeris Source

The tidal corrections are driven by the Sun and Moon positions. The ephemeris_source field of TidesConfiguration selects how those positions are computed and defaults to EphemerisSource.LowPrecision (the analytic geocentric ephemerides), which is accurate enough for the ~\(10^{-7}\) m/s² tidal perturbation. Set it to a high-precision source (EphemerisSource.DE440s/DE440) to match a third-body perturbation configured against the same source — when the sources match, the propagator evaluates the Sun and Moon positions once per epoch and shares them between the tidal and third-body force terms. ForceModelConfig.high_fidelity() sets ephemeris_source=EphemerisSource.DE440s for exactly this consistency with its third-body configuration.

A configuration serialized before this field existed fails to deserialize; add an explicit ephemeris_source key (e.g. LowPrecision) when loading older serialized configurations.

Permanent Tide Only

Normalizes the static field to conventional tide-free without adding any time-varying tidal accelerations. Use this when you want a field with no tidal contributions (e.g. for consistency across models, or before layering your own tide model). If you instead want to keep a zero-tide model's \(\bar{C}_{20}\) as-is, use PermanentTideConfig.OFF.

import brahe as bh

# Permanent-tide correction only: normalize the loaded model's C̄20 to the
# conventional tide-free system, but add no time-varying solid-tide accelerations.
tides = bh.TidesConfiguration(
    permanent=bh.PermanentTideConfig.AUTO,
    solid=None,
)

force_config = bh.ForceModelConfig.earth_gravity()
force_config.tides = tides

# `force_config` is now ready to hand to a NumericalOrbitPropagator.
assert force_config.tides is not None
use brahe as bh;

fn main() {
    // Permanent-tide correction only: normalize the loaded model's C̄20 to the
    // conventional tide-free system, but add no time-varying solid-tide accelerations.
    let tides = bh::TidesConfiguration {
        ephemeris_source: bh::EphemerisSource::LowPrecision,
        permanent: bh::PermanentTideConfig::Auto,
        solid: None,
        ocean: None,
    };

    let mut force_config = bh::ForceModelConfig::earth_gravity();
    force_config.tides = Some(tides);

    // `force_config` is now ready to hand to a NumericalOrbitPropagator.
    assert!(force_config.tides.is_some());
}

Permanent + Static Solid Earth Tide

import brahe as bh

# Permanent tide + the static (frequency-independent) solid Earth tide correction.
# frequency_dependent=False keeps only the always-on static part.
tides = bh.TidesConfiguration(
    permanent=bh.PermanentTideConfig.AUTO,
    solid=bh.SolidTideConfig(frequency_dependent=False),
)

force_config = bh.ForceModelConfig.earth_gravity()
force_config.tides = tides

# `force_config` is now ready to hand to a NumericalOrbitPropagator.
assert force_config.tides is not None
use brahe as bh;

fn main() {
    // Permanent tide + the static (frequency-independent) solid Earth tide correction.
    // `frequency_dependent: false` keeps only the always-on static part.
    let tides = bh::TidesConfiguration {
        ephemeris_source: bh::EphemerisSource::LowPrecision,
        permanent: bh::PermanentTideConfig::Auto,
        solid: Some(bh::SolidTideConfig {
            frequency_dependent: false,
            pole_tide: false,
        }),
        ocean: None,
    };

    let mut force_config = bh::ForceModelConfig::earth_gravity();
    force_config.tides = Some(tides);

    // `force_config` is now ready to hand to a NumericalOrbitPropagator.
    assert!(force_config.tides.is_some());
}

Permanent + Static + Time-Varying Solid Earth Tide

import brahe as bh

# Permanent tide + both the static and time-varying (frequency-dependent) parts
# of the solid Earth tide correction. frequency_dependent=True adds the
# tidal-line refinements (IERS Tables 6.5a/b/c) on top of the static correction.
tides = bh.TidesConfiguration(
    permanent=bh.PermanentTideConfig.AUTO,
    solid=bh.SolidTideConfig(frequency_dependent=True),
)

force_config = bh.ForceModelConfig.earth_gravity()
force_config.tides = tides

# `force_config` is now ready to hand to a NumericalOrbitPropagator.
assert force_config.tides is not None
use brahe as bh;

fn main() {
    // Permanent tide + both the static and time-varying (frequency-dependent) parts
    // of the solid Earth tide correction. `frequency_dependent: true` adds the
    // tidal-line refinements (IERS Tables 6.5a/b/c) on top of the static correction.
    let tides = bh::TidesConfiguration {
        ephemeris_source: bh::EphemerisSource::LowPrecision,
        permanent: bh::PermanentTideConfig::Auto,
        solid: Some(bh::SolidTideConfig {
            frequency_dependent: true,
            pole_tide: false,
        }),
        ocean: None,
    };

    let mut force_config = bh::ForceModelConfig::earth_gravity();
    force_config.tides = Some(tides);

    // `force_config` is now ready to hand to a NumericalOrbitPropagator.
    assert!(force_config.tides.is_some());
}

Full Tide Model

Enables every tidal correction Brahe supports: static and time-varying solid Earth tides, the solid Earth pole tide, and FES2004 ocean tides (with admittance and the ocean pole tide) to degree/order 30. This is the same tide configuration used internally by ForceModelConfig.high_fidelity(). Building the configuration does not touch the network; the FES2004 download (see Ocean Tides) happens once, the first time a propagator with ocean tides enabled is constructed.

import brahe as bh

# Solid Earth tides: static + time-varying (frequency-dependent) corrections,
# plus the solid Earth pole tide.
solid = bh.SolidTideConfig(frequency_dependent=True, pole_tide=True)

# Ocean tides: FES2004 to degree/order 30, admittance-completed, plus the
# ocean pole tide. Building this configuration does not touch the network;
# the FES2004 coefficient file (~3.7 MB) is downloaded once into
# `$BRAHE_CACHE/tides/` the first time a propagator with ocean tides enabled
# is constructed.
ocean = bh.OceanTideConfig(degree=30, order=30, include_admittance=True, pole_tide=True)

tides = bh.TidesConfiguration(
    permanent=bh.PermanentTideConfig.AUTO,
    solid=solid,
    ocean=ocean,
)

force_config = bh.ForceModelConfig.earth_gravity()
force_config.tides = tides

# `force_config` is now ready to hand to a NumericalOrbitPropagator.
assert force_config.tides is not None
assert force_config.tides.ocean is not None
use brahe as bh;

fn main() {
    // Solid Earth tides: static + time-varying (frequency-dependent)
    // corrections, plus the solid Earth pole tide.
    let solid = bh::SolidTideConfig {
        frequency_dependent: true,
        pole_tide: true,
    };

    // Ocean tides: FES2004 to degree/order 30, admittance-completed, plus the
    // ocean pole tide. Building this configuration does not touch the
    // network; the FES2004 coefficient file (~3.7 MB) is downloaded once
    // into `$BRAHE_CACHE/tides/` the first time a propagator with ocean
    // tides enabled is constructed.
    let ocean = bh::OceanTideConfig {
        degree: 30,
        order: 30,
        include_admittance: true,
        pole_tide: true,
    };

    let tides = bh::TidesConfiguration {
        ephemeris_source: bh::EphemerisSource::LowPrecision,
        permanent: bh::PermanentTideConfig::Auto,
        solid: Some(solid),
        ocean: Some(ocean),
    };

    let mut force_config = bh::ForceModelConfig::earth_gravity();
    force_config.tides = Some(tides);

    // `force_config` is now ready to hand to a NumericalOrbitPropagator.
    assert!(force_config.tides.is_some());
    assert!(force_config.tides.unwrap().ocean.is_some());
}

Worked Example

The example below propagates a 500 km LEO satellite for one full orbital period with tides enabled and disabled, then prints the peak position difference.

import math
import numpy as np
import brahe as bh

# EOP is required for the ITRF frame transformations used inside the tidal model.
bh.initialize_eop()
bh.initialize_sw()

epoch = bh.Epoch.from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, bh.TimeSystem.UTC)

# LEO satellite: 500 km altitude, slightly elliptical, sun-synchronous inclination.
oe = np.array([bh.R_EARTH + 500e3, 0.001, 97.8, 15.0, 30.0, 45.0])
state0 = bh.state_koe_to_eci(oe, bh.AngleFormat.DEGREES)


def make_propagator(tides_config):
    """Build a gravity+third-body propagator with the given tides config."""
    force_config = bh.ForceModelConfig.earth_gravity()
    # Add Sun+Moon third-body perturbations. Note: tidal accelerations use their
    # own internal low-precision ephemeris and do NOT depend on this setting.
    force_config.third_body = [bh.ThirdBody.SUN, bh.ThirdBody.MOON]
    force_config.tides = tides_config
    return bh.NumericalOrbitPropagator(
        epoch,
        state0.copy(),
        bh.NumericalPropagationConfig.default(),
        force_config,
    )


# Tides-ON configuration: IERS Step 1 + Step 2 (frequency-dependent) solid
# Earth tides plus the solid Earth pole tide, FES2004 ocean tides (30x30,
# admittance-completed) plus the ocean pole tide, and Auto permanent-tide
# handling (converts the model's C̄20 to conventional tide-free).
solid = bh.SolidTideConfig(frequency_dependent=True, pole_tide=True)
ocean = bh.OceanTideConfig(degree=30, order=30, include_admittance=True, pole_tide=True)
tides_on = bh.TidesConfiguration(
    permanent=bh.PermanentTideConfig.AUTO, solid=solid, ocean=ocean
)

prop_on = make_propagator(tides_on)
prop_off = make_propagator(None)

# Propagate one full orbital period, sampling every ~minute.
sma = bh.R_EARTH + 500e3
period = 2.0 * math.pi * math.sqrt(sma**3 / bh.GM_EARTH)
n_steps = 90
dt = period / n_steps

max_diff_m = 0.0
t = epoch
for _ in range(n_steps):
    t = t + dt
    prop_on.propagate_to(t)
    prop_off.propagate_to(t)

    pos_on = np.array(prop_on.current_state()[:3])
    pos_off = np.array(prop_off.current_state()[:3])
    diff = np.linalg.norm(pos_on - pos_off)
    if diff > max_diff_m:
        max_diff_m = diff

print("Tidal corrections example")
print(f"  Orbital period:               {period / 60.0:.1f} min")
print(
    f"  Max tidal position difference: {max_diff_m:.3f} m  ({max_diff_m / 1e3:.3f} km)"
)
assert 0.0 < max_diff_m < 1000.0, f"Unexpected tidal difference: {max_diff_m} m"
print("Example validated successfully!")
use bh::traits::DStatePropagator;
use brahe as bh;
use nalgebra as na;
use std::f64::consts::PI;

fn make_propagator(
    epoch: bh::Epoch,
    state: na::DVector<f64>,
    tides: Option<bh::TidesConfiguration>,
) -> bh::DNumericalOrbitPropagator {
    // Gravity-only: no drag/SRP so both propagators are directly comparable.
    let mut force_config = bh::ForceModelConfig::earth_gravity();
    // Add Sun+Moon third-body perturbations. Note: tidal accelerations use their
    // own internal low-precision ephemeris and do NOT depend on this setting.
    force_config.third_body = Some(vec![bh::ThirdBody::Sun.into(), bh::ThirdBody::Moon.into()]);
    force_config.tides = tides;

    bh::DNumericalOrbitPropagator::new(
        epoch,
        state,
        bh::NumericalPropagationConfig::default(),
        force_config,
        None,
        None,
        None,
        None,
    )
    .unwrap()
}

fn main() {
    // EOP is required for the ITRF frame transformations used inside the tidal model.
    bh::initialize_eop().unwrap();
    bh::initialize_sw().unwrap();

    let epoch = bh::Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, bh::TimeSystem::UTC);

    // LEO satellite: 500 km altitude, slightly elliptical, sun-synchronous inclination.
    let oe = na::SVector::<f64, 6>::new(
        bh::R_EARTH + 500e3, // semi-major axis (m)
        0.001,               // eccentricity
        97.8,                // inclination (deg)
        15.0,                // RAAN (deg)
        30.0,                // argument of perigee (deg)
        45.0,                // mean anomaly (deg)
    );
    let state0 = bh::state_koe_to_eci(oe, bh::AngleFormat::Degrees);
    let state_dv = na::DVector::from_column_slice(state0.as_slice());

    // Propagate one full orbital period with and without solid Earth tides.
    let sma = bh::R_EARTH + 500e3;
    let period = 2.0 * PI * (sma.powi(3) / bh::GM_EARTH).sqrt();
    let n_steps = 90; // check every ~minute
    let dt = period / n_steps as f64;

    // Tides-ON configuration: IERS Step 1 + Step 2 (frequency-dependent) solid
    // Earth tides plus the solid Earth pole tide, FES2004 ocean tides (30x30,
    // admittance-completed) plus the ocean pole tide, and Auto permanent-tide
    // handling (converts the model's C̄20 to conventional tide-free).
    let tides_on = bh::TidesConfiguration {
        ephemeris_source: bh::EphemerisSource::LowPrecision,
        permanent: bh::PermanentTideConfig::Auto,
        solid: Some(bh::SolidTideConfig {
            frequency_dependent: true,
            pole_tide: true,
        }),
        ocean: Some(bh::OceanTideConfig {
            degree: 30,
            order: 30,
            include_admittance: true,
            pole_tide: true,
        }),
    };

    let mut prop_on = make_propagator(epoch, state_dv.clone(), Some(tides_on));
    let mut prop_off = make_propagator(epoch, state_dv.clone(), None);

    let mut max_diff_m: f64 = 0.0;
    let mut t = epoch;
    for _ in 0..n_steps {
        t = t + dt;
        prop_on.propagate_to(t);
        prop_off.propagate_to(t);

        let pos_on = prop_on.current_state().fixed_rows::<3>(0).into_owned();
        let pos_off = prop_off.current_state().fixed_rows::<3>(0).into_owned();
        let diff = (pos_on - pos_off).norm();
        if diff > max_diff_m {
            max_diff_m = diff;
        }
    }

    println!("Tidal corrections example");
    println!("  Orbital period:               {:.1} min", period / 60.0);
    println!(
        "  Max tidal position difference: {:.3} m  ({:.3} km)",
        max_diff_m,
        max_diff_m / 1e3
    );
    assert!(
        max_diff_m > 0.0 && max_diff_m < 1000.0,
        "Unexpected tidal difference: {max_diff_m} m"
    );
    println!("Example validated successfully!");
}
Output


Evaluation Cost

When gravity is configured as spherical harmonics, the propagator folds the static \(\bar{C}_{nm}/\bar{S}_{nm}\) coefficients together with every enabled tidal correction (solid Step 1/2, both pole tides, and the ocean tide) into a single packed coefficient table, evaluated with one Clenshaw pass per dynamics call. This fold-in is exact by linearity of the spherical-harmonic sum: the marginal cost of enabling tides is only the per-epoch \(\Delta\bar{C}_{nm}/\Delta\bar{S}_{nm}\) computation, not a second gravity evaluation. Point-mass and zonal gravity configurations still get a delta-only field, since there is no static spherical-harmonic table to fold into.

Ephemeris Source Notes

Solid-tide computation requires Sun and Moon positions in the ECEF frame. The propagator computes these internally using its own low-precision analytical ephemeris (sun_position / moon_position) — it does not read positions from the third_body force model entries. Enabling or disabling third_body entries has no effect on the tidal correction; you can use solid tides with or without a third-body configuration.

See Also

References

  • Petit, G., & Luzum, B. (Eds.) (2010). IERS Conventions (2010), Technical Note 36, Chapter 6: Geopotential. IERS. https://iers-conventions.obspm.fr/content/chapter6/icc6.pdf
  • Petit, G., & Luzum, B. (Eds.) (2010). IERS Conventions (2010), Technical Note 36, Chapter 1: General Definitions and Numerical Standards. IERS. (§1.1 tide-system definitions.) https://iers-conventions.obspm.fr/content/chapter1/icc1.pdf
  • Petit, G., & Luzum, B. (Eds.) (2010). IERS Conventions (2010), Technical Note 36, Chapter 7: Displacement of Reference Points (§7.1.4, secular pole; updated for the linear secular-pole model). IERS. https://iers-conventions.obspm.fr/content/chapter7/icc7.pdf
  • Lemoine, F. G., et al. (1998). The Development of the Joint NASA GSFC and the National Imagery and Mapping Agency (NIMA) Geopotential Model EGM96. NASA/TP-1998-206861. (§11.1: tide-system definitions, direct/indirect terminology, and permanent-tide conversion formulas.) https://cddis.nasa.gov/926/egm96/doc/S11.HTML
  • Lyard, F., Lefèvre, F., Letellier, T., & Francis, O. (2006). Modelling the global ocean tides: modern insights from FES2004. Ocean Dynamics, 56, 394–415. (FES2004 ocean tide model.)
  • Desai, S. D. (2002). Observing the pole tide with satellite altimetry. Journal of Geophysical Research, 107(C11), 3186. (Ocean pole tide equilibrium model.)