Skip to content

Conversation

@lewisn-met
Copy link

@lewisn-met lewisn-met commented Nov 5, 2025

Description

This PR relates to issue #328 and adds:

  • A SphericalPolygon3D class as an alternative interpolation method to the Triag3D and Quad3D classes currently used in the finite-element interpolation method (FiniteElement.h). This computes weights using the spherical mean value formula rather than the bilinear/barycentric formulae currently implemented.

  • A test suite for the new class, in test_SphericalPolygon3D.cc, which validates that the interpolation method correctly computes the areas, normals, and weights of an example triangle, planar quadrilateral, and non-planar quadrilateral on the surface of the unit sphere.

(Draft) PR integrating this class into the existing set of interpolation methods in atlas to follow, with the exact implementation to be fixed after further discussion with myself and @odlomax.

Contributor Declaration

By opening this pull request, I affirm the following:

  • All authors agree to the Contributor License Agreement.
  • The code follows the project's coding standards.
  • I have performed self-review and added comments where needed.
  • I have added or updated tests to verify that my changes are effective and functional.
  • I have run all existing tests and confirmed they pass.

@wdeconinck
Copy link
Member

Thanks @lewisn-met for this contribution. I am wondering in how far there is overlap with the ConvexSphericalPolygon which is used in the "ConservativeSphericalPolygon" interpolation, and if missing functionality can be merged perhaps. @sbrdar could you also have a look please?

@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 84.61538% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.78%. Comparing base (4a5d4e9) to head (70ce3ff).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
src/tests/interpolation/test_SphericalPolygon3D.cc 80.00% 32 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #329      +/-   ##
===========================================
+ Coverage    79.27%   79.78%   +0.50%     
===========================================
  Files          909      909              
  Lines        61597    70950    +9353     
===========================================
+ Hits         48832    56605    +7773     
- Misses       12765    14345    +1580     

☔ 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.

@lewisn-met
Copy link
Author

Thanks for taking a look at this and for the kind words @wdeconinck. From a brief read of the ConvexSphericalPolygon class, the method used to check for the left of a great circle is precisely what I used to check if a point was inside SphericalPolygon3D. As for the intersection method, I will need to look a bit further, but they seem to be distinct.

@lewisn-met
Copy link
Author

On a slightly unrelated note, I notice that the linux intel-classic ci and gnu-14 ci are are currently failing for this PR. These failures seem to be related to the pluto-benchmark and either the SphericalVector or Cubic2D classes. Is this something you'd like me to take a look at?

@wdeconinck
Copy link
Member

wdeconinck commented Nov 7, 2025

On a slightly unrelated note, I notice that the linux intel-classic ci and gnu-14 ci are are currently failing for this PR. These failures seem to be related to the pluto-benchmark and either the SphericalVector or Cubic2D classes. Is this something you'd like me to take a look at?

My hunch is that it is due to an updated Eigen version 5.0!!! that happened under the hood without us realising:
linux intel-classic:

  -- Found Eigen3: /home/linuxbrew/.linuxbrew/share/eigen3/cmake/Eigen3Config.cmake (found version "5.0.0")

Any help to fix this is always welcome of course.

@wdeconinck
Copy link
Member

wdeconinck commented Nov 7, 2025

I have followed up on my hunch, and reverting the CI to use "eigen@3" seems to fix the compilation.
I have added this fix to develop and a 0.44.1 hotfix tag.

This is not a proper fix for compatibility Eigen 5.0.0 but perhaps these are infancy issues that will be sorted upstream in Eigen in due course. At least we can work without failing CI now.
Please rebase on develop.

@lewisn-met
Copy link
Author

lewisn-met commented Nov 10, 2025

I have now merged the fix from develop into this PR. Thanks for following your hunch so promptly - fingers crossed that any issues with Eigen 5. get sorted out soon!

I also noticed that the validate method included in your ConvexSphericalPolygon class nicely verifies the polygon is on the sphere and points are not too close. I could not see anywhere in the FiniteElement class that does a similar check, so it may be worth sharing this functionality between the methods.

