Skip to content

Add integral and localintegral#1327

Closed
juliohm wants to merge 48 commits intomasterfrom
integration
Closed

Add integral and localintegral#1327
juliohm wants to merge 48 commits intomasterfrom
integration

Conversation

@juliohm
Copy link
Copy Markdown
Member

@juliohm juliohm commented Feb 27, 2026

Ports integral from MeshIntegrals.jl and adds localintegral for integration of functions that are defined locally in terms of parametric coordinates.

@JoshuaLampert do you have suggestions before I start writing tests?

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 27, 2026

Benchmark Results (Julia vlts)

Time benchmarks
master d316707... master / d316707...
clipping/SutherlandHodgman 3.68 ± 0.18 μs 3.71 ± 0.17 μs 0.992 ± 0.067
discretization/simplexify 0.669 ± 0.11 ms 0.647 ± 0.018 ms 1.03 ± 0.17
intersection/ray-triangle 0.05 ± 0.001 μs 0.05 ± 0 μs 1 ± 0.02
sideof/ring/large 6.53 ± 0.01 μs 6.53 ± 0.01 μs 1 ± 0.0022
sideof/ring/small 0.05 ± 0.001 μs 0.05 ± 0.001 μs 1 ± 0.028
topology/half-edge 2.71 ± 0.042 ms 2.69 ± 0.037 ms 1.01 ± 0.021
winding/mesh 16 ± 0.37 ms 16 ± 0.27 ms 1 ± 0.029
time_to_load 1.2 ± 0.016 s 1.17 ± 0.0031 s 1.02 ± 0.014
Memory benchmarks
master d316707... master / d316707...
clipping/SutherlandHodgman 0.053 k allocs: 4.97 kB 0.053 k allocs: 4.97 kB 1
discretization/simplexify 18.1 k allocs: 1.92 MB 18.1 k allocs: 1.92 MB 1
intersection/ray-triangle 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/large 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/small 0 allocs: 0 B 0 allocs: 0 B
topology/half-edge 18.1 k allocs: 2.92 MB 18.1 k allocs: 2.92 MB 1
winding/mesh 23.2 k allocs: 3.08 MB 23.2 k allocs: 3.08 MB 1
time_to_load 0.153 k allocs: 14.5 kB 0.153 k allocs: 14.5 kB 1

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 27, 2026

Benchmark Results (Julia v1)

Time benchmarks
master d316707... master / d316707...
clipping/SutherlandHodgman 2.69 ± 0.33 μs 2.71 ± 0.33 μs 0.989 ± 0.17
discretization/simplexify 0.389 ± 0.017 ms 0.391 ± 0.018 ms 0.994 ± 0.064
intersection/ray-triangle 0.05 ± 0.001 μs 0.06 ± 0.01 μs 0.833 ± 0.14
sideof/ring/large 6.79 ± 0.01 μs 6.84 ± 0.01 μs 0.993 ± 0.0021
sideof/ring/small 0.06 ± 0.001 μs 0.06 ± 0.001 μs 1 ± 0.024
topology/half-edge 2.75 ± 0.28 ms 2.76 ± 0.24 ms 0.997 ± 0.13
winding/mesh 15.4 ± 0.3 ms 15.4 ± 0.39 ms 1 ± 0.032
time_to_load 1.04 ± 0.0085 s 1.05 ± 0.0041 s 0.998 ± 0.009
Memory benchmarks
master d316707... master / d316707...
clipping/SutherlandHodgman 0.064 k allocs: 5.55 kB 0.064 k allocs: 5.55 kB 1
discretization/simplexify 0.0362 M allocs: 1.93 MB 0.0362 M allocs: 1.93 MB 1
intersection/ray-triangle 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/large 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/small 0 allocs: 0 B 0 allocs: 0 B
topology/half-edge 25.9 k allocs: 3.17 MB 25.9 k allocs: 3.17 MB 1
winding/mesh 0.0413 M allocs: 3.08 MB 0.0413 M allocs: 3.08 MB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.62%. Comparing base (0e6a436) to head (d316707).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1327      +/-   ##
==========================================
+ Coverage   85.50%   85.62%   +0.12%     
==========================================
  Files         199      200       +1     
  Lines        6376     6409      +33     
