Skip to content

Commit a375579

Browse files
mikeingoldgithub-actions[bot]JoshuaLampert
authored
Implement _ParametricGeometry (#138)
* Add a discrete specializations section * Add Unitful and explicit julia compat * Implement _ParametricGeometry and use it for BezierCurve and Tetrahedron * Update Tetrahedron tests * Update tests * Update a remnant usage of zeros * Fix Unitful integrand handling in Triangle HAdaptiveCubature method * Add missing reapplication of units * Remove extended tag from Tetrahedron tests * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Add tests for _parametric bounds checks * Fix typo * Fix typo * Add checks for t1 and t2 args of _parametric Tetrahedron * Update Tetrahedron status * Apply format suggestion Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Joshua Lampert <[email protected]> * Update src/specializations/BezierCurve.jl Co-authored-by: Joshua Lampert <[email protected]> * Update src/specializations/Tetrahedron.jl Co-authored-by: Joshua Lampert <[email protected]> * Apply suggestions from code review Co-authored-by: Joshua Lampert <[email protected]> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Joshua Lampert <[email protected]>
1 parent 3635a37 commit a375579

File tree

11 files changed

+221
-152
lines changed

11 files changed

+221
-152
lines changed

benchmark/Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
33
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
44
Meshes = "eacbb407-ea5a-433e-ab97-5258b1ca43fa"
5+
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
56

67
[compat]
78
BenchmarkTools = "1.5"
89
LinearAlgebra = "1"
910
Meshes = "0.50, 0.51, 0.52"
11+
Unitful = "1.19"
12+
julia = "1.9"

benchmark/benchmarks.jl

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ using BenchmarkTools
22
using LinearAlgebra
33
using Meshes
44
using MeshIntegrals
5+
using Unitful
56

67
const SUITE = BenchmarkGroup()
78

@@ -19,10 +20,8 @@ rules = (
1920
(name = "HAdaptiveCubature", rule = HAdaptiveCubature(), N = 500)
2021
)
2122
geometries = (
22-
(name = "Meshes.BezierCurve",
23-
item = BezierCurve([Point(t, sin(t), 0.0) for t in range(-π, π, length = 361)])),
24-
(name = "Meshes.Segment", item = Segment(Point(0, 0, 0), Point(1, 1, 1))),
25-
(name = "Meshes.Sphere", item = Sphere(Point(0, 0, 0), 1.0))
23+
(name = "Segment", item = Segment(Point(0, 0, 0), Point(1, 1, 1))),
24+
(name = "Sphere", item = Sphere(Point(0, 0, 0), 1.0))
2625
)
2726

2827
SUITE["Integrals"] = let s = BenchmarkGroup()
@@ -33,6 +32,43 @@ SUITE["Integrals"] = let s = BenchmarkGroup()
3332
s
3433
end
3534

35+
############################################################################################
36+
# Specializations
37+
############################################################################################
38+
39+
spec = (
40+
f = p -> norm(to(p)),
41+
f_exp = p::Point -> exp(-norm(to(p))^2 / u"m^2"),
42+
g = (
43+
bezier = BezierCurve([Point(t, sin(t), 0) for t in range(-pi, pi, length = 361)]),
44+
line = Line(Point(0, 0, 0), Point(1, 1, 1)),
45+
plane = Plane(Point(0, 0, 0), Vec(0, 0, 1)),
46+
ray = Ray(Point(0, 0, 0), Vec(0, 0, 1)),
47+
triangle = Triangle(Point(1, 0, 0), Point(0, 1, 0), Point(0, 0, 1)),
48+
tetrahedron = let
49+
a = Point(0, 3, 0)
50+
b = Point(-7, 0, 0)
51+
c = Point(8, 0, 0)
52+
= Vec(0, 0, 1)
53+
Tetrahedron(a, b, c, a + ẑ)
54+
end
55+
),
56+
rule_gl = GaussLegendre(100),
57+
rule_gk = GaussKronrod(),
58+
rule_hc = HAdaptiveCubature()
59+
)
60+
61+
SUITE["Specializations/Scalar GaussLegendre"] = let s = BenchmarkGroup()
62+
s["BezierCurve"] = @benchmarkable integral($spec.f, $spec.g.bezier, $spec.rule_gl)
63+
s["Line"] = @benchmarkable integral($spec.f_exp, $spec.g.line, $spec.rule_gl)
64+
s["Plane"] = @benchmarkable integral($spec.f_exp, $spec.g.plane, $spec.rule_gl)
65+
s["Ray"] = @benchmarkable integral($spec.f_exp, $spec.g.ray, $spec.rule_gl)
66+
s["Triangle"] = @benchmarkable integral($spec.f, $spec.g.triangle, $spec.rule_gl)
67+
# s["Tetrahedron"] = @benchmarkable integral($spec.f, $spec.g.tetrahedron, $spec.rule)
68+
# TODO re-enable Tetrahedron-GaussLegendre test when supported by main branch
69+
s
70+
end
71+
3672
############################################################################################
3773
# Differentials
3874
############################################################################################

docs/src/supportmatrix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ using an equivalent H-Adaptive Cubature rule, so support for this usage is not r
5555
| `SimpleMesh` | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/27) | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/27) | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/27) |
5656
| `Sphere` in `𝔼{2}` ||||
5757
| `Sphere` in `𝔼{3}` || ⚠️ ||
58-
| `Tetrahedron` in `𝔼{3}` | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/40) | | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/40) |
58+
| `Tetrahedron` | | ⚠️ ||
5959
| `Triangle` ||||
6060
| `Torus` || ⚠️ ||
6161
| `Wedge` | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/28) | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/28) | [🎗️](https://github.com/JuliaGeometry/MeshIntegrals.jl/issues/28) |

src/MeshIntegrals.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ include("integral_aliases.jl")
2424
export lineintegral, surfaceintegral, volumeintegral
2525

2626
# Integration methods specialized for particular geometries
27+
include("specializations/_ParametricGeometry.jl")
2728
include("specializations/BezierCurve.jl")
2829
include("specializations/ConeSurface.jl")
2930
include("specializations/CylinderSurface.jl")

src/integral.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function _integral(
104104

105105
# HCubature doesn't support functions that output Unitful Quantity types
106106
# Establish the units that are output by f
107-
testpoint_parametriccoord = zeros(FP, N)
107+
testpoint_parametriccoord = _zeros(FP, N)
108108
integrandunits = Unitful.unit.(integrand(testpoint_parametriccoord))
109109
# Create a wrapper that returns only the value component in those units
110110
uintegrand(ts) = Unitful.ustrip.(integrandunits, integrand(ts))

src/specializations/BezierCurve.jl

Lines changed: 13 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -34,93 +34,22 @@ steep performance cost, especially for curves with a large number of control poi
3434
function integral(
3535
f,
3636
curve::Meshes.BezierCurve,
37-
rule::GaussLegendre;
38-
diff_method::DM = _default_method(curve),
39-
FP::Type{T} = Float64,
40-
alg::Meshes.BezierEvalMethod = Meshes.Horner()
41-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
42-
# Compute Gauss-Legendre nodes/weights for x in interval [-1,1]
43-
xs, ws = _gausslegendre(FP, rule.n)
44-
45-
# Change of variables: x [-1,1] ↦ t [0,1]
46-
t(x) = (1 // 2) * x + (1 // 2)
47-
point(x) = curve(t(x), alg)
48-
integrand(x) = f(point(x)) * differential(curve, (t(x),), diff_method)
49-
50-
# Integrate f along curve and apply domain-correction for [-1,1] ↦ [0, length]
51-
return (1 // 2) * sum(w .* integrand(x) for (w, x) in zip(ws, xs))
52-
end
53-
54-
function integral(
55-
f,
56-
curve::Meshes.BezierCurve,
57-
rule::GaussKronrod;
58-
diff_method::DM = _default_method(curve),
59-
FP::Type{T} = Float64,
60-
alg::Meshes.BezierEvalMethod = Meshes.Horner()
61-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
62-
point(t) = curve(t, alg)
63-
integrand(t) = f(point(t)) * differential(curve, (t,), diff_method)
64-
return QuadGK.quadgk(integrand, zero(FP), one(FP); rule.kwargs...)[1]
65-
end
66-
67-
function integral(
68-
f,
69-
curve::Meshes.BezierCurve,
70-
rule::HAdaptiveCubature;
71-
diff_method::DM = _default_method(curve),
72-
FP::Type{T} = Float64,
73-
alg::Meshes.BezierEvalMethod = Meshes.Horner()
74-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
75-
point(t) = curve(t, alg)
76-
integrand(ts) = f(point(only(ts))) * differential(curve, ts, diff_method)
77-
78-
# HCubature doesn't support functions that output Unitful Quantity types
79-
# Establish the units that are output by f
80-
testpoint_parametriccoord = zeros(FP, 1)
81-
integrandunits = Unitful.unit.(integrand(testpoint_parametriccoord))
82-
# Create a wrapper that returns only the value component in those units
83-
uintegrand(ts) = Unitful.ustrip.(integrandunits, integrand(ts))
84-
# Integrate only the unitless values
85-
value = HCubature.hcubature(uintegrand, _zeros(FP, 1), _ones(FP, 1); rule.kwargs...)[1]
86-
87-
# Reapply units
88-
return value .* integrandunits
37+
rule::IntegrationRule;
38+
alg::Meshes.BezierEvalMethod = Meshes.Horner(),
39+
kwargs...
40+
)
41+
# Generate a _ParametricGeometry whose parametric function auto-applies the alg kwarg
42+
paramfunction(t) = _parametric(curve, t, alg)
43+
param_curve = _ParametricGeometry(paramfunction, Meshes.paramdim(curve))
44+
45+
# Integrate the _ParametricGeometry using the standard methods
46+
return _integral(f, param_curve, rule; kwargs...)
8947
end
9048

9149
################################################################################
92-
# jacobian
50+
# Parametric
9351
################################################################################
9452

95-
function jacobian(
96-
bz::Meshes.BezierCurve,
97-
ts::V,
98-
diff_method::Analytical
99-
) where {V <: Union{AbstractVector, Tuple}}
100-
t = only(ts)
101-
# Parameter t restricted to domain [0,1] by definition
102-
if t < 0 || t > 1
103-
throw(DomainError(t, "b(t) is not defined for t outside [0, 1]."))
104-
end
105-
106-
# Aliases
107-
P = bz.controls
108-
N = Meshes.degree(bz)
109-
110-
# Ensure that this implementation is tractible: limited by ability to calculate
111-
# binomial(N, N/2) without overflow. It's possible to extend this range by
112-
# converting N to a BigInt, but this results in always returning BigFloat's.
113-
N <= 1028 || error("This algorithm overflows for curves with ⪆1000 control points.")
114-
115-
# Generator for Bernstein polynomial functions
116-
B(i, n) = t -> binomial(Int128(n), i) * t^i * (1 - t)^(n - i)
117-
118-
# Derivative = N Σ_{i=0}^{N-1} sigma(i)
119-
# P indices adjusted for Julia 1-based array indexing
120-
sigma(i) = B(i, N - 1)(t) .* (P[(i + 1) + 1] - P[(i) + 1])
121-
derivative = N .* sum(sigma, 0:(N - 1))
122-
123-
return (derivative,)
53+
function _parametric(curve::Meshes.BezierCurve, t, alg::Meshes.BezierEvalMethod)
54+
return curve(t, alg)
12455
end
125-
126-
_has_analytical(::Type{T}) where {T <: Meshes.BezierCurve} = true

src/specializations/Tetrahedron.jl

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,44 +12,32 @@
1212
function integral(
1313
f,
1414
tetrahedron::Meshes.Tetrahedron,
15-
rule::GaussLegendre;
16-
diff_method::DM = Analytical(),
17-
FP::Type{T} = Float64
18-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
19-
_error_unsupported_combination("Tetrahedron", "GaussLegendre")
20-
end
21-
22-
function integral(
23-
f,
24-
tetrahedron::Meshes.Tetrahedron,
25-
rule::GaussKronrod;
26-
diff_method::DM = Analytical(),
27-
FP::Type{T} = Float64
28-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
29-
_guarantee_analytical(Meshes.Tetrahedron, diff_method)
30-
31-
o = zero(FP)
32-
∫uvw(u, v, w) = f(tetrahedron(u, v, w))
33-
∫vw(v, w) = QuadGK.quadgk(u -> ∫uvw(u, v, w), o, FP(1 - v - w); rule.kwargs...)[1]
34-
∫w(w) = QuadGK.quadgk(v -> ∫vw(v, w), o, FP(1 - w); rule.kwargs...)[1]
35-
= QuadGK.quadgk(∫w, o, one(FP); rule.kwargs...)[1]
36-
37-
# Apply barycentric domain correction (volume: 1/6 → actual)
38-
return 6 * Meshes.volume(tetrahedron) *
39-
end
15+
rule::IntegrationRule;
16+
kwargs...
17+
)
18+
# Generate a _ParametricGeometry whose parametric function domain spans [0,1]^3
19+
paramfunction(t1, t2, t3) = _parametric(tetrahedron, t1, t2, t3)
20+
tetra = _ParametricGeometry(paramfunction, Meshes.paramdim(tetrahedron))
4021

41-
function integral(
42-
f,
43-
tetrahedron::Meshes.Tetrahedron,
44-
rule::HAdaptiveCubature;
45-
diff_method::DM = Analytical(),
46-
FP::Type{T} = Float64
47-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
48-
_error_unsupported_combination("Tetrahedron", "HAdaptiveCubature")
22+
# Integrate the _ParametricGeometry using the standard methods
23+
return _integral(f, tetra, rule; kwargs...)
4924
end
5025

5126
################################################################################
52-
# jacobian
27+
# Parametric
5328
################################################################################
5429

55-
_has_analytical(::Type{T}) where {T <: Meshes.Tetrahedron} = true
30+
function _parametric(tetrahedron::Meshes.Tetrahedron, t1, t2, t3)
31+
if (t3 < 0) || (t3 > 1)
32+
msg = "tetrahedron(t1, t2, t3) is not defined for t3 outside [0, 1]."
33+
throw(DomainError(t3, msg))
34+
end
35+
36+
# Take a triangular cross-section at t3
37+
a = tetrahedron(t3, 0, 0)
38+
b = tetrahedron(0, t3, 0)
39+
c = tetrahedron(0, 0, t3)
40+
cross_section = Meshes.Triangle(a, b, c)
41+
42+
return _parametric(cross_section, t1, t2)
43+
end

src/specializations/Triangle.jl

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,40 @@ function integral(
8787
v = R * (1 - b / (a + b))
8888
return f(triangle(u, v)) * R / (a + b)^2
8989
end
90-
= HCubature.hcubature(integrand, _zeros(FP, 2), (FP(1), FP/ 2)), rule.kwargs...)[1]
90+
91+
# HCubature doesn't support functions that output Unitful Quantity types
92+
# Establish the units that are output by f
93+
testpoint_parametriccoord = _zeros(T, 2)
94+
integrandunits = Unitful.unit.(integrand(testpoint_parametriccoord))
95+
# Create a wrapper that returns only the value component in those units
96+
uintegrand(ts) = Unitful.ustrip.(integrandunits, integrand(ts))
97+
# Integrate only the unitless values
98+
= HCubature.hcubature(uintegrand, _zeros(FP, 2), (T(1), T/ 2)), rule.kwargs...)[1]
9199

92100
# Apply a linear domain-correction factor 0.5 ↦ area(triangle)
93-
return 2 * Meshes.area(triangle) .*
101+
return 2 * Meshes.area(triangle) * integrandunits .*
94102
end
95103

96104
################################################################################
97105
# jacobian
98106
################################################################################
99107

100108
_has_analytical(::Type{T}) where {T <: Meshes.Triangle} = true
109+
110+
################################################################################
111+
# Parametric
112+
################################################################################
113+
114+
function _parametric(triangle::Meshes.Triangle, t1, t2)
115+
if (t1 < 0 || t1 > 1) || (t2 < 0 || t2 > 1)
116+
msg = "triangle(t1, t2) is not defined for (t1, t2) outside [0, 1]^2."
117+
throw(DomainError((t1, t2), msg))
118+
end
119+
120+
# Form a line segment between sides
121+
a = triangle(0, t2)
122+
b = triangle(t2, 0)
123+
segment = Meshes.Segment(a, b)
124+
125+
return segment(t1)
126+
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
_ParametricGeometry <: Meshes.Primitive <: Meshes.Geometry
3+
4+
_ParametricGeometry is used internally in MeshIntegrals.jl to behave like a generic wrapper
5+
for geometries with custom parametric functions. This type is used for transforming other
6+
geometries to enable integration over the standard rectangular `[0,1]^n` domain.
7+
8+
Meshes.jl adopted a `ParametrizedCurve` type that performs a similar role as of `v0.51.20`,
9+
but only supports geometries with one parametric dimension. Support is additionally planned
10+
for more types that span surfaces and volumes, at which time this custom type will probably
11+
no longer be required.
12+
13+
# Fields
14+
- `fun` - anything callable representing a parametric function: (ts...) -> Meshes.Point
15+
16+
# Type Structure
17+
- `M <: Meshes.Manifold` - same usage as in `Meshes.Geometry{M, C}`
18+
- `C <: CoordRefSystems.CRS` - same usage as in `Meshes.Geometry{M, C}`
19+
- `F` - type of the callable parametric function
20+
- `Dim` - number of parametric dimensions
21+
"""
22+
struct _ParametricGeometry{M <: Meshes.Manifold, C <: CRS, F, Dim} <:
23+
Meshes.Primitive{M, C}
24+
fun::F
25+
26+
function _ParametricGeometry{M, C}(
27+
fun::F,
28+
Dim::Int64
29+
) where {M <: Meshes.Manifold, C <: CRS, F}
30+
return new{M, C, F, Dim}(fun)
31+
end
32+
end
33+
34+
"""
35+
_ParametricGeometry(fun, dims)
36+
37+
Construct a `_ParametricGeometry` using a provided parametric function `fun` for a geometry
38+
with `dims` parametric dimensions.
39+
40+
# Arguments
41+
- `fun` - anything callable representing a parametric function mapping `(ts...) -> Meshes.Point`
42+
- `dims::Int64` - number of parametric dimensions, i.e. `length(ts)`
43+
"""
44+
function _ParametricGeometry(
45+
fun,
46+
Dim::Int64
47+
)
48+
p = fun(_zeros(Dim)...)
49+
return _ParametricGeometry{Meshes.manifold(p), Meshes.crs(p)}(fun, Dim)
50+
end
51+
52+
# Allow a _ParametricGeometry to be called like a Geometry
53+
(g::_ParametricGeometry)(ts...) = g.fun(ts...)
54+
55+
Meshes.paramdim(::_ParametricGeometry{M, C, F, Dim}) where {M, C, F, Dim} = Dim
56+
57+
"""
58+
_parametric(geometry::G, ts...) where {G <: Meshes.Geometry}
59+
60+
Used in MeshIntegrals.jl for defining parametric functions that transform non-standard
61+
geometries into a form that can be integrated over the standard rectangular [0,1]^n limits.
62+
"""
63+
function _parametric end

0 commit comments

Comments
 (0)