Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile the objective #639

Merged
merged 103 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
7eb5f7a
Revising profiling of fixed-effects parameters.
dmbates Sep 13, 2022
223836b
Create a single Table from profileβ
dmbates Sep 14, 2022
c6dbfd2
Incorporate some of Phillip's suggestions.
dmbates Sep 15, 2022
d701e67
use a copy of remat.λ when profiling, change initial
dmbates Sep 15, 2022
432f352
shallow copy ReMats
palday Sep 16, 2022
a82277d
contravariance
palday Sep 16, 2022
d3d9c72
Add tests, names in MixedModelProfile, export
dmbates Sep 16, 2022
f6a970a
using BSplineKit, add confint for model and prof
dmbates Sep 19, 2022
799999e
Add tests for confint, be more careful of types
dmbates Sep 19, 2022
9bdac8e
Shuffle fields in profile object and adjust test
dmbates Sep 20, 2022
74c4706
Document functions and structs
dmbates Sep 20, 2022
7e19d32
Initial profilelogσ with fixed stepsz
dmbates Sep 21, 2022
fc082a6
Add profilelogσ and tests
dmbates Sep 21, 2022
39c3a3b
reformat
dmbates Sep 21, 2022
78b0f36
Suppress progress display on refits
dmbates Sep 21, 2022
48920d5
Avoid destructuring to allow for LTS test
dmbates Sep 21, 2022
c9406a2
use Compat.jl for destructuring syntax
palday Sep 22, 2022
49a39b8
Add BSplineKit and TypedTables to Project.toml
dmbates Oct 2, 2022
9c29952
New version of BSplineKit (w periodic splines)
dmbates Oct 4, 2022
beb7264
Merge branch 'main' into profile
dmbates Oct 11, 2022
030d5b7
Merge branch 'main' into profile
dmbates Oct 18, 2022
44fa4a7
Allow [email protected] for testing on LTS julia
dmbates Oct 18, 2022
428fa73
Use profile generic, return a table.
dmbates Oct 25, 2022
8d43e07
Back out a comment, fix tests.
dmbates Oct 25, 2022
927a805
Merge branch 'main' of github.com:JuliaStats/MixedModels.jl into profile
palday Oct 26, 2022
a156265
Be more careful with initial values in profileσ
dmbates Nov 2, 2022
1469cd9
Copy the vector, not just the reference to it
dmbates Nov 2, 2022
1027373
profile function with sigma, beta and theta
dmbates Nov 15, 2022
75cfed1
Fix confint method and comparison values in tests.
dmbates Nov 15, 2022
608d4a4
Add estimate to confint table, smaller δj, tests
dmbates Nov 16, 2022
3cc7b53
Store m and the vc's values. Use Tuples.
dmbates Nov 20, 2022
16dc0b3
Per discussion of #639
dmbates Nov 23, 2022
0c7c65b
Bump version of BSplineKit to allow extrapolation
dmbates Nov 24, 2022
4221eeb
Add and export objective! methods.
dmbates Nov 25, 2022
f81f44c
Split and update src/profile.jl
dmbates Jan 7, 2023
bb5dc6b
Minor correction in a test.
dmbates Jan 7, 2023
d7d314f
Merge branch 'main' into profile
dmbates Jan 7, 2023
f7ac63e
Remove unused definition.
dmbates Jan 27, 2023
7ab2697
Export Table, add .tbl property for bootstrap.
dmbates Jan 27, 2023
5f7d60c
Interim version of profiling, σs have problems.
dmbates Jan 27, 2023
8bf48e9
Fix σ profiles by moving lowerbd on θ
dmbates Jan 28, 2023
d192ba0
Merge current main
dmbates Jan 29, 2023
3f05c8a
Add documentation.
dmbates Jan 31, 2023
d063859
Require julia version 1.8 (b/c BSplineKit 0.14)
dmbates Feb 3, 2023
cd30dd4
Add confint for bootstrap, clean up for profile.
dmbates Feb 3, 2023
c1e78aa
Modify test to match current code
dmbates Feb 3, 2023
deae52c
Bump julia version for documenter to 1,8
dmbates Feb 3, 2023
a628555
Bump version of Aqua
dmbates Feb 7, 2023
f26a825
Check monotone splines. Comment out unused method
dmbates Feb 7, 2023
b9ffc30
Add more tests of profiling. Clean up method use.
dmbates Feb 7, 2023
29be05e
Merge branch 'main' of github.com:JuliaStats/MixedModels.jl into profile
palday Feb 8, 2023
1ac38be
Merge branch 'profile' of https://github.com/JuliaStats/MixedModels.j…
dmbates Feb 8, 2023
d0be208
'tbl' field of MixedModelProfile as a Table
dmbates Feb 10, 2023
3a7e616
JuliaFormatter
palday Feb 16, 2023
2bee46d
JuliaFormatter.format("src")
dmbates Feb 21, 2023
fd80166
Merge branch 'main' into profile
dmbates Feb 22, 2023
a9d7068
Clean up warning message.
dmbates Feb 22, 2023
f32cda2
Merge branch 'main' into profile
dmbates Feb 28, 2023
dc5de78
Return raneftables as a NamedTuple of DictTables
dmbates Feb 28, 2023
688833b
Update src/mixedmodel.jl
dmbates Feb 28, 2023
6aace76
Return a NamedTuple of Tables - easier to sort.
dmbates Mar 1, 2023
48e04d6
Merge branch 'main' into profile
dmbates Mar 10, 2023
17008ff
drop Compat
palday Mar 17, 2023
951adfc
Update src/profile/fixefpr.jl
dmbates Mar 20, 2023
15bd235
Update src/MixedModels.jl
dmbates Mar 20, 2023
8a4ab2c
Update src/linearmixedmodel.jl
dmbates Mar 20, 2023
9918f92
Update src/profile/fixefpr.jl
dmbates Mar 20, 2023
0df4893
Update src/profile/utilities.jl
dmbates Mar 20, 2023
06498ad
Replace Tuple type with stronger NTuple{N,Symbol}
dmbates Mar 20, 2023
1cfb445
Update src/profile/sigmapr.jl
dmbates Mar 20, 2023
fa241f4
add CI method comparison
palday Mar 21, 2023
bba580e
Merge branch 'profile' of github.com:JuliaStats/MixedModels.jl into p…
palday Mar 21, 2023
c16e674
Update src/profile/vcpr.jl
dmbates Mar 21, 2023
182d24f
Fix a syntax error
dmbates Mar 22, 2023
45fd0b7
Remove unnecessary qualifiers
dmbates Mar 22, 2023
1d664c5
Merge branch 'main' of github.com:JuliaStats/MixedModels.jl into profile
palday Apr 12, 2023
33b20b5
Merge branch 'main' into profile
dmbates Apr 12, 2023
a535e1f
Bump compat for BSplineKit
dmbates Apr 13, 2023
5ee3f72
JuliaFormatter change
dmbates Apr 13, 2023
2f54b68
Merge branch 'main' into profile
dmbates Apr 15, 2023
c89480c
Changes suggested in palday's review.
dmbates Apr 17, 2023
9f2ec89
Merge branch 'main' into profile
dmbates Apr 26, 2023
9d85ca4
Merge branch 'main' of github.com:JuliaStats/MixedModels.jl into profile
palday May 3, 2023
4f55a4f
Add news
palday May 3, 2023
afa4827
version bump
palday May 3, 2023
622c9ed
Add docstring and drop type parameter
dmbates May 4, 2023
69e6286
Document MixedModelProfile struct
dmbates May 4, 2023
a938c5a
Document a method as internal and potentially volatile
dmbates May 4, 2023
0c3904b
Document a method src/profile/sigmapr.jl as volatile
dmbates May 4, 2023
c65c61d
Document a method as internal and potentially volatile
dmbates May 4, 2023
16447b7
Document confint method
dmbates May 4, 2023
c064f74
Document confint method
dmbates May 4, 2023
a7926e2
Document an internal method
dmbates May 4, 2023
3a6baeb
Document confint method
dmbates May 4, 2023
c381204
Document profilevcl
dmbates May 4, 2023
40006af
Document the internal method profileσs!
dmbates May 4, 2023
1821e42
Refine target values in test/pls.jl
dmbates May 4, 2023
b35fdef
correct a typo of dot as separator
dmbates May 4, 2023
f031a03
Generalize a type comparison in test/pls.jl
dmbates May 4, 2023
48dee3e
Generalize a type comparison in test/pls.jl [ci skip]
dmbates May 4, 2023
664b033
Suppress progress updates in profile in tests [ci skip]
dmbates May 4, 2023
2c1be4c
Gene
dmbates May 4, 2023
f8f4559
fix indents
palday May 4, 2023
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 @@ -5,6 +5,7 @@ version = "4.13.1"

