What's missing
MFO has no domain masking. Every mask in src/ is bounds-masking inside adjoint gathers (_maskedget, src/operators/abstract.jl:210) — there is no active-cell indicator, no irregular-geometry / cut-cell support, and no Field of Bool that participates in stencils. BlockForest refinement changes resolution, not participation, so AMR is not a substitute.
Why (downstream)
The microvascular transport model solves a reaction–diffusion system on a hemisphere of tissue embedded in a Cartesian box. Cells outside the hemisphere must be excluded from both the reaction and the diffusion stencil. Today the downstream package carries a per-cell active::Vector that it multiplies into every reaction term by hand, because a masked operator does not exist.
Getting this wrong is not cosmetic: leaving the box corners active created a spurious O₂ sink outside the domain that broke the radial-O₂ comparator entirely. The mask is load-bearing physics, and it currently lives downstream where the operator algebra can't see it.
Sketch
g = CartesianGrid(...; bc = ((Neumann(), Neumann()), ...))
active = scalar_field(Bool, g); set!(active, inside_dome)
L = laplacian(g; active) # inactive cells contribute no flux and get no update
The interesting constraints:
- Adjoint correctness. Whatever
apply! does, apply_adjoint! has to remain its exact discrete transpose — masking a face flux must mask the same face on the way back, or ⟨Lx,y⟩ = ⟨x,Lᵀy⟩ breaks. That's the test that should gate this.
operator_diagonal. An inactive neighbour looks like a no-flux face, so the diagonal correction is the same (-2 + bc_sign)h⁻² form already handled for BC faces — but it now varies cell to cell, which the current implementation doesn't allow for.
- Singularity. A fully masked-off region turns the operator singular, which matters for the multigrid coarsest-level LU (
_build_coarsest, src/multigrid.jl:288) and for coarsening a mask consistently.
Scope note
Uniform CartesianGrid only would already unblock the downstream use. Masked BlockForest / distributed slabs can stay out of scope — same boundary you drew for Diffusion in #54.
🤖 Filed by Claude, who read the code so Kyle didn't have to scroll.
What's missing
MFO has no domain masking. Every
maskinsrc/is bounds-masking inside adjoint gathers (_maskedget,src/operators/abstract.jl:210) — there is no active-cell indicator, no irregular-geometry / cut-cell support, and noFieldofBoolthat participates in stencils.BlockForestrefinement changes resolution, not participation, so AMR is not a substitute.Why (downstream)
The microvascular transport model solves a reaction–diffusion system on a hemisphere of tissue embedded in a Cartesian box. Cells outside the hemisphere must be excluded from both the reaction and the diffusion stencil. Today the downstream package carries a per-cell
active::Vectorthat it multiplies into every reaction term by hand, because a masked operator does not exist.Getting this wrong is not cosmetic: leaving the box corners active created a spurious O₂ sink outside the domain that broke the radial-O₂ comparator entirely. The mask is load-bearing physics, and it currently lives downstream where the operator algebra can't see it.
Sketch
The interesting constraints:
apply!does,apply_adjoint!has to remain its exact discrete transpose — masking a face flux must mask the same face on the way back, or⟨Lx,y⟩ = ⟨x,Lᵀy⟩breaks. That's the test that should gate this.operator_diagonal. An inactive neighbour looks like a no-flux face, so the diagonal correction is the same(-2 + bc_sign)h⁻²form already handled for BC faces — but it now varies cell to cell, which the current implementation doesn't allow for._build_coarsest,src/multigrid.jl:288) and for coarsening a mask consistently.Scope note
Uniform
CartesianGridonly would already unblock the downstream use. MaskedBlockForest/ distributed slabs can stay out of scope — same boundary you drew forDiffusionin #54.🤖 Filed by Claude, who read the code so Kyle didn't have to scroll.