Repository: open-space-collective/open-space-toolkit-astrodynamics
Pull request #540: feat: update velocity computation ground strip
Author: login: vishwa2710
Created: 2025-04-16T05:48:49Z | Merged: 2025-04-16T19:14:46Z

- Previously the velocity was hardcoded to zero, which is incorrect. We now finite difference the velocity based on the provided instants
- Added a method to compute a trajectory that follows the geodetic nadir path of the provided orbit

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to generate a ground strip trajectory that follows the geodetic nadir of an orbit.
- **Bug Fixes**
  - Improved velocity calculations for ground strip trajectories to ensure accurate velocity estimation along the path.
- **Tests**
  - Introduced new tests for geodetic nadir ground strip trajectories.
  - Updated existing ground strip tests to improve validation of velocity and trajectory accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp b/bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp
index d42f1943e..7133ddba4 100644
--- a/bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp
+++ b/bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp
@@ -201,6 +201,24 @@ inline void OpenSpaceToolkitAstrodynamicsPy_Trajectory(pybind11::module& aModule
             arg("instants"),
             arg_v("celestial_object", Earth::WGS84(), "Earth.WGS84()")
         )
+        .def_static(
+            "ground_strip_geodetic_nadir",
+            &Trajectory::GroundStripGeodeticNadir,
+            R"doc(
+                Create a `Trajectory` object representing a ground strip that follows the geodetic nadir of the provided orbit.
+
+                Args:
+                    orbit (Orbit): The orbit.
+                    instants (list[Instant]): The instants.
+                    celestial_object (Celestial): The celestial object. Defaults to Earth.WGS84().
+
+                Returns:
+                    Trajectory: The `Trajectory` object representing the ground strip.  
+            )doc",
+            arg("orbit"),
+            arg("instants"),
+            arg_v("celestial_object", Earth::WGS84(), "Earth.WGS84()")
+        )
 
         ;
 
diff --git a/bindings/python/test/test_trajectory.py b/bindings/python/test/test_trajectory.py
index 67fa99acf..0a5c79bd4 100644
--- a/bindings/python/test/test_trajectory.py
+++ b/bindings/python/test/test_trajectory.py
@@ -9,9 +9,12 @@
 from ostk.physics.time import Instant
 from ostk.physics.time import Duration
 from ostk.physics.unit import Derived
+from ostk.physics.unit import Length
+from ostk.physics.unit import Angle
 
 from ostk.astrodynamics import Trajectory
 from ostk.astrodynamics.trajectory import State
+from ostk.astrodynamics.trajectory import Orbit
 
 
 @pytest.fixture
@@ -61,6 +64,16 @@ def states(trajectory: Trajectory) -> list[State]:
     )
 
 
+@pytest.fixture
+def orbit() -> Orbit:
+    return Orbit.circular(
+        epoch=Instant.J2000(),
+        altitude=Length.meters(545000.0),
+        inclination=Angle.degrees(0.0),
+        celestial_object=Earth.WGS84(),
+    )
+
+
 class TestTrajectory:
     def test_trajectory(self, states: list[State]):
         assert Trajectory(states) is not None
@@ -116,3 +129,17 @@ def test_ground_strip(
 
         assert Trajectory.ground_strip(start_lla, end_lla, instants, earth) is not None
         assert Trajectory.ground_strip(start_lla, end_lla, instants) is not None
+
+    def test_ground_strip_geodetic_nadir(
+        self, orbit: Orbit, instants: list[Instant], earth: Earth
+    ):
+        assert (
+            Trajectory.ground_strip_geodetic_nadir(
+                orbit=orbit, instants=instants, celestial_object=earth
+            )
+            is not None
+        )
+        assert (
+            Trajectory.ground_strip_geodetic_nadir(orbit=orbit, instants=instants)
+            is not None
+        )
diff --git a/include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp b/include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
index 26a8609a0..95c5c1fbd 100644
--- a/include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
+++ b/include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
@@ -32,6 +32,7 @@ using ostk::core::type::String;
 using ostk::core::type::Unique;
 
 using ostk::physics::coordinate::spherical::LLA;
+using ostk::physics::coordinate::Velocity;
 using ostk::physics::environment::object::Celestial;
 using ostk::physics::environment::object::celestial::Earth;
 using ostk::physics::time::Duration;
@@ -42,6 +43,11 @@ using ostk::physics::unit::Derived;
 using ostk::astrodynamics::trajectory::Model;
 using ostk::astrodynamics::trajectory::State;
 
+namespace trajectory
+{
+class Orbit;
+}
+
 /// @brief Path followed by an object through space as a function of time
 ///
 /// @ref                        https://en.wikipedia.org/wiki/Trajectory
@@ -229,10 +235,41 @@ class Trajectory
         const Celestial& aCelestial = Earth::WGS84()
     );
 
+    /// @brief Constructs a trajectory representing a ground strip that follows the geodetic nadir of the provided orbit
+    ///
+    /// @code{.cpp}
+    ///             Instant startInstant = Instant::DateTime(DateTime::Parse("2020-01-01 00:00:00"), Scale::UTC);
+    ///             Earth earth = Earth::WGS84();
+    ///             Trajectory trajectory = Trajectory::GroundStripGeodeticNadir(anOrbit, anInstantArray, earth);
+    /// @endcode
+    ///
+    /// @param anOrbit An orbit
+    /// @param anInstantArray An array of instants
+    /// @param aCelestial Celestial body
+    /// @return Trajectory
+    static Trajectory GroundStripGeodeticNadir(
+        const trajectory::Orbit& anOrbit,
+        const Array<Instant>& anInstantArray,
+        const Celestial& aCelestial = Earth::WGS84()
+    );
+
    private:
     Unique<Model> modelUPtr_;
 
     Trajectory();
