A Relation answers one mathematical question: for each item in its domain, which bounded items in its codomain may be read or published? The relation descriptor owns that meaning. Storage, when required, supplies its concrete endpoints without becoming the descriptor.
| Scientific intent | LocalMath form | Bind relation storage? |
|---|---|---|
| Same-item read or write | field[i] |
No; identity is authored automatically |
| Cartesian interior stencil | interior(space, width) |
No; affine mappings are authored automatically |
| Cartesian periodic stencil | periodic(space) |
No; periodic boundary mappings are automatic |
| Fixed mesh incidence or graph endpoints | FixedRelation |
Yes |
| Endpoint keys held in a Field | IndexRelation |
No; bind the key Field |
| Optional Field-held key | IndexRelation(...; optional=true) |
No; use samples for absent lanes |
| Compose bounded mappings | compose |
Bind only stored factors |
| Select a base mapping through an injection | SelectedRelation |
Bind only stored factors |
| Source mask over another relation | MaskedRelation |
Bind the Boolean mask Field and stored factors |
| Reverse adjacency | InverseRelation |
Yes |
| Runtime-keyed publication | RuntimeRelation |
No; keys are evaluator results |
| Mutable bounded incidence | PackedRelation |
Yes, through MutableRelationStorage |
| Exterior ghost mapping | BoundaryRelation(..., GhostBoundary(...)) |
Yes, for the ghost mapping |
Ordinary stencil authors normally write no relation constructor. The equation supplies identity or Cartesian topology directly:
cells = Space((nx, ny))
u = Field(cells, Float32)
residual = Field(cells, Float32)
law = @localmath (i, j) ∈ interior(cells, 1) begin
residual[i, j] =
u[i - 1, j] + u[i + 1, j] +
u[i, j - 1] + u[i, j + 1] - 4f0 * u[i, j]
endA field's element type describes one logical value, independently of its spatial shape. Immutable named products may combine admitted Boolean, integer, floating, tuple, and bounded fixed-array values. Names are compile-time field labels; runtime Symbols, pointers, references, and mutable arrays are not numeric leaves.
using LocalMath, KernelAbstractions
@inline function advance_record(value)
return (active = !value.active, count = value.count + Int32(1),
polarity = (value.polarity[1] + 0.5f0, value.polarity[2]))
end
initial = (active = false, count = Int32(2), polarity = (1.0f0, 2.0f0))
cells = Space(3)
before = Field(cells, typeof(initial))
after = Field(cells, typeof(initial))
law = @localmath i ∈ cells begin
after[i] = advance_record(before[i])
end
prepared = prepare(law, before => fill(initial, 3),
after => LocalMath.Allocate(undef); backend = KernelAbstractions.CPU())
wait(execute!(prepared))
@assert LocalMath.storage(prepared, after) ==
fill((active = true, count = Int32(3), polarity = (1.5f0, 2.0f0)), 3)
nothing
Backend preparation still checks record layout and every leaf's load/store support. Field admission does not imply that every reduction, atomic operation, arbitrary record size, or numerical type is supported on every device.
Declare the mathematical direction and exact lane bound independently of the endpoint array:
elements = Space(element_count)
nodes = Space(node_count)
values = Field(nodes, Float32)
residual = Field(nodes, Float32)
incidence = FixedRelation(elements => nodes; degree=4)
law = @localmath element ∈ elements begin
local_values = values[incidence(element)]
residual[incidence(element)] += element_residual(local_values)
end
prepared = @prepare (law; backend) begin
values = nodal_values
residual = allocate(0f0)
incidence = incidence_endpoints
endincidence_endpoints is lane-major with at least degree lanes and one entry
per source item. A direct array represents a full-degree relation. Optional
lanes use (; endpoints, counts), where counts has one bounded integer per
source item.
IndexRelation is computed from an integer Field and therefore receives no
relation binding:
particles = Space(particle_count)
cells = Space(cell_count)
cell_key = Field(particles, Int32)
mass = Field(cells, Float32)
particle_cell = IndexRelation(cell_key => cells)
law = @localmath particle ∈ particles begin
mass[particle_cell(particle)] += particle_mass(particle)
end
prepared = @prepare (law; backend) begin
cell_key = particle_cell_keys
mass = allocate(0f0)
endStrict invalid keys fail the transaction. With optional=true, invalid keys
are absent lanes and must be consumed through sample-aware access or optional
publication.
compose(a, b) applies bounded relations left-to-right. SelectedRelation
uses one relation as an injection into another relation's domain. Both are
computed descriptors: bind their stored factors, not the composed result.
Their display and LocalMath.inspect(law; level=:relations) retain factor
identity and total degree.
PackedRelation is intended for domain compilers that own relationship
generation, validation, and transaction meaning. Its runtime state remains
canonical packed storage on CPU and GPU:
relationships = PackedRelation(
owners => records;
degree_bound=max_degree,
capacity=owner_count,
)
prepared = prepare(law,
relationships => LocalMath.MutableRelationStorage(
packed_storage;
generation,
status,
),
generated_bindings...;
backend,
)Ordinary models should not select PackedRelation merely because their
topology is stored in an array. Immutable mesh and graph topology uses
FixedRelation.
At the REPL, display the law or inspect its relation projection:
display(law)
LocalMath.inspect(law; level=:relations)The display distinguishes computed and stored Relations. If preparation is incomplete, LocalMath reports every missing descriptor in encounter order. Malformed fixed topology reports the expected degree/domain layout and the actual array shape without changing or adapting caller storage.