[deps]
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"
BSplineKit = "093aae92-e908-43d7-9660-e50ee39d5a0a"
DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a"
Expand All @@ -30,6 +31,7 @@ TypedTables = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9"

palday marked this conversation as resolved.
Show resolved Hide resolved
[compat]
Arrow = "1, 2"
BSplineKit = "0.14,0.15"
DataAPI = "1"
Distributions = "0.21, 0.22, 0.23, 0.24, 0.25"
GLM = "1.8.2"
Expand Down
9 changes: 9 additions & 0 deletions src/MixedModels.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module MixedModels

using Arrow
using BSplineKit
using DataAPI
using Distributions
using GLM
Expand Down Expand Up @@ -51,6 +52,7 @@ export @formula,
LogLink,
MixedModel,
MixedModelBootstrap,
MixedModelProfile,
Normal,
OptSummary,
Poisson,
Expand All @@ -60,6 +62,7 @@ export @formula,
ReMat,
SeqDiffCoding,
SqrtLink,
Table,
dmbates marked this conversation as resolved.
Show resolved Hide resolved
UniformBlockDiagonal,
VarCorr,
aic,
Expand All @@ -73,6 +76,7 @@ export @formula,
cond,
condVar,
condVartables,
confint,
deviance,
dispersion,
dispersion_parameter,
Expand Down Expand Up @@ -101,9 +105,13 @@ export @formula,
model_response,
nobs,
objective,
objective!,
parametricbootstrap,
pirls!,
predict,
profile,
profileσ,
dmbates marked this conversation as resolved.
Show resolved Hide resolved
profilevc,
pwrss,
ranef,
raneftables,
Expand Down Expand Up @@ -178,6 +186,7 @@ include("blockdescription.jl")
include("grouping.jl")
include("mimeshow.jl")
include("serialization.jl")
include("profile/profile.jl")