@sbrdar
Copy link
Collaborator

sbrdar commented Nov 18, 2025

Hi @lewisn-met. Many thanks for this interesting development. It seems to me that SphericalPolygon3D can be class-derived from ConvexSphericalPolygon. Can we assume that SphericalPolygon3D has to be convex spherical polygon?

@lewisn-met
Copy link
Author

Hi @sbrdar, thank you for taking a look at this. Indeed, we can assume SphericalPolygon3D is convex, though it need not be (the weights are still positive for star-shaped polygons and well-defined but negative otherwise).

With regards to merging functionality, incorporating the methods of SphericalPolygon3D into ConvexSphericalPolygon, either directly or via inheritance, is certainly an option. The main differences/obstacles here are the representation of the polygon (SphericalPolygon3D requires the triple products from your inLeftHemisphere() function to be stored in addition to the vertices) and encapsulating what is actually necessary for each use case.

Hopefully we can discuss this soon.

@sbrdar
Copy link
Collaborator

sbrdar commented Nov 19, 2025

Hi @lewisn-met I am thinking that the Floater's barycentre could be used for the 2nd order cons. remapping in Atlas, so I cherry-picked your implementation with the unit test on SphericalVector3D, and added it to ConvexSphericalPolygon, see the usage here. Maybe something to discuss soon.

@lewisn-met
Copy link
Author

Would incorporating this method into the cons. remapping be in addition or as an alternative to using Floater's interpolation in the finite-element class?

Our original motivation for the algorithm was as a drop-in alternative to the Quad3D and Triag3D classes used for "continuous" interpolation, see the implementation here. However, I can see how Floater's ideas could be useful in both settings and I don't expect the use of ConvexSphericalPolygon in FiniteElement.cc to be too different from the branch I have linked.

@lewisn-met lewisn-met marked this pull request as draft December 12, 2025 16:08
@lewisn-met lewisn-met marked this pull request as ready for review January 6, 2026 10:25
@lewisn-met
Copy link
Author

@sbrdar I see there is an issue with the preview-publish/deploy action - can you confirm that this is an issue on your end and not with this PR itself? It appears to be related to the python authenticator being called by the job runner.

On a separate note, would you like me to merge from develop, since I see there are several commits which have not yet made their way onto this branch?

@sbrdar
Copy link
Collaborator

sbrdar commented Jan 9, 2026

@lewisn-met Thanks very much for the merger with ConvexSphericalPolygon - great work! The tests pass correctly. The failing unrelated static_analyzer problem we can ignore for now and continue with the merge. Can you in the very last step merge from the latest develop?

@lewisn-met
Copy link
Author

@sbrdar many thanks for your kind words, I'm glad you like the work! The latest ECMWF/develop has now been merged into this branch and we are good to go.

I have a few small improvements for the spherical mean-value interpolator in mind, but will raise those in a separate PR when I get the chance to work on it. Thanks again for all of your help!

Copy link
Collaborator

@sbrdar sbrdar left a comment

Choose a reason for hiding this comment

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

All good for the merge. Very nice results and code implementation. Many thanks!

config.get("type", interpType);
config.get("normalisation", normalisationMode);

SECTION("using " + interpType + " " + normalisationMode) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

A tiny comment here. This output can return, for instance, "using spherical-mean-value true" or "using spherical-mean-value false", but 'false'/'true' refers to normalisation. Can we have normalisationMode replaced with (normalisationMode ? "w/ normalisation" : "")?

config.get("type", interpType);
config.get("normalisation", normalisationMode);

SECTION("using " + interpType + " " + normalisationMode) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

A similar comment as before also here. This output can return, for instance, "using spherical-mean-value true" or "using spherical-mean-value false", but 'false'/'true' refers to normalisation. Can we have normalisationMode replaced with (normalisationMode ? "w/ normalisation" : "")?

}

gmshOutput("cubedsphere_source.msh", FieldSet(sourceField));
gmshOutput("cubedsphere_source_" + interpType + normalisationMode + ".msh", FieldSet(sourceField));
Copy link
Collaborator