==========================================
+ Hits         5452     5488      +36     
+ Misses        924      921       -3     

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

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Feb 27, 2026

Regarding support for multiple quadrature libraries, we need to think if it is really necessary. If it is necessary, we could start by creating an IntegrationInterface.jl package after this PR is merged, similar to the DifferentiationInterface.jl approach.

@JoshuaLampert
Copy link
Copy Markdown
Member

JoshuaLampert commented Feb 27, 2026

If it is necessary, we could start by creating an IntegrationInterface.jl package after this PR is merged, similar to the DifferentiationInterface.jl approach.

There is Integrals.jl, which basically provides exactly that.

@JoshuaLampert
Copy link
Copy Markdown
Member

Regarding support for multiple quadrature libraries, we need to think if it is really necessary.

I think it would be nice because Gauss-Kronrod should be more efficient to get a certain accuracy in 1D and H-adaptive cubature should be better suited in multi D.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Feb 27, 2026

There is Integrals.jl, which basically provides exactly that.

I saw it, but it is not at the same level of DifferentiationInterface.jl. It includes a bunch of dependencies, including backends and SciML stuff.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Feb 27, 2026

Please feel free to add tests if you have some already written in MeshIntegrals.jl.

I will try to continue the work over the weekend, and next week we should have a final version to review 👍🏽

Copy link
Copy Markdown
Member

@JoshuaLampert JoshuaLampert left a comment

Choose a reason for hiding this comment

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

I have two questions:

  • How do we deal with the specializations? Do we add them in (a) later PR(s)?
  • How should we write tests? In MeshIntegrals.jl we have quite extensive tests, which take some time though.
    • Should we have tests for every (supported) geometry? I think we should because just because the integral works fine for one or a few geometries does not mean it works for all, see the need for the specializations. I think we can use the non-trivial (i.e., we do not just integrate a constant function) analytical tests we have in the test suite of MeshIntegrals.jl.
    • Should we use some test infrastructure to automate the testing and reduce code duplication and redundancy like in MeshIntegrals, see here or should we rather go with a straightforward approach and testing each geometry individually?

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 1, 2026

  • How do we deal with the specializations? Do we add them in (a) later PR(s)?

We should add them here as well. I will try to find the time tomorrow to finish up the additions 👍🏽

  • Should we have tests for every (supported) geometry?

Yes. We should have full coverage.

  • Should we use some test infrastructure to automate the testing and reduce code duplication and redundancy like in MeshIntegrals, see here or should we rather go with a straightforward approach and testing each geometry individually?

I think we can be more straightforward testing each geometry individually. It gives us more freedom to add more tests later without affecting all other tests.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 2, 2026

@JoshuaLampert I've ported some tests from MeshIntegrals.jl to see if everything was working as expected, but the results are not matching. Do you have any idea of what might be happening? Could it be related to the default backend? Should we adjust the tests?

@JoshuaLampert
Copy link
Copy Markdown
Member

JoshuaLampert commented Mar 2, 2026

@JoshuaLampert I've ported some tests from MeshIntegrals.jl to see if everything was working as expected, but the results are not matching. Do you have any idea of what might be happening? Could it be related to the default backend? Should we adjust the tests?

They are very likely failing due to the low number of Gauss-Legendre points you are using by default, i.e., related to your comment #1327 (comment). I think for general non-linear functions, we should provide a much higher value for order than 1 or 2. In the tests of MeshIntegrals.jl we use 100. Otherwise we can have big errors, which is normal and expected. Maybe 100 is a bit too much, but 50 maybe is enough?

@JoshuaLampert
Copy link
Copy Markdown
Member

JoshuaLampert commented Mar 2, 2026

