Skip to content

Fix RK4 stage weighting to keep amplitude bounded#375

Merged
vissarion merged 1 commit intoGeomScale:developfrom
Mohit-Lakra:develop
Jan 28, 2026
Merged

Fix RK4 stage weighting to keep amplitude bounded#375
vissarion merged 1 commit intoGeomScale:developfrom
Mohit-Lakra:develop

Conversation

@Mohit-Lakra
Copy link
Copy Markdown
Contributor

@Mohit-Lakra Mohit-Lakra commented Dec 24, 2025

Resolves a bug in Runge-Kutta.
Changed the Runge–Kutta solver so each stage uses the right weight (bs[ord]) instead of the state index. This stops the
x'' = -x simulation from gaining energy and keeps the sine solution bounded.

@TolisChal
Copy link
Copy Markdown
Member

Hi @Mohit-Lakra! LGTM! I'm approving it.
@vissarion what do you think?

@Mohit-Lakra Mohit-Lakra changed the title Fix RK4 stage weighting to keep amplitude bounded Fix issue : #120 RK4 stage weighting to keep amplitude bounded Dec 24, 2025
@Mohit-Lakra Mohit-Lakra changed the title Fix issue : #120 RK4 stage weighting to keep amplitude bounded Fix RK4 stage weighting to keep amplitude bounded Dec 24, 2025
Copy link
Copy Markdown
Member

@vissarion vissarion left a comment

Choose a reason for hiding this comment

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

Thanks, good catch! This solve a bug in RK solver, but I do not see a connection to #120. @Mohit-Lakra is this accidentally linked to #120 or is here something that I miss?

@Mohit-Lakra
Copy link
Copy Markdown
Contributor Author

Thanks for the review and approval, @vissarion

Regarding #120: You are correct that the issue was originally reported as an instability in the 'Integral Collocation' method. However, upon debugging, the root cause of those unbounded oscillations (where the simulation artificially gained energy) turned out to be this incorrect stage weighting in the RK4 solver.

Since this solver bug was the source of the instability described in #120, this PR resolves the underlying problem.

@vissarion
Copy link
Copy Markdown
Member

Thanks for your reply. Could you please explain in detail how this PR fixes the collocation method instability? Probably by adding an example that with current code does not converge and converge after applying this patch.

@Mohit-Lakra
Copy link
Copy Markdown
Contributor Author

Mohit-Lakra commented Jan 20, 2026

Hi, @vissarion
I tracked down the root cause of the instability to include/ode_solvers/runge_kutta.hpp (line 106).The Issue:The loop was using bs[i] (the state component index) instead of bs[ord] (the RK stage index) to accumulate the weighted slopes.

The Bug: For coupled systems like (x, v), this meant the position component always used the first RK weight (1/6) and velocity always used the second (1/3), regardless of the actual stage.

The Result: This broke the Butcher tableau requirements, causing the significant energy drift we saw in oscillatory systems.
The Fix:I changed the index to bs[ord]. This ensures the correct Runge-Kutta weights are applied to all state components at every stage.

Proof of Stability (Harmonic Oscillator Test):I verified the fix using a standalone Harmonic Oscillator (x'' = -x) reproduction script using volesti's RK solver.

Before Fix : The solver fails to conserve energy. The amplitude collapses to 0.5, showing the "collocation instability."
Screenshot 2026-01-20 at 10 08 57 PM

After Fix : With the correct index, the amplitude stays strictly bounded at 1.0, preserving energy as expected.
Screenshot 2026-01-20 at 10 09 10 PM

Reproduction Code:
Here is the rk_demo.cpp used to generate these results:

Click to view rk_demo.cpp
#define DISABLE_NLP_ORACLES
#include "cartesian_geom/cartesian_kernel.h"
#include "ode_solvers/runge_kutta.hpp"
#include "ode_solvers/oracle_functors.hpp"
#include <Eigen/Eigen>
#include <vector>
#include <limits>
#include <iostream>

template<class Point>
struct DummyPoly {
    using FT = typename Point::FT;
    using VT = Eigen::Matrix<FT, Eigen::Dynamic, 1>;
    using MT = Eigen::Matrix<FT, Eigen::Dynamic, Eigen::Dynamic>;
    std::pair<FT,int> line_positive_intersect(const Point&, const Point&, VT&, VT&) const {
        return {std::numeric_limits<FT>::max(), -1};
    }
    std::pair<FT,FT> line_intersect(const Point&, const Point&) const {
        return {std::numeric_limits<FT>::max(), FT(0)};
    }
    int is_in(const Point&, FT = FT(0)) const { return -1; }
    void compute_reflection(Point&, const Point&, int) const {}
    void compute_reflection(Point&, const Point&, int, const MT&) const {}
    FT num_of_hyperplanes() const { return 0; }
    MT get_mat() const { return MT(); }
    VT get_vec() const { return VT(); }
    void resetFlags() const {}
    void update_position_internal(FT) const {}
};