+
+    /// @brief Compute states using the provided position generator, and finite differenced velocities, at the provided
+    /// instants
+    ///
+    /// @param aPositionGenerator A position generator
+    /// @param anInstantArray An array of instants
+    /// @param aStepSize A step size
+    /// @return Array of states
+    static Array<State> computeStates(
+        const std::function<physics::coordinate::Position(const Instant&)>& aPositionGenerator,
+        const Array<Instant>& anInstantArray,
+        const Duration& aStepSize = Duration::Seconds(1e-6)
+    );
 };
 
 }  // namespace astrodynamics
diff --git a/src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp b/src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
index 10656e89a..73296181e 100644
--- a/src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
+++ b/src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
@@ -1,26 +1,36 @@
 /// Apache License 2.0
 
 #include <OpenSpaceToolkit/Core/Error.hpp>
+#include <OpenSpaceToolkit/Core/Type/Shared.hpp>
 #include <OpenSpaceToolkit/Core/Utility.hpp>
 
+#include <OpenSpaceToolkit/Mathematics/Object/Vector.hpp>
+
 #include <OpenSpaceToolkit/Physics/Coordinate/Velocity.hpp>
 #include <OpenSpaceToolkit/Physics/Time/Duration.hpp>
 
 #include <OpenSpaceToolkit/Astrodynamics/Trajectory.hpp>
 #include <OpenSpaceToolkit/Astrodynamics/Trajectory/Model/Static.hpp>
 #include <OpenSpaceToolkit/Astrodynamics/Trajectory/Model/Tabulated.hpp>
+#include <OpenSpaceToolkit/Astrodynamics/Trajectory/Orbit.hpp>
 
 namespace ostk
 {
 namespace astrodynamics
 {
 
+using ostk::core::type::Shared;
+using ostk::core::type::Size;
+
+using ostk::mathematics::object::Vector3d;
+
 using ostk::physics::coordinate::Frame;
 using ostk::physics::coordinate::Velocity;
 using ostk::physics::time::Duration;
 using ostk::physics::unit::Length;
 
 using ostk::astrodynamics::trajectory::model::Tabulated;
+using ostk::astrodynamics::trajectory::Orbit;
 
 Trajectory::Trajectory(const Model& aModel)
     : modelUPtr_(aModel.clone())
@@ -209,30 +219,61 @@ Trajectory Trajectory::GroundStrip(
         throw ostk::core::error::RuntimeError("LLA altitude must be zero.");
     }
 
-    const Velocity velocity = Velocity::MetersPerSecond({0.0, 0.0, 0.0}, aCelestial.accessFrame());
-
-    Array<State> states = Array<State>::Empty();
-
     const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
 
-    for (const auto& instant : anInstantArray)
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
+
+    const auto positionGenerator = [&aStartLLA,
+                                    &anEndLLA,
+                                    &celestialSPtr,
+                                    startInstant = anInstantArray.accessFirst(),
+                                    duration = duration](const Instant& anInstant) -> physics::coordinate::Position
     {
-        const Real ratio = (instant - anInstantArray.accessFirst()).inSeconds() / duration.inSeconds();
+        const Real ratio = (anInstant - startInstant).inSeconds() / duration.inSeconds();
 
         const LLA intermediateLLA = aStartLLA.calculateIntermediateTo(
-            anEndLLA, ratio, aCelestial.getEquatorialRadius(), aCelestial.getFlattening()
+            anEndLLA, ratio, celestialSPtr->getEquatorialRadius(), celestialSPtr->getFlattening()
         );
 
-        const physics::coordinate::Position position = physics::coordinate::Position::Meters(
-            intermediateLLA.toCartesian(aCelestial.getEquatorialRadius(), aCelestial.getFlattening()),
-            aCelestial.accessFrame()
-        );
+        return physics::coordinate::Position::FromLLA(intermediateLLA, celestialSPtr).inFrame(Frame::GCRF(), anInstant);
+    };
+
+    const Array<State> states = computeStates(positionGenerator, anInstantArray);
+
+    return Trajectory(states);
+}
 
-        const State state = State(instant, position, velocity);
+Trajectory Trajectory::GroundStripGeodeticNadir(
+    const trajectory::Orbit& anOrbit, const Array<Instant>& anInstantArray, const Celestial& aCelestial
+)
+{
+    if (!anOrbit.isDefined())
+    {
+        throw ostk::core::error::runtime::Undefined("Orbit");
+    }
 
-        states.add(state);
+    if (anInstantArray.getSize() < 2)
+    {
+        throw ostk::core::error::RuntimeError("Atleast 2 instants must be provided.");
     }
 
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
+
+    const auto positionGenerator = [&anOrbit, &celestialSPtr](const Instant& anInstant) -> physics::coordinate::Position
+    {
+        const State state = anOrbit.getStateAt(anInstant);
+
+        const LLA lla =
+            LLA::FromPosition(state.getPosition().inFrame(Frame::ITRF(), anInstant), celestialSPtr).onSurface();
+
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(lla, celestialSPtr).inFrame(Frame::GCRF(), anInstant);
+
+        return position;
+    };
+
+    const Array<State> states = computeStates(positionGenerator, anInstantArray);
+
     return Trajectory(states);
 }
 
@@ -241,5 +282,57 @@ Trajectory::Trajectory()
 {
 }
 
+Array<State> Trajectory::computeStates(
+    const std::function<physics::coordinate::Position(const Instant&)>& aPositionFunction,
+    const Array<Instant>& anInstantArray,
+    const Duration& aStepSize
+)
+{
+    Array<State> states = Array<State>::Empty();
+    states.reserve(anInstantArray.getSize());
+
+    Vector3d velocityCoordinates = Vector3d::Zero();
+    for (Size i = 0; i < anInstantArray.getSize(); i++)
+    {
+        physics::coordinate::Position currentPosition = aPositionFunction(anInstantArray.at(i));
+        const Instant currentInstant = anInstantArray.at(i);
+
+        if (i == 0)
+        {
+            const Instant nextInstant = currentInstant + aStepSize;
+            const physics::coordinate::Position nextPosition = aPositionFunction(nextInstant);
+
+            velocityCoordinates =
+                (nextPosition.getCoordinates() - currentPosition.getCoordinates()) / aStepSize.inSeconds();
+        }
+
+        else if (i == anInstantArray.getSize() - 1)
+        {
+            const Instant previousInstant = currentInstant - aStepSize;
+            const physics::coordinate::Position previousPosition = aPositionFunction(previousInstant);
+
+            velocityCoordinates =
+                (currentPosition.getCoordinates() - previousPosition.getCoordinates()) / aStepSize.inSeconds();
+        }
+
+        else
+        {
+            const Instant nextInstant = currentInstant + aStepSize;
+            const Instant previousInstant = currentInstant - aStepSize;
+            const physics::coordinate::Position nextPosition = aPositionFunction(nextInstant);
+            const physics::coordinate::Position previousPosition = aPositionFunction(previousInstant);
+
+            velocityCoordinates =
+                (nextPosition.getCoordinates() - previousPosition.getCoordinates()) / (2.0 * aStepSize.inSeconds());
+        }
+
+        const Velocity currentVelocity = Velocity::MetersPerSecond(velocityCoordinates, currentPosition.accessFrame());
+
+        states.add(State(currentInstant, currentPosition, currentVelocity));
+    }
+
+    return states;
+}
+
 }  // namespace astrodynamics
 }  // namespace ostk
