Pass Dirichlet boundary conditions as std::span in the assembly API
- Dominant language
- C++
- Stars
- 1.2k
- Forks
- 261
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 65
Description
`AGENTS.md` asks for `std::span` for contiguous read-only array views, and reserves `std::vector` for parameters that are stored, mutated in place, or are container element types. The `bcs` parameter of the assembly API is none of those, but is spelled as a vector everywhere:
- `fem::assemble_matrix` — `cpp/dolfinx/fem/assembler.h:543`, `:590`
- `fem::set_diagonal` — `cpp/dolfinx/fem/assembler.h:672`
- `fem::petsc::set_bc` — `cpp/dolfinx/fem/petsc.h:577`
- `fem::petsc::apply_lifting` — `cpp/dolfinx/fem/petsc.h:407`, `:500` (nested, `vector>`)
all taking `const std::vector>>&`.
### Proposal
```cpp
std::span>> bcs
```
The outer `const` makes the elements non-assignable, the inner one keeps the boundary conditions read-only. It binds implicitly from the vectors callers already build, so most call sites are unaffected.
### Why it matters beyond style
A function that accepts boundary conditions and forwards them has to spell the same vector type, or allocate on every call. `fem::petsc::assemble_residual` and `assemble_jacobian` (added in #4433, `cpp/dolfinx/fem/petsc.h:713`, `:789`) take `const std::vector>&` purely to forward to `assemble_matrix`, `set_diagonal`, `set_bc` and `apply_lifting`. They are called once per residual or Jacobian evaluation, so once per Newton step and once per line search trial point. With spans throughout, nothing converts anywhere.
### Caveats
- `std::span` is not constructible from an `initializer_list` until C++26 ([P2447](https://wg21.link/p2447)), so braced call sites need a named local. `fem::apply_lifting(b.array(), {a}, {{bc}}, {}, T(1))` in the poisson, biharmonic and mixed_poisson demos is the pattern affected. An empty `{}`, as in `assemble_matrix(A.mat_add_values(), a, {})`, still works.
- The nested `vector>` of `apply_lifting` is more disruptive than the flat case, since a `span>` needs the caller to own the inner sequences. It could keep its current type, or be handled separately.
- The nanobind wrappers build vectors from Python lists and would bind unchanged, provided the vector outlives the call.
There are around 60 call sites of these functions in the repository.
AI assistance: this issue was drafted with Claude Code (Opus 5) while reviewing #4433. I reviewed and take responsibility for its contents.
Contributor guide
Research direction
Read AGENTS.md, then inspect the listed declarations in cpp/dolfinx/fem/assembler.h and cpp/dolfinx/fem/petsc.h, including assemble_residual and assemble_jacobian. Search the repository’s roughly 60 call sites and the poisson, biharmonic, and mixed_poisson demos; done means the API and callers compile, initializer-list cases are addressed, and the nested apply_lifting decision is resolved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend-api-design, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100