using PrecompileTools

Expand Down
63 changes: 63 additions & 0 deletions src/bootstrap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,28 @@ function allpars(bsamp::MixedModelFitCollection{T}) where {T}
)
end

function StatsBase.confint(bsamp::MixedModelBootstrap{T}; level::Real=0.95) where {T}
dmbates marked this conversation as resolved.
Show resolved Hide resolved
cutoff = sqrt(quantile(Chisq(1), level))
# Creating the table is somewhat wasteful because columns are created then immediately skipped.
tbl = Table(bsamp.tbl)
palday marked this conversation as resolved.
Show resolved Hide resolved
lower = T[]
upper = T[]
v = similar(tbl.σ)
par = sort!(
collect(
filter(
k -> !(startswith(string(k), 'θ') || string(k) == "obj"), propertynames(tbl)
),
),
)
for p in par
l, u = shortestcovint(sort!(copyto!(v, getproperty(tbl, p))), level)
push!(lower, l)
push!(upper, u)
end
return DictTable(; par, lower, upper)
end

function Base.getproperty(bsamp::MixedModelFitCollection, s::Symbol)
palday marked this conversation as resolved.
Show resolved Hide resolved
if s ∈ [:objective, :σ, :θ, :se]
getproperty.(getfield(bsamp, :fits), s)
Expand All @@ -176,6 +198,8 @@ function Base.getproperty(bsamp::MixedModelFitCollection, s::Symbol)
tidyσs(bsamp)
elseif s == :allpars
allpars(bsamp)
elseif s == :tbl
pbstrtbl(bsamp)
else
getfield(bsamp, s)
end
Expand Down Expand Up @@ -209,6 +233,7 @@ function Base.propertynames(bsamp::MixedModelFitCollection)
:lowerbd,
:fits,
:fcnames,
:tbl,
]
end