diff --git a/test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp b/test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp
index 47a50b2fa..7f206a74d 100644
--- a/test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp
+++ b/test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp
@@ -1,9 +1,18 @@
 /// Apache License 2.0
 
 #include <OpenSpaceToolkit/Physics/Coordinate/Frame.hpp>
+#include <OpenSpaceToolkit/Physics/Coordinate/Spherical/LLA.hpp>
+#include <OpenSpaceToolkit/Physics/Data/Direction.hpp>
+#include <OpenSpaceToolkit/Physics/Data/Provider/Nadir.hpp>
+#include <OpenSpaceToolkit/Physics/Environment/Object/Celestial.hpp>
+#include <OpenSpaceToolkit/Physics/Environment/Object/Celestial/Earth.hpp>
+#include <OpenSpaceToolkit/Physics/Unit/Derived.hpp>
+#include <OpenSpaceToolkit/Physics/Unit/Derived/Angle.hpp>
+#include <OpenSpaceToolkit/Physics/Unit/Length.hpp>
 
 #include <OpenSpaceToolkit/Astrodynamics/Trajectory.hpp>
 #include <OpenSpaceToolkit/Astrodynamics/Trajectory/Model/Tabulated.hpp>
+#include <OpenSpaceToolkit/Astrodynamics/Trajectory/Orbit.hpp>
 
 #include <Global.test.hpp>
 
@@ -11,20 +20,27 @@ using ostk::core::container::Array;
 using ostk::core::type::Real;
 using ostk::core::type::Shared;
 
+using ostk::mathematics::object::Vector3d;
+
 using ostk::physics::coordinate::Frame;
 using ostk::physics::coordinate::Position;
 using ostk::physics::coordinate::spherical::LLA;
 using ostk::physics::coordinate::Velocity;
+using ostk::physics::data::Direction;
+using ostk::physics::data::provider::Nadir;
+using ostk::physics::Environment;
 using ostk::physics::environment::object::celestial::Earth;
 using ostk::physics::time::DateTime;
 using ostk::physics::time::Duration;
 using ostk::physics::time::Instant;
 using ostk::physics::time::Scale;
+using ostk::physics::unit::Angle;
 using ostk::physics::unit::Derived;
 using ostk::physics::unit::Length;
 
 using ostk::astrodynamics::Trajectory;
 using ostk::astrodynamics::trajectory::model::Tabulated;
+using ostk::astrodynamics::trajectory::Orbit;
 using ostk::astrodynamics::trajectory::State;
 
 TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, Constructor)
@@ -487,7 +503,7 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
 {
     const Earth earth = Earth::WGS84();
     const LLA startLLA = LLA::Vector({0.0, 0.0, 0.0});
-    const LLA endLLA = LLA::Vector({1.0, 0.0, 0.0});
+    const LLA endLLA = LLA::Vector({0.0, 1.0, 0.0});
 
     {
         const Array<Instant> instants = {Instant::J2000(), Instant::J2000() + Duration::Seconds(10.0)};
@@ -541,34 +557,6 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
                 EXPECT_TRUE(trajectory.isDefined());
             }
         }