Also I just see from the failing Float32 tests, e.g., here that integral does not preserve the floating point type, i.e., the result is in Float64 even though the input is Float32. I think we want the same datatype as output as we had for the geometry, right?
Edit: Ah, I now remember that we had a kwarg FP for this in MeshIntegrals.jl. Maybe this is not strictly necessary to add now.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 2, 2026

Maybe 100 is a bit too much, but 50 maybe is enough?

Got it! I will try to find a small enough number that gives us a good default for moderately nonlinear functions.

I think we want the same datatype as output as we had for the geometry, right?

We want to preserve the type of the input. I thought the implementation would guarantee that, but maybe the backend is promoting the arguments? I will double check.

@JoshuaLampert
Copy link
Copy Markdown
Member

I think we want the same datatype as output as we had for the geometry, right?

We want to preserve the type of the input. I thought the implementation would guarantee that, but maybe the backend is promoting the arguments? I will double check.

I think the problem is that the Gauss-Legendre nodes and weights provided by FastGaussQuadrature.jl are Float64. We would need to convert them to the datatype we want before performing the quadrature.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 2, 2026

I've increased the default order to 100 to match what you had in MeshIntegrals.jl, but the tests are still failing. I think 100 is too much, but we need to figure out why the tests are not passing.

After we sort out the source of the issue, we could pick something like 3 by default (exact approximation of polynomials of degree 5). It is hard to imagine applications where this default wouldn't be enough.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 2, 2026

Could the change in results be related to the change of variable formula? I used t -> (t+1)/2 but MeshIntegrals.jl was using rationals here.

@JoshuaLampert
Copy link
Copy Markdown
Member

Could the change in results be related to the change of variable formula? I used t -> (t+1)/2 but MeshIntegrals.jl was using rationals here.

Hm, currently, I don't see another semantic difference between the two implementations. So give it a try?

@JoshuaLampert
Copy link
Copy Markdown
Member

JoshuaLampert commented Mar 2, 2026

What is different is the finite difference scheme used to compute the differential. This could of course lead to numerical differences to the previous version, but I would have hoped that the scheme we have here based on FiniteDifferences.jl is a bit more accurate than the custom one from MeshIntegrals.jl.
Oh, now that I am writing this I see, we use a custom rtol in isapprox, see here and here. Maybe try that.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 2, 2026

we use a custom rtol in isapprox

That was it. I will adjust the tests to be more explicit about the n and rtol used.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 9, 2026

@JoshuaLampert we have all methods in place. The tests for Ray, Line, Plane, Triangle and Tetrahedron are failing. They are precisely the ones that require custom change of variable. Any idea of what we are missing?

Copy link
Copy Markdown
Member

@JoshuaLampert JoshuaLampert left a comment

Choose a reason for hiding this comment

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

Thanks! This looks mostly good to me. I left a few minor suggestions/questions below.

  • A test for SimpleMesh is still missing.
  • Should we also add a test that the floating point type is preserved by integral or do you think this is already implicitly tested for by the accuracy tests?
  • I don't see right now why the geometries, which need a custom transformation fail. It seems like they have the same reason why they fail. So to debug this I would pick the simplest one (I guess Ray), add some debug statements at different steps in Meshes.jl and MeshIntegrals.jl and see where they differ to see what might be the problem. Did you already try something like this? I do not have the time to do that right now though (I am on vacation). Otherwise maybe AI also has an idea?

Comment on lines +61 to +62
# specialize quadrangle for performance
localintegral(fun, quad::Quadrangle; n=3) = _uvwintegral(fun, quad, n)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why does this specialization improve performance?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We had a method for general Polygon that got erased. The method for general polygon relied on discretization, and this method for Quadrangle relies on direct sampling with the parametric function. I will review the code to see if this method is still necessary.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 9, 2026

  • A test for SimpleMesh is still missing.

Will add it right after we fix Triangle and Tetrahedron

  • Should we also add a test that the floating point type is preserved by integral or do you think this is already implicitly tested for by the accuracy tests?