Expand Down Expand Up @@ -364,3 +389,41 @@ function tidyσs(bsamp::MixedModelFitCollection{T}) where {T}
end
return result
end

function pbstrtbl(bsamp::MixedModelFitCollection{T}) where {T}
(; fits, λ, inds) = bsamp
row1 = first(fits)
cnms = [:obj, :σ]
pos = Dict{Symbol,UnitRange{Int}}(:obj => 1:1, :σ => 2:2)
βsz = length(row1.β)
append!(cnms, _generatesyms('β', βsz))
lastpos = 2 + βsz
pos[:β] = 3:lastpos
σsz = sum(m -> size(m, 1), bsamp.λ)
append!(cnms, _generatesyms('σ', σsz))
pos[:σs] = (lastpos + 1):(lastpos + σsz)
lastpos += σsz
θsz = length(row1.θ)
append!(cnms, _generatesyms('θ', θsz))
pos[:θ] = (lastpos + 1):(lastpos + θsz)
tblrowtyp = NamedTuple{(cnms...,),NTuple{length(cnms),T}}
val = sizehint!(tblrowtyp[], length(bsamp.fits))
v = Vector{T}(undef, length(cnms))
for (i, r) in enumerate(bsamp.fits)
v[1] = r.objective
v[2] = coalesce(r.σ, one(T))
copyto!(view(v, pos[:β]), r.β)
fill!(view(v, pos[:σs]), zero(T))
copyto!(view(v, pos[:θ]), r.θ)
setθ!(bsamp, i)
vpos = first(pos[:σs])
for l in λ
for λr in eachrow(l)
v[vpos] = r.σ * norm(λr)
vpos += 1
end
end
push!(val, tblrowtyp(v))
end
return val
end
66 changes: 62 additions & 4 deletions src/linearmixedmodel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Linear mixed-effects model representation
"""
struct LinearMixedModel{T<:AbstractFloat} <: MixedModel{T}
formula::FormulaTerm
reterms::Vector{AbstractReMat{T}}
reterms::Vector{<:AbstractReMat{T}}
Xymat::FeMat{T}
feterm::FeTerm{T}
sqrtwts::Vector{T}
Expand Down Expand Up @@ -359,12 +359,22 @@ function condVartables(m::MixedModel{T}) where {T}
return NamedTuple{fnames(m)}((map(_cvtbl, condVar(m), m.reterms)...,))
end

function StatsBase.confint(m::MixedModel{T}; level=0.95) where {T}
dmbates marked this conversation as resolved.
Show resolved Hide resolved
cutoff = sqrt.(quantile(Chisq(1), level))
β, std = m.β, m.stderror
return DictTable(;
coef=coefnames(m),
lower=β .- cutoff .* std,
upper=β .+ cutoff .* std
Comment on lines +375 to +379
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens in rank deficiency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. Part of me just wants to tell users to ensure that their model matrices have full rank but that might be an unpopular suggestion. I'll have to look up what happens with the stderror property in that case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not adverse to adding a !!! warning entry to the docstring about rank deficiency and we can follow up on this when we have time / energy / necessity.

)
end

function _pushALblock!(A, L, blk)
push!(L, blk)
return push!(A, deepcopy(isa(blk, BlockedSparse) ? blk.cscmat : blk))
end

function createAL(reterms::Vector{AbstractReMat{T}}, Xy::FeMat{T}) where {T}
function createAL(reterms::Vector{<:AbstractReMat{T}}, Xy::FeMat{T}) where {T}
k = length(reterms)
vlen = kchoose2(k + 1)
A = sizehint!(AbstractMatrix{T}[], vlen)
Expand Down Expand Up @@ -548,7 +558,6 @@ the length of `v` can be the rank of `X` or the number of columns of `X`. In th
case the calculated coefficients are padded with -0.0 out to the number of columns.
"""
function fixef!(v::AbstractVector{Tv}, m::LinearMixedModel{T}) where {Tv,T}
Xtrm = m.feterm
fill!(v, -zero(Tv))
XyL = m.L[end]
L = feL(m)
Expand Down Expand Up @@ -765,7 +774,7 @@ end

