feat(operators): anisotropic diffusion ∇·(D∇u) with a full tensor D
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 45/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- julia
- Área
- backend, performance, testing
Línea de trabajo
Start with src/operators/diffusion.jl, src/operators/scaling.jl, src/Fields.jl, src/boundaries.jl, and the existing operator_diagonal implementation in src/operators/diagonal.jl. Run the CartesianGrid diffusion and adjoint tests before tracing the axis-aligned stencil path. Done means the scoped Diffusion leaf supports tensor fields with arithmetic face averaging on CartesianGrid, has an exact materialized-grid adjoint and updated diagonal, and includes the hostile anisotropy test and benchmark guidance described here.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
From Dennis Ogiermann, 2026-08-14: "With AI we can add anisotropic 3D finite difference stencils (from the work by Nick Trefethen)."
Which anisotropy — needs Dennis
That sentence reads two ways, and they are different pieces of work:
- Physical anisotropy — a full conductivity/diffusivity tensor
D, so cardiac fiber orientation enters the operator. This is what the issue below assumes. - Numerical anisotropy — stencils whose truncation error is isotropic, so a wave on a Cartesian grid does not propagate faster along the axes than along the diagonals. That is closer to Trefethen's known line (group velocity and numerical anisotropy in difference schemes), and it means a wider isotropic-error Laplacian: 9-point in 2D, 19- or 27-point in 3D.
Both are worth having and neither implies the other. Filing under the first reading because that is what monodomain needs to express its own physics; if Dennis meant the second, this issue splits.
What's missing
κ is scalar-only, and it is enforced at both entry points:
src/operators/diffusion.jl:545-549—eltype(κ.data) <: Number || throw(ArgumentError("diffusion coefficient must be a scalar-eltype Field, …")), with the same guard on theBlockForestmethod at:608-612src/operators/scaling.jl:47-54— same guard onscaling(κ::Field), and onscaling(κ::AbstractBlockField)at:55-62
So the fused Diffusion leaf is closed to a tensor, and so is the composed escape hatch divergence(g) * scaling(K) * gradient(g). There is no tensor_field constructor, and _scalar_eltype handles Number and SVector only (src/Fields.jl:247-248). This contradicts DESIGN.md:145-149, which already promises a tensor field over Array{SMatrix{…}} with "the backing array's eltype is the only thing that changes."
The face-averaging policies are also scalar binary ops — ArithmeticMean and HarmonicMean at src/operators/diffusion.jl:9-25 are (a+b)/2 and 2ab/(a+b).
Why
Cardiac conduction is fiber-aligned: D = σ_l ff^T + σ_t (I − ff^T), with σ_l/σ_t commonly 3–10. Every monodomain result this package produces — including examples/niederer_benchmark.jl, whose whole point is a standard cardiac verification problem — is currently restricted to isotropic conduction because the operator cannot hold the tensor. That is a physics gap, not an ergonomics one.
Sketch
g = CartesianGrid(((0.0, 1.0), (0.0, 1.0), (0.0, 1.0)), (n, n, n); bc = …)
D = tensor_field(g) # Field over Array{SMatrix{3,3,T,9}}
set!(D, x -> σ_t * I + (σ_l - σ_t) * (f(x) * f(x)'))
L = diffusion(g, D) # ∇·(D∇u), cross terms included
Staged, because the third stage is where the actual work is:
tensor_field+_scalar_eltype(::SMatrix)+ lift the two eltype guards. Mostly plumbing — with one exception that must land before any guard is lifted:adjoint_operator(::ScalingOp{<:Field})returnsField(conj.(κ.data), κ.grid)(src/operators/scaling.jl:78-82, and theAbstractBlockFieldmethod at:83-91does the same per block). Elementwiseconjis the adjoint of a scalar coefficient, not of a matrix one — for anSMatrixκ it must beadjoint.(κ)(the pointwise transpose for real κ). Lift the guard without that andscaling(K)with a non-symmetric tensor silently gets a wrong adjoint, which is exactly what "adjoints are declared, never assumed" forbids. Open PR #107's generic scaling (GHScalingOpin its title,scaling(f, args...)in the diff) is not a shortcut here: it requiresfto return a real scalar, trusts the user on that, and declares itself self-adjoint unconditionally (isselfadjoint(S::ScalingOp) = true,adjoint_operator(S::ScalingOp) = S), so it must not become the tensor-κ path. (That PR also renames today's single-coefficient op toCoeffScaling, an alias of its newScalingOp; if it merges first, theconj→adjointfix moves toadjoint_operator(::CoeffScaling{<:Field})unchanged.)- Tensor-aware face averaging. Arithmetic generalizes componentwise. Harmonic does not — the scalar
2ab/(a+b)has several inequivalent matrix generalizations, and which one preserves flux continuity across a fiber-orientation jump is a real question, not a naming choice. - The cross-derivative terms
∂/∂xᵢ(D_ij ∂u/∂xⱼ)fori ≠ j. Everything below is about this.
Constraints
- No leaf reads a diagonal neighbour today, and the infrastructure says so out loud.
src/transfer.jl:514-518: "Per-leaf stencil adjoints must leave corner ghosts exactly zero (true for axis-aligned stencils, whose transposes never scatter there) … A future cross-derivative leaf needs a corner-aware exchange." Same statement for slabs atsrc/distributed.jl:650-655, and for the packed forest atDESIGN.md:425("last dim wins; no axis-aligned stencil reads them"). So the forest and distributed corner ghosts are currently garbage by design, and a cross-derivative leaf on those paths reads them. - A plain
CartesianGridis fine already. Corner ghosts are filled consistently as ghost-of-ghost values atsrc/boundaries.jl:107-108. That makesCartesianGrid-only the obvious first slice, withBlockForest/ slab support and its corner-aware exchange and corner-aware adjoint fold as an explicit follow-up. - Adjoint. The cross terms have to be transposed exactly, including how they touch corner cells near a boundary.
⟨Lx,y⟩ = ⟨x,Lᵀy⟩on a materialized small grid is the gate, per the package's standing rule that adjoints are declared and never assumed. operator_diagonal.src/operators/diagonal.jl:125-135handlesDiffusiononCartesianGridonly; the tensor form changes the diagonal and multigrid depends on it.- Monotonicity. A standard 7-point-plus-cross-terms discretization of a strongly anisotropic tensor loses the M-matrix property once the off-diagonal entries dominate, which breaks the discrete maximum principle and degrades Jacobi/Gauss–Seidel smoothing exactly where multigrid needs it most. This deserves a named test with a deliberately hostile
σ_l/σ_tand a fiber direction at 45° to the grid, not a footnote. - The
ntupletrap.CLAUDE.mdrecords that a stencil reading two arrays must not unroll dimensions withntuple(Val(N))— the closure stops inlining, vectorization dies, and a 256² sweep went from 33 µs to 260 µs with identical numerics. A tensor stencil reads more arrays than that. Unroll by recursion overVal(D), following_diff_axesinsrc/operators/diffusion.jl:377-383, and benchmark before believing anything.
Scope note
CartesianGrid only, Diffusion leaf only, arithmetic face averaging only, for the first slice. Forest/distributed corner exchange, harmonic tensor averaging, and the wider isotropic-error stencils from the second reading above are all separate follow-ups.
—
🤖 Claude added a transpose warning before anyone learns it the hard way.
claude-opus-5-5[1m] (high) · claude-code 2.1.285 · unreviewed
- Lenguaje dominante
- Julia
- Estrellas
- 3
- Forks
- 1
- Merge medio
- 1 d 28 min
- PR fusionados (30 d)
- 6
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de RallypointOne/MatrixFreeOperators.jl
-
Roadmap: open-issue priorityAbierto
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
-
enhancement
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
-
performance
Dificultad 5/5 Más de una semana Aptitud para principiantes 45/100
-
perf(forest): thread the reference BlockForest path — leaf sweeps and exchange descriptor loopsAbiertoperformance
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
-
enhancement
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
Todos los issues de RallypointOne/MatrixFreeOperators.jl
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
-
bignums
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
JuliaNLSolvers/NLsolve.jl#319 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100