Niederer Benchmark#
In this demo we compare the pure cbcbeat
SplittingSolver
and the GOSSplittingSolver
on the Niederer benchmark problem. The code here is based in the original implementation in the cbcbeat
repo
First we import the required packages (including the GOSSplittingSolver
)
import dolfin
import cbcbeat
import goss
import numpy as np
import gotran
from cbcbeat.gossplittingsolver import GOSSplittingSolver
and define the given initial conditions from the problem descriptions
ic = {
"V": -85.23, # mV
"Xr1": 0.00621,
"Xr2": 0.4712,
"Xs": 0.0095,
"m": 0.00172,
"h": 0.7444,
"j": 0.7045,
"d": 3.373e-05,
"f": 0.7888,
"f2": 0.9755,
"fCass": 0.9953,
"s": 0.999998,
"r": 2.42e-08,
"Ca_i": 0.000126, # millimolar
"R_prime": 0.9073,
"Ca_SR": 3.64, # millimolar
"Ca_ss": 0.00036, # millimolar
"Na_i": 8.604, # millimolar
"K_i": 136.89, # millimolar
}
We make a general method for setting up the model
def setup_model(dx=0.5):
Lx = 20.0 # mm
Ly = 7.0 # mm
Lz = 3.0 # mm
N = lambda v: int(np.rint(v))
mesh = dolfin.BoxMesh(
dolfin.MPI.comm_world,
dolfin.Point(0.0, 0.0, 0.0),
dolfin.Point(Lx, Ly, Lz),
N(Lx / dx),
N(Ly / dx),
N(Lz / dx),
)
L = mesh.coordinates().max()
# Surface to volume ratio
chi = 140.0 # mm^{-1}
# Membrane capacitance
C_m = 0.01 # mu F / mm^2
# Conductivities as defined by page 4339 of Niederer benchmark
sigma_il = 0.17 # mS / mm
sigma_it = 0.019 # mS / mm
sigma_el = 0.62 # mS / mm
sigma_et = 0.24 # mS / mm
# Compute monodomain approximation by taking harmonic mean in each
# direction of intracellular and extracellular part
def harmonic_mean(a, b):
return a * b / (a + b)
sigma_l = harmonic_mean(sigma_il, sigma_el)
sigma_t = harmonic_mean(sigma_it, sigma_et)
# Scale conductivities by 1/(C_m * chi)
s_l = sigma_l / (C_m * chi) # mm^2 / ms
s_t = sigma_t / (C_m * chi) # mm^2 / ms
M = dolfin.as_tensor(((s_l, 0, 0), (0, s_t, 0), (0, 0, s_t)))
# Define time
time = dolfin.Constant(0.0)
# Mark stimulation region defined as [0, L]^3
S1_marker = 1
L = 1.5
S1_subdomain = dolfin.CompiledSubDomain(
"x[0] <= L + DOLFIN_EPS && x[1] <= L + DOLFIN_EPS && x[2] <= L + DOLFIN_EPS",
L=L,
)
S1_markers = dolfin.MeshFunction("size_t", mesh, mesh.topology().dim())
S1_subdomain.mark(S1_markers, S1_marker)
# Define stimulation (NB: region of interest carried by the mesh
# and assumptions in cbcbeat)
duration = 2.0 # ms
A = 50000.0 # mu A/cm^3
cm2mm = 10.0
factor = 1.0 / (chi * C_m) # NB: cbcbeat convention
amplitude = factor * A * (1.0 / cm2mm) ** 3 # mV/ms
I_s = dolfin.Expression(
"time >= start ? (time <= (duration + start) ? amplitude : 0.0) : 0.0",
time=time,
start=0.0,
duration=duration,
amplitude=amplitude,
degree=0,
)
# Store input parameters in cardiac model
stimulus = cbcbeat.Markerwise((I_s,), (1,), S1_markers)
return mesh, time, M, stimulus
One method for running the benchmark using goss
def run_benchmark_goss(mesh, time, M, stimulus, T, dt, scheme="GRL1"):
gotran_ode = gotran.load_ode("tentusscher_panfilov_2006_M_cell.ode")
cellmodel = goss.dolfinutils.DOLFINParameterizedODE(
gotran_ode,
field_states=gotran_ode.state_symbols,
)
# Do not apply any stimulus in the cell model
cellmodel.set_parameter("stim_amplitude", 0)
cellmodel.set_initial_conditions(**ic)
heart = cbcbeat.CardiacModel(mesh, time, M, None, cellmodel, stimulus)
ps = GOSSplittingSolver.default_parameters()
ps["pde_solver"] = "monodomain"
ps["MonodomainSolver"]["linear_solver_type"] = "iterative"
ps["MonodomainSolver"]["preconditioner"] = "sor"
ps["MonodomainSolver"]["theta"] = 0.5
ps["MonodomainSolver"]["default_timestep"] = dt
ps["MonodomainSolver"]["use_custom_preconditioner"] = False
ps["theta"] = 0.5
ps["enable_adjoint"] = False
ps["apply_stimulus_current_to_pde"] = True
ps["ode_solver"]["solver"] = scheme
V_index = heart.cell_models().state_names.index("V")
solver = GOSSplittingSolver(heart, ps, V_index=V_index)
# Extract the solution fields and set the initial conditions
(vs_, vs, vur) = solver.solution_fields()
# Set-up separate potential function for post processing
VS0 = vs.function_space().sub(V_index)
V = VS0.collapse()
v = dolfin.Function(V)
# Set-up object to optimize assignment from a function to subfunction
assigner = dolfin.FunctionAssigner(V, VS0)
assigner.assign(v, vs_.sub(V_index))
# Solve
solutions = solver.solve((0, T), dt)
vfile = dolfin.XDMFFile(dolfin.MPI.comm_world, "niederer_benchmark_goss.xdmf")
vfile.write(v, 0.0)
for i, ((t0, t1), fields) in enumerate(solutions):
if (i % 20 == 0) and dolfin.MPI.rank(dolfin.MPI.comm_world) == 0:
print("Reached t=%g/%g, dt=%g" % (t0, T, dt))
assigner.assign(v, vs_.sub(V_index))
vfile.write(v, t1)
and one method for running the benchmark using the original (vanilla)
def run_benchmark_vanilla(mesh, time, M, stimulus, T, dt, scheme="GRL1"):
CellModel = cbcbeat.Tentusscher_panfilov_2006_epi_cell
ps = cbcbeat.SplittingSolver.default_parameters()
ps["pde_solver"] = "monodomain"
ps["MonodomainSolver"]["linear_solver_type"] = "iterative"
ps["MonodomainSolver"]["theta"] = 0.5
ps["MonodomainSolver"]["preconditioner"] = "sor"
ps["MonodomainSolver"]["default_timestep"] = dt
ps["MonodomainSolver"]["use_custom_preconditioner"] = False
ps["theta"] = 0.5
ps["apply_stimulus_current_to_pde"] = True
ps["CardiacODESolver"]["scheme"] = scheme
cellmodel = CellModel(init_conditions=ic)
# Set-up cardiac model
heart = cbcbeat.CardiacModel(mesh, time, M, None, cellmodel, stimulus)
solver = cbcbeat.SplittingSolver(heart, ps)
# Extract the solution fields and set the initial conditions
(vs_, vs, vur) = solver.solution_fields()
vs_.assign(cellmodel.initial_conditions())
# Set-up separate potential function for post processing
VS0 = vs.function_space().sub(0)
V = VS0.collapse()
v = dolfin.Function(V)
# Set-up object to optimize assignment from a function to subfunction
assigner = dolfin.FunctionAssigner(V, VS0)
assigner.assign(v, vs_.sub(0))
t0 = 0.0
solutions = solver.solve((t0, T), dt)
vfile = dolfin.XDMFFile(dolfin.MPI.comm_world, "niederer_benchmark_vanilla.xdmf")
vfile.write(v, 0.0)
for i, ((t0, t1), fields) in enumerate(solutions):
if (i % 20 == 0) and dolfin.MPI.rank(dolfin.MPI.comm_world) == 0:
print("Reached t=%g/%g, dt=%g" % (t0, T, dt))
assigner.assign(v, vs_.sub(0))
vfile.write(v, t1)
Now lets run the two benchmarks and list the timings for comparison
dt = 0.5
dx = 0.1
T = 100.0
mesh, time, M, stimulus = setup_model(dx=dx)
with dolfin.Timer("Goss"):
run_benchmark_goss(mesh=mesh, time=time, M=M, stimulus=stimulus, dt=dt, T=T)
with dolfin.Timer("Vanilla"):
run_benchmark_vanilla(mesh=mesh, time=time, M=M, stimulus=stimulus, dt=dt, T=T)
dolfin.list_timings(dolfin.TimingClear.keep, [dolfin.TimingType.wall])
In my case the output of the timings are
[MPI_AVG] Summary of timings | reps wall avg wall tot
----------------------------------------------------------------------------
Apply (PETScMatrix) | 2 0.0015301 0.0030603
Apply (PETScVector) | 2425 7.7884e-06 0.018887
Assemble cells | 402 0.089612 36.024
Assemble rhs | 400 0.090153 36.061
Build BoxMesh | 1 0.018548 0.018548
Build sparsity | 2 0.059989 0.11998
Compute SCOTCH graph re-ordering | 6 0.0079531 0.047719
Compute connectivity 0-3 | 1 0.018011 0.018011
Compute connectivity 2-3 | 1 0.020575 0.020575
Compute entities dim = 2 | 1 0.18813 0.18813
Delete sparsity | 2 2.139e-06 4.278e-06
DistributedMeshTools: reorder vertex values | 402 0.0068479 2.7529
Goss | 1 90.596 90.596
Init dof vector | 9 0.01619 0.14571
Init dofmap | 6 0.32169 1.9302
Init dofmap from UFC dofmap | 6 0.037678 0.22607
Init tensor | 2 0.0034466 0.0068933
Merge step | 400 0.0014565 0.5826
Number distributed mesh entities | 6 1.7222e-06 1.0333e-05
ODE step | 800 0.13433 107.47
PDE Step | 400 0.10686 42.742
PETSc Krylov solver | 400 0.01652 6.6078
PointIntegralSolver::apply | 400 2.7312e-05 0.010925
PointIntegralSolver::step | 400 0.14869 59.477
SCOTCH: call SCOTCH_graphBuild | 6 2.4337e-05 0.00014602
SCOTCH: call SCOTCH_graphOrder | 6 0.00578 0.03468
Vanilla | 1 103.25 103.25
and we see that goss
is about 10% faster than the original “Vanilla” version.