lowerbd(m::LinearMixedModel) = m.optsum.lowerbd

function mkparmap(reterms::Vector{AbstractReMat{T}}) where {T}
function mkparmap(reterms::Vector{<:AbstractReMat{T}}) where {T}
parmap = NTuple{3,Int}[]
for (k, trm) in enumerate(reterms)
n = LinearAlgebra.checksquare(trm.λ)
Expand Down Expand Up @@ -796,6 +805,37 @@ function objective(m::LinearMixedModel{T}) where {T}
return isempty(wts) ? val : val - T(2.0) * sum(log, wts)
end

"""
objective!(m::LinearMixedModel, θ)
objective!(m::LinearMixedModel)

Equivalent to `objective(updateL!(setθ!(m, θ)))`.

When `m` has a single, scalar random-effects term, `θ` can be a scalar.

The one-argument method curries and returns a single-argument function of `θ`.

Note that these methods modify `m`.
The calling function is responsible for restoring the optimal `θ`.
"""
function objective! end

function objective!(m::LinearMixedModel)
return Base.Fix1(objective!, m)
end

function objective!(m::LinearMixedModel{T}, θ) where {T}
return objective(updateL!(setθ!(m, θ)))
end

function objective!(m::LinearMixedModel{T}, x::Number) where {T}
retrm = only(m.reterms)
isa(retrm, ReMat{T,1}) ||
throw(DimensionMismatch("length(m.θ) = $(length(m.θ)), should be 1"))
copyto!(retrm.λ.data, x)
return objective(updateL!(m))
end

function Base.propertynames(m::LinearMixedModel, private::Bool=false)
return (
fieldnames(LinearMixedModel)...,
Expand Down Expand Up @@ -996,6 +1036,24 @@ function setθ!(m::LinearMixedModel{T}, θ::AbstractVector) where {T}
return m
end

# This method is nearly identical to the previous one but determining a common signature
# to collapse these to a single definition would be tricky, so we repeat ourselves.
function setθ!(m::LinearMixedModel{T}, θ::NTuple{N,T}) where {T,N}
palday marked this conversation as resolved.
Show resolved Hide resolved
parmap, reterms = m.parmap, m.reterms
N == length(parmap) || throw(DimensionMismatch())
reind = 1
λ = first(reterms).λ
for (tv, tr) in zip(θ, parmap)
tr1 = first(tr)
if reind ≠ tr1
reind = tr1
λ = reterms[tr1].λ
end
λ[tr[2], tr[3]] = tv
end
return m
end

function Base.setproperty!(m::LinearMixedModel, s::Symbol, y)
return s == :θ ? setθ!(m, y) : setfield!(m, s, y)
end
Expand Down
107 changes: 107 additions & 0 deletions src/profile/fixefpr.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
struct FeProfile{T<:AbstractFloat} # derived model with the j'th fixed-effects coefficient held constant
m::LinearMixedModel{T} # copy of original model after removing the j'th column from X
tc::TableColumns{T}
y₀::Vector{T} # original response vector
xⱼ::Vector{T} # the column that was removed from X
j::Integer
end

"""
Base.copy(ReMat{T,S})

Return a shallow copy of ReMat.

A shallow copy shares as much internal storage as possible with the original ReMat.
Only the vector `λ` and the `scratch` matrix are copied.
"""
function Base.copy(ret::ReMat{T,S}) where {T,S}
return ReMat{T,S}(ret.trm,
ret.refs,
ret.levels,
ret.cnames,
ret.z,
ret.wtz,
copy(ret.λ),
ret.inds,
ret.adjA,
copy(ret.scratch))
end

## FIXME: also create a shallow copy of a LinearMixedModel object that performs a shallow copy of the reterms and the optsum.
## Probably don't bother to copy the components of L as we will always assume that an updateL! call precedes a call to
## objective.

function FeProfile(m::LinearMixedModel, tc::TableColumns, j::Integer)
Xy = m.Xymat.xy
xcols = collect(axes(Xy, 2))
ycol = pop!(xcols)
notj = deleteat!(xcols, j) # indirectly check that j ∈ xcols
y₀ = Xy[:, ycol]
xⱼ = Xy[:, j]
feterm = FeTerm(Xy[:, notj], m.feterm.cnames[notj])
reterms = [copy(ret) for ret in m.reterms]
mnew = fit!(
LinearMixedModel(y₀ - xⱼ * m.β[j], feterm, reterms, m.formula); progress=false
)
palday marked this conversation as resolved.
Show resolved Hide resolved
# not sure this next call makes sense - should the second argument be m.optsum.final?
palday marked this conversation as resolved.
Show resolved Hide resolved
_copy_away_from_lowerbd!(
mnew.optsum.initial, mnew.optsum.final, mnew.lowerbd; incr=0.05
)
return FeProfile(mnew, tc, y₀, xⱼ, j)
end

function betaprofile!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even though this is an internal function, it might be good to add a docstring for future us to distinguish it from profileβ

pr::FeProfile{T}, tc::TableColumns{T}, βⱼ::T, j::Integer, obj::T, neg::Bool
) where {T}
prm = pr.m
refit!(prm, mul!(copyto!(prm.y, pr.y₀), pr.xⱼ, βⱼ, -1, 1); progress=false)
(; positions, v) = tc
v[1] = (-1)^neg * sqrt(prm.objective - obj)
getθ!(view(v, positions[:θ]), prm)
v[first(positions[:σ])] = prm.σ
σvals!(view(v, positions[:σs]), prm)
β = prm.β
bpos = 0
for (i, p) in enumerate(positions[:β])
v[p] = (i == j) ? βⱼ : β[(bpos += 1)]
end
return first(v)
end

function profileβj!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring with expected entries in val?

val::NamedTuple, tc::TableColumns{T,N}, sym::Symbol; threshold=4
) where {T,N}
m = val.m
(; β, θ, σ, stderror, objective) = m
(; cnames, v) = tc
pnm = (; p=sym)
j = parsej(sym)
prj = FeProfile(m, tc, j)
st = stderror[j] * 0.5
bb = β[j] - st
tbl = [merge(pnm, mkrow!(tc, m, zero(T)))]
while true
ζ = betaprofile!(prj, tc, bb, j, objective, true)
push!(tbl, merge(pnm, NamedTuple{cnames,NTuple{N,T}}((v...,))))
if abs(ζ) > threshold
break
end
bb -= st
end
reverse!(tbl)
bb = β[j] + st
while true
palday marked this conversation as resolved.
Show resolved Hide resolved
ζ = betaprofile!(prj, tc, bb, j, objective, false)
push!(tbl, merge(pnm, NamedTuple{cnames,NTuple{N,T}}((v...,))))
if abs(ζ) > threshold
break
end
bb += st
end
append!(val.tbl, tbl)
ζv = getproperty.(tbl, :ζ)
βv = getproperty.(tbl, sym)
val.fwd[sym] = interpolate(βv, ζv, BSplineOrder(4), Natural())
val.rev[sym] = interpolate(ζv, βv, BSplineOrder(4), Natural())
return val
end
Loading