-
-        {
-            const Trajectory trajectory = Trajectory::GroundStrip(startLLA, endLLA, instants, earth);
-
-            {
-                const State state = trajectory.getStateAt(instants.accessFirst()).inFrame(Frame::ITRF());
-
-                EXPECT_TRUE(state.getVelocity().getCoordinates().isNear({0.0, 0.0, 0.0}, 1e-12));
-
-                EXPECT_TRUE(LLA::Cartesian(
-                                state.getPosition().getCoordinates(), earth.getEquatorialRadius(), earth.getFlattening()
-                )
-                                .toVector()
-                                .isNear(startLLA.toVector(), 1e-12));
-            }
-
-            {
-                const State state = trajectory.getStateAt(instants.accessLast()).inFrame(Frame::ITRF());
-
-                EXPECT_TRUE(state.getVelocity().getCoordinates().isNear({0.0, 0.0, 0.0}, 1e-12));
-
-                EXPECT_TRUE(LLA::Cartesian(
-                                state.getPosition().getCoordinates(), earth.getEquatorialRadius(), earth.getFlattening()
-                )
-                                .toVector()
-                                .isNear(endLLA.toVector(), 1e-12));
-            }
-        }
     }
 
     {
@@ -625,6 +613,13 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
                     Trajectory::GroundStrip(startLLA, endLLA, groundSpeed, startInstant, earth, Duration::Seconds(1.0));
 
                 EXPECT_TRUE(trajectory.isDefined());
+
+                // Speed in ITRF frame should be approximately equal to ground speed
+                EXPECT_LT(
+                    trajectory.getStateAt(startInstant).inFrame(Frame::ITRF()).getVelocity().getCoordinates().norm() -
+                        groundSpeed.in(Derived::Unit::MeterPerSecond()),
+                    1e-6
+                );
             }
 
             {
@@ -647,8 +642,6 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
             {
                 const State state = trajectory.getStateAt(startInstant).inFrame(Frame::ITRF());
 
-                EXPECT_TRUE(state.getVelocity().getCoordinates().isNear({0.0, 0.0, 0.0}, 1e-13));
-
                 EXPECT_TRUE(LLA::Cartesian(
                                 state.getPosition().getCoordinates(), earth.getEquatorialRadius(), earth.getFlattening()
                 )
@@ -663,8 +656,6 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
                     Duration::Seconds(distance.inMeters() / groundSpeed.in(Derived::Unit::MeterPerSecond()));
                 const State state = trajectory.getStateAt(startInstant + duration).inFrame(Frame::ITRF());
 
-                EXPECT_TRUE(state.getVelocity().getCoordinates().isNear({0.0, 0.0, 0.0}, 1e-13));
-
                 EXPECT_TRUE(LLA::Cartesian(
                                 state.getPosition().getCoordinates(), earth.getEquatorialRadius(), earth.getFlattening()
                 )
@@ -674,3 +665,54 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
         }
     }
 }
+
+TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStripGeodeticNadir)
+{
+    const Array<Instant> instants = {Instant::J2000(), Instant::J2000() + Duration::Seconds(10.0)};
+    const Earth earth = Earth::WGS84();
+
+    const Environment environment = Environment::Default();
+
+    const Shared<Earth> earthSPtr = std::make_shared<Earth>(earth);
+
+    const Orbit orbit = Orbit::Circular(Instant::J2000(), Length::Meters(545000.0), Angle::Degrees(0.0), earthSPtr);
+
+    {
+        EXPECT_THROW(
+            Trajectory::GroundStripGeodeticNadir(Orbit::Undefined(), instants, earth), ostk::core::error::RuntimeError
+        );
+    }
+
+    {
+        EXPECT_THROW(
+            Trajectory::GroundStripGeodeticNadir(orbit, Array<Instant>::Empty(), earth), ostk::core::error::RuntimeError
+        );
+    }
+
+    {
+        const Trajectory trajectory = Trajectory::GroundStripGeodeticNadir(orbit, instants, earth);
+
+        EXPECT_TRUE(trajectory.isDefined());
+
+        const Instant instant = Instant::J2000();
+
+        const State trajectoryState = trajectory.getStateAt(instant);
+        const State orbitState = orbit.getStateAt(instant);
+
+        // check that the trajectory state is the same as the geodetic nadir coordinate of the orbit
+
+        const Vector3d orbitLLACoordinates =
+            Position::FromLLA(
+                LLA::FromPosition(orbitState.inFrame(Frame::ITRF()).getPosition(), earthSPtr).onSurface(), earthSPtr
+            )
+                .getCoordinates();
+        const Vector3d trajectoryLLACoordinates =
+            Position::FromLLA(
+                LLA::FromPosition(trajectoryState.inFrame(Frame::ITRF()).getPosition(), earthSPtr).onSurface(),
+                earthSPtr
+            )
+                .getCoordinates();
+
+        EXPECT_VECTORS_ALMOST_EQUAL(trajectoryLLACoordinates, orbitLLACoordinates, 1e-12);
+    }
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>bindings/python/test/test_trajectory.py (1)</summary><blockquote>

`67-75`: **New 'orbit' fixture**  
Building a circular orbit fixture is a clean approach for testing geodetic nadir functionality. The parameter choices (altitude, inclination, etc.) appear valid for typical LEO orbits.  
  
Consider adding a small docstring or clarifying comment explaining the rationale for this specific orbit setup (e.g., “equatorial LEO orbit at 545 km altitude”) to aid future maintainers.

</blockquote></details>
<details>
<summary>include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp (1)</summary><blockquote>

`238-254`: **New static method 'GroundStripGeodeticNadir'**  
This addition clearly states the method’s purpose (orbital ground strip in geodetic nadir) and includes parameter validations. Throws an error if the orbit is undefined or if fewer than two instants are provided. Well-aligned with the existing style.  
  
There is a minor spelling issue in the error message: “Atleast 2 instants must be provided.” Consider correcting it to “At least 2 instants must be provided.”

</blockquote></details>
<details>
<summary>src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp (1)</summary><blockquote>

`302-351`: **Finite-difference velocity calculation in 'computeVelocities'**  
This central difference scheme for interior points and forward/backward difference at boundaries is typical for numerical velocity estimation. It is straightforward and well-implemented. Be mindful that large time steps or high dynamics can reduce accuracy.  
  
Consider a more sophisticated interpolation or continuous approach in the future for higher accuracy or non-uniform time steps.

</blockquote></details>
<details>
<summary>test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp (2)</summary><blockquote>

`702-702`: **Fix typo in comment.**

There's a small typo in the comment.


```diff
-        // check that the trahectory state is the same as the geodetic nadir coordinate of the orbit
+        // check that the trajectory state is the same as the geodetic nadir coordinate of the orbit
```

---

`669-718`: **Consider adding velocity verification test.**

While the position is verified to match the geodetic nadir, there's no explicit verification of the velocity computation. Since improving velocity computation is a key objective of this PR, consider adding an assertion to verify that the velocity is computed correctly for the GroundStripGeodeticNadir method.


You could add something like:

```cpp
// Verify velocity is correctly computed - not zero and matches expected direction
const Vector3d velocity = trajectoryState.inFrame(Frame::ITRF()).getVelocity().getCoordinates();
EXPECT_GT(velocity.norm(), 0.0);

// Optionally, verify velocity direction is tangential to the ground track
// This would require calculating the expected direction based on the orbit
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3af4f20c33f0dd9b3846eab3793cd0cd93f789c9 and 30204a30630722d8720445e1849b1717271264ab.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp` (1 hunks)
* `bindings/python/test/test_trajectory.py` (3 hunks)
* `include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp` (3 hunks)
* `src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp` (4 hunks)
* `test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp (1)</summary><blockquote>

<details>
<summary>src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp (2)</summary>

* `GroundStripGeodeticNadir` (253-295)
* `GroundStripGeodeticNadir` (253-255)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Build Development Image / Build Development Image

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp (1)</summary>

`204-221`: **Expose 'ground_strip_geodetic_nadir' method in Python bindings successfully**  
This static method cleanly exposes the newly introduced C++ functionality, allowing Python users to construct a ground strip trajectory following an orbit’s geodetic nadir with the option of specifying a celestial body. The docstring is descriptive, and the argument defaults (Earth.WGS84) are well-defined.

</details>
<details>
<summary>bindings/python/test/test_trajectory.py (3)</summary>

`12-13`: **Imports for Length and Angle**  
Adding these imports for length and angle is consistent with the new orbit fixture and test usage, ensuring type definitions are available. No issues detected.

---

`17-17`: **Orbit import**  
Importing `Orbit` in this file is appropriate since the new test fixture and method rely on the `Orbit` class.

---

`133-145`: **Test coverage for 'ground_strip_geodetic_nadir'**  
This test verifies the newly introduced Python binding both with and without an explicitly provided celestial body. Coverage looks good, and it ensures the method returns a defined `Trajectory`.

</details>
<details>
<summary>include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp (3)</summary>

`35-35`: **Introducing 'Velocity' type usage**  
Including `using ostk::physics::coordinate::Velocity;` is consistent with computing velocity arrays. This ensures the type is directly accessible for subsequent definitions.

---

`46-49`: **Forward declaration of 'Orbit'**  
Introducing a forward declaration for the `trajectory::Orbit` class prevents unnecessary header dependencies, improving compilation times.

---

`261-268`: **Private static helper 'computeVelocities' prototype**  
Declaring a dedicated helper for numerically estimating velocities from position and time arrays is a neat design choice. This fosters code reuse and maintains separation of concerns.

</details>
<details>
<summary>src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp (5)</summary>

`4-4`: **Additional includes for 'Shared', 'Vector', and 'Orbit'**  
These includes are necessary for handling shared pointers (`Shared`) and mathematical vectors (`Vector3d`), as well as referencing the new `Orbit` type. All changes appear correct and are localized.  



Also applies to: 7-8, 15-15

---

`22-25`: **Namespace usage for 'Shared' and 'Vector3d'**  
Explicitly bringing these identifiers into scope is consistent with the updated code’s design. No issues spotted.

---

`32-32`: **Using 'ostk::astrodynamics::trajectory::Orbit'**  
This aligns with the forward-declared `Orbit` class in the header. Required for referencing `Orbit` in the new `GroundStripGeodeticNadir` method.

---

`221-251`: **Refactoring 'GroundStrip' to compute velocities**  
The revised approach to generate positions in GCRF and then compute velocities via the new diff-based method is logical and consistent. It eliminates previous assumptions of zero velocity at endpoints, ensuring each state is assigned a velocity.

---

`253-295`: **'GroundStripGeodeticNadir' implementation**  
Projects orbit states onto the celestial surface in geodetic coordinates, converts back to GCRF, and uses `computeVelocities`. This ensures the new geodetic approach is well integrated. Error checks (orbit undefined, fewer than 2 instants) align with best practices.

</details>
<details>
<summary>test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp (7)</summary>

`4-11`: **LGTM! Well-structured include directives.**

The added includes provide all the necessary components for working with geodetic coordinates, nadir data providers, celestial objects, and angle units, which are essential for the new geodetic nadir trajectory functionality.

---

`15-15`: **LGTM! Added Orbit trajectory include.**

Properly added the include for Orbit trajectory, which is required for the new GeodeticNadir functionality.

---

`23-43`: **LGTM! Necessary using declarations added.**

All required namespace aliases are properly added for the new functionality, maintaining consistent coding style with the existing declarations.

---

`506-506`: **LGTM! Updated test parameters.**

The end LLA coordinates in the GroundStrip test have been changed from latitude 1.0 to longitude 1.0, which helps test a different trajectory path.

---

`617-622`: **Well-implemented velocity verification.**

This addition properly tests that the updated velocity computation is working correctly by verifying that the velocity magnitude in the ITRF frame matches the specified ground speed. This aligns with the PR objective of replacing the previously hardcoded zero velocity with a finite difference method.

---

`669-691`: **Good error case handling for GroundStripGeodeticNadir.**

Thorough testing of error conditions for the new GroundStripGeodeticNadir method, including checks for undefined orbit and empty instants array. This follows best practices for validating input validation logic.

---

`692-717`: **LGTM! Comprehensive test for geodetic nadir trajectory.**

The test effectively verifies that the trajectory position matches the geodetic nadir of the orbit position with a very tight tolerance (1e-12), ensuring high precision in the implementation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T05:52:26Z
user: login: apaletta3
body: Great stuff! Mostly nits
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:42:32Z
user: login: vishwa2710
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:36:56Z
user: login: vishwa2710
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T18:37:27Z

Review comments:
diff_hunk: @@ -42,6 +43,11 @@ using ostk::physics::unit::Derived;
 using ostk::astrodynamics::trajectory::Model;
 using ostk::astrodynamics::trajectory::State;
 
+namespace trajectory
+{
+class Orbit;
path: include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
user: login: apaletta3
body: forward declaring instead of importing here to avoid cyclical dependency? 

nit: Does this not work?
```suggestion
class trajectory::Orbit;
```
created_at: 2025-04-16T15:15:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -229,10 +235,37 @@ class Trajectory
         const Celestial& aCelestial = Earth::WGS84()
     );
 
+    /// @brief Constructs a trajectory for the given orbit, with a geodetic nadir pointing attitude law
path: include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
user: login: apaletta3
body: ```suggestion
    /// @brief Constructs a trajectory representing a ground strip that follows the geodetic nadir of the provided orbit
```
created_at: 2025-04-16T15:17:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,11 +218,11 @@ Trajectory Trajectory::GroundStrip(
         throw ostk::core::error::RuntimeError("LLA altitude must be zero.");
     }
 
-    const Velocity velocity = Velocity::MetersPerSecond({0.0, 0.0, 0.0}, aCelestial.accessFrame());
+    const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
 
-    Array<State> states = Array<State>::Empty();
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
 
-    const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
+    Array<physics::coordinate::Position> positions = Array<physics::coordinate::Position>::Empty();
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: nit: Perhaps define the position properly with `using physics::coordinate::Position`? and apply to the rest of the file?

```suggestion
    Array<Position> positions = Array<Position>::Empty();
```
created_at: 2025-04-16T15:23:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -223,14 +232,63 @@ Trajectory Trajectory::GroundStrip(
             anEndLLA, ratio, aCelestial.getEquatorialRadius(), aCelestial.getFlattening()
         );
 
-        const physics::coordinate::Position position = physics::coordinate::Position::Meters(
-            intermediateLLA.toCartesian(aCelestial.getEquatorialRadius(), aCelestial.getFlattening()),
-            aCelestial.accessFrame()
-        );
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(intermediateLLA, celestialSPtr).inFrame(Frame::GCRF(), instant);
+
+        positions.add(position);
+    }
+
+    const Array<Velocity> velocities = computeVelocities(positions, anInstantArray);
+
+    Array<State> states = Array<State>::Empty();
+
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    Array<State> states = Array<State>::Empty();
    states.reserve(anInstantArray.getSize());
```
created_at: 2025-04-16T15:26:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -223,14 +232,63 @@ Trajectory Trajectory::GroundStrip(
             anEndLLA, ratio, aCelestial.getEquatorialRadius(), aCelestial.getFlattening()
         );
 
-        const physics::coordinate::Position position = physics::coordinate::Position::Meters(
-            intermediateLLA.toCartesian(aCelestial.getEquatorialRadius(), aCelestial.getFlattening()),
-            aCelestial.accessFrame()
-        );
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(intermediateLLA, celestialSPtr).inFrame(Frame::GCRF(), instant);
+
+        positions.add(position);
+    }
+
+    const Array<Velocity> velocities = computeVelocities(positions, anInstantArray);
+
+    Array<State> states = Array<State>::Empty();
+
+    for (size_t i = 0; i < anInstantArray.getSize(); i++)
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    for (Size i = 0; i < anInstantArray.getSize(); i++)
```
created_at: 2025-04-16T15:27:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,11 +218,11 @@ Trajectory Trajectory::GroundStrip(
         throw ostk::core::error::RuntimeError("LLA altitude must be zero.");
     }
 
-    const Velocity velocity = Velocity::MetersPerSecond({0.0, 0.0, 0.0}, aCelestial.accessFrame());
+    const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
 
-    Array<State> states = Array<State>::Empty();
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
 
-    const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
+    Array<physics::coordinate::Position> positions = Array<physics::coordinate::Position>::Empty();
 
     for (const auto& instant : anInstantArray)
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    positions.reserve(anInstantArray.getSize());
    
    for (const auto& instant : anInstantArray)
```
created_at: 2025-04-16T15:29:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -223,14 +232,63 @@ Trajectory Trajectory::GroundStrip(
             anEndLLA, ratio, aCelestial.getEquatorialRadius(), aCelestial.getFlattening()
         );
 