I think we can assume the implementation preserves the number type.

  • I don't see right now why the geometries, which need a custom transformation fail. It seems like they have the same reason why they fail. So to debug this I would pick the simplest one (I guess Ray), add some debug statements at different steps in Meshes.jl and MeshIntegrals.jl and see where they differ to see what might be the problem.

Will check that.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026

The results are very sensitive to the transformation. Replacing t/(1-t) by -log(1-t) in the Ray case, leads to a 0.2 difference. I wonder how we could pick good defaults. I am discussing with @pablosanjose over Zulip to see if we can come up with a robust API against these transformation choices.

@JoshuaLampert
Copy link
Copy Markdown
Member

I see, but the different transformations you use here compared to MeshIntegrals.jl alone is not the reason for the broken tests, are they? I would first like to fix the bug we still have here (by using the same transformations we had in MeshIntegrals.jl and comparing with the results we get there) before trying different transformations.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026 via email

@JoshuaLampert
Copy link
Copy Markdown
Member

JoshuaLampert commented Mar 17, 2026

I think I have an idea what the issue with the current transformation implementation is. We need to take the Jacobian of the transformation into account. The way this works in MeshIntegrals.jl is that we create a helper geometry _ParametricGeometry, which has the transformation built in, i.e., when we integrate, say, a Ray the geometry we integrate is actually a _ParametricGeometry, which in turn means differential(geom, uvw) also takes into account the transformed geometry. This ensures we apply the correct Jacobian, which involves both Jacobians: One from the transformation from [0, 1] to whatever domain the parametric function uses and the other Jacobian from the parametric function itself. It's currently not that straightforward to fix that here because we would need to pass the transformed geometry to differential, which has geom ∘ trans as parametric function (we cannot simply pass geom ∘ trans because differential and functions therein are specialized on Geoemetry). I see a few solutions:

  • Use a similar approach as MeshIntegrals.jl with a new geometry.
  • Define parametric functions for the geometries in question mapping from [0,1] as the other geometries. We would need a good interface for that to differentiate the two versions.
  • Keep differential(geom, uvw), but additionally explicitly multiply by the Jacobian of the transformation.
  • Wait for IntegrationInterface.jl and use the transformations from there, i.e., pass the boundaries appropriately. I think this should also fix this issue.

Probably the easiest is the fourth option. Edit: Ah, now I see your message above. Looks like we go with option 4 😄

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026 via email

@JoshuaLampert
Copy link
Copy Markdown
Member

I'm planning to refactor the PR in terms of the IntegrationInterface.jl draft. That way we make sure things are working as expected before the package is registered. After registration we could adjust the env, run tests, review and merge.

Sounds like a good plan.

That way we don't waste time debugging code that will be discarded later anyways.

Yeah, I just did a bit of debugging and I am at least happy that now I have an understanding of why it was failing before (I would have been pretty unsatisfied if I would have never find out what the issue was 😅). But yes, having this handled in IntegrationInterface.jl is the cleanest and easiest solution on our side. I am pretty confident that this will automatically be fixed once we switch to IntegrationInterface.jl since it seems that Pablo figured out a reasonable implementation for transformations also for unbounded domains. So let's keep fingers crossed. I'm excited to see a draft using IntegrationInterface.jl in this PR.

@JoshuaLampert
Copy link
Copy Markdown
Member

Thanks for starting the IntegrationInterface.jl implementation. Two suggestions/questions:

  • Could you maybe put the changes regarding returning Disk for bottom and top and all related changes in a separate PR please, @juliohm? This makes it easier to review. This should be mostly unrelated to the rest of the PR.
  • How do you plan to run CI with IntegrationInterface.jl before it is registered? We could add it as [sources] in Project.toml, but this only works for the tests running Julia 1 not running Julia lts, but would be simple enough to do and help to see if it runs. Otherwise we would need a bit more CI fiddling I think.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026

  • Could you maybe put the changes regarding returning Disk for bottom and top and all related changes in a separate PR please, @juliohm? This makes it easier to review. This should be mostly unrelated to the rest of the PR.

