Skip to content

Conversation

@phc1990
Copy link
Contributor

@phc1990 phc1990 commented Oct 28, 2025

WIP

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ability to retrieve maneuver intervals from trajectory segments.
    • Introduced optional maneuver constraint parameters—minimum duration, minimum gap, and maximum count—providing enhanced control over maneuver generation.
  • Performance

    • Improved maneuver data retrieval through internal caching mechanisms.

@phc1990 phc1990 changed the title feat: add operational settings to maneuvering segments feat: add operational settings to maneuvering segments [WIP] Oct 28, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

Walkthrough

This PR enhances trajectory segment solving by introducing maneuver constraints (minimum duration, gap, maximum count) to control maneuver segmentation. It adds a caching mechanism for maneuver extraction in Segment::Solution, exposes maneuver intervals via a new get_maneuver_intervals() method, refactors Sequence solving logic with a private solve_() helper, and extends Python bindings to accept the new parameters.

Changes

Cohort / File(s) Summary
Core Header Declarations
include/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.hpp
Adds getManeuverIntervals() method to Segment::Solution; introduces mutable cache (cachedManeuvers_) for maneuvers; exposes Pair and Frame types via using declarations; extends Maneuver() and ConstantLocalOrbitalFrameDirectionManeuver() factory signatures with optional maneuver-constraint parameters (minimum_maneuver_duration, minimum_maneuver_gap, maximum_maneuver_count); adds private data members for constraint storage.
include/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.hpp
Core Implementation
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp
Implements maneuver caching in extractManeuvers(); adds getManeuverIntervals() accessor; extends constructor to accept and store maneuver-constraint parameters; updates Coast(), Maneuver(), and ConstantLocalOrbitalFrameDirectionManeuver() factory methods to propagate constraints; implements constraint enforcement in solving pathways (truncates states when maneuver constraints violated).
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.cpp
Python Bindings
bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory/Segment.cpp
Adds get_maneuver_intervals() binding to Segment::Solution; extends maneuver() and constant_local_orbital_frame_direction_maneuver() method signatures with optional maneuver-constraint parameters; introduces Integer alias in bindings scope; updates docstrings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Maneuver caching mechanism (Segment.cppextractManeuvers() and cache invalidation logic): Verify frame-matching logic for cache validity and potential thread-safety concerns with the mutable cache.
  • State truncation during constraint enforcement (Segment.cppSolve_() and Solve() pathways): Ensure correct identification of first-violated maneuver and accurate state trimming without data loss.
  • Sequence solver refactoring (Sequence.cppsolve_() helper integration): Verify that moving solving logic into the helper preserves all edge cases, condition handling, and accumulation behavior from the original inline code.
  • Parameter propagation chain: Confirm that new maneuver-constraint parameters flow correctly through factory methods (Maneuver(), ConstantLocalOrbitalFrameDirectionManeuver()) and are consistently initialized/stored across all code paths.

Possibly related PRs

Suggested reviewers

  • apaletta3
  • vishwa2710
  • alex-liang3

Poem

🐰 A rabbit hops through segments bright,
With maneuvers cached just right,
Constraints tamed, trajectories flow,
Solve helpers guide the way we go,
Cache it, track it, bind it well—
Python and C++ together dwell! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: add operational settings to maneuvering segments [WIP]" directly and accurately reflects the main changes in the changeset. The pull request introduces three new optional parameters (minimum_maneuver_duration, minimum_maneuver_gap, and maximum_maneuver_count) to maneuvering segment constructors and factory methods, along with logic to enforce these constraints and retrieve maneuver intervals. The title uses clear, concise language that accurately conveys the primary objective without noise or vague terminology. A developer scanning commit history would immediately understand that this change adds new configurable operational parameters to maneuvering segments.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-operational-settings-to-maneuvering-segments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.cpp (1)

328-361: Potential infinite loop when all segments are skipped (no progress).

If every segment is skipped by solve_ (e.g., all maneuvers < minimum), neither propagationDuration nor initialState advance; the while loop repeats forever.

