Interpolation: add derivative method to SpanInterpolators#7294
Conversation
fbe5323 to
f72d3c4
Compare
| /// Evaluate the derivative of the interpolant of the function represented by | ||
| /// complex `values` at `source_points`, evaluated at the requested | ||
| /// `target_point`. | ||
| std::complex<double> derivative( |
There was a problem hiding this comment.
I see that this is just copying the implementation of interpolate, but don't you really want this to be virtual? This generic implementation is extremely inefficient compared to the specialized implementations in the derived classes.
There was a problem hiding this comment.
Related: do we want an (additional?) function that returns the value and derivative? I could see it being cheaper to compute both simultaneously than back-to-back.
There was a problem hiding this comment.
For the other PRs for the CCE ID it won't be needed as I need Du(DyJ) but not DyJ itself. But if you want I can add it
There was a problem hiding this comment.
I made the complex derivative overload virtual in the base class and marked the Linear/Cubic complex overloads override. The generic split into real/imaginary implementation is now just the fallback for derived classes (like BarycentricRational) that don't specialize the complex path.
| linear_interpolator_values.size()}, | ||
| target_point); | ||
| CHECK(real_linear_derivative == | ||
| approx(linear_interpolator_values[1] - linear_interpolator_values[0])); |
There was a problem hiding this comment.
Test with a denominator that isn't 1.
There was a problem hiding this comment.
test_linear_interpolator now uses points {0.5, 2.0} (spacing 1.5, nonzero offset), and the expected value/derivative are computed with the actual spacing and offset, so the interpolation denominator is not 1.
| interpolator_values[i] = | ||
| amplitude * cos(frequency * interpolator_points[i]); | ||
| } | ||
| const double target_time = value_dist(*gen) / 100.0; |
There was a problem hiding this comment.
I'm confused about why this is working. The control points generated above range from 0 to a bit less than 0.01, and this generates a target point between 0.001 and 0.01, so this could generate a target outside the data range. Even beyond that, don't you have to obey required_number_of_points_before_and_after, which should restrict the valid target points to a small interval in the center?
There was a problem hiding this comment.
The points are now a uniform grid point_spacing * i, and the target is drawn inside the central interval [points[n-1], points[n]) with n = required_number_of_points_before_and_after(), so the required number of points always lies on each side of the target.
| return result; | ||
| }; | ||
| // Unit-spaced points keep the Lagrange/barycentric formulas well conditioned | ||
| // so the exact result is recovered to roundoff. |
There was a problem hiding this comment.
This would be a much stronger test if you didn't do this. You want to make sure no points are close together, but making all the spacings exactly 1 is not necessary. Perturbing each point by a random number of magnitude below 0.1 should work fine.
There was a problem hiding this comment.
Ok, the points are now i + delta_i with delta_i drawn uniformly from [-0.1, 0.1] and the points stay well separated
| const auto interpolator_derivative_result = interpolator.derivative( | ||
| gsl::span<const double>{points.data(), points.size()}, | ||
| gsl::span<const ValueType>{values.data(), values.size()}, target_point); | ||
| const Approx exact_approx = Approx::custom().epsilon(1.0e-10).scale(1.0); |
There was a problem hiding this comment.
You claim repeatedly above that this should be exact to roundoff.
There was a problem hiding this comment.
here you mean to tighten up the epsilon to true roundoff, or rewording?
There was a problem hiding this comment.
Depends on if it actually is accurate to roundoff. If it is, using a tighter tolerance would be preferable, but if it's not that won't work and the comments should be changed.
There was a problem hiding this comment.
Tightened the tolerance from 1e-10 to 1e-13 and reworded the comment to say the derivative matches the analytic value "to roundoff". Confirmed the exactness test passes at 1e-13 for the linear, cubic, and barycentric interpolators.
| // The linear derivative is a midpoint finite-difference approximation; | ||
| // its error grows by one factor of h relative to the interpolated value. | ||
| const Approx derivative_approx = | ||
| Approx::custom().epsilon(1.0e-1).scale(1.0); |
There was a problem hiding this comment.
You should be able to do better than this. The linear approximation should be able to get the value to better than 1e-4 and the derivative to 1e-2. Evan a zeroth-order approximation should get the value to 1e-4 for the chosen test function.
There was a problem hiding this comment.
Tightened the linear tolerances to 1e-4 (value) and 1e-2 (derivative).
| test_interpolator_derivative_approximate_fidelity<DataVector>( | ||
| make_not_null(&gen), LinearSpanInterpolator{}, derivative_approx); | ||
| test_interpolator_derivative_approximate_fidelity<ComplexDataVector>( | ||
| make_not_null(&gen), LinearSpanInterpolator{}, derivative_approx); |
There was a problem hiding this comment.
Call test_interpolator_derivative_is_exact?
| @@ -41,5 +41,23 @@ double BarycentricRationalSpanInterpolator::interpolate( | |||
| return interpolant(target_point); | |||
There was a problem hiding this comment.
[Arbitrary line for threading purposes] Do you have some reason to believe there might be out-of-tree implementations of SpanInterpolator? If not, I really don't think we need the details of the interface changes in the release notes' upgrade instructions.
There was a problem hiding this comment.
No, I've dropped the interface change details from the upgrade instructions block.
| return derivative_impl(source_points, values, target_point); | ||
| } | ||
|
|
||
| // NOLINTNEXTLINE(readability-convert-member-functions-to-static) |
There was a problem hiding this comment.
If you don't end up making this a virtual function overload, why not do the clang-tidy thing instead of NOLINTing?
There was a problem hiding this comment.
I removed the NOLINT
667aac8 to
ab34d2c
Compare
Adds a `derivative(source_points, values, target_point)` method to the `intrp::SpanInterpolator` interface and its derived classes, returning the derivative of the interpolant at the requested point (the analogue of the existing `interpolate`). This is needed by downstream code that must time-differentiate an interpolated worldtube quantity. - `SpanInterpolator` declares the pure-virtual real overload and a virtual complex overload whose base implementation splits the values into real/imaginary parts; derived classes override it when a specialized complex implementation is more efficient. - `LinearSpanInterpolator` returns the constant slope between the two points. - `CubicSpanInterpolator` adds `derivative_impl`, the analytic derivative of the cubic Lagrange interpolant. - `BarycentricRationalSpanInterpolator` uses boost's `barycentric_rational::prime`. Tested with an approximate-fidelity check against the analytic derivative of `A cos(w t)`, and -- since a degree-N interpolant reproduces a degree-N polynomial exactly -- with exact-polynomial derivative checks for the linear, cubic, and barycentric interpolators. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ab34d2c to
38246fb
Compare
wthrowe
left a comment
There was a problem hiding this comment.
Looks good.
In the future, please push fixup commits if you have to make significant changes after review.
|
@gda98re looks like this is your first contribution to SpECTRE. Welcome! 🎉 Your contribution is much appreciated, and we invite you to add your name to the author list:
Once the pull request is merged, your name will appear on the SpECTRE DOI on Zenodo with the next public release. |
Adds a
derivative(source_points, values, target_point)method to theintrp::SpanInterpolatorinterface and its derived classes, returning the derivative of the interpolant at the requested point (the analogue of the existinginterpolate). This is needed by downstream code that must time-differentiate an interpolated worldtube quantity.SpanInterpolatordeclares the pure-virtual real overload and a virtual complex overload; the base implementation of the complex overload splits the values into real/imaginary parts and calls the real overload. Derived classes override the complex overload when a specialized version is more efficient.LinearSpanInterpolatorreturns the constant slope between the two points.CubicSpanInterpolatoraddsderivative_impl, the analytic derivative of the cubic Lagrange interpolant.BarycentricRationalSpanInterpolatoruses boost'sbarycentric_rational::prime.Tested with an approximate-fidelity check against the analytic derivative of
A cos(w t), and -- since a degree-N interpolant reproduces a degree-N polynomial exactly -- with exact-polynomial derivative checks for the linear, cubic, and barycentric interpolators.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Proposed changes
Code review checklist
make docto generate the documentation locally intoBUILD_DIR/docs/html.Then open
index.html.code review guide.
bugfixornew featureif appropriate."Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com",
"Co-Authored-by: Codex noreply@openai.com", or
"Co-Authored-By: GitHub Copilot CLI noreply@microsoft.com"
as the last line of the commit, depending on the agent.
Further comments
Part of the CauchySecondOrder initial-data stack.