Skip to content

Commit 33dbb41

Browse files
committed
feat: chebyshev-gauss quadrature instead of quadgk
1 parent 0f448e5 commit 33dbb41

File tree

6 files changed

+240
-162
lines changed

6 files changed

+240
-162
lines changed

docs/src/API.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ P3Scheme.∫liquid_ice_collisions
198198
### Supporting integral methods
199199

200200
```@docs
201-
P3Scheme.∫fdD
202-
P3Scheme.∫fdD_error
201+
P3Scheme.integrate
202+
P3Scheme.ChebyshevGauss
203203
P3Scheme.integral_bounds
204204
```
205205

src/P3_integral_properties.jl

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,143 @@
11

22
"""
3-
∫fdD(f, dist; [p = 1e-6], kwargs...)
3+
integrate(f, a, b, quad = ChebyshevGauss(100))
4+
5+
Approximate the definite integral ∫ₐᵇ f(x) dx using Chebyshev-Gauss quadrature of the first kind.
6+
7+
# Mathematical Background
8+
9+
This method transforms the integral to the standard interval [-1, 1] and applies
10+
Chebyshev-Gauss quadrature. The transformation is:
11+
12+
y = (2x - (a+b)) / (b-a) → x = (b-a)y/2 + (a+b)/2
13+
14+
with Jacobian dx/dy = (b-a)/2.
15+
16+
The integral becomes:
17+
18+
∫ₐᵇ f(x) dx = (b-a)/2 ∫₋₁¹ f(x(y)) dy
19+
20+
Using the Chebyshev-Gauss quadrature identity:
21+
22+
∫₋₁¹ g(y) dy = ∫₋₁¹ [g(y)√(1-y²)] / √(1-y²) dy ≈ (π/n) ∑ᵢ₌₁ⁿ g(yᵢ)√(1-yᵢ²)
23+
24+
where yᵢ = cos((2i-1)π/(2n)) are the Chebyshev nodes of the first kind.
25+
26+
# Final Formula
27+
28+
∫ₐᵇ f(x) dx ≈ (b-a)π/(2n) ∑ᵢ₌₁ⁿ f(xᵢ)√(1-yᵢ²)
29+
30+
where:
31+
- yᵢ = cos((2i-1)π/(2n)) for i = 1, ..., n
32+
- xᵢ = (b-a)yᵢ/2 + (a+b)/2
33+
- All quadrature weights are equal: π/n
434
5-
Integrate the function `f` over the size distribution `dist`
35+
# Arguments
36+
- `f`: Function to integrate
37+
- `a`, `b`: Integration bounds
38+
39+
# Keyword arguments
40+
- `quad`: Quadrature scheme, default: `ChebyshevGauss(100)`
41+
42+
# Returns
43+
Approximation to the definite integral ∫ₐᵇ f(x) dx
44+
45+
# Notes
46+
This method achieves spectral convergence for smooth functions and is particularly
47+
effective for analytic functions. The √(1-y²) weighting factor helps handle
48+
functions with mild singularities at the interval endpoints.
49+
Ref: https://en.wikipedia.org/wiki/Chebyshev–Gauss_quadrature
50+
"""
51+
function integrate(f, a, b; quad = ChebyshevGauss(100))
52+
FT = eltype(float(a))
53+
# Pre-compute transformation parameters
54+
scale_factor = (b - a) / 2
55+
shift = (a + b) / 2
56+
57+
# Compute integral using Chebyshev-Gauss quadrature
58+
result = zero(f(a))
59+
a == b && return result # return early if a == b
60+
(; n) = quad
61+
for i in 1:n
62+
# Node on [-1, 1] interval
63+
y = node(quad, FT(i), n)
64+
65+
# Node on [a, b] interval
66+
x = scale_factor * y + shift
67+
68+
# Total weight √(1 - y²) * wᵢ
69+
w = inv_weight_fun(quad, y) * weight(quad, FT(i), n)
70+
71+
# Accumulate: f(x) * √(1 - y²) * wᵢ
72+
result += f(x) * w
73+
end
74+
75+
return scale_factor * result
76+
end
677