Add a “progress guard” to break/return when no segment solution was added in a full pass:

   while (!eventConditionIsSatisfied && propagationDuration <= aMaximumPropagationDuration)
   {
-      for (const Segment& segment : segments_)
+      const Size solutionsAtLoopStart = segmentSolutions.getSize();
+      bool progressed = false;
+      for (const Segment& segment : segments_)
       {
           const Duration segmentPropagationDurationLimit =
               std::min(segmentPropagationDurationLimit_, aMaximumPropagationDuration - propagationDuration);

           const Size segmentSolutionSizeBefore = segmentSolutions.getSize();
 
           if (!solve_(segment, segmentSolutions, initialState, segmentPropagationDurationLimit, ""))
           {
               return {segmentSolutions, false};
           }
 
           // Check if a segment solution was added (it might have been skipped due to minimum maneuver duration)
           if (segmentSolutions.getSize() > segmentSolutionSizeBefore)
           {
+              progressed = true;
               const Segment::Solution& segmentSolution = segmentSolutions.accessLast();
               eventConditionIsSatisfied = sequenceCondition->isSatisfied(segmentSolution.states.accessLast(), initialState);
               // Terminate Sequence successfully if the provided event condition was satisfied
               if (eventConditionIsSatisfied)
               {
                   return {segmentSolutions, true};
               }
               propagationDuration += segmentSolution.getPropagationDuration();
           }
       }
+      if (!progressed && segmentSolutions.getSize() == solutionsAtLoopStart)
+      {
+          BOOST_LOG_TRIVIAL(warning) << "No progress made in solveToCondition loop; aborting to avoid infinite loop.";
+          return {segmentSolutions, false};
+      }
   }
🧹 Nitpick comments (6)
include/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.hpp (2)

192-194: Mutable cache without synchronization can race under concurrency.

Segment::Solution caches maneuvers in a mutable member with no locking. If a Solution is accessed from multiple threads, this can data race.

  • Document non-thread-safety or guard updates with a lightweight mutex.
  • Alternatively, use std::optional cached value returned by value semantics to avoid shared mutation.

269-281: Maneuver-constraint parameters: clarify precedence and intent.

Good additions. Please ensure docs specify:

  • Undefined ⇒ constraint disabled.
  • Precedence between Sequence-level minimumManeuverDuration vs per-segment fields (prefer per-segment only to avoid duplicate policy).

Consider removing Sequence-level duration checks and relying solely on per-segment constraints for consistency.

Also applies to: 303-316, 343-353

bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory/Segment.cpp (1)

456-459: Docstring casing inconsistency.

Use Angle.undefined() for consistency with other defaults (Duration.undefined(), Integer.undefined()).

Update the docstring default example string from "Angle.Undefined()" to "Angle.undefined()".

src/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.cpp (1)

422-438: Duplicate “minimum maneuver duration” enforcement across Sequence and Segment.

Sequence::solve_ skips entire maneuver segments if any interval < minimumManeuverDuration_. Segment::Solve_ already enforces per‑segment constraints and truncates states. Two policies can conflict.

  • Drop the Sequence-level duration check to a single source of truth (Segment). If retained, update log to “Skipping maneuver segment” and document precedence.
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp (2)

166-194: Exact zero test for “thrusting” is brittle; use a tolerance and ignore mass column.

Using full row norm != 0.0 mixes velocity contribution and mass flow; and exact zero comparisons are numerically unstable.

  • Use a small epsilon on the 3× velocity columns only, e.g.:
-        if (fullSegmentContributions.row(i).norm() != 0.0)
+        const double thrustNorm = fullSegmentContributions.block<1,3>(i, 0).norm();
+        if (thrustNorm > 1e-12)  // Tolerance to account for numerical noise

Adjust epsilon based on solver precision.


150-153: Frame comparison in cache: consider pointer equality for speed.

Dereferencing and comparing Frames may be costlier than pointer equality if frames are interned (e.g., Frame::GCRF()). If semantic equality is required, keep as-is; if identity suffices, prefer cachedManeuvers_.first == aFrameSPtr.

