|
2 | 2 |
|
3 | 3 | using BSplineKit
|
4 | 4 | using QuadGK: quadgk
|
| 5 | +using ReverseDiff |
5 | 6 | using Test
|
6 | 7 |
|
| 8 | +# This is the objective function that `fit` is supposed to minimise. |
| 9 | +# We can verify this using automatic differentiation: the gradient wrt the spline |
| 10 | +# coefficients should be zero. |
| 11 | +function smoothing_objective(cs, R::AbstractBSplineBasis, xs, ys; weights = nothing, λ) |
| 12 | + # Construct spline from coefficients and knots |
| 13 | + S = Spline(R, cs) |
| 14 | + S″ = Derivative(2) * S |
| 15 | + |
| 16 | + # Compute first term of objective (loss) function |
| 17 | + T = eltype(cs) |
| 18 | + loss = zero(T) |
| 19 | + for i in eachindex(xs, ys) |
| 20 | + w = weights === nothing ? 1 : weights[i] |
| 21 | + loss += w * abs2(ys[i] - S(xs[i])) |
| 22 | + end |
| 23 | + |
| 24 | + # Integrate roughness term interval by interval |
| 25 | + for i in eachindex(xs)[1:end-1] |
| 26 | + # Note: S″(x) is linear within each interval, and thus the integrand is quadratic. |
| 27 | + # Therefore, a two-point GL quadrature is exact (weights = 1 and locations = ±1/√3). |
| 28 | + a = xs[i] |
| 29 | + b = xs[i + 1] |
| 30 | + Δ = (b - a) / 2 |
| 31 | + xc = (a + b) / 2 |
| 32 | + gl_weight = 1 |
| 33 | + gl_ξ = 1 / sqrt(T(3)) |
| 34 | + for ξ in (-gl_ξ, +gl_ξ) |
| 35 | + x = Δ * ξ + xc |
| 36 | + loss += λ * Δ * gl_weight * abs2(S″(x)) |
| 37 | + end |
| 38 | + end |
| 39 | + |
| 40 | + loss |
| 41 | +end |
| 42 | + |
| 43 | +function check_zero_gradient(S::Spline, xs, ys; weights = nothing, λ) |
| 44 | + cs = coefficients(S) |
| 45 | + R = basis(S) # usually a RecombinedBSplineBasis |
| 46 | + |
| 47 | + # Not sure how useful this is... |
| 48 | + ∇f = similar(cs) # gradient wrt coefficients |
| 49 | + inputs = (cs,) |
| 50 | + results = (∇f,) |
| 51 | + # all_results = map(DiffResults.GradientResult, results) |
| 52 | + cfg = ReverseDiff.GradientConfig(inputs) |
| 53 | + |
| 54 | + # Compute gradient |
| 55 | + ReverseDiff.gradient!(results, cs -> smoothing_objective(cs, R, xs, ys; weights, λ), inputs, cfg) |
| 56 | + |
| 57 | + # Verify that |∇f|² is negligible. Note that is has the same units as |y_i|² ≡ Y², since |
| 58 | + # f ~ Y² and therefore ∂f/∂cⱼ ~ Y. So we compare it with the sum of |y_i|². |
| 59 | + reference = sum(abs2, ys) |
| 60 | + err = sum(abs2, ∇f) |
| 61 | + @test err / reference < 1e-12 |
| 62 | + |
| 63 | + nothing |
| 64 | +end |
| 65 | + |
7 | 66 | # Returns the integral of |S''(x)| (the "curvature") over the whole spline.
|
8 | 67 | function total_curvature(S::Spline)
|
9 | 68 | ts = knots(S)
|
|
41 | 100 | λs = [0.0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]
|
42 | 101 | curvatures = similar(λs)
|
43 | 102 | distances = similar(λs)
|
44 |
| - for i in eachindex(λs) |
45 |
| - S = @inferred fit(xs, ys, λs[i]) |
| 103 | + for (i, λ) in pairs(λs) |
| 104 | + S = @inferred fit(xs, ys, λ) |
46 | 105 | curvatures[i] = total_curvature(S)
|
47 | 106 | distances[i] = distance_from_data(S, xs, ys)
|
| 107 | + check_zero_gradient(S, xs, ys; λ) |
48 | 108 | end
|
49 | 109 | @test issorted(curvatures; rev = true) # in decreasing order (small λ => large curvature)
|
50 | 110 | @test issorted(distances) # in increasing order (large λ => large distance from data)
|
|
55 | 115 | weights = fill!(similar(xs), 1)
|
56 | 116 | S = fit(xs, ys, λ)
|
57 | 117 | Sw = @inferred fit(xs, ys, λ; weights) # equivalent to the default (all weights are 1)
|
| 118 | + check_zero_gradient(Sw, xs, ys; λ, weights) |
58 | 119 | @test coefficients(S) == coefficients(Sw)
|
59 | 120 | # Now give more weight to point i = 3
|
60 | 121 | weights[3] = 1000
|
61 | 122 | Sw = fit(xs, ys, λ; weights)
|
| 123 | + check_zero_gradient(Sw, xs, ys; λ, weights) |
62 | 124 | @test abs(Sw(xs[3]) - ys[3]) < abs(S(xs[3]) - ys[3]) # the new curve is closer to the data point i = 3
|
63 | 125 | @test total_curvature(Sw) > total_curvature(S) # since we give more importance to data fitting (basically, the sum of weights is larger)
|
64 | 126 | @test distance_from_data(Sw, xs, ys) < distance_from_data(S, xs, ys)
|
|
0 commit comments