Will do. If you can help later rebasing this PR on top of the latest master branch after the other PR is merged, that would be great. I always mess up with Git rebase 😮‍💨

  • How do you plan to run CI with IntegrationInterface.jl before it is registered?

I was planning to wait for the registration. This PR would only be merged after the dependency is ready. What do you think?

@JoshuaLampert
Copy link
Copy Markdown
Member

  • Could you maybe put the changes regarding returning Disk for bottom and top and all related changes in a separate PR please, @juliohm? This makes it easier to review. This should be mostly unrelated to the rest of the PR.

Will do. If you can help later rebasing this PR on top of the latest master branch after the other PR is merged, that would be great. I always mess up with Git rebase 😮‍💨

Because I would mess up a proper Git rebase, I would probably do it manually 😅. I.e., just create a new branch, add and commit the corresponding code to that branch (should be doable manually because AFAICT, it affects everything in this PR except src/integrals.jl, src/Meshes.jl, test/integration.jl, and Project.toml; so it should be easy to separate), create and merge that PR, and then merge master into this PR (potentially fix merge conflicts).

  • How do you plan to run CI with IntegrationInterface.jl before it is registered?

I was planning to wait for the registration. This PR would only be merged after the dependency is ready. What do you think?

I think it would be nice to test already earlier. Using the [sources] approach should be straightforward and we already get the tests for julia v1, which should be mostly representative. Then we can already give early feedback to IntegrationInterface.jl and potentially fix issues before registration/as early as possible. I see you created already pablosanjose/IntegrationInterface.jl#21.

@juliohm juliohm mentioned this pull request Mar 17, 2026
@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026

I think it would be nice to test already earlier.

Notice that this PR already adds ] add https://github.com/pablosanjose/IntegrationInterface.jl directly from the URL to the Project.toml. I believe it tracks the master branch and tests should run fine. The errors we are getting currently are due to limitations in IntegrationInterface.jl reported in pablosanjose/IntegrationInterface.jl#21

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026

After #1332 is merged, I will try to ask GitHub Copilot to rebase for me. It should be better at Git for sure 🤣

@JoshuaLampert
Copy link
Copy Markdown
Member

Notice that this PR already adds ] add https://github.com/pablosanjose/IntegrationInterface.jl directly from the URL to the Project.toml.

Am I missing something? I don't see that.

The errors we are getting currently are due to limitations in IntegrationInterface.jl reported in pablosanjose/IntegrationInterface.jl#21

The errors I currently see in CI are

ERROR: LoadError: expected package `IntegrationInterface [03ca86a4]` to be registered

see https://github.com/JuliaGeometry/Meshes.jl/actions/runs/23212027058/job/67463028776?pr=1327.

@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026

Oh I see, there must be some difference between the GitHub Action and the local tests I am running. Locally I can execute the code just fine. My Project.toml+Manifest.toml are pointing to the URL on GitHub.

@JoshuaLampert
Copy link
Copy Markdown
Member

Yeah, probably that is in the Manifest.toml.

juliohm added a commit that referenced this pull request Mar 17, 2026
@juliohm
Copy link
Copy Markdown
Member Author

juliohm commented Mar 17, 2026

Closing in favor of #1333

@juliohm juliohm closed this Mar 17, 2026
@juliohm juliohm deleted the integration branch March 17, 2026 20:38
juliohm added a commit that referenced this pull request Mar 28, 2026
* Copy files from #1327

* Minor adjustments

* Update tests

* Update tests

* Update default method

* Add tests

* Add tests

* Fix integrand

* Update tests

* add IntegrationInterface.jl to [sources]

* most tests not broken anymore

* test for plane passes with rtol = 1e-3

* Decompose Rope and Ring into Segment parts

* update to new integral interface

* Strip units from integrand to help backends

* Undo strip units

* Use HAdaptiveIntegration

* Minor refactor

* Update tests

* Update tests

* Use rtol in HAdaptiveIntegration backend

* Uncomment test with PolyArea

* remove sources section again

* format

---------

Co-authored-by: Joshua Lampert <joshua.lampert@uni-hamburg.de>
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