-        const physics::coordinate::Position position = physics::coordinate::Position::Meters(
-            intermediateLLA.toCartesian(aCelestial.getEquatorialRadius(), aCelestial.getFlattening()),
-            aCelestial.accessFrame()
-        );
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(intermediateLLA, celestialSPtr).inFrame(Frame::GCRF(), instant);
+
+        positions.add(position);
+    }
+
+    const Array<Velocity> velocities = computeVelocities(positions, anInstantArray);
+
+    Array<State> states = Array<State>::Empty();
+
+    for (size_t i = 0; i < anInstantArray.getSize(); i++)
+    {
+        states.add(State(anInstantArray.at(i), positions.at(i), velocities.at(i)));
+    }
+
+    return Trajectory(states);
+}
+
+Trajectory Trajectory::GroundStripGeodeticNadir(
+    const trajectory::Orbit& anOrbit, const Array<Instant>& anInstantArray, const Celestial& aCelestial
+)
+{
+    if (!anOrbit.isDefined())
+    {
+        throw ostk::core::error::runtime::Undefined("Orbit");
+    }
+
+    if (anInstantArray.getSize() < 2)
+    {
+        throw ostk::core::error::RuntimeError("Atleast 2 instants must be provided.");
+    }
+
+    Array<physics::coordinate::Position> positions = Array<physics::coordinate::Position>::Empty();
+    positions.reserve(anInstantArray.getSize());
+
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
+
+    for (const auto& instant : anInstantArray)
+    {
+        const State state = anOrbit.getStateAt(instant);
+
+        const LLA lla =
+            LLA::FromPosition(state.getPosition().inFrame(Frame::ITRF(), instant), celestialSPtr).onSurface();
+
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(lla, celestialSPtr).inFrame(Frame::GCRF(), instant);
+
+        positions.add(position);
+    }
+
+    Array<Velocity> velocities = computeVelocities(positions, anInstantArray);
 
-        const State state = State(instant, position, velocity);
+    Array<State> states = Array<State>::Empty();
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    Array<State> states = Array<State>::Empty();
    states.reserve(anInstantArray.getSize());
```
created_at: 2025-04-16T15:30:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -223,14 +232,63 @@ Trajectory Trajectory::GroundStrip(
             anEndLLA, ratio, aCelestial.getEquatorialRadius(), aCelestial.getFlattening()
         );
 
-        const physics::coordinate::Position position = physics::coordinate::Position::Meters(
-            intermediateLLA.toCartesian(aCelestial.getEquatorialRadius(), aCelestial.getFlattening()),
-            aCelestial.accessFrame()
-        );
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(intermediateLLA, celestialSPtr).inFrame(Frame::GCRF(), instant);
+
+        positions.add(position);
+    }
+
+    const Array<Velocity> velocities = computeVelocities(positions, anInstantArray);
+
+    Array<State> states = Array<State>::Empty();
+
+    for (size_t i = 0; i < anInstantArray.getSize(); i++)
+    {
+        states.add(State(anInstantArray.at(i), positions.at(i), velocities.at(i)));
+    }
+
+    return Trajectory(states);
+}
+
+Trajectory Trajectory::GroundStripGeodeticNadir(
+    const trajectory::Orbit& anOrbit, const Array<Instant>& anInstantArray, const Celestial& aCelestial
+)
+{
+    if (!anOrbit.isDefined())
+    {
+        throw ostk::core::error::runtime::Undefined("Orbit");
+    }
+
+    if (anInstantArray.getSize() < 2)
+    {
+        throw ostk::core::error::RuntimeError("Atleast 2 instants must be provided.");
+    }
+
+    Array<physics::coordinate::Position> positions = Array<physics::coordinate::Position>::Empty();
+    positions.reserve(anInstantArray.getSize());
+
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
+
+    for (const auto& instant : anInstantArray)
+    {
+        const State state = anOrbit.getStateAt(instant);
+
+        const LLA lla =
+            LLA::FromPosition(state.getPosition().inFrame(Frame::ITRF(), instant), celestialSPtr).onSurface();
+
+        const physics::coordinate::Position position =
+            physics::coordinate::Position::FromLLA(lla, celestialSPtr).inFrame(Frame::GCRF(), instant);
+
+        positions.add(position);
+    }
+
+    Array<Velocity> velocities = computeVelocities(positions, anInstantArray);
 
-        const State state = State(instant, position, velocity);
+    Array<State> states = Array<State>::Empty();
 
-        states.add(state);
+    for (size_t i = 0; i < anInstantArray.getSize(); i++)
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    for (Size i = 0; i < anInstantArray.getSize(); i++)
```
created_at: 2025-04-16T15:30:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -241,5 +299,56 @@ Trajectory::Trajectory()
 {
 }
 
+Array<Velocity> Trajectory::computeVelocities(
+    const Array<physics::coordinate::Position>& aPositionArray, const Array<Instant>& anInstantArray
+)
+{
+    Array<Velocity> velocities = Array<Velocity>::Empty();
+
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    Array<Velocity> velocities = Array<Velocity>::Empty();
    velocities.reserve(anInstantArray.getSize());
```
created_at: 2025-04-16T15:32:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -241,5 +299,56 @@ Trajectory::Trajectory()
 {
 }
 
+Array<Velocity> Trajectory::computeVelocities(
+    const Array<physics::coordinate::Position>& aPositionArray, const Array<Instant>& anInstantArray
+)
+{
+    Array<Velocity> velocities = Array<Velocity>::Empty();
+
+    for (size_t i = 0; i < anInstantArray.getSize(); i++)
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: apaletta3
body: ```suggestion
    for (Size i = 0; i < anInstantArray.getSize(); i++)
```
created_at: 2025-04-16T15:34:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -674,3 +665,54 @@ TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStrip)
         }
     }
 }
+
+TEST(OpenSpaceToolkit_Astrodynamics_Trajectory, GroundStripGeodeticNadir)
+{
+    const Array<Instant> instants = {Instant::J2000(), Instant::J2000() + Duration::Seconds(10.0)};
+    const Earth earth = Earth::WGS84();
+
+    const Environment environment = Environment::Default();
+
+    const Shared<Earth> earthSPtr = std::make_shared<Earth>(earth);
+
+    const Orbit orbit = Orbit::Circular(Instant::J2000(), Length::Meters(545000.0), Angle::Degrees(0.0), earthSPtr);
+
+    {
+        EXPECT_THROW(
+            Trajectory::GroundStripGeodeticNadir(Orbit::Undefined(), instants, earth), ostk::core::error::RuntimeError
+        );
+    }
+
+    {
+        EXPECT_THROW(
+            Trajectory::GroundStripGeodeticNadir(orbit, Array<Instant>::Empty(), earth), ostk::core::error::RuntimeError
+        );
+    }
+
+    {
+        const Trajectory trajectory = Trajectory::GroundStripGeodeticNadir(orbit, instants, earth);
+
+        EXPECT_TRUE(trajectory.isDefined());
+
+        const Instant instant = Instant::J2000();
+
+        const State trajectoryState = trajectory.getStateAt(instant);
+        const State orbitState = orbit.getStateAt(instant);
+
+        // check that the trahectory state is the same as the geodetic nadir coordinate of the orbit
path: test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp
user: login: apaletta3
body: ```suggestion
        // check that the trajectory state is the same as the geodetic nadir coordinate of the orbit
```
created_at: 2025-04-16T15:40:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +43,11 @@ using ostk::physics::unit::Derived;
 using ostk::astrodynamics::trajectory::Model;
 using ostk::astrodynamics::trajectory::State;
 
+namespace trajectory
+{
+class Orbit;
path: include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
user: login: vishwa2710
body: It didn't work when I tried it as the namespace trajectory does not exist.
created_at: 2025-04-16T18:36:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,11 +218,11 @@ Trajectory Trajectory::GroundStrip(
         throw ostk::core::error::RuntimeError("LLA altitude must be zero.");
     }
 
-    const Velocity velocity = Velocity::MetersPerSecond({0.0, 0.0, 0.0}, aCelestial.accessFrame());
+    const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
 
-    Array<State> states = Array<State>::Empty();
+    const Shared<Celestial> celestialSPtr = std::make_shared<Celestial>(aCelestial);
 
-    const Duration duration = anInstantArray.accessLast() - anInstantArray.accessFirst();
+    Array<physics::coordinate::Position> positions = Array<physics::coordinate::Position>::Empty();
path: src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp
user: login: vishwa2710
body: We can't because `Position` is a named function in Trajectory :(
created_at: 2025-04-16T18:37:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-16T05:48:56Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new static method, `GroundStripGeodeticNadir`, has been added to the `Trajectory` class, allowing the construction of a trajectory that follows the geodetic nadir of an orbit over a celestial body. This method is exposed to Python bindings, and corresponding tests have been implemented in both C++ and Python. Internally, trajectory construction now separates position calculation, velocity computation (using a new finite-difference helper), and state assembly. Existing ground strip trajectory logic has been refactored to use these new patterns, and related tests have been updated to reflect the changes.

## Changes

| File(s)                                                                                   | Change Summary                                                                                                                                                                                                                                                                                                         |
|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory.cpp`                      | Added Python binding for the new static method `ground_strip_geodetic_nadir` in the `Trajectory` class.                                                                                                                                                                                                              |
| `bindings/python/test/test_trajectory.py`                                                 | Added fixture for `Orbit` and a new test method for `Trajectory.ground_strip_geodetic_nadir`, verifying its behavior with and without explicit celestial input.                                                                                                                |
| `include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp`                                   | Added the static method `GroundStripGeodeticNadir` and private static helper `computeStates` to the `Trajectory` class, updated includes and forward declarations.                                                                                                         |
| `src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp`                                       | Implemented `Trajectory::GroundStripGeodeticNadir` and `Trajectory::computeStates`. Refactored ground strip construction to use geodetic interpolation and velocity computation.                                                                                           |
| `test/OpenSpaceToolkit/Astrodynamics/Trajectory.test.cpp`                                 | Added tests for geodetic nadir ground strip trajectories. Modified existing ground strip tests to change endpoint, remove zero-velocity assertions, and check velocity magnitude consistency. No changes to exported/public entity declarations.                                 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant PythonBindings
    participant Trajectory
    participant Orbit
    participant Celestial

    User->>PythonBindings: Call Trajectory.ground_strip_geodetic_nadir(orbit, instants, celestial)
    PythonBindings->>Trajectory: GroundStripGeodeticNadir(orbit, instants, celestial)
    Trajectory->>Orbit: Get states at instants
    Trajectory->>Celestial: Project states to geodetic nadir positions
    Trajectory->>Trajectory: computeStates(positions, instants)
    Trajectory-->>PythonBindings: Return Trajectory object
    PythonBindings-->>User: Trajectory object (ground strip along geodetic nadir)
```

## Suggested reviewers

- lucas-bremond

## Poem

> A new path traced beneath the sky,  
> Where nadir’s gaze will softly lie.  
> Orbits above, ground tracks below,  
> With code and tests, the rabbits show—  
> From Python, C++, to test’s delight,  
> The geodetic strip is now in sight!  
> 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6e2afc3207d728f798fc74efd4d4d59cc3bea1c9 and db7f85acea174576209d22d57771577c80294c58.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp` (3 hunks)
* `src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* include/OpenSpaceToolkit/Astrodynamics/Trajectory.hpp
* src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Build Development Image / Build Development Image

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=open-space-collective/open-space-toolkit-astrodynamics&utm_content=540):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-16T06:06:27Z
body: ## [Codecov](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/pull/540?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective) Report
Attention: Patch coverage is `98.07692%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 91.63%. Comparing base [(`3af4f20`)](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/commit/3af4f20c33f0dd9b3846eab3793cd0cd93f789c9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective) to head [(`db7f85a`)](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/commit/db7f85acea174576209d22d57771577c80294c58?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/pull/540?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective) | Patch % | Lines |
|---|---|---|
| [src/OpenSpaceToolkit/Astrodynamics/Trajectory.cpp](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/pull/540?src=pr&el=tree&filepath=src%2FOpenSpaceToolkit%2FAstrodynamics%2FTrajectory.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective#diff-c3JjL09wZW5TcGFjZVRvb2xraXQvQXN0cm9keW5hbWljcy9UcmFqZWN0b3J5LmNwcA==) | 98.03% | [1 Missing :warning: ](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/pull/540?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #540      +/-   ##
==========================================
+ Coverage   91.61%   91.63%   +0.02%     
==========================================
  Files         101      101              
  Lines        9600     9641      +41     
==========================================
+ Hits         8795     8835      +40     
- Misses        805      806       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/open-space-collective/open-space-toolkit-astrodynamics/pull/540?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-space-collective).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE