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

Reaction tangent controller #138

Merged
merged 42 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b6aaeec
init
AbdAlazezAhmed Aug 13, 2024
83d4b6b
now it stops
AbdAlazezAhmed Aug 14, 2024
93d1eba
.
AbdAlazezAhmed Aug 14, 2024
ec3bd11
quick quick
AbdAlazezAhmed Aug 14, 2024
85bc9ba
restructuring
AbdAlazezAhmed Aug 14, 2024
a5ccab2
test?
AbdAlazezAhmed Aug 14, 2024
0bf9c9e
error and a couple of returns
AbdAlazezAhmed Aug 14, 2024
aaf7eec
some docs
AbdAlazezAhmed Aug 14, 2024
03827ed
ugh zero default
AbdAlazezAhmed Aug 14, 2024
6451030
.
AbdAlazezAhmed Aug 14, 2024
7a39637
Now it doesn't allocate
AbdAlazezAhmed Aug 15, 2024
5d78f91
some formatting
AbdAlazezAhmed Aug 15, 2024
f909309
example formatting
AbdAlazezAhmed Aug 15, 2024
ca0cd7c
retcode
AbdAlazezAhmed Aug 15, 2024
e5dbc21
add \pi so it doesn't align with tstops hehe
AbdAlazezAhmed Aug 15, 2024
d2af0f9
nans but commented out
AbdAlazezAhmed Aug 15, 2024
071ecd4
assume only one problem
AbdAlazezAhmed Aug 15, 2024
921ab74
docs oopsie
AbdAlazezAhmed Aug 15, 2024
c98c348
adaptive test UwU
AbdAlazezAhmed Aug 16, 2024
bad3da1
Merge branch 'main' of https://github.com/AbdAlazezAhmed/Thunderbolt.…
AbdAlazezAhmed Aug 16, 2024
b5b4195
Better?
AbdAlazezAhmed Aug 19, 2024
0ccbd02
Docs
AbdAlazezAhmed Aug 19, 2024
b505899
oopsie
AbdAlazezAhmed Aug 19, 2024
6ef8ba3
Merge branch 'main' into RTC
AbdAlazezAhmed Aug 20, 2024
f3ad8d3
Next: Error checking for NaNs
AbdAlazezAhmed Aug 20, 2024
f1a72de
Merge branch 'RTC' of https://github.com/AbdAlazezAhmed/Thunderbolt.j…
AbdAlazezAhmed Aug 20, 2024
c685bf3
Try mabe CI works now?
AbdAlazezAhmed Aug 20, 2024
06a541b
Don't merge, homeoffice push
AbdAlazezAhmed Aug 21, 2024
ffe2128
nans test
AbdAlazezAhmed Aug 26, 2024
61ecbc9
multiple pwodef
AbdAlazezAhmed Aug 26, 2024
fbeed40
Inf
AbdAlazezAhmed Aug 26, 2024
e39a773
indentation
AbdAlazezAhmed Aug 26, 2024
e19fd50
access u from caches directly
AbdAlazezAhmed Aug 26, 2024
c981cf7
Merge branch 'main' into RTC
termi-official Aug 28, 2024
09b7993
Merge branch 'main' into RTC
termi-official Aug 28, 2024
68bd74c
remove unroll filter
AbdAlazezAhmed Aug 30, 2024
b7f6773
adaptivity -> time/rtc
AbdAlazezAhmed Aug 30, 2024
934a4d3
remove unused type
AbdAlazezAhmed Aug 30, 2024
0843020
yes
AbdAlazezAhmed Aug 30, 2024
c912e1a
or not to do
AbdAlazezAhmed Aug 30, 2024
3a64dcc
use only current R
AbdAlazezAhmed Aug 30, 2024
5023e38
inf
AbdAlazezAhmed Aug 30, 2024
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
8 changes: 8 additions & 0 deletions docs/src/api-reference/solver.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ Thunderbolt.OS.LieTrotterGodunov
Thunderbolt.OS.GenericSplitFunction
Thunderbolt.OS.OperatorSplittingIntegrator
```

## Operator Splitting Adaptivity

```@docs
Thunderbolt.AdaptiveOperatorSplittingAlgorithm
termi-official marked this conversation as resolved.
Show resolved Hide resolved
Thunderbolt.ReactionTangentController
Thunderbolt.get_reaction_tangent
termi-official marked this conversation as resolved.
Show resolved Hide resolved
```
10 changes: 10 additions & 0 deletions docs/src/assets/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,13 @@ @article{PotDubRicVinGul:2006:cmb
pages={2425-2435},
doi={10.1109/TBME.2006.880875}
}
@article{OgiBalPer:2023:seats,
author = {Ogiermann, Dennis and Perotti, Luigi E. and Balzani, Daniel},
title = {A simple and efficient adaptive time stepping technique for low-order operator splitting schemes applied to cardiac electrophysiology},
journal = {International Journal for Numerical Methods in Biomedical Engineering},
volume = {39},
number = {2},
pages = {e3670},
url = {https://onlinelibrary.wiley.com/doi/abs/10.1002/cnm.3670},
year = {2023}
termi-official marked this conversation as resolved.
Show resolved Hide resolved
}
28 changes: 16 additions & 12 deletions examples/conduction-velocity-benchmark.jl
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,22 @@ steady_state_initializer!(u₀, odeform)

# io = ParaViewWriter("spiral-wave-test")


timestepper = OS.LieTrotterGodunov((
BackwardEulerSolver(
solution_vector_type=Vector{Float32},
system_matrix_type=Thunderbolt.ThreadedSparseMatrixCSR{Float32, Int32},
inner_solver=LinearSolve.KrylovJL_CG(atol=1.0f-6, rtol=1.0f-5),
),
AdaptiveForwardEulerSubstepper(
solution_vector_type=Vector{Float32},
reaction_threshold=0.1f0,
),
))
timestepper = Thunderbolt.AdaptiveOperatorSplittingAlgorithm(
OS.LieTrotterGodunov((
BackwardEulerSolver(
solution_vector_type=Vector{Float32},
system_matrix_type=Thunderbolt.ThreadedSparseMatrixCSR{Float32, Int32},
inner_solver=LinearSolve.KrylovJL_CG(atol=1.0f-6, rtol=1.0f-5),
),
AdaptiveForwardEulerSubstepper(
solution_vector_type=Vector{Float32},
reaction_threshold=0.1f0,
)
)),
Thunderbolt.ReactionTangentController(
0.5, 1.0, (0.01, 0.3)
)
)

problem = OS.OperatorSplittingProblem(odeform, u₀, tspan)

Expand Down
4 changes: 3 additions & 1 deletion src/Thunderbolt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Thunderbolt

using TimerOutputs

import Unrolled: @unroll
import Unrolled: @unroll, unrolled_filter
termi-official marked this conversation as resolved.
Show resolved Hide resolved

using Reexport, UnPack
import LinearAlgebra: mul!
Expand Down Expand Up @@ -72,6 +72,8 @@ include("solver/interface.jl")
include("solver/linear.jl")
include("solver/nonlinear.jl")
include("solver/time_integration.jl")
include("solver/adaptivity.jl")


include("processing/ecg.jl")

Expand Down
99 changes: 99 additions & 0 deletions src/solver/adaptivity.jl
termi-official marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
abstract type AbstractTimeAdaptionAlgorithm end
termi-official marked this conversation as resolved.
Show resolved Hide resolved

"""
ReactionTangentController{T <: Real} <: AbstractTimeAdaptionAlgorithm
A timestep length controller for [`LieTrotterGodunov`](@ref) [Lie:1880:tti,Tro:1959:psg,God:1959:dmn](@cite)
operator splitting using the reaction tangent as proposed in [OgiBalPer:2023:seats](@cite)
# Fields
- `σ_s::T`: steepness
- `σ_c::T`: offset in R axis
- `Δt_bounds::NTuple{2,T}`: lower and upper timestep length bounds
- `Rₙ₊₁::T`: updated maximal reaction magnitude
- `Rₙ::T`: previous reaction magnitude
"""
mutable struct ReactionTangentController{T <: Real} <: AbstractTimeAdaptionAlgorithm
const σ_s::T
const σ_c::T
const Δt_bounds::NTuple{2,T}
Rₙ₊₁::T
Rₙ::T
termi-official marked this conversation as resolved.
Show resolved Hide resolved
end
termi-official marked this conversation as resolved.
Show resolved Hide resolved

function ReactionTangentController(σ_s::T, σ_c::T, Δt_bounds::NTuple{2,T}) where {T <: Real}
return ReactionTangentController(σ_s, σ_c, Δt_bounds, 0.0, 0.0)
end


"""
AdaptiveOperatorSplittingAlgorithm{TOperatorSplittingAlg <: OS.AbstractOperatorSplittingAlgorithm, TTimeAdaptionAlgorithm <: AbstractTimeAdaptionAlgorithm} <: OS.AbstractOperatorSplittingAlgorithm
A generic operator splitting algorithm `operator_splitting_algorithm` with adaptive timestepping using the controller `controller`.
# Fields
- `operator_splitting_algorithm::TOperatorSplittingAlg`: steepness
- `controller::TTimeAdaptionAlgorithm`: offset in R axis
"""
struct AdaptiveOperatorSplittingAlgorithm{TOperatorSplittingAlg <: OS.AbstractOperatorSplittingAlgorithm, TTimeAdaptionAlgorithm <: AbstractTimeAdaptionAlgorithm} <: OS.AbstractOperatorSplittingAlgorithm
operator_splitting_algorithm::TOperatorSplittingAlg
controller::TTimeAdaptionAlgorithm
end

@inline DiffEqBase.isadaptive(::AdaptiveOperatorSplittingAlgorithm) = true

"""
get_reaction_tangent(integrator::OS.OperatorSplittingIntegrator)
Returns the maximal reaction magnitude using the [`PointwiseODEFunction`](@ref) of an operator splitting integrator that uses [`LieTrotterGodunov`](@ref) [Lie:1880:tti,Tro:1959:psg,God:1959:dmn](@cite).
It is assumed that the problem containing the reaction tangent is a [`PointwiseODEFunction`](@ref).
"""
@inline function get_reaction_tangent(integrator::OS.OperatorSplittingIntegrator)
reaction_subintegrators = unrolled_filter(subintegrator -> subintegrator.f isa PointwiseODEFunction, integrator.subintegrators)
isempty(reaction_subintegrators) && @error "PointwiseODEFunction not found"
_get_reaction_tangent(reaction_subintegrators)
end

@inline #=@unroll=# function _get_reaction_tangent(subintegrators)
subintegrator = subintegrators[1]
# @unroll for subintegrator in subintegrators
#TODO: It should be either all the the same type or just one subintegrator after filtering, don't unroll?
φₘidx = transmembranepotential_index(subintegrator.f.ode)
return maximum(@view subintegrator.cache.dumat[:, φₘidx])
# end
end

@inline function OS.stepsize_controller!(integrator::OS.OperatorSplittingIntegrator, alg::AdaptiveOperatorSplittingAlgorithm)
OS.stepsize_controller!(integrator, alg.controller, alg)
end

@inline function OS.step_accept_controller!(integrator::OS.OperatorSplittingIntegrator, alg::AdaptiveOperatorSplittingAlgorithm, q)
OS.step_accept_controller!(integrator, alg.controller, alg, q)
end

@inline function OS.step_reject_controller!(integrator::OS.OperatorSplittingIntegrator, alg::AdaptiveOperatorSplittingAlgorithm, q)
OS.step_reject_controller!(integrator, alg.controller, alg, q)
end

@inline function OS.stepsize_controller!(integrator::OS.OperatorSplittingIntegrator, controller::ReactionTangentController, alg::AdaptiveOperatorSplittingAlgorithm{<:OS.LieTrotterGodunov})
@unpack σ_s, σ_c, Δt_bounds, Rₙ₊₁, Rₙ = controller
controller.Rₙ = controller.Rₙ₊₁
controller.Rₙ₊₁ = get_reaction_tangent(integrator)
return nothing
end

@inline function OS.step_accept_controller!(integrator::OS.OperatorSplittingIntegrator, controller::ReactionTangentController, alg::AdaptiveOperatorSplittingAlgorithm{<:OS.LieTrotterGodunov}, q)
@unpack σ_s, σ_c, Δt_bounds, Rₙ₊₁, Rₙ = controller
R = max(Rₙ, Rₙ₊₁)
integrator.dt = (1 - 1/(1+exp((σ_c - R)*σ_s)))*(Δt_bounds[2] - Δt_bounds[1]) + Δt_bounds[1]
return nothing
end

@inline function OS.step_reject_controller!(integrator::OS.OperatorSplittingIntegrator, controller::ReactionTangentController, alg::AdaptiveOperatorSplittingAlgorithm{<:OS.LieTrotterGodunov}, q)
return nothing # Do nothing
end

# Dispatch for outer construction
function OS.init_cache(prob::OS.OperatorSplittingProblem, alg::AdaptiveOperatorSplittingAlgorithm; dt, kwargs...) # TODO
OS.init_cache(prob, alg.operator_splitting_algorithm;dt = dt, kwargs...)
end

# Dispatch for recursive construction
function OS.construct_inner_cache(f::OS.AbstractOperatorSplitFunction, alg::AdaptiveOperatorSplittingAlgorithm, u::AbstractArray, uprev::AbstractArray)
OS.construct_inner_cache(f, alg.operator_splitting_algorithm, u, uprev)
end
termi-official marked this conversation as resolved.
Show resolved Hide resolved
44 changes: 38 additions & 6 deletions src/solver/operator_splitting/integrator.jl
termi-official marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function DiffEqBase.__init(
callback = nothing,
advance_to_tstop = false,
save_func = (u, t) -> copy(u), # custom kwarg
dtchangeable = true, # custom kwarg
dtchangeable = DiffEqBase.isadaptive(alg), # custom kwarg
kwargs...,
)
(; u0, p) = prob
Expand Down Expand Up @@ -111,6 +111,7 @@ function DiffEqBase.reinit!(
tstops = integrator._tstops,
saveat = integrator._saveat,
reinit_callbacks = true,
reinit_retcode = true
)
(t0,tf) = tspan
integrator.u .= u0
Expand All @@ -126,6 +127,9 @@ function DiffEqBase.reinit!(
saving_callback = integrator.callback.discrete_callbacks[end]
DiffEqBase.initialize!(saving_callback, u0, t0, integrator)
end
if reinit_retcode
integrator.sol = DiffEqBase.solution_new_retcode(integrator.sol, DiffEqBase.ReturnCode.Default)
end
end

# called by DiffEqBase.solve
Expand Down Expand Up @@ -166,8 +170,6 @@ function DiffEqBase.step!(integrator::OperatorSplittingIntegrator, dt, stop_at_t
end
end



# TimeChoiceIterator API
@inline function DiffEqBase.get_tmp_cache(integrator::OperatorSplittingIntegrator)
DiffEqBase.get_tmp_cache(integrator, integrator.alg, integrator.cache)
Expand All @@ -184,7 +186,35 @@ function (integrator::OperatorSplittingIntegrator)(tmp, t)
linear_interpolation!(tmp, t, integrator.uprev, integrator.u, integrator.tprev, integrator.t)
end

"""
stepsize_controller!(::OperatorSplittingIntegrator)
Updates the controller using the current state of the integrator if the operator splitting algorithm is adaptive.
"""
@inline function stepsize_controller!(integrator::OperatorSplittingIntegrator)
algorithm = integrator.alg
DiffEqBase.isadaptive(algorithm) || return nothing
stepsize_controller!(integrator, algorithm)
end

"""
step_accept_controller!(::OperatorSplittingIntegrator)
Updates `_dt` of the integrator if the step is accepted and the operator splitting algorithm is adaptive.
"""
@inline function step_accept_controller!(integrator::OperatorSplittingIntegrator)
algorithm = integrator.alg
DiffEqBase.isadaptive(algorithm) || return nothing
step_accept_controller!(integrator, algorithm, nothing)
end

"""
step_reject_controller!(::OperatorSplittingIntegrator)
Updates `_dt` of the integrator if the step is rejected and the the operator splitting algorithm is adaptive.
"""
@inline function step_reject_controller!(integrator::OperatorSplittingIntegrator)
algorithm = integrator.alg
DiffEqBase.isadaptive(algorithm) || return nothing
step_reject_controller!(integrator, algorithm, nothing)
end

# helper functions for dealing with time-reversed integrators in the same way
# that OrdinaryDiffEq.jl does
Expand Down Expand Up @@ -220,18 +250,21 @@ end

function __step!(integrator)
(; dtchangeable, tstops) = integrator
_dt = DiffEqBase.get_dt(integrator)
_dt = DiffEqBase.isadaptive(integrator.alg) ? DiffEqBase.get_dt(integrator) : integrator.dt

# update dt before incrementing u; if dt is changeable and there is
# a tstop within dt, reduce dt to tstop - t
integrator.dt =
!isempty(tstops) && dtchangeable ? tdir(integrator) * min(_dt, abs(first(tstops) - integrator.t)) :
tdir(integrator) * _dt

# Propagate information down into the subintegrators
synchronize_subintegrators!(integrator)
termi-official marked this conversation as resolved.
Show resolved Hide resolved
tnext = integrator.t + integrator.dt

# Solve inner problems
advance_solution_to!(integrator, tnext)
stepsize_controller!(integrator)

# Update integrator
# increment t by dt, rounding to the first tstop if that is roughly
Expand All @@ -242,8 +275,7 @@ function __step!(integrator)
integrator.tprev = integrator.t
integrator.t = !isempty(tstops) && abs(first(tstops) - tnext) < max_t_error ? first(tstops) : tnext

# Propagate information down into the subintegrators
synchronize_subintegrators!(integrator)
step_accept_controller!(integrator)

# remove tstops that were just reached
while !isempty(tstops) && reached_tstop(integrator, first(tstops))
Expand Down
6 changes: 3 additions & 3 deletions src/solver/operator_splitting/solver.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Lie-Trotter-Godunov Splitting Implementation
"""
LieTrotterGodunov <: AbstractOperatorSplittingAlgorithm
Expand All @@ -9,6 +8,8 @@ struct LieTrotterGodunov{AlgTupleType} <: AbstractOperatorSplittingAlgorithm
# transfer_algs::TransferTupleType # Tuple of transfer algorithms from the master solution into the individual ones
end

@inline DiffEqBase.isadaptive(::AbstractOperatorSplittingAlgorithm) = false

struct LieTrotterGodunovCache{uType, tmpType, iiType} <: AbstractOperatorSplittingCache
u::uType
uprev::uType # True previous solution
Expand Down Expand Up @@ -54,5 +55,4 @@ end
advance_solution_to!(subinteg, inner_caches[i], tnext)
finalize_local_step!(subinteg)
end
end

end
2 changes: 1 addition & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ end
# Transfer the element data into a vector
function ea_collapse!(b::Vector, bes::EAVector)
ndofs = size(b, 1)
@batch minbatch= max(1, ndofs÷Threads.nthreads()) for dof ∈ 1:ndofs
@batch minbatch=max(1, ndofs÷Threads.nthreads()) for dof ∈ 1:ndofs
_ea_collapse_kernel!(b, dof, bes)
end
end
Expand Down
47 changes: 36 additions & 11 deletions test/integration/test_electrophysiology.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ using Thunderbolt
end
end

function solve_waveprop(mesh, coeff, subdomains)
function solve_waveprop(mesh, coeff, subdomains, isadaptive = false)
termi-official marked this conversation as resolved.
Show resolved Hide resolved
cs = CoordinateSystemCoefficient(CartesianCoordinateSystem(mesh))
model = MonodomainModel(
ConstantCoefficient(1.0),
Expand All @@ -45,10 +45,18 @@ using Thunderbolt
mesh
)

timestepper = LieTrotterGodunov((
_timestepper = LieTrotterGodunov((
BackwardEulerSolver(),
ForwardEulerCellSolver()
))
if isadaptive
timestepper = Thunderbolt.AdaptiveOperatorSplittingAlgorithm(
_timestepper,
Thunderbolt.ReactionTangentController(0.5, 1.0, (0.01, 0.3))
)
else
timestepper = _timestepper
end

u₀ = zeros(Float64, OS.function_size(odeform))
simple_initializer!(u₀, odeform)
Expand All @@ -60,26 +68,43 @@ using Thunderbolt
DiffEqBase.solve!(integrator)
@test integrator.sol.retcode == DiffEqBase.ReturnCode.Success
@test integrator.u ≉ u₀
return integrator.u
end

mesh = generate_mesh(Hexahedron, (4, 4, 4), Vec{3}((0.0,0.0,0.0)), Vec{3}((1.0,1.0,1.0)))
coeff = ConstantCoefficient(SymmetricTensor{2,3,Float64}((4.5e-5, 0, 0, 2.0e-5, 0, 1.0e-5)))
solve_waveprop(mesh, coeff, [""])
u = solve_waveprop(mesh, coeff, [""])
u_adaptive = solve_waveprop(mesh, coeff, [""], true)
@test u ≈ u_adaptive

mesh = generate_ideal_lv_mesh(4,1,1)
coeff = ConstantCoefficient(SymmetricTensor{2,3,Float64}((4.5e-5, 0, 0, 2.0e-5, 0, 1.0e-5)))
solve_waveprop(mesh, coeff, [""])

u = solve_waveprop(mesh, coeff, [""])
u_adaptive = solve_waveprop(mesh, coeff, [""], true)
@test u ≈ u_adaptive

mesh = to_mesh(generate_mixed_grid_2D())
coeff = ConstantCoefficient(SymmetricTensor{2,2,Float64}((4.5e-5, 0, 2.0e-5)))
solve_waveprop(mesh, coeff, ["Pacemaker", "Myocardium"])
solve_waveprop(mesh, coeff, ["Pacemaker"])
solve_waveprop(mesh, coeff, ["Myocardium"])
u = solve_waveprop(mesh, coeff, ["Pacemaker", "Myocardium"])
u_adaptive = solve_waveprop(mesh, coeff, ["Pacemaker", "Myocardium"], true)
@test u ≈ u_adaptive
u = solve_waveprop(mesh, coeff, ["Pacemaker"])
u_adaptive = solve_waveprop(mesh, coeff, ["Pacemaker"], true)
@test u ≈ u_adaptive
u = solve_waveprop(mesh, coeff, ["Myocardium"])
u_adaptive = solve_waveprop(mesh, coeff, ["Myocardium"], true)
@test u ≈ u_adaptive

mesh = to_mesh(generate_mixed_dimensional_grid_3D())
coeff = ConstantCoefficient(SymmetricTensor{2,3,Float64}((4.5e-5, 0, 0, 2.0e-5, 0, 1.0e-5)))
solve_waveprop(mesh, coeff, ["Ventricle"])
u = solve_waveprop(mesh, coeff, ["Ventricle"])
u_adaptive = solve_waveprop(mesh, coeff, ["Ventricle"])
@test u ≈ u_adaptive
coeff = ConstantCoefficient(SymmetricTensor{2,3,Float64}((5e-5, 0, 0, 5e-5, 0, 5e-5)))
solve_waveprop(mesh, coeff, ["Purkinje"])
solve_waveprop(mesh, coeff, ["Ventricle", "Purkinje"])
u = solve_waveprop(mesh, coeff, ["Purkinje"])
u_adaptive = solve_waveprop(mesh, coeff, ["Purkinje"])
@test u ≈ u_adaptive
u = solve_waveprop(mesh, coeff, ["Ventricle", "Purkinje"])
u_adaptive = solve_waveprop(mesh, coeff, ["Ventricle", "Purkinje"])
@test u ≈ u_adaptive
end
Loading