Skip to content

[WIP] Reaction-Eikonal example#241

Draft
AbdAlazezAhmed wants to merge 21 commits intomainfrom
ahm/eikonal
Draft

[WIP] Reaction-Eikonal example#241
AbdAlazezAhmed wants to merge 21 commits intomainfrom
ahm/eikonal

Conversation

@AbdAlazezAhmed
Copy link
Collaborator

No description provided.

@codecov
Copy link

codecov bot commented Jan 29, 2026

Codecov Report

❌ Patch coverage is 62.13992% with 92 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.59%. Comparing base (552693a) to head (ae6209d).

Files with missing lines Patch % Lines
src/mesh/tools.jl 79.19% 36 Missing ⚠️
src/discretization/eikonal.jl 0.00% 22 Missing ⚠️
src/modeling/electrophysiology.jl 5.55% 17 Missing ⚠️
src/discretization/fem.jl 31.81% 15 Missing ⚠️
src/modeling/functions.jl 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #241      +/-   ##
==========================================
- Coverage   73.03%   72.59%   -0.44%     
==========================================
  Files          78       79       +1     
  Lines        6865     7018     +153     
==========================================
+ Hits         5014     5095      +81     
- Misses       1851     1923      +72     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@termi-official
Copy link
Collaborator

What is the state of this PR?
Also, where is the Eikonal package? And we might want to add some test beyond the integration test in the docs.

@AbdAlazezAhmed
Copy link
Collaborator Author

AbdAlazezAhmed commented Mar 6, 2026

What is the state of this PR?

I'll write some tests and check documentation. Already working fine in another project using this branch.

Also, where is the Eikonal package? And we might want to add some test beyond the integration test in the docs.

It's in the noc git instance. Will make it public and link to it after adding docs

Copy link
Collaborator

@termi-official termi-official left a comment

Choose a reason for hiding this comment

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

There are literally hundreds of random ass changes related to code style. Can you revert these please?

@@ -1,7 +1,9 @@
[deps]
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why? I explicitly try to avoid pulling all solvers under the sun in.

end
end
end
cells = getproperty.(cells_ferrite, :nodes)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the FastIterativeMethod.jl I didn't want to depend on Ferrite's definition of a cell and used just a tuple of ints (which is what Ferrite cell wraps)

end

function semidiscretize(
::EikonalModel,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we need to pass the coefficient?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For what specifically? if you mean diffusivity I think we can take it from the inner MonodomainModel for consistency (or remove it). For now it's provided at solve


function semidiscretize(
::EikonalModel,
discretization::SimplicialEikonalDiscretization,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be FastIterativeMethod or so? Also, can't this be an extension?

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, since here we define the discretization not the -solver?- method. It's just a current constraint that the FIM implementaiton we have now is limited to linear tets

Comment on lines +1 to +30
"""
Coordinates used for defining activation points when solving the eikonal equation.
"""
struct ActivationCoordinate{CoordT <: Union{LVCoordinate, BiVCoordinate}, T}
coord::CoordT
time_offset::T
end

abstract type AbstractEikonalActivationProtocol end

"""
Activation protocol for activating specific points with time offsets to mimic the behavior of a Purkinje network.
"""
struct NodalEikonalActivationProtocol{VectorT <: AbstractVector{<:ActivationCoordinate}} <:
AbstractEikonalActivationProtocol
nodes::VectorT
end

"""
Activation protocol for uniformally activating the endocardium with zero wave arrival time.
"""
struct UniformEndocardialEikonalActivationProtocol <: AbstractEikonalActivationProtocol end

"""
Activation protocol for uniformally activating the endocardium with zero wave arrival time.
"""
struct AnalyticalEikonalActivationProtocol{fT} <: AbstractEikonalActivationProtocol
f::fT
end

Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't discretization info, but model, right?.

end

function get_nodes(
::UniformEndocardialEikonalActivationProtocol,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this special to Eikonal?

A wrapper function containing the cell model dynamics function, as a pointwise ODE,
and the eiknal function that needs to be solved once before solving the cell dynamics part.
"""
struct EikonalCoupledODEFunction{ODEFunctionT, EikonalFunctionT}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this more like ReactionEikonalFunction (to properly attribute the origin)?

enids = new_edge_offset .+ global_edge_indices
fnids = new_face_offset .+ global_face_indices
cnid = new_face_offset + num_faces(mgrid) + cell_idx
cnid = new_face_offset + num_faces(mgrid) + cell_idx
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you please revert all the noise here? It makes really hard to track down what the actual relevant changes are...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah I thought last commits reverted them. Will do.

Comment on lines +894 to +901
function tetrahedralize(grid::Grid{3, Hexahedron})
return _tetrahedralize(to_mesh(grid))
end

function tetrahedralize(mesh::SimpleMesh{3, Hexahedron})
grid = _tetrahedralize(mesh)
return to_mesh(grid)
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why restrict to Hex. We allow Tet below, right?

for (cell_idx, cell) in enumerate(cells)
push!(cell_offsets, length(new_cells))
start_idx = length(new_cells) + 1
if isa(cell, Hexahedron)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use dispatch


"""
ReactionEikonalSplit(model)
ReactionEikonalSplit(model, coeff)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
ReactionEikonalSplit(model, coeff)
ReactionEikonalSplit(model, cs)

Comment on lines +259 to +260
Annotation for the reaction-Eikonal split of a given model. The
second argument is a coefficient describing the input `x` for the reaction model rhs,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Annotation for the reaction-Eikonal split of a given model. The
second argument is a coefficient describing the input `x` for the reaction model rhs,
Annotation for the reaction-Eikonal split of a given model. The
argument `cs` is a coefficient describing the input `x` for the reaction model rhs,

end
else
if m.stim_offset ≤ t ≤ m.stim_offset + m.stim_length
τᶠ = 0.25
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is an input parameter, right?

Comment on lines +958 to +961
t1 = orient_to_positive((v1, v3, v2, cnid), new_nodes)
t2 = orient_to_positive((v1, v4, v3, cnid), new_nodes)
push!(new_cells, Tetrahedron(t1))
push!(new_cells, Tetrahedron(t2))
Copy link
Collaborator

Choose a reason for hiding this comment

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

What kind of algorithm is this and what is the insertion order (in terms of space-filling curves)?

Comment on lines +996 to +1020
triangles =
length(fnodes) == 3 ? [(fnodes[1], fnodes[2], fnodes[3])] :
[(fnodes[1], fnodes[3], fnodes[2]), (fnodes[1], fnodes[4], fnodes[3])]
rng = cell_ranges[cellidx]
for tri in triangles
tri_set = Set(tri)
matched = false
for newcid in rng
for localf = 1:length(Ferrite.faces(new_cells[newcid]))
if Set(Ferrite.faces(new_cells[newcid])[localf]) == tri_set
push!(s, FacetIndex(newcid, localf))
matched = true
break
end
end
matched && break
end
if !matched
throw(
error(
"Could not map facet (cell=$cellidx, face=$lfi, tri=$tri) into tetrahedral mesh",
),
)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is an overengineered solution. The mapping is predetermined if you remove the orient_to_positive function above and replace it with a deterministic insertion which follows a given convention. There really should be a single loop up table for this which should be always true. This kind of refactor is something Claude/Codex/whatever (or DIY) can do locally (if you give them also access to Ferrite an and LSP). Also, use dispatches.

As a test case you can use a single element (one Hex, one Tet) and mark all faces individually with different names. Then you can test if the face center of the new faces is where you expect them.

Comment on lines +946 to +950
center = zero(new_nodes[vnids[1]].x)
for vid in vnids
center += new_nodes[vid].x
end
center /= length(vnids)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not necessarily true in general. There should be a function to compute the center which we should use to compute the center with the ansatz functions (which probably right now implements the same approximation scheme).

end

function _tetrahedralize(mgrid::SimpleMesh{3, <:Any, T}) where {T}
materialize_all_entities!(mgrid)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why all and where do you use them?


new_nodes = copy(grid.nodes)
new_cells = Tetrahedron[]
cell_ranges = UnitRange{Int}[]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there some advantage of a UnitRange over just an Int?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants