Skip to content

Commit ce99b3e

Browse files
Implement more _ParametricGeometry transforms, remove Analytical (#139)
* Implement _ParametricGeometry for Line * Implement _ParametricGeometry for Plane * Implement _ParametricGeometry for Ray * Update and reorganize analytical tests * Add comments * Use paramdim for clarity * Test _parametric -> anonymous function * Update _parametric API to return an anonymous function * Remove obsolete N, enable Tetrahedron test * Remove hanging comma * Update field name * Add analytical derivative in comment * Update _parametric for Triangle and Tetrahedron * Update tests * put parametrized to .typos.toml * Implement _ParametricGeometry for Triangle * Temp disable some Analytical tests * Temp disable more tests * Test disabling bound check * Restore bounds check, try new formulation * Remove old commented-out code * Add comments * Remove obsolete tests * Remove obsolete utils and update default diff_method * Remove obsolete imports * Remove obsolete Analytical * Remove obsolete export * Formatting * Remove obsolete derivation * Update src/specializations/Tetrahedron.jl Co-authored-by: Joshua Lampert <[email protected]> * Remove docstring ref to Analytical * Update src/specializations/Triangle.jl Co-authored-by: Joshua Lampert <[email protected]> * Update src/specializations/Triangle.jl Co-authored-by: Joshua Lampert <[email protected]> --------- Co-authored-by: Joshua Lampert <[email protected]> Co-authored-by: Joshua Lampert <[email protected]>
1 parent 6c67fca commit ce99b3e

File tree

14 files changed

+115
-473
lines changed

14 files changed

+115
-473
lines changed

.typos.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[default.extend-words]
2+
parametrized = "parametrized"

benchmark/benchmarks.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ integrands = (
1515
(name = "Vector", f = p -> fill(norm(to(p)), 3))
1616
)
1717
rules = (
18-
(name = "GaussLegendre", rule = GaussLegendre(100), N = 100),
19-
(name = "GaussKronrod", rule = GaussKronrod(), N = 100),
20-
(name = "HAdaptiveCubature", rule = HAdaptiveCubature(), N = 500)
18+
(name = "GaussLegendre", rule = GaussLegendre(100)),
19+
(name = "GaussKronrod", rule = GaussKronrod()),
20+
(name = "HAdaptiveCubature", rule = HAdaptiveCubature())
2121
)
2222
geometries = (
2323
(name = "Segment", item = Segment(Point(0, 0, 0), Point(1, 1, 1))),
@@ -26,7 +26,8 @@ geometries = (
2626

2727
SUITE["Integrals"] = let s = BenchmarkGroup()
2828
for (int, rule, geometry) in Iterators.product(integrands, rules, geometries)
29-
n1, n2, N = geometry.name, "$(int.name) $(rule.name)", rule.N
29+
n1 = geometry.name
30+
n2 = "$(int.name) $(rule.name)"
3031
s[n1][n2] = @benchmarkable integral($int.f, $geometry.item, $rule.rule)
3132
end
3233
s
@@ -64,8 +65,7 @@ SUITE["Specializations/Scalar GaussLegendre"] = let s = BenchmarkGroup()
6465
s["Plane"] = @benchmarkable integral($spec.f_exp, $spec.g.plane, $spec.rule_gl)
6566
s["Ray"] = @benchmarkable integral($spec.f_exp, $spec.g.ray, $spec.rule_gl)
6667
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
68+
s["Tetrahedron"] = @benchmarkable integral($spec.f, $spec.g.tetrahedron, $spec.rule_gl)
6969
s
7070
end
7171

docs/src/specializations.md

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,3 @@ There are several notable exceptions to how Meshes.jl defines [parametric functi
1111
- `Meshes.Rope` is a composite type that lacks a parametric function, but can be decomposed into `Meshes.Segment`s and then integrated by adding together the individual integrals.
1212
- `Meshes.Triangle` has a parametric function that takes coordinates on a 2D barycentric coordinate system. So, for `(::Meshes.Triangle)(t1, t2)`, the coordinates must obey: $t_1, t_2 \in [0,1]$ where $t_1 + t_2 \le 1$.
1313
- `Meshes.Tetrahedron` has a parametric function that takes coordinates on a 3D barycentric coordinate system. So, for `(::Meshes.Tetrahedron)(t1, t2)`, the coordinates must obey: $t_1, t_2, t_3 \in [0,1]$ where $t_1 + t_2 + t_3 \le 1$.
14-
15-
## Triangle
16-
17-
For a specified `Meshes.Triangle` surface with area $A$, let $u$ and $v$ be Barycentric coordinates that span the surface.
18-
```math
19-
\int_\triangle f(\bar{r}) \, \text{d}A
20-
= \iint_\triangle f\left( \bar{r}(u,v) \right) \, \left( \text{d}u \wedge \text{d}v \right)
21-
```
22-
23-
Since the geometric transformation from the originally-arbitrary domain to a Barycentric domain is linear, the magnitude of the surface element $\text{d}u \wedge \text{d}v$ is constant throughout the integration domain. This constant will be equal to twice the magnitude of $A$.
24-
```math
25-
\int_\triangle f(\bar{r}) \, \text{d}A
26-
= 2A \int_0^1 \int_0^{1-v} f\left( \bar{r}(u,v) \right) \, \text{d}u \, \text{d}v
27-
```
28-
29-
Since the integral domain is a right-triangle in the Barycentric domain, a nested application of Gauss-Kronrod quadrature rules is capable of computing the result, albeit inefficiently. However, many numerical integration methods that require rectangular bounds can not be directly applied.
30-
31-
In order to enable integration methods that operate over rectangular bounds, two coordinate system transformations are applied: the first maps from Barycentric coordinates $(u, v)$ to polar coordinates $(r, \phi)$, and the second is a non-linear map from polar coordinates to a new curvilinear basis $(R, \phi)$.
32-
33-
For the first transformation, let $u = r~\cos\phi$ and $v = r~\sin\phi$ where $\text{d}u~\text{d}v = r~\text{d}r~\text{d}\phi$. The Barycentric triangle's hypotenuse boundary line is described by the function $v(u) = 1 - u$. Substituting in the previous definitions leads to a new boundary line function in polar coordinate space $r(\phi) = 1 / (\sin\phi + \cos\phi)$.
34-
```math
35-
\int_0^1 \int_0^{1-v} f\left( \bar{r}(u,v) \right) \, \text{d}u \, \text{d}v =
36-
\int_0^{\pi/2} \int_0^{1/(\sin\phi+\cos\phi)} f\left( \bar{r}(r,\phi) \right) \, r \, \text{d}r \, \text{d}\phi
37-
```
38-
39-
These integral boundaries remain non-rectangular, so an additional transformation will be applied to a curvilinear $(R, \phi)$ space that normalizes all of the hypotenuse boundary line points to $R=1$. To achieve this, a function $R(r,\phi)$ is required such that $R(r_0, \phi) = 1$ where $r_0 = 1 / (\sin\phi + \cos\phi)$
40-
41-
To achieve this, let $R(r, \phi) = r~(\sin\phi + \cos\phi)$. Now, substituting some terms leads to
42-
```math
43-
\int_0^{\pi/2} \int_0^{1/(\sin\phi+\cos\phi)} f\left( \bar{r}(r,\phi) \right) \, r \, \text{d}r \, \text{d}\phi
44-
= \int_0^{\pi/2} \int_0^{r_0} f\left( \bar{r}(r,\phi) \right) \, \left(\frac{R}{\sin\phi + \cos\phi}\right) \, \text{d}r \, \text{d}\phi
45-
```
46-
47-
Since $\text{d}R/\text{d}r = \sin\phi + \cos\phi$, a change of integral domain leads to
48-
```math
49-
\int_0^{\pi/2} \int_0^{r_0} f\left( \bar{r}(r,\phi) \right) \, \left(\frac{R}{\sin\phi + \cos\phi}\right) \, \text{d}r \, \text{d}\phi
50-
= \int_0^{\pi/2} \int_0^1 f\left( \bar{r}(R,\phi) \right) \, \left(\frac{R}{\left(\sin\phi + \cos\phi\right)^2}\right) \, \text{d}R \, \text{d}\phi
51-
```
52-
53-
The second term in this new integrand function serves as a correction factor that corrects for the impact of the non-linear domain transformation. Since all of the integration bounds are now constants, specialized integration methods can be defined for triangles that performs these domain transformations and then solve the new rectangular integration problem using a wider range of solver options.

src/MeshIntegrals.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import QuadGK
1010
import Unitful
1111

1212
include("differentiation.jl")
13-
export DifferentiationMethod, Analytical, FiniteDifference, jacobian
13+
export DifferentiationMethod, FiniteDifference, jacobian
1414

1515
include("utils.jl")
1616

src/differentiation.jl

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A category of types used to specify the desired method for calculating derivativ
99
Derivatives are used to form Jacobian matrices when calculating the differential
1010
element size throughout the integration region.
1111
12-
See also [`FiniteDifference`](@ref), [`Analytical`](@ref).
12+
See also [`FiniteDifference`](@ref).
1313
"""
1414
abstract type DifferentiationMethod end
1515

@@ -27,22 +27,6 @@ end
2727
FiniteDifference{T}() where {T <: AbstractFloat} = FiniteDifference{T}(T(1e-6))
2828
FiniteDifference() = FiniteDifference{Float64}()
2929

30-
"""
31-
Analytical()
32-
33-
Use to specify use of analytically-derived solutions for calculating derivatives.
34-
These solutions are currently defined only for a subset of geometry types.
35-
36-
# Supported Geometries:
37-
- `BezierCurve`
38-
- `Line`
39-
- `Plane`
40-
- `Ray`
41-
- `Tetrahedron`
42-
- `Triangle`
43-
"""
44-
struct Analytical <: DifferentiationMethod end
45-
4630
# Future Support:
4731
# struct AutoEnzyme <: DifferentiationMethod end
4832
# struct AutoZygote <: DifferentiationMethod end

src/specializations/BezierCurve.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ function integral(
3939
kwargs...
4040
)
4141
# 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))
42+
param_curve = _ParametricGeometry(_parametric(curve, alg), Meshes.paramdim(curve))
4443

4544
# Integrate the _ParametricGeometry using the standard methods
4645
return _integral(f, param_curve, rule; kwargs...)
@@ -50,6 +49,7 @@ end
5049
# Parametric
5150
################################################################################
5251

53-
function _parametric(curve::Meshes.BezierCurve, t, alg::Meshes.BezierEvalMethod)
54-
return curve(t, alg)
52+
# Wrap (::BezierCurve)(t, ::BezierEvalMethod) into f(t) by embedding second argument
53+
function _parametric(curve::Meshes.BezierCurve, alg::Meshes.BezierEvalMethod)
54+
return t -> curve(t, alg)
5555
end

src/specializations/Line.jl

Lines changed: 18 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -10,81 +10,26 @@
1010
function integral(
1111
f,
1212
line::Meshes.Line,
13-
rule::GaussLegendre;
14-
diff_method::DM = Analytical(),
15-
FP::Type{T} = Float64
16-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
17-
_guarantee_analytical(Meshes.Line, diff_method)
18-
19-
# Compute Gauss-Legendre nodes/weights for x in interval [-1,1]
20-
xs, ws = _gausslegendre(FP, rule.n)
21-
22-
# Normalize the Line s.t. line(t) is distance t from origin point
23-
line = Meshes.Line(line.a, line.a + Meshes.unormalize(line.b - line.a))
24-
25-
# Domain transformation: x ∈ [-1,1] ↦ t ∈ (-∞,∞)
26-
t(x) = x / (1 - x^2)
27-
t′(x) = (1 + x^2) / (1 - x^2)^2
28-
29-
# Integrate f along the Line
30-
differential(line, x) = t′(x) * _units(line(0))
31-
integrand(x) = f(line(t(x))) * differential(line, x)
32-
return sum(w .* integrand(x) for (w, x) in zip(ws, xs))
33-
end
34-
35-
function integral(
36-
f,
37-
line::Meshes.Line,
38-
rule::GaussKronrod;
39-
diff_method::DM = Analytical(),
40-
FP::Type{T} = Float64
41-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
42-
_guarantee_analytical(Meshes.Line, diff_method)
43-
44-
# Normalize the Line s.t. line(t) is distance t from origin point
45-
line = Meshes.Line(line.a, line.a + Meshes.unormalize(line.b - line.a))
46-
47-
# Integrate f along the Line
48-
domainunits = _units(line(0))
49-
integrand(t) = f(line(t)) * domainunits
50-
return QuadGK.quadgk(integrand, FP(-Inf), FP(Inf); rule.kwargs...)[1]
51-
end
52-
53-
function integral(
54-
f,
55-
line::Meshes.Line,
56-
rule::HAdaptiveCubature;
57-
diff_method::DM = Analytical(),
58-
FP::Type{T} = Float64
59-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
60-
_guarantee_analytical(Meshes.Line, diff_method)
61-
62-
# Normalize the Line s.t. line(t) is distance t from origin point
63-
line = Meshes.Line(line.a, line.a + Meshes.unormalize(line.b - line.a))
64-
65-
# Domain transformation: x ∈ [-1,1] ↦ t ∈ (-∞,∞)
66-
t(x) = x / (1 - x^2)
67-
t′(x) = (1 + x^2) / (1 - x^2)^2
68-
69-
# Integrate f along the Line
70-
differential(line, x) = t′(x) * _units(line(0))
71-
integrand(xs) = f(line(t(xs[1]))) * differential(line, xs[1])
72-
73-
# HCubature doesn't support functions that output Unitful Quantity types
74-
# Establish the units that are output by f
75-
testpoint_parametriccoord = (FP(0.5),)
76-
integrandunits = Unitful.unit.(integrand(testpoint_parametriccoord))
77-
# Create a wrapper that returns only the value component in those units
78-
uintegrand(uv) = Unitful.ustrip.(integrandunits, integrand(uv))
79-
# Integrate only the unitless values
80-
value = HCubature.hcubature(uintegrand, (-one(FP),), (one(FP),); rule.kwargs...)[1]
81-
82-
# Reapply units
83-
return value .* integrandunits
13+
rule::IntegrationRule;
14+
kwargs...
15+
)
16+
# Generate a _ParametricGeometry whose parametric function spans the domain [0, 1]
17+
param_line = _ParametricGeometry(_parametric(line), Meshes.paramdim(line))
18+
19+
# Integrate the _ParametricGeometry using the standard methods
20+
return _integral(f, param_line, rule; kwargs...)
8421
end
8522

8623
################################################################################
87-
# jacobian
24+
# Parametric
8825
################################################################################
8926

90-
_has_analytical(::Type{T}) where {T <: Meshes.Line} = true
27+
# Map argument domain from [0, 1] to (-∞, ∞) for (::Line)(t)
28+
function _parametric(line::Meshes.Line)
29+
# [-1, 1] ↦ (-∞, ∞)
30+
f1(t) = t / (1 - t^2)
31+
# [0, 1] ↦ [-1, 1]
32+
f2(t) = 2t - 1
33+
# Compose the two transforms
34+
return t -> line((f1 f2)(t))
35+
end

src/specializations/Plane.jl

Lines changed: 21 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -11,96 +11,27 @@
1111
function integral(
1212
f,
1313
plane::Meshes.Plane,
14-
rule::GaussLegendre;
15-
diff_method::DM = Analytical(),
16-
FP::Type{T} = Float64
17-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
18-
_guarantee_analytical(Meshes.Plane, diff_method)
19-
20-
# Get Gauss-Legendre nodes and weights for a 2D region [-1,1]²
21-
xs, ws = _gausslegendre(FP, rule.n)
22-
wws = Iterators.product(ws, ws)
23-
xxs = Iterators.product(xs, xs)
24-
25-
# Normalize the Plane's orthogonal vectors
26-
uu = Meshes.unormalize(plane.u)
27-
uv = Meshes.unormalize(plane.v)
28-
uplane = Meshes.Plane(plane.p, uu, uv)
29-
30-
# Domain transformation: x ∈ [-1,1] ↦ t ∈ (-∞,∞)
31-
t(x) = x / (1 - x^2)
32-
t′(x) = (1 + x^2) / (1 - x^2)^2
33-
34-
# Integrate f over the Plane
35-
domainunits = _units(uplane(0, 0))
36-
function integrand(((wi, wj), (xi, xj)))
37-
wi * wj * f(uplane(t(xi), t(xj))) * t′(xi) * t′(xj) * domainunits^2
38-
end
39-
return sum(integrand, zip(wws, xxs))
14+
rule::IntegrationRule;
15+
kwargs...
16+
)
17+
# Generate a _ParametricGeometry whose parametric function spans the domain [0, 1]²
18+
param_plane = _ParametricGeometry(_parametric(plane), Meshes.paramdim(plane))
19+
20+
# Integrate the _ParametricGeometry using the standard methods
21+
return _integral(f, param_plane, rule; kwargs...)
4022
end
4123

42-
function integral(
43-
f,
44-
plane::Meshes.Plane,
45-
rule::GaussKronrod;
46-
diff_method::DM = Analytical(),
47-
FP::Type{T} = Float64
48-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
49-
_guarantee_analytical(Meshes.Plane, diff_method)
50-
51-
# Normalize the Plane's orthogonal vectors
52-
uu = Meshes.unormalize(plane.u)
53-
uv = Meshes.unormalize(plane.v)
54-
uplane = Meshes.Plane(plane.p, uu, uv)
55-
56-
# Integrate f over the Plane
57-
domainunits = _units(uplane(0, 0))^2
58-
integrand(u, v) = f(uplane(u, v)) * domainunits
59-
inner∫(v) = QuadGK.quadgk(u -> integrand(u, v), FP(-Inf), FP(Inf); rule.kwargs...)[1]
60-
return QuadGK.quadgk(inner∫, FP(-Inf), FP(Inf); rule.kwargs...)[1]
24+
############################################################################################
25+
# Parametric
26+
############################################################################################
27+
28+
# Map argument domain from [0, 1]² to (-∞, ∞)² for (::Plane)(t1, t2)
29+
function _parametric(plane::Meshes.Plane)
30+
# [-1, 1] ↦ (-∞, ∞)
31+
f1(t) = t / (1 - t^2)
32+
# [0, 1] ↦ [-1, 1]
33+
f2(t) = 2t - 1
34+
# Compose the two transforms
35+
f = f1 f2
36+
return (t1, t2) -> plane(f(t1), f(t2))
6137
end
62-
63-
function integral(
64-
f,
65-
plane::Meshes.Plane,
66-
rule::HAdaptiveCubature;
67-
diff_method::DM = Analytical(),
68-
FP::Type{T} = Float64
69-
) where {DM <: DifferentiationMethod, T <: AbstractFloat}
70-
_guarantee_analytical(Meshes.Plane, diff_method)
71-
72-
# Normalize the Plane's orthogonal vectors
73-
uu = Meshes.unormalize(plane.u)
74-
uv = Meshes.unormalize(plane.v)
75-
uplane = Meshes.Plane(plane.p, uu, uv)
76-
77-
# Domain transformation: x ∈ [-1,1] ↦ t ∈ (-∞,∞)
78-
t(x) = x / (1 - x^2)
79-
t′(x) = (1 + x^2) / (1 - x^2)^2
80-
81-
# Integrate f over the Plane
82-
domainunits = _units(uplane(0, 0))
83-
function integrand(xs)
84-
f(uplane(t(xs[1]), t(xs[2]))) * t′(xs[1]) * t′(xs[2]) * domainunits^2
85-
end
86-
87-
# HCubature doesn't support functions that output Unitful Quantity types
88-
# Establish the units that are output by f
89-
testpoint_parametriccoord = zeros(FP, 2)
90-
integrandunits = Unitful.unit.(integrand(testpoint_parametriccoord))
91-
# Create a wrapper that returns only the value component in those units
92-
uintegrand(uv) = Unitful.ustrip.(integrandunits, integrand(uv))
93-
# Integrate only the unitless values
94-
a = .-_ones(FP, 2)
95-
b = _ones(FP, 2)
96-
value = HCubature.hcubature(uintegrand, a, b; rule.kwargs...)[1]
97-
98-
# Reapply units
99-
return value .* integrandunits
100-
end
101-
102-
################################################################################
103-
# jacobian
104-
################################################################################
105-
106-
_has_analytical(::Type{T}) where {T <: Meshes.Plane} = true

0 commit comments

Comments
 (0)