7-
!!! note "Usage"
8-
This function is useful for integrating functions over the size distribution `dist`.
9-
It is a light wrapper around `QGK.quadgk` that automatically inserts appropriate
10-
size distribution thresholds as integration limits.
78+
"""
79+
integrate(f, bnds...; quad = ChebyshevGauss(100))
1180
12-
This method calls [`∫fdD_error`](@ref), which returns both the value of the integral
13-
and the estimated error. Since the error is typically not of interest, this method
14-
only returns the value of the integral.
81+
Integrate the function `f` over each subinterval of the integration bounds, `bnds`.
1582
1683
# Arguments
17-
- `f`: The function to integrate
18-
- `dist`: The distribution object, passed to [`integral_bounds`](@ref) to set the integration limits.
19-
- `p`: The integration bounds are set to the `p`-th and `1-p`-th quantiles of the size distribution.
20-
Default: `p = 1e-6` (i.e. 99.9998% of the size distribution is integrated).
21-
- `kwargs`: Additional optional keyword arguments to pass to [`QGK.quadgk`](https://juliamath.github.io/QuadGK.jl/stable/api/#QuadGK.quadgk)
22-
- `rtol`: The relative tolerance for the integration, default: `rtol = sqrt(eps(FT))`
23-
- `atol`: The absolute tolerance for the integration, default: `atol = 0`
24-
- `maxevals`: The maximum number of function evaluations, default: `maxevals = 10^7`
25-
- `order`: The order of the quadrature rule, default: `order = 7`
84+
- `f`: Function to integrate
85+
- `bnds`: A tuple of bounds, `(a, b, c, d, ...)`
86+
- `quad`: Quadrature scheme, default: `ChebyshevGauss(100)`
2687
27-
# Returns
28-
- `value`: The value of the integral
88+
The integral is computed as the sum of the integrals over each subinterval,
89+
`(a, b), (b, c), (c, d), ...`.
90+
"""
91+
function integrate(f, bnds...; quad = ChebyshevGauss(100))
92+
# compute integral over each subinterval (a, b), (b, c), (c, d), ...
93+
return sum(integrate(f, a, b; quad) for (a, b) in zip(Base.front(bnds), Base.tail(bnds)))
94+
end
2995