int main() {
    using Kernel = Cartesian<double>;
    using Point  = Kernel::Point;
    using Poly   = DummyPoly<Point>;
    using Func   = IsotropicQuadraticFunctor::GradientFunctor<Point>;

    IsotropicQuadraticFunctor::parameters<double> params;
    params.order = 2; // x, v
    params.alpha = 1;
    Func F(params);

    // Initial state: x=0, v=1 (Amplitude should be 1)
    Point x0(1); x0.set_coord(0, 0.0);
    Point v0(1); v0.set_coord(0, 1.0);
    std::vector<Point> state{x0, v0};
    std::vector<Poly*> bounds{nullptr, nullptr};

    RKODESolver<Point, double, Poly, Func> solver(0.0, 0.05, state, F, bounds);

    double max_abs = 0.0;
    for (int step = 0; step < 2000; ++step) {
        solver.step(step, true);
        max_abs = std::max(max_abs, std::abs(solver.xs[0][0]));
        if (step % 200 == 0) {
             std::cout << step << "  x=" << solver.xs[0][0]
                       << "  v=" << solver.xs[1][0] << "\n";
        }
    }
    std::cout << "max |x| = " << max_abs << std::endl;
}

@vissarion
Copy link
Copy Markdown
Member

I think there’s a mismatch here: issue #120 is about the integral collocation solver (include/ode_solvers/integral_collocation.hpp), but this PR + the oscillator demo are Runge–Kutta (runge_kutta.hpp/RKODESolver).

So this doesn’t demonstrate “Resolves #120”. Please drop that linkage, or add a minimal example that actually uses the collocation path and shows change in behavior before and after.

@Mohit-Lakra
Copy link
Copy Markdown
Contributor Author

Thanks for the clarification, @vissarion . You are absolutely right.

I realized I conflated the two because they both exhibited similar energy drift. I was debugging with RKODESolver and found a legitimate bug there (the stage weighting), falsely assuming it was the root cause of #120.

I have updated the title and removed the link to #120 to reflect that this is specifically a fix for the Runge-Kutta solver. Since this fixes a confirmed bug in the RK engine, I believe this PR is ready to be merged on its own merits.

I’ll tackle the actual Collocation instability in a separate PR once I have a proper reproduction case for it.

@Mohit-Lakra
Copy link
Copy Markdown
Contributor Author

@vissarion What do you think about this, is there anything that I can modify??

@vissarion
Copy link
Copy Markdown
Member

Looks good to me now! Thanks.

@vissarion vissarion merged commit 410a90f into GeomScale:develop Jan 28, 2026
Mohit-Lakra added a commit to Mohit-Lakra/volesti that referenced this pull request Feb 19, 2026
Add missing headers

Improve build and run instructions for hpolytope-volume example (GeomScale#376)

Fix RK4 stage weighting to keep amplitude bounded (GeomScale#375)

Added test
Mohit-Lakra added a commit to Mohit-Lakra/volesti that referenced this pull request Feb 19, 2026
Add missing headers

Improve build and run instructions for hpolytope-volume example (GeomScale#376)

Fix RK4 stage weighting to keep amplitude bounded (GeomScale#375)

Added test
Mohit-Lakra added a commit to Mohit-Lakra/volesti that referenced this pull request Feb 19, 2026
Add missing headers

Improve build and run instructions for hpolytope-volume example (GeomScale#376)

Fix RK4 stage weighting to keep amplitude bounded (GeomScale#375)

Added test
Mohit-Lakra added a commit to Mohit-Lakra/volesti that referenced this pull request Feb 19, 2026
…cale#120)

Add regression test for stability

Fix CI build: Guard NLP oracle includes

Fix CI build: Guard NLP oracle includes

Add missing headers

Improve build and run instructions for hpolytope-volume example (GeomScale#376)

Fix RK4 stage weighting to keep amplitude bounded (GeomScale#375)

Added test

Restore upstream files — not part of this fix

Restore README to upstream state
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.

3 participants