Skip to content

Commit 51e7461

Browse files
Fix remaining issues
1 parent 4c3955d commit 51e7461

3 files changed

Lines changed: 77 additions & 63 deletions

File tree

src/mesh/meshes.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function add_subdomain!(dh::DofHandler{<:Any, <:SimpleMesh}, name::String, appro
4040
cells = mesh.grid.cells
4141
haskey(mesh.volumetric_subdomains, name) || error("Volumetric Subdomain $name not found on mesh. Available subdomains: $(keys(mesh.volumetric_subdomains))")
4242
for (celltype, cellset) in mesh.volumetric_subdomains[name].data
43+
# @info name, length(cellset)
4344
sdh = SubDofHandler(dh, OrderedSet{Int}([idx.idx for idx in cellset]))
4445
for ad in approxmations
4546
add!(sdh, ad.sym, getinterpolation(ad.ipc, cells[first(sdh.cellset)]))

src/modeling/electrophysiology/ecg.jl

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,10 @@ stiffness matrix with applied homogeneous Dirichlet boundary condition at the fi
154154
"""
155155
struct PoissonECGReconstructionCache{DiffusionOperatorType1, DiffusionOperatorType2, TransferOperatorType, SolutionVectorType, SolverCacheType, PHType, CHType}
156156
torso_op::DiffusionOperatorType1 # Operator on the torso mesh for ∇κ∇
157-
heart_op::DiffusionOperatorType2 # Operator on the heart mesh for ∇κᵢ∇
157+
source_op::DiffusionOperatorType2 # Operator on the heart mesh for ∇κᵢ∇
158158
transfer_op::TransferOperatorType # Transfer from heart to torso mesh
159159
ϕₑ::SolutionVectorType # Solution vector buffer
160-
κ∇φₘ_h::SolutionVectorType # Source term buffer on heart
160+
φₘ_t::SolutionVectorType # Solution vector buffer on torso
161161
κ∇φₘ_t::SolutionVectorType # Source term buffer on torso
162162
inner_solver::SolverCacheType # Linear solver
163163
ph::PHType # PointEvalHandler on the torso
@@ -237,16 +237,16 @@ function PoissonECGReconstructionCache(
237237
subdomains_to = get_subdofhandler_indices_on_subdomains(torso_dh, torso_heart_domain)
238238
)
239239

240-
heart_op = setup_assembled_operator(
240+
source_op = setup_assembled_operator(
241241
BilinearDiffusionIntegrator(
242242
heart_diffusion_tensor_field,
243243
qrc,
244244
extracellular_potential_symbol,
245245
),
246246
system_matrix_type,
247-
heart_dh,
247+
torso_dh,
248248
)
249-
update_operator!(heart_op, 0.) # Trigger assembly
249+
update_operator!(source_op, 0.) # Trigger assembly
250250

251251
torso_op = setup_assembled_operator(
252252
BilinearDiffusionIntegrator(
@@ -268,7 +268,7 @@ function PoissonECGReconstructionCache(
268268
PoissonECGReconstructionCache(
269269
heart_fun,
270270
torso_fun,
271-
heart_op,
271+
source_op,
272272
torso_op,
273273
transfer_op,
274274
ph;
@@ -280,7 +280,7 @@ end
280280
function PoissonECGReconstructionCache(
281281
heart_fun::AffineODEFunction,
282282
torso_fun::AffineSteadyStateFunction,
283-
heart_op::AssembledBilinearOperator,
283+
source_op::AssembledBilinearOperator,
284284
torso_op::AssembledBilinearOperator,
285285
transfer_op::AbstractTransferOperator,
286286
ph::PointEvalHandler;
@@ -292,7 +292,7 @@ function PoissonECGReconstructionCache(
292292
grid = get_grid(torso_dh)
293293
length(torso_dh.field_names) == 1 || @warn "Multiple fields detected. Setup might be broken..."
294294

295-
κ∇φₘh = create_system_vector(solution_vector_type, heart_fun) # RHS buffer before transfer
295+
φₘt = create_system_vector(solution_vector_type, torso_fun) # RHS buffer for source term
296296
κ∇φₘt = create_system_vector(solution_vector_type, torso_fun) # RHS buffer after transfer
297297
κ∇φₘt .= 0.0
298298
ϕₑ = create_system_vector(solution_vector_type, torso_fun) # Solution vector
@@ -302,14 +302,14 @@ function PoissonECGReconstructionCache(
302302
)
303303
lincache = init(linprob, linear_solver)
304304

305-
return PoissonECGReconstructionCache(torso_op, heart_op, transfer_op, ϕₑ, κ∇φₘh, κ∇φₘt, lincache, ph, torso_ch)
305+
return PoissonECGReconstructionCache(torso_op, source_op, transfer_op, ϕₑ, φₘt, κ∇φₘt, lincache, ph, torso_ch)
306306
end
307307

308308
function update_ecg!(cache::PoissonECGReconstructionCache, φₘ::AbstractVector)
309-
# Compute κᵢ∇φₘ on the heart
310-
mul!(cache.κ∇φₘ_h, cache.heart_op, φₘ)
311-
# Transfer κᵢ∇φₘ to the torso
312-
transfer!(cache.κ∇φₘ_t, cache.transfer_op, cache.κ∇φₘ_h)
309+
# Transfer φₘ to the torso
310+
transfer!(cache.φₘ_t, cache.transfer_op, φₘ)
311+
# Compute κᵢ∇φₘ on the torso
312+
mul!(cache.κ∇φₘ_t, cache.source_op, cache.φₘ_t)
313313
cache.κ∇φₘ_t[isnan.(cache.κ∇φₘ_t)] .= 0.0 # FIXME
314314
# "Move to right hand side
315315
cache.κ∇φₘ_t .*= -1.0
@@ -348,9 +348,9 @@ V(t)=\\int \\nabla Z(\\boldsymbol{x}) \\cdot \\boldsymbol{\\kappa}_\\mathrm{i} \
348348
```
349349
"""
350350
struct Geselowitz1989ECGLeadCache{TZ <: AbstractMatrix, DiffusionOperatorType, TransferOperatorType, SolutionVectorType <: AbstractVector, ElectrodesVecType}
351-
heart_op::DiffusionOperatorType # Operator on the heart mesh for ∇κᵢ∇
351+
source_op::DiffusionOperatorType # Operator on the heart mesh for ∇κᵢ∇
352352
transfer_op::TransferOperatorType # Transfer from heart to torso mesh
353-
κ∇φₘ_h::SolutionVectorType # Source term buffer on heart
353+
φₘ_t::SolutionVectorType # Potential field on torso
354354
κ∇φₘ_t::SolutionVectorType # Source term buffer on torso
355355
Z::TZ # Lead field
356356
electrode_positions::ElectrodesVecType
@@ -419,7 +419,7 @@ end
419419
function Geselowitz1989ECGLeadCache(
420420
heart_fun::AffineODEFunction,
421421
torso_grid::AbstractGrid,
422-
heart_diffusion_tensor_field, # κᵢ - diffusion tensor description for heart on heart grid
422+
heart_diffusion_tensor_field, # κᵢ - diffusion tensor description for heart on heart grid alone
423423
full_diffusion_tensor_field, # κ - diffusion tensor description for heart and torso on torso grid
424424
electrode_positions::AbstractVector{Vector{VertexIndex}};
425425
ipc = LagrangeCollection{1}(),
@@ -454,15 +454,14 @@ function Geselowitz1989ECGLeadCache(
454454
torso_grid
455455
)
456456

457-
heart_grid = get_grid(heart_fun.dh)
458457
sourcefun = semidiscretize(
459458
source_model,
460459
FiniteElementDiscretization(
461460
Dict(tmpsym => ipc),
462461
Dirichlet[],
463-
subdomain_names(heart_grid),
462+
subdomain_names(torso_grid),
464463
),
465-
heart_grid,
464+
torso_grid,
466465
)
467466

468467
ϕₘ_op = setup_assembled_operator(
@@ -472,7 +471,7 @@ function Geselowitz1989ECGLeadCache(
472471
tmpsym,
473472
),
474473
system_matrix_type,
475-
sourcefun.dh,
474+
lead_field_fun.dh,
476475
)
477476
update_operator!(ϕₘ_op, 0.) # Trigger assembly
478477

@@ -525,14 +524,13 @@ function Geselowitz1989ECGLeadCache(
525524
lead_dh = lead_op.dh
526525
length(lead_dh.field_names) == 1 || @warn "Multiple fields detected. Setup might be broken..."
527526
nelectrodes = length(electrode_positions)
528-
∇Njκ∇φₘ_h = create_system_vector(solution_vector_type, heart_fun) # Solution vector
529-
Njκ∇φₘ_t = create_system_vector(solution_vector_type, lead_fun) # Solution vector
530-
Z = zeros(eltype(∇Njκ∇φₘ_t), nelectrodes, length(∇Njκ∇φₘ_t))
527+
φₘ_t = create_system_vector(solution_vector_type, lead_fun) # Solution vector
528+
∇φₘ_t = create_system_vector(solution_vector_type, lead_fun) # Solution vector
529+
Z = zeros(eltype(∇φₘ_t), nelectrodes, length(∇φₘ_t))
531530
ϕₑ = zeros(nelectrodes)
532531

533-
lead_rhs = zeros(eltype(∇Njκ∇φₘ_t), nelectrodes, length(∇Njκ∇φₘ_t))
532+
lead_rhs = zeros(eltype(∇φₘ_t), nelectrodes, length(∇φₘ_t))
534533

535-
@info size(lead_op.A), size(lead_rhs[1,:])
536534
leadprob = LinearSolve.LinearProblem(
537535
lead_op.A, copy(lead_rhs[1,:])
538536
)
@@ -544,17 +542,12 @@ function Geselowitz1989ECGLeadCache(
544542
for j in 2:length(electrode_set)
545543
_add_electrode!(current_rhs, lead_dh, electrode_set[j], -1.0/(length(electrode_set)-1), lead_field_sym)
546544
end
547-
# leadprob = LinearSolve.LinearProblem(
548-
# lead_op.A, copy(current_rhs);# u0=Z[i,:]
549-
# )
550-
# lincache = init(leadprob, linear_solver)
551-
# LinearSolve.solve!(lincache)
552-
LinearSolve.set_b(lincache, copy(current_rhs))
545+
lincache.b .= current_rhs
553546
LinearSolve.solve!(lincache)
554547
Z[i,:] .= lincache.u
555548
end
556549

557-
return Geselowitz1989ECGLeadCache(source_op, transfer_op, ∇Njκ∇φₘ_h, ∇Njκ∇φₘ_t, Z, electrode_positions)
550+
return Geselowitz1989ECGLeadCache(source_op, transfer_op, φₘ_t, ∇φₘ_t, Z, electrode_positions)
558551
end
559552

560553
function _add_electrode!(f::AbstractVector{T}, dh::DofHandler, electrode::VertexIndex, weight, lead_field_sym::Symbol) where {T<:Number}
@@ -565,10 +558,10 @@ function _add_electrode!(f::AbstractVector{T}, dh::DofHandler, electrode::Vertex
565558
end
566559

567560
function update_ecg!(cache::Geselowitz1989ECGLeadCache, φₘ::AbstractVector)
568-
# Compute κᵢ∇φₘ on the heart
569-
mul!(cache.κ∇φₘ_h, cache.heart_op, φₘ)
570561
# Transfer κᵢ∇φₘ to the torso
571-
transfer!(cache.κ∇φₘ_t, cache.transfer_op, cache.κ∇φₘ_h)
562+
transfer!(cache.φₘ_t, cache.transfer_op, φₘ)
563+
# Compute κᵢ∇φₘ on the heart
564+
mul!(cache.κ∇φₘ_t, cache.source_op, cache.φₘ_t)
572565
cache.κ∇φₘ_t[isnan.(cache.κ∇φₘ_t)] .= 0.0 # FIXME
573566
return nothing
574567
end

test/integration/test_ecg.jl

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
using Thunderbolt, Test, SparseArrays
2+
import Thunderbolt: to_mesh, OrderedSet
3+
14
@testset "ECG" begin
2-
@testset "Blocks with $geo" for geo in (Hexahedron, Tetrahedron)
3-
size = 4.
5+
@testset "Blocks with $geo" for geo in (Tetrahedron,Hexahedron)
6+
size = 2.
47
signal_strength = 0.04
5-
nel_heart = (18, 18, 16)
6-
nel_torso = (4, 4, 4) .* Int(size) .* 2
8+
nel_heart = (6, 6, 6) .* 1
9+
nel_torso = (8, 8, 8) .* Int(size)
710
ground_vertex = Vec(0.0, 0.0, 0.0)
811
electrodes = [
912
ground_vertex,
@@ -17,13 +20,16 @@
1720
electrode_pairs = [[i,1] for i in 2:length(electrodes)]
1821

1922
heart_grid = generate_mesh(geo, nel_heart)
20-
# Ferrite.transform_coordinates!(heart_grid, x->Vec{3}(sign.(x) .* x.^2))
23+
Ferrite.transform_coordinates!(heart_grid, x->Vec{3}(sign.(x) .* x.^2))
2124

22-
κ = SymmetricTensor{2,3,Float64}((1.0, 0, 0, 1.0, 0, 1.0))
23-
κᵢ = SymmetricTensor{2,3,Float64}((1.0, 0, 0, 1.0, 0, 1.0))
25+
κ = ConstantCoefficient(SymmetricTensor{2,3,Float64}((1.0, 0, 0, 1.0, 0, 1.0)))
26+
κᵢ = AnalyticalCoefficient(
27+
(x,t) -> norm(x,Inf) 1.0 ? SymmetricTensor{2,3,Float64}((1.0, 0, 0, 1.0, 0, 1.0)) : SymmetricTensor{2,3,Float64}((0.0, 0, 0, 0.0, 0, 0.0)),
28+
CartesianCoordinateSystem{3}()
29+
)
2430

2531
heart_model = TransientDiffusionModel(
26-
ConstantCoefficient(κ),
32+
κᵢ,
2733
NoStimulationProtocol(), # Poisoning to detecte if we accidentally touch these
2834
:φₘ
2935
)
@@ -38,7 +44,7 @@
3844

3945
op = Thunderbolt.setup_assembled_operator(
4046
Thunderbolt.BilinearDiffusionIntegrator(
41-
ConstantCoefficient(κ),
47+
κ,
4248
QuadratureRuleCollection(2),
4349
:φₘ,
4450
),
@@ -48,24 +54,38 @@
4854
Thunderbolt.update_operator!(op, 0.0) # trigger assembly
4955

5056
torso_grid_ = generate_grid(geo, nel_torso, Vec((-size,-size,-size)), Vec((size,size,size)))
51-
addcellset!(torso_grid_, "heart", x->norm(x,1) 1.0)
52-
addcellset!(torso_grid_, "surrounding-tissue", x->norm(x,1) 1.0)
57+
addcellset!(torso_grid_, "heart", x->norm(x,Inf) 1.0)
58+
# addcellset!(torso_grid_, "surrounding-tissue", x->norm(x,Inf) ≥ 1.0)
59+
torso_grid_.cellsets["surrounding-tissue"] = OrderedSet([i for i in 1:getncells(torso_grid_) if i torso_grid_.cellsets["heart"]])
5360
torso_grid = to_mesh(torso_grid_)
54-
torso_fun = semidiscretize(
55-
heart_model,
56-
FiniteElementDiscretization(
57-
Dict(:φₘ => LagrangeCollection{1}()),
58-
Dirichlet[],
59-
["heart", "surrounding-tissue"]
60-
),
61-
torso_grid
62-
)
6361
u = zeros(Thunderbolt.solution_size(heart_fun))
6462
plonsey_ecg = Thunderbolt.Plonsey1964ECGGaussCache(op, u)
65-
poisson_ecg = Thunderbolt.PoissonECGReconstructionCache(heart_fun, torso_grid, ConstantCoefficient(κᵢ), ConstantCoefficient(κ), electrodes; ground = OrderedSet([Thunderbolt.get_closest_vertex(ground_vertex, torso_grid)]), torso_heart_domain=["heart"])
63+
poisson_ecg = Thunderbolt.PoissonECGReconstructionCache(
64+
heart_fun,
65+
torso_grid,
66+
κᵢ, κ,
67+
electrodes;
68+
ground = OrderedSet([Thunderbolt.get_closest_vertex(ground_vertex, torso_grid)]),
69+
torso_heart_domain = ["heart"],
70+
ipc = LagrangeCollection{1}(),
71+
qrc = QuadratureRuleCollection(2),
72+
linear_solver = Thunderbolt.LinearSolve.UMFPACKFactorization(),
73+
system_matrix_type = SparseMatrixCSC{Float64,Int64}
74+
)
6675

6776
geselowitz_electrodes = [[electrodes[1], electrodes[i]] for i in 2:length(electrodes)]
68-
geselowitz_ecg = Thunderbolt.Geselowitz1989ECGLeadCache(heart_fun, torso_grid, ConstantCoefficient(κᵢ), ConstantCoefficient(κ), geselowitz_electrodes; ground = OrderedSet([Thunderbolt.get_closest_vertex(ground_vertex, torso_grid)]), torso_heart_domain=["heart"])
77+
geselowitz_ecg = Thunderbolt.Geselowitz1989ECGLeadCache(
78+
heart_fun,
79+
torso_grid,
80+
κᵢ, κ,
81+
geselowitz_electrodes;
82+
ground = OrderedSet([Thunderbolt.get_closest_vertex(ground_vertex, torso_grid)]),
83+
torso_heart_domain=["heart"],
84+
ipc = LagrangeCollection{1}(),
85+
qrc = QuadratureRuleCollection(3),
86+
linear_solver = Thunderbolt.LinearSolve.UMFPACKFactorization(),
87+
system_matrix_type = SparseMatrixCSC{Float64,Int64}
88+
)
6989

7090
@testset "Equilibrium" begin
7191
u .= 0.0
@@ -115,8 +135,8 @@
115135
end
116136
end
117137

118-
@testset "Planar wave dim=$dim" for dim in 1:3
119-
Ferrite.apply_analytical!(u, heart_fun.dh, :φₘ, x->x[dim]^3 )
138+
@testset "Planar wave dim=$dim" for dim in 1:1# 1:3
139+
Ferrite.apply_analytical!(u, heart_fun.dh, :φₘ, x->x[dim]^3)
120140

121141
@testset "Plonsey1964 xᵢ³" begin
122142
Thunderbolt.update_ecg!(plonsey_ecg, u)
@@ -135,7 +155,7 @@
135155
@test ecg_vals[1] 0.0 atol=1e-12 # Ground
136156
for dim2 in 1:3
137157
if dim2 == dim
138-
@test ecg_vals[2*dim2+1]-ecg_vals[2*dim2] < 0.0
158+
@test ecg_vals[2*dim2+1]-ecg_vals[2*dim2] -2*0.37 atol=1e-2
139159
else
140160
@test ecg_vals[2*dim2+1]-ecg_vals[2*dim2] 0.0 atol=1e-4
141161
end
@@ -147,7 +167,7 @@
147167
ecg_vals = Thunderbolt.evaluate_ecg(geselowitz_ecg)
148168
for dim2 in 1:3
149169
if dim2 == dim
150-
@test ecg_vals[2*dim2]-ecg_vals[2*dim2-1] < 0.0
170+
@test ecg_vals[2*dim2]-ecg_vals[2*dim2-1] -2*0.37 atol=1e-2
151171
else
152172
@test ecg_vals[2*dim2]-ecg_vals[2*dim2-1] 0.0 atol=1e-4
153173
end
@@ -175,7 +195,7 @@
175195
ecg_vals = Thunderbolt.evaluate_ecg(poisson_ecg)
176196

177197
if i == dim
178-
@test ecg_vals[2*i+1]-ecg_vals[2*i] > 0.0
198+
@test ecg_vals[2*i+1]-ecg_vals[2*i] 2*0.37 atol=1e-2
179199
else
180200
@test ecg_vals[2*i+1]-ecg_vals[2*i] 0.0 atol=1e-4
181201
end
@@ -189,7 +209,7 @@
189209
ecg_vals = Thunderbolt.evaluate_ecg(geselowitz_ecg)
190210

191211
if i == dim
192-
@test ecg_vals[2*i]-ecg_vals[2*i-1] > 0.0
212+
@test ecg_vals[2*i]-ecg_vals[2*i-1] 2*0.37 atol=1e-2
193213
else
194214
@test ecg_vals[2*i]-ecg_vals[2*i-1] 0.0 atol=1e-4
195215
end

0 commit comments

Comments
 (0)