30-
!!! note "Integral accuracy"
31-
To achieve highest accuracy, which can be challenging when integrating over
32-
the size distribution, it is recommended to increase the `order` of the
33-
quadrature rule and set `rtol = 0`. Experimentally, `order = 44` has been found
34-
to be sufficient for most cases.
96+
"""
97+
ChebyshevGauss(n)
3598
36-
For convenience, passing `accurate = true` will set `rtol = 0` and `order = 44`.
99+
Quadrature scheme for Chebyshev-Gauss quadrature of the first kind.
37100
38-
# Examples
101+
# Arguments
102+
- `n`: Number of quadrature points
39103
40-
```jldoctest
41-
julia> import CloudMicrophysics.Parameters as CMP,
42-
CloudMicrophysics.P3Scheme as P3
104+
# Available methods
43105
44-
julia> params = CMP.ParametersP3(Float64);
106+
node(::ChebyshevGauss, i::FT, n) where {FT}
107+
weight(::ChebyshevGauss, i::FT, n) where {FT}
108+
inv_weight_fun(::ChebyshevGauss, y)
45109
46-
julia> state = P3.get_state(params; F_rim = 0.0, ρ_rim = 400.0, L_ice = 0.002, N_ice = 1000.0);
110+
- `node(quad, i, n)`: Return the `i`-th node of the `n`-point Chebyshev-Gauss quadrature scheme.
111+
- `weight(quad, i, n)`: Return the `i`-th weight of the `n`-point Chebyshev-Gauss quadrature scheme.
112+
- `inv_weight_fun(quad, y)`: Return the inverse of the weight function `w(x)`.
47113
48-
julia> logλ = P3.get_distribution_logλ(state);
49114
50-
julia> f(D) = D^3 * P3.size_distribution(state, logλ)(D); # Define a function to integrate
51115
52-
julia> P3.∫fdD(f, state, logλ; p = 0.01) # Integrate the function
53-
0.0008519464332296608
116+
# Mathematical Background
54117
55-
julia> P3.∫fdD(state, logλ; p = 0.01) do D # Integrate with a `do`-block
56-
P3.ice_mass(state, D) * P3.size_distribution(state, logλ)(D)
57-
end
58-
0.0017027833723511712
59-
```
60-
"""
61-
function ∫fdD(f, state::P3State, logλ; p = 1e-6, moment_order = 0, kwargs...)
62-
return ∫fdD_error(f, state, logλ; p, moment_order, kwargs...)[1]
63-
end
118+
A method to approximate the value of integrals of the kind
64119
65-
"""
66-
∫fdD_error(f, dist; p, [moment_order = 0], [accurate = false], kwargs...)
120+
∫_{-1}^{1} f(x) w(x) dx ≈ ∑_1^n f(x_i) wᵢ(x_i)
67121
68-
Integrate the function `f` over the size distribution `dist`
122+
where `w(x) = 1 / √(1 - x^2)` is the weight function, `x_i` are the nodes, and `wᵢ` are the weights.
69123
70-
# Returns
71-
- `value`: The value of the integral
72-
- `error`: The estimated error of the integral
124+
If we are interested in only the integral of `f(x)`, we can instead integrate `g(x) = f(x) / w(x)`,
73125
74-
# Notes
75-
See [`∫fdD`](@ref), which only returns the value of the integral and not the error, for details.
126+
∫_{-1}^{1} g(x) w(x) dx ≈ ∑_1^n g(x_i) wᵢ(x_i) = ∑_1^n f(x_i) wᵢ(x_i) / w(x_i)
127+
128+
# References
129+
130+
- https://en.wikipedia.org/wiki/Chebyshev–Gauss_quadrature
131+
- https://en.wikipedia.org/wiki/Gaussian_quadrature
76132
"""
77-
function ∫fdD_error(f, state::P3State, logλ; p, moment_order = 0, accurate = false, kwargs...)
78-
# Get integration bounds
79-
bnds = integral_bounds(state, logλ; p, moment_order)
80-
# Use a more accurate quadrature rule if requested
81-
accurate && (kwargs = (; rtol = 0, order = 44, kwargs...))
82-
return QGK.quadgk(f, bnds...; kwargs...)
133+
struct ChebyshevGauss
134+
n::Int
83135
end
136+
Base.broadcastable(quad::ChebyshevGauss) = (quad,)
137+
138+
@inline node(::ChebyshevGauss, i::FT, n) where {FT} = cospi((2float(FT(i)) - 1) / (2n))
139+
@inline weight(::ChebyshevGauss, i::FT, n) where {FT} = float(FT(π)) / n
140+
@inline inv_weight_fun(::ChebyshevGauss, y) = (1 - y^2)
84141