Replace with pointer equality or use aFrameSPtr->isSame(cachedManeuvers_.first) if available.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6703a3f and ec8d70b.

📒 Files selected for processing (5)
  • bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory/Segment.cpp (6 hunks)
  • include/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.hpp (9 hunks)
  • include/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.hpp (1 hunks)
  • src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp (9 hunks)
  • src/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.cpp (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.cpp (2)
include/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.hpp (1)
  • aSegment (246-252)
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp (2)
  • Maneuver (639-662)
  • Maneuver (639-648)
bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory/Segment.cpp (1)
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp (2)
  • getManeuverIntervals (247-263)
  • getManeuverIntervals (247-247)
include/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.hpp (2)
include/OpenSpaceToolkit/Astrodynamics/Trajectory/LocalOrbitalFrameFactory.hpp (1)
  • aType (116-118)
include/OpenSpaceToolkit/Astrodynamics/Trajectory/LocalOrbitalFrameTransformProvider.hpp (4)
  • aType (93-95)
  • aType (102-104)
  • aType (111-111)
  • aType (122-122)
src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp (3)
include/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.hpp (8)
  • aFrameSPtr (130-130)
  • aFrameSPtr (171-171)
  • aName (255-260)
  • aName (273-282)
  • aName (305-316)
  • aDynamicsArray (374-374)
  • aState (240-240)
  • aState (362-367)
src/OpenSpaceToolkit/Astrodynamics/Trajectory/State.cpp (11)
  • Undefined (530-533)
  • Undefined (530-530)
  • aState (165-175)
  • aState (165-165)
  • aState (177-213)
  • aState (177-177)
  • aState (215-218)
  • aState (215-215)
  • aState (220-263)
  • aState (220-220)
  • aState (265-308)
src/OpenSpaceToolkit/Astrodynamics/Flight/Maneuver.cpp (1)
  • Maneuver (44-115)
🔇 Additional comments (2)
bindings/python/src/OpenSpaceToolkitAstrodynamicsPy/Trajectory/Segment.cpp (1)

220-233: Nice addition: get_maneuver_intervals.

Binding mirrors C++ API and documents cache fallback to GCRF. Looks good.

src/OpenSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp (1)

247-263: getManeuverIntervals defaulting to GCRF: good; ensure invariant in docs.

Intervals are frame-invariant; defaulting to GCRF to populate cache is reasonable. Looks good.

Comment on lines +246 to +252
bool solve_(
const Segment& aSegment,
Array<Segment::Solution>& aSegmentSolutions,
State& anInitialState,
const Duration& aSegmentPropagationLimit,
const String& aSegmentNameSuffix
) const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing include for String (compile-time break).

solve_ takes const String& aSegmentNameSuffix but this header doesn’t include OpenSpaceToolkit/Core/Type/String.hpp. Add it to avoid relying on transitive includes.

Apply this diff near the top includes:

 #include <OpenSpaceToolkit/Core/Container/Array.hpp>
 #include <OpenSpaceToolkit/Core/Type/Size.hpp>
+#include <OpenSpaceToolkit/Core/Type/String.hpp>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In include/OpenSpaceToolkit/Astrodynamics/Trajectory/Sequence.hpp around lines
246 to 252, the declaration of solve_ uses const String& aSegmentNameSuffix but
this header doesn't include OpenSpaceToolkit/Core/Type/String.hpp; add the
include for OpenSpaceToolkit/Core/Type/String.hpp near the top of the include
block so the String type is declared explicitly (avoid relying on transitive
includes).

Comment on lines +718 to +807
// Check maneuver constraints if applicable
if (type_ == Segment::Type::Maneuver &&
(minimumManeuverDuration_.isDefined() || minimumManeuverGap_.isDefined() || maximumManeuverCount_.isDefined()))
{
// Create preliminary solution to extract maneuvers
const Segment::Solution preliminarySolution = {
name_,
aDynamicsArray,
states,
conditionSolution.conditionIsSatisfied,
type_,
};

const Array<flightManeuver> maneuvers = preliminarySolution.extractManeuvers(aState.accessFrame());

// Check if we need to truncate based on constraints
Size maneuverCountLimit = maneuvers.getSize();

for (Size i = 0; i < maneuvers.getSize(); ++i)
{
const flightManeuver& currentManeuver = maneuvers[i];

// Check minimum maneuver duration
if (minimumManeuverDuration_.isDefined())
{
const Duration maneuverDuration = currentManeuver.getInterval().getDuration();
if (maneuverDuration < minimumManeuverDuration_)
{
maneuverCountLimit = i;
break;
}
}

// Check minimum maneuver gap (if not the first maneuver)
if (minimumManeuverGap_.isDefined() && i > 0)
{
const Instant previousManeuverEnd = maneuvers[i - 1].getInterval().accessEnd();
const Instant currentManeuverStart = currentManeuver.getInterval().accessStart();
const Duration gap = currentManeuverStart - previousManeuverEnd;

if (gap < minimumManeuverGap_)
{
maneuverCountLimit = i;
break;
}
}

// Check maximum maneuver count
if (maximumManeuverCount_.isDefined() && (i + 1) > static_cast<Size>(maximumManeuverCount_))
{
maneuverCountLimit = i;
break;
}
}

// Truncate states if necessary
if (maneuverCountLimit < maneuvers.getSize())
{
// Find the first instant of the first excluded maneuver
const Instant firstExcludedManeuverStart = maneuvers[maneuverCountLimit].getInterval().accessStart();

// Find the first state at or after the excluded maneuver start
Size truncationIndex = 0;
for (Size j = 0; j < states.getSize(); ++j)
{
if (states[j].accessInstant() >= firstExcludedManeuverStart)
{
truncationIndex = j;
break;
}
}

// Truncate the states array to exclude the first excluded maneuver and everything after
Array<State> truncatedStates = Array<State>::Empty();
if (truncationIndex > 0)
{
truncatedStates.reserve(truncationIndex);
for (Size j = 0; j < truncationIndex; ++j)
{
truncatedStates.add(states[j]);
}
}
else
{
// If the first excluded maneuver starts at or before the first state, return only the initial state
truncatedStates.add(states.accessFirst());
}
states = truncatedStates;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

conditionIsSatisfied becomes stale after truncation.

States are truncated when constraints hit, but the returned flag uses the pre‑truncation conditionSolution.conditionIsSatisfied, which may be true even though the truncated timeline never reaches the event.

Track and return a corrected flag:

-    const NumericalSolver::ConditionSolution conditionSolution = propagator.calculateStateToCondition(
+    const NumericalSolver::ConditionSolution conditionSolution = propagator.calculateStateToCondition(
         aState, aState.accessInstant() + maximumPropagationDuration, *anEventConditionSPtr
     );
+    bool conditionReached = conditionSolution.conditionIsSatisfied;
@@
-        if (maneuverCountLimit < maneuvers.getSize())
+        if (maneuverCountLimit < maneuvers.getSize())
         {
@@
-            states = truncatedStates;
+            states = truncatedStates;
+            // Truncation prevents reaching the original event condition
+            conditionReached = false;
         }
     }
@@
-    return {
+    return {
         name_,
         aDynamicsArray,
         states,
-        conditionSolution.conditionIsSatisfied,
+        conditionReached,
         type_,
     };

Also applies to: 809-816

@codecov
Copy link

codecov bot commented Oct 28, 2025

Codecov Report

❌ Patch coverage is 56.31068% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.63%. Comparing base (6703a3f) to head (ec8d70b).

Files with missing lines Patch % Lines
...nSpaceToolkit/Astrodynamics/Trajectory/Segment.cpp 37.50% 40 Missing ⚠️
...nSpaceToolkit/Astrodynamics/Trajectory/Segment.hpp 33.33% 4 Missing ⚠️
...SpaceToolkit/Astrodynamics/Trajectory/Sequence.cpp 96.96% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #594      +/-   ##
==========================================
- Coverage   91.98%   91.63%   -0.36%     
==========================================
  Files         113      113              
  Lines       10686    10743      +57     
==========================================
+ Hits         9830     9844      +14     
- Misses        856      899      +43     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants