Skip to content
Closed
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d2e040a
Add integral and localintegral
juliohm Feb 27, 2026
fcb384f
Add tests from MeshIntegrals.jl
juliohm Mar 2, 2026
f1dd042
Increase default order to 100
juliohm Mar 2, 2026
9bd228c
Rename order --> n
juliohm Mar 2, 2026
55c1649
Propagate number type
juliohm Mar 2, 2026
98bdd41
Change default to n=3
juliohm Mar 2, 2026
8f3c1cf
Update tests
juliohm Mar 3, 2026
fcb05d3
Fix formatting issues
juliohm Mar 3, 2026
ecbcf48
Add integration method for ConeSurface
juliohm Mar 5, 2026
e5795f0
Add more special cases
juliohm Mar 5, 2026
da77b44
Add method for Domain
juliohm Mar 5, 2026
c5dd5ed
Fix typo
juliohm Mar 5, 2026
623861b
Remove method for Point
juliohm Mar 5, 2026
d5754f8
Add more tests
juliohm Mar 5, 2026
9252b6e
Fix Cylinder/CylinderSurface bottom and top
juliohm Mar 5, 2026
f9fd569
Minor refactor
juliohm Mar 5, 2026
a6d59f1
Refactor tests
juliohm Mar 5, 2026
00c6e9e
Add methods for geometries with infinite measure
juliohm Mar 6, 2026
4a612cf
Minor refactor
juliohm Mar 6, 2026
55fe6d9
Add TODO in test/integration.jl
juliohm Mar 6, 2026
07c68ae
Add more TODO tests
juliohm Mar 6, 2026
98e17fe
Add more implementations
juliohm Mar 6, 2026
0b09024
Fix formatting issues
juliohm Mar 6, 2026
29e4507
Fix composition in ray method
juliohm Mar 6, 2026
e903f57
fix CylinderSurface test
JoshuaLampert Mar 7, 2026
8e08a41
Apply suggestions from code review
JoshuaLampert Mar 7, 2026
433bf24
Remove localintegral methods
juliohm Mar 8, 2026
f014999
Add tests for infinite geometries
juliohm Mar 8, 2026
36c42d5
Remove 0 <= t <= 1 check from Chain
juliohm Mar 8, 2026
f46c538
Add more tests
juliohm Mar 8, 2026
5692ebd
Refactor change of variable
juliohm Mar 9, 2026
b52f395
Fix parametrization of Chain
juliohm Mar 9, 2026
7bd93a7
Fix tests for Rope and Ring
juliohm Mar 9, 2026
ffb13cc
Minor refactor
juliohm Mar 9, 2026
2a0089a
Relax rtol in Ring test
juliohm Mar 9, 2026
b56976e
Fix formatting issues
juliohm Mar 9, 2026
408c516
Add more tests
juliohm Mar 9, 2026
0b982e4
Fix formatting issues
juliohm Mar 9, 2026
5685d5e
Refactor trans kwarg for multivariate case
juliohm Mar 9, 2026
c6f2fa1
Add methods for simplices
juliohm Mar 9, 2026
32de14c
Add more tests
juliohm Mar 9, 2026
9daa5dd
Add method for Multi
juliohm Mar 9, 2026
947f967
Fix formatting issues
juliohm Mar 9, 2026
dd4082d
Add more tests
juliohm Mar 9, 2026
bcf0275
Incorporate suggestions
juliohm Mar 9, 2026
d316707
Add comments
juliohm Mar 17, 2026
afff1a5
Rely on IntegrationInterface.jl
juliohm Mar 17, 2026
f0acc81
Refactor in terms of backends
juliohm Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ CoordRefSystems = "b46f11dc-f210-4604-bfba-323c1ec968cb"
DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df"
DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63"
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838"
FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
Expand Down Expand Up @@ -38,6 +39,7 @@ CoordRefSystems = "0.19"
DelaunayTriangulation = "1.0"
DifferentiationInterface = "0.7"
Distances = "0.10"
FastGaussQuadrature = "1.1"
FiniteDifferences = "0.12"
LinearAlgebra = "1.10"
Makie = "0.24"
Expand Down
6 changes: 6 additions & 0 deletions src/Meshes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import NearestNeighbors: MinkowskiMetric
import DifferentiationInterface as DI
import FiniteDifferences: central_fdm

# Integration API
import FastGaussQuadrature: gausslegendre

# Transforms API
import TransformsBase: Transform, →
import TransformsBase: isrevertible, isinvertible
Expand Down Expand Up @@ -104,6 +107,7 @@ include("predicates.jl")

# calculus
include("differentation.jl")
include("integration.jl")

# operations
include("centroid.jl")
Expand Down Expand Up @@ -458,6 +462,8 @@ export
derivative,
jacobian,
differential,
integral,
localintegral,

# centroids
centroid,
Expand Down
2 changes: 1 addition & 1 deletion src/boundary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ boundary(::Circle) = nothing

embedboundary(c::Circle) = c

boundary(c::Cylinder) = CylinderSurface(bottom(c), top(c), radius(c))
boundary(c::Cylinder) = CylinderSurface(plane(bottom(c)), plane(top(c)), radius(c))

embedboundary(c::Cylinder) = boundary(c)

Expand Down
22 changes: 12 additions & 10 deletions src/geometries/polytopes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,22 @@ function angles(c::Chain)
end

function (c::Chain)(t)
if t < 0 || t > 1
throw(DomainError(t, "c(t) is not defined for t outside [0, 1]."))
end
segs = segments(c)
sums = cumsum(measure.(segs))
sums /= last(sums)
# find k such that sums[k] ≤ t < sums[k + 1]
k = searchsortedfirst(sums, t) - 1
# select segment s at index k
s, _ = iterate(segs, k)
# find first k such that t ≤ sums[k]
k = searchsortedfirst(sums, clamp(t, zero(t), one(t)))
# reparametrization of t within s
∑ₖ = iszero(k) ? zero(eltype(sums)) : sums[k]
∑ₖ₊₁ = sums[k + 1]
s((t - ∑ₖ) / (∑ₖ₊₁ - ∑ₖ))
if isone(k)
s = first(segs)
Σₖ = sums[k]
s(t / Σₖ)
else
s = first(Iterators.drop(segs, k - 1))
Σₖ = sums[k]
Σₖ₋₁ = sums[k - 1]
s((t - Σₖ₋₁) / (Σₖ - Σₖ₋₁))
end
end

# implementations of Chain
Expand Down
22 changes: 14 additions & 8 deletions src/geometries/primitives/cylinder.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ end

paramdim(::Type{<:Cylinder}) = 3

bottom(c::Cylinder) = c.bot
bottom(c::Cylinder) = Disk(c.bot, bottomradius(c))

top(c::Cylinder) = c.top
top(c::Cylinder) = Disk(c.top, topradius(c))

bottomradius(c::Cylinder) = norm(c(1, 0, 0) - c(0, 0, 0))

topradius(c::Cylinder) = norm(c(1, 0, 1) - c(0, 0, 1))

radius(c::Cylinder) = c.radius

Expand All @@ -76,10 +80,12 @@ Base.isapprox(c₁::Cylinder, c₂::Cylinder; atol=atol(lentype(c₁)), kwargs..
function (c::Cylinder)(ρ, φ, z)
ℒ = lentype(c)
T = numtype(ℒ)
t = top(c)
b = bottom(c)
r = radius(c)
a = axis(c)
C = crs(c)
D = datum(C)
b = c.bot
t = c.top
r = c.radius
a = Line(b(0, 0), t(0, 0))
d = a(T(1)) - a(T(0))

# rotation to align z axis with cylinder axis
Expand All @@ -91,8 +97,8 @@ function (c::Cylinder)(ρ, φ, z)
# project a parametric segment between the top and bottom planes
ρ′ = T(ρ) * r
φ′ = T(φ) * 2 * T(π) * u"rad"
p₁ = Point(convert(crs(c), Cylindrical(ρ′, φ′, zero(ℒ))))
p₂ = Point(convert(crs(c), Cylindrical(ρ′, φ′, norm(d))))
p₁ = Point(convert(C, Cylindrical{D}(ρ′, φ′, zero(ℒ))))
p₂ = Point(convert(C, Cylindrical{D}(ρ′, φ′, norm(d))))
l = Line(p₁, p₂) |> Affine(R, o)
s = Segment(l ∩ b, l ∩ t)
s(T(z))
Expand Down
29 changes: 16 additions & 13 deletions src/geometries/primitives/cylindersurface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,38 @@ end

paramdim(::Type{<:CylinderSurface}) = 2

bottom(c::CylinderSurface) = c.bot
bottom(c::CylinderSurface) = Disk(c.bot, bottomradius(c))

top(c::CylinderSurface) = c.top
top(c::CylinderSurface) = Disk(c.top, topradius(c))

bottomradius(c::CylinderSurface) = bottomradius(Cylinder(c.bot, c.top, c.radius))

topradius(c::CylinderSurface) = topradius(Cylinder(c.bot, c.top, c.radius))

radius(c::CylinderSurface) = c.radius

axis(c::CylinderSurface) = Line(bottom(c)(0, 0), top(c)(0, 0))
axis(c::CylinderSurface) = Line(c.bot(0, 0), c.top(0, 0))

# cylinder is right if axis is aligned with plane normals
function isright(c::CylinderSurface)
T = numtype(lentype(c))
a = axis(c)
d = a(T(1)) - a(T(0))
u = normal(bottom(c))
v = normal(top(c))
u = normal(c.bot)
v = normal(c.top)
isapproxzero(norm(d × u)) && isapproxzero(norm(d × v))
end

function hasintersectingplanes(c::CylinderSurface)
l = bottom(c)top(c)
!isnothing(l) && evaluate(Euclidean(), axis(c), l) < radius(c)
l = c.botc.top
!isnothing(l) && evaluate(Euclidean(), axis(c), l) < c.radius
end

==(c₁::CylinderSurface, c₂::CylinderSurface) =
bottom(c₁) == bottom(c₂) && top(c₁) == top(c₂) && radius(c₁) == radius(c₂)
==(c₁::CylinderSurface, c₂::CylinderSurface) = c₁.bot == c₂.bot && c₁.top == c₂.top && c₁.radius == c₂.radius

Base.isapprox(c₁::CylinderSurface, c₂::CylinderSurface; atol=atol(lentype(c₁)), kwargs...) =
isapprox(bottom(c₁), bottom(c₂); atol, kwargs...) &&
isapprox(top(c₁), top(c₂); atol, kwargs...) &&
isapprox(radius(c₁), radius(c₂); atol, kwargs...)
isapprox(c₁.bot, c₂.bot; atol, kwargs...) &&
isapprox(c₁.top, c₂.top; atol, kwargs...) &&
isapprox(c₁.radius, c₂.radius; atol, kwargs...)

(c::CylinderSurface)(φ, z) = Cylinder(bottom(c), top(c), radius(c))(1, φ, z)
(c::CylinderSurface)(φ, z) = Cylinder(c.bot, c.top, c.radius)(1, φ, z)
94 changes: 94 additions & 0 deletions src/integration.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
integral(fun, geom; n=3)

Calculate the integral over the `geom`etry of the `fun`ction that maps
[`Point`](@ref)s to values in a linear space.

integral(fun, dom; n=3)

Alternatively, calculate the integral over the `dom`ain (e.g., mesh) by
summing the integrals for each constituent geometry.

Polynomials of degree up to `2n-1` are integrated exactly.

See also [`localintegral`](@ref).
"""
integral(fun, geom::Geometry; n=3) = localintegral(fun ∘ geom, geom; n)

integral(fun, cylsurf::CylinderSurface; n=3) =
localintegral(fun ∘ cylsurf, cylsurf; n) + integral(fun, top(cylsurf); n) + integral(fun, bottom(cylsurf); n)

integral(fun, conesurf::ConeSurface; n=3) =
localintegral(fun ∘ conesurf, conesurf; n) + integral(fun, base(conesurf); n)

integral(fun, frustumsurf::FrustumSurface; n=3) =
localintegral(fun ∘ frustumsurf, frustumsurf; n) +
integral(fun, top(frustumsurf); n) +
integral(fun, bottom(frustumsurf); n)

integral(fun, multi::Multi; n=3) = sum(integral(fun, geom; n) for geom in parent(multi))

integral(fun, dom::Domain; n=3) = sum(integral(fun, geom; n) for geom in dom)

"""
localintegral(fun, geom; n=3)

Calculate the integral over the `geom`etry of the `fun`ction that maps
parametric coordinates `uvw` to values in a linear space.

Polynomials of degree up to `2n-1` are integrated exactly.

See also [`integral`](@ref).
"""
localintegral(fun, geom::Geometry; n=3) = _uvwintegral(fun, geom, n)

# ray is parametrized over [0, ∞] interval
localintegral(fun, ray::Ray; n=3) = _uvwintegral(fun, ray, n, trans=t -> @. t / (1 - t))

# line is parametrized over [-∞, ∞] interval
localintegral(fun, line::Line; n=3) = _uvwintegral(fun, line, n, trans=t -> @. log(t) - log(1 - t))

# plane is parametrized over [-∞, ∞] interval
localintegral(fun, plane::Plane; n=3) = _uvwintegral(fun, plane, n, trans=t -> @. log(t) - log(1 - t))

# triangle is parametrized with barycentric coordinates
localintegral(fun, tri::Triangle; n=3) = _uvwintegral(fun, tri, n, trans=t -> (t[1] * t[2], t[2] - t[1] * t[2]))

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


# tetrahedron is parametrized with barycentric coordinates
localintegral(fun, tetra::Tetrahedron; n=3) =
_uvwintegral(fun, tetra, n, trans=t -> (t[2] * t[3] - t[1] * t[2] * t[3], t[1] * t[2] * t[3], t[3] - t[2] * t[3]))

# -----------------
# HELPER FUNCTIONS
# -----------------

function _uvwintegral(fun, geom, n; trans=identity)
# parametric dimension and number type
N = paramdim(geom)
T = numtype(lentype(geom))

# Gauss-Legendre quadrature points and weights
ts, ws = gausslegendre(n)
tgrid = Iterators.product(ntuple(_ -> T.(ts), N)...)
wgrid = Iterators.product(ntuple(_ -> T.(ws), N)...)

# map quadrature points in [-1, 1] to parametric coordinates in [0, 1],
# and then map parametric coordinates in [0, 1] to uvw parametrization
g = trans ∘ (t -> @. (t + 1) / 2)

# compute integral with change of variable and differential element
Σwᵢfᵢ = sum(zip(tgrid, wgrid)) do (t, w)
uvw = g(t)
prod(w) * fun(uvw...) * differential(geom, uvw)
end

# adjust for change of variable
Σwᵢfᵢ / 2^N
end
Loading
Loading