Choose a reason for hiding this comment

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

A tiny suggestion. Perhaps one can replace normalisationMode with (normalisationMode ? "_normalised" : "")

targetFields.add(targetField);
targetFields.add(errorField);
targetFields.add(partField);
gmshOutput("cubedsphere_target_" + interpType + "_" + normalisationMode + ".msh", targetFields);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here as well. A tiny suggestion. Perhaps one can replace normalisationMode with (normalisationMode ? "_normalised" : "")

SECTION("test interpolation outputs") {
Field field_source = fs.createField<double>(option::name("source"));
Field field_target("target", array::make_datatype<double>(), array::make_shape(pointcloud.size()));
SECTION("using " + interpType + " " + normalisationMode) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

A tiny suggestion. Perhaps one can replace normalisationMode with (normalisationMode ? "_normalised" : "")

Copy link
Member

@wdeconinck wdeconinck left a comment

Choose a reason for hiding this comment

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

Hello @lewisn-met I think this is looks a good merge of your previous polygon and this one.
I have some questions and suggestions.

}

// cf. M. Floater, “Generalized barycentric coordinates and applications” Acta Numerica, p. 001, 2016.
std::optional<std::vector<double>> ConvexSphericalPolygon::compute_vertex_weights(
Copy link
Member

Choose a reason for hiding this comment

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

Why optional?
Since it is a dynamically sized vector, would it not be same when just passing an empty vector in the "not present" case?
And then in the client code check for vertex_weights.empty()

Copy link
Member

Choose a reason for hiding this comment

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

Actually ignore this, and see below for other suggested API, which I added as a review comment later.

ATLAS_ASSERT(size_ > 2, "Polygon must have at least 3 points");
switch (mode) {
case SMV:
validateAndComputeNormals();
Copy link
Member

Choose a reason for hiding this comment

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

validateAndComputeNormals() cannot be split into the following, reusing the already available validate?

validate();
compute_edge_normals();


template <class Points, typename std::enable_if<contains_PointLonLat<Points>::value, void>::type* = nullptr>
ConvexSphericalPolygon(const Points& points): ConvexSphericalPolygon(points.data(), points.size()) {}
ConvexSphericalPolygon(const Points& points, const interpolationMode mode = interpolationMode::GRID):
Copy link
Member

Choose a reason for hiding this comment

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

The interpolationMode to me seems fishy.
What is fundamentally different depending on the mode?
The computed area or centroid do not seem different, so why the mode? I would really like to avoid this.


size_t size() const { return size_; }

std::vector<PointXYZ> edge_normals() const { return edge_normals_; }
Copy link
Member

Choose a reason for hiding this comment

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

This can be computed lazily on demand, just like the area.
Perhaps that could avoid the use of mode.


int previous(const int index) const { return (index == 0) ? size_ - 1 : index - 1; };

std::optional<std::vector<double>> compute_vertex_weights(const PointXYZ& candidatePoint) const;
Copy link
Member

Choose a reason for hiding this comment

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

Another API may be desired and more performant, avoiding dynamic allocations:

int compute_vertex_weights(const PointXYZ& point, double vertex_weights[], size_t vertex_weights_size) {
  ATLAS_ASSERT(vertex_weights_size == size());
  ...
  return return_code; // instead of optional
}

The intended use would then be compatible with many data container types where the control of allocation is then on the client side.

std::vector<double> vertex_weights;
vertex_weights.reserve(max_size); // allocate outside of a loop
for (auto polygon : polygons ) {
    vertex_weights.resize( polygon.size() ); // no reallocation happening
    if (polygon.compute_vertex_weights( point, vertex_weights.data(), vertex_weights.size() ) == OK){
        // success, use vertex_weights
    }
}

or example with std::span (C++20):

std::array<double,1000> buffer;
for (auto polygon : polygons ) {
    std::span<double> vertex_weights(buffer.data(), polygon.size());
    if (polygon.compute_vertex_weights( point, vertex_weights.data(), vertex_weights.size() ) == OK) {
        // success, use vertex_weights
    }
}

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants