Multi-species and multi-block programs

A multi-species model is a pops.Case with several named blocks. Each block has its own physics model and spatial descriptors. Shared fields, sources, and diagnostics reference blocks by stable names.

Assemble blocks

case = (
    pops.Case(layout=layout, name="two_species")
    .block("electrons", physics=electron_model, spatial=electron_spatial)
    .block("ions", physics=ion_model, spatial=ion_spatial)
    .field(poisson)
    .time(program)
)

poisson can assemble its right-hand side from multiple blocks:

from pops.fields.rhs import ChargeDensity

rhs = ChargeDensity.from_blocks("electrons", "ions")

Program handles

from pops.time import Program

T = Program("two_species_step")
e = T.state("Ue", block="electrons")
i = T.state("Ui", block="ions")

fields = T.solve_fields_from_blocks([e.n, i.n])
Re = electron_rate(e.n, fields)
Ri = ion_rate(i.n, fields)

T.define(e.next, e.n + T.dt * Re)
T.define(i.next, i.n + T.dt * Ri)
T.commit_many({"electrons": e.next, "ions": i.next})

The state versions are handles, not runtime arrays. Bind provides the actual state data.

AMR

Multi-block cases use the same Uniform(...) or AMR(...) layout descriptor as single-block cases. AMR policies should tag fields or roles with enough block context to avoid ambiguity.