85142
"""
86143
integral_bounds(state::P3State, logλ; p, moment_order = 0)

src/P3_processes.jl

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ end
6060
- `logλ`: the log of the slope parameter [log(1/m)]
6161
6262
# Keyword arguments
63-
- `∫kwargs`: Named tuple of keyword arguments passed to [`∫fdD`](@ref)
63+
- `∫kwargs...`: Additional keyword arguments passed to the quadrature rule
6464
6565
Returns the melting rate of ice (QIMLT in Morrison and Mildbrandt (2015)).
6666
"""
6767
function ice_melt(
6868
velocity_params::CMP.Chen2022VelType, aps::CMP.AirProperties, tps::TDI.PS, Tₐ, ρₐ, dt, state::P3State, logλ;
69-
∫kwargs = (;),
69+
∫kwargs...,
7070
)
7171
# Note: process not dependent on `F_liq`
7272
# (we want ice core shape params)
@@ -83,7 +83,8 @@ function ice_melt(
8383

8484
# Integrate
8585
fac = 4 * K_therm / L_f * (Tₐ - T_freeze)
86-
dLdt = fac * ∫fdD(state, logλ; ∫kwargs...) do D
86+
bnds = integral_bounds(state, logλ; p = 1e-6)
87+
dLdt = fac * integrate(bnds...; ∫kwargs...) do D
8788
∂ice_mass_∂D(state, D) * F_v(D) * N′(D) / D
8889
end
8990

@@ -257,7 +258,7 @@ Returns a function `liquid_integrals(Dᵢ)` that computes the liquid particle in
257258
- `liq_bounds`: integration bounds for liquid particles
258259
259260
# Keyword arguments
260-
- `∫kwargs...`: Additional keyword arguments passed to `QuadGK.quadgk`
261+
- `∫kwargs...`: Additional keyword arguments passed to the quadrature rule
261262
262263
# Notes
263264
The function `liquid_integrals(Dᵢ)` returns a tuple `(∂ₜN_col, ∂ₜM_col, ∂ₜB_col)`
@@ -268,17 +269,16 @@ The function `liquid_integrals(Dᵢ)` returns a tuple `(∂ₜN_col, ∂ₜM_col
268269
"""
269270
function get_liquid_integrals(n, ∂ₜV, m_liq, ρ′_rim, liq_bounds; ∫kwargs...)
270271
function liquid_integrals(Dᵢ)
271-
((∂ₜN_col, ∂ₜM_col, ∂ₜB_col), _) =
272-
QGK.quadgk(liq_bounds...; ∫kwargs...) do D
273-
return SA.SVector(
274-
# ∂ₜN_col = ∫ ∂ₜV ⋅ n ⋅ dD
275-
∂ₜV(Dᵢ, D) * n(D),
276-
# ∂ₜM_col = ∫ ∂ₜV ⋅ n ⋅ m_liq ⋅ dD
277-
∂ₜV(Dᵢ, D) * n(D) * m_liq(D),
278-
# ∂ₜB_col = ∫ ∂ₜV ⋅ n ⋅ m_liq / ρ′_rim ⋅ dD
279-
∂ₜV(Dᵢ, D) * n(D) * m_liq(D) / ρ′_rim(Dᵢ, D),
280-
)
281-
end
272+
(∂ₜN_col, ∂ₜM_col, ∂ₜB_col) = integrate(liq_bounds...; ∫kwargs...) do D
273+
return SA.SVector(
274+
# ∂ₜN_col = ∫ ∂ₜV ⋅ n ⋅ dD
275+
∂ₜV(Dᵢ, D) * n(D),
276+
# ∂ₜM_col = ∫ ∂ₜV ⋅ n ⋅ m_liq ⋅ dD
277+
∂ₜV(Dᵢ, D) * n(D) * m_liq(D),
278+
# ∂ₜB_col = ∫ ∂ₜV ⋅ n ⋅ m_liq / ρ′_rim ⋅ dD
279+
∂ₜV(Dᵢ, D) * n(D) * m_liq(D) / ρ′_rim(Dᵢ, D),
280+
)
281+
end
282282
return ∂ₜN_col, ∂ₜM_col, ∂ₜB_col
283283
end
284284
return liquid_integrals
@@ -299,7 +299,7 @@ Computes the bulk collision rate integrands between ice and liquid particles.
299299
- `ice_bounds`: integration bounds for ice particles, from [`integral_bounds`](@ref)
300300
301301
# Keyword arguments
302-
- `∫kwargs...`: Additional keyword arguments passed to `QuadGK.quadgk`
302+
- `∫kwargs...`: Additional keyword arguments passed to the quadrature rule
303303
304304
# Returns
305305
A tuple of 8 integrands, see [`∫liquid_ice_collisions`](@ref) for details.
@@ -333,9 +333,7 @@ function ∫liquid_ice_collisions(n_i, ∂ₜM_max, cloud_integrals, rain_integr
333333
n * 𝟙_wet * ∂ₜM_col, # ∫𝟙_wet_M_col, wet growth indicator
334334
)
335335
end
336-
337-
(rates, _) = QGK.quadgk(liquid_ice_collisions_integrands, ice_bounds...; ∫kwargs...)
338-
return rates
336+
return integrate(liquid_ice_collisions_integrands, ice_bounds...; ∫kwargs...)
339337
end
340338

341339
"""
@@ -378,7 +376,7 @@ A tuple `(QCFRZ, QCSHD, NCCOL, QRFRZ, QRSHD, NRCOL, ∫M_col, BCCOL, BRCOL, ∫
378376
function ∫liquid_ice_collisions(
379377
state, logλ,
380378
psd_c, psd_r, L_c, N_c, L_r, N_r,
381-
aps, tps, vel, ρₐ, T, m_liq,
379+
aps, tps, vel, ρₐ, T, m_liq; ∫kwargs...,
382380
)
383381
FT = eltype(state)
384382

@@ -388,13 +386,10 @@ function ∫liquid_ice_collisions(
388386
n_i = DT.size_distribution(state, logλ) # n_i(Dᵢ)
389387

390388
# Initialize integration buffers by evaluating a representative integral
391-
ice_bounds = integral_bounds(state, logλ; p = 0.00001)
392-
mm = FT(1e-3)
393-
bounds_c = FT[0; 0.01mm; 0.1mm; 1mm; 10mm; 100mm; 1] # TODO: Replace by quantiles method
394-
bounds_r = FT[0; 0.01mm; 0.1mm; 1mm; 10mm; 100mm; 1] # TODO: Replace by quantiles method
395-
segbuf_c = QGK.quadgk_segbuf(n_c, bounds_c...)[3]
396-
segbuf_r = QGK.quadgk_segbuf(n_r, bounds_r...)[3]
397-
segbuf_ice = QGK.quadgk_segbuf(n_i, ice_bounds...)[3]
389+
p = FT(0.00001)
390+
ice_bounds = integral_bounds(state, logλ; p)
391+
bounds_c = CM2.get_size_distribution_bounds(psd_c, L_c, ρₐ, N_c, p)
392+
bounds_r = CM2.get_size_distribution_bounds(psd_r, L_r, ρₐ, N_r, p)
398393

399394
# Integrand components
400395
# NOTE: We assume collision efficiency, shape (spherical), and terminal velocity is the
@@ -403,12 +398,10 @@ function ∫liquid_ice_collisions(
403398
ρ′_rim = compute_local_rime_density(vel, ρₐ, T, state) # ρ′_rim(Dᵢ, Dₗ)
404399
∂ₜM_max = compute_max_freeze_rate(aps, tps, vel, ρₐ, T, state) # ∂ₜM_max(Dᵢ)
405400

406-
cloud_integrals = get_liquid_integrals(n_c, ∂ₜV, m_liq, ρ′_rim, bounds_c; eval_segbuf = segbuf_c) # (∂ₜN_c_col, ∂ₜM_c_col, ∂ₜB_c_col)
407-
rain_integrals = get_liquid_integrals(n_r, ∂ₜV, m_liq, ρ′_rim, bounds_r; eval_segbuf = segbuf_r) # (∂ₜN_r_col, ∂ₜM_r_col, ∂ₜB_r_col)
401+
cloud_integrals = get_liquid_integrals(n_c, ∂ₜV, m_liq, ρ′_rim, bounds_c; ∫kwargs...) # (∂ₜN_c_col, ∂ₜM_c_col, ∂ₜB_c_col)
402+
rain_integrals = get_liquid_integrals(n_r, ∂ₜV, m_liq, ρ′_rim, bounds_r; ∫kwargs...) # (∂ₜN_r_col, ∂ₜM_r_col, ∂ₜB_r_col)
408403

409-
return ∫liquid_ice_collisions(
410-
n_i, ∂ₜM_max, cloud_integrals, rain_integrals, ice_bounds; eval_segbuf = segbuf_ice,
411-
)
404+
return ∫liquid_ice_collisions(n_i, ∂ₜM_max, cloud_integrals, rain_integrals, ice_bounds; ∫kwargs...)
412405
end
413406

414407
"""
@@ -451,7 +444,7 @@ A `NamedTuple` of `(; ∂ₜq_c, ∂ₜq_r, ∂ₜN_c, ∂ₜN_r, ∂ₜL_rim,
451444
function bulk_liquid_ice_collision_sources(
452445
params, logλ, L_ice, F_rim, ρ_rim,
453446
psd_c, psd_r, L_c, N_c, L_r, N_r,
454-
aps, tps, vel, ρₐ, T,
447+
aps, tps, vel, ρₐ, T; ∫kwargs...,
455448
)
456449
FT = eltype(params)
457450
(; τ_wet) = params
@@ -466,7 +459,7 @@ function bulk_liquid_ice_collision_sources(
466459
(QCFRZ, QCSHD, NCCOL, QRFRZ, QRSHD, NRCOL, ∫∂ₜM_col, BCCOL, BRCOL, ∫𝟙_wet_M_col) = ∫liquid_ice_collisions(
467460
state, logλ,
468461
psd_c, psd_r, L_c, N_c, L_r, N_r,
469-
aps, tps, vel, ρₐ, T, m_liq,
462+
aps, tps, vel, ρₐ, T, m_liq; ∫kwargs...,
470463
)
471464

472465
# Bulk wet growth fraction

0 commit comments

Comments
 (0)