Skip to content

Problems

Define what a system compiles to: the problem, its kernels, and the values the runtime reads.

BaseKernel

Bases: BaseConfig[object, RunStateSpec]

Base of the kernels: laws lowered to numbers, the parts of a problem that a solver runs at a state.

A kernel stores in its constructor what the compiler lowered, such as compiled functions and the indices they read. check() validates them, prepare(problem) sets what a run needs, such as the first renewal times, and run(state) acts at a RunStateSpec, the time, the state vector, the parameters, and the random number generator, and keeps what the kernel computes, such as the next state or a matrix, in values_.

A developer adds a law kind by subclassing it, together with the pass that builds the kernel and a solver that runs it. A kernel takes any context to prepare on, since the problem that holds it is built from its kernels.

Attributes:

Name Type Description
values_ object

What the last run computed. Each kernel narrows its type.

Examples:

>>> import numpy as np
>>> from skcomplex.engine.problems import BaseKernel
>>> from skcomplex.specs import RunStateSpec
>>> class DecayKernel(BaseKernel):
...     def __init__(self, rate=1.0):
...         self.rate = rate
...
...     def _run(self, inputs):
...         self.values_ = -self.rate * inputs.x
>>> DecayKernel(0.5).check().prepare().run(RunStateSpec(0.0, np.array([2.0]), np.zeros(0))).values_.tolist()
[-1.0]

BasePrinter

Bases: BaseConfig[ContextT, InputT]

Base of the printers: they print Sage expressions as code of one target.

A printer stores its target in its constructor, prepare(symbols) takes the symbols the code reads, in order, and run(expressions) keeps the code of the expressions, or a function that evaluates them, in code_. A printer names the types of the symbols it prepares on and of the expressions it prints, which differ between targets.

Attributes:

Name Type Description
code_ object

The code of the last run. Each printer narrows its type.

BaseProblemSpec(form, states, parameters, initial_state, initial_parameters, time, *, symbols=dict(), elements=(), interactions=(), system=None, agents=(), agent_space=None, companions=dict(), renewals=(), matrices=(), transfer_network=None, topology=None, observables=()) dataclass

Bases: BaseSpec

Base of the problems a system compiles to: what every solver, observer, and visual reads of one.

A problem is a value. Its fields hold what the runtime reads, with trivial defaults for what a form does not have, and its readers answer the runtime's questions, trivially by default. A formalism's problem adds the fields its own solver reads.

Parameters:

Name Type Description Default
form FormKind | str

The mathematical form.

required
states LayoutSpec

The layout of the state vector.

required
parameters LayoutSpec

The layout of the parameter vector.

required
initial_state Array

The initial state vector.

required
initial_parameters Array

The initial parameter vector.

required
time Expression

The time symbol.

required
symbols Mapping[str, Expression | FreeModuleElement]

A mapping from dotted name to the symbol of a scalar or the column of symbols of a vector. The symbols of a group stand for one member.

dict()
elements tuple[CompiledElementSpec, ...]

The compiled elements and groups.

()
interactions tuple[CompiledInteractionSpec, ...]

The compiled interactions in tree order.

()
system System | None

The system the problem was compiled from, or None.

None
agents tuple[CompiledAgentsSpec, ...]

The agent elements and groups of an agent-based problem.

()
agent_space BaseSpace | None

The space the agents of an agent-based problem live in, or None.

None
companions Mapping[str, tuple[str, str, int]]

A mapping from the name of every companion state to its kind, 'derivative' or 'lag', the name of the state it belongs to, and the order of the derivative or the number of steps back.

dict()
renewals tuple[RenewalKernel, ...]

The renewal laws, whose members renew on their own clocks.

()
matrices tuple[MarkovKernel, ...]

The transition and emission matrices of the Markov laws, checked when their parameters change.

()
transfer_network TransferNetworkSpec | None

The quantities, flows, objectives, and feedback terms of the system, or None without them.

None
topology TopologyKernel | None

The topology requests of the laws and the networks they change, or None for a problem without them.

None
observables tuple[tuple[str, ObservableSpec], ...]

The observables of the system tree, each with the path of the system that declares it, empty for the root, which the compiler collects.

()

Examples:

>>> import numpy as np
>>> from skcomplex.types import FormKind
>>> from skcomplex.engine._utils import symbol
>>> from skcomplex.specs import LayoutSpec
>>> from skcomplex.engine.problems import BaseProblemSpec
>>> layout = LayoutSpec(())
>>> problem = BaseProblemSpec(FormKind.ODE, layout, layout, np.zeros(0), np.zeros(0), symbol('t'))
>>> problem.stochastic, problem.adaptive_parameters, problem.lattice_shape('a.n')
(False, {}, None)

adaptive_parameters property

Declarations of the adaptive parameters, states that a schedule may set, by dotted state name.

partition_problems property

Problems of the partitions of a co-simulation problem, none for a problem of one form.

stochastic property

Whether the problem draws random values, so its solver takes the random number generator.

lattice_shape(name)

Return the lattice shape of a state that lives on the sites of a lattice, or None for other states.

Parameters:

Name Type Description Default
name str

The dotted name of the state.

required

Returns:

Name Type Description
shape tuple[int, ...] | None

The number of sites along every dimension, or None; no state is on a lattice by default.

Source code in src/skcomplex/engine/problems/_base_problem_spec.py
141
142
143
144
145
146
147
148
149
150
151
152
153
def lattice_shape(self, name: str) -> tuple[int, ...] | None:
    """Return the lattice shape of a state that lives on the sites of a lattice, or `None` for other states.

    Args:
        name:
            The dotted name of the state.

    Returns:
        shape:
            The number of sites along every dimension, or `None`; no state is on a lattice by default.
    """
    del name
    return None

BaseSearch

Bases: BaseConfig[None, RunStateSpec]

Base of the neighbour searches: the neighbours a kernel reads at a state.

A search stores the entries and the neighbourhood it reads in its constructor, and run(state) keeps the neighbours at a RunStateSpec in pairs_: the row starts and the neighbours of every member, or the members of every pair.

Attributes:

Name Type Description
pairs_ object

The neighbours of the last run. Each search narrows its type.

BlockSpec(node, symbols=dict(), entries=dict(), references=dict(), per=frozenset()) dataclass

Bases: BaseSpec

Variables of a component with the symbols of one member and the components they stand for.

Parameters:

Name Type Description Default
node NodeSpec

The component with its place in the system tree.

required
symbols Mapping[str, Expression | FreeModuleElement]

The symbol, or the vector of symbols, of every variable of one member by name.

dict()
entries Mapping[str, EntrySpec]

The layout entry of every variable by name.

dict()
references Mapping[Expression, ReferenceSpec]

The component of a state or parameter every symbol stands for.

dict()
per frozenset[str]

The names of the parameters with one value per entry of a variadic role.

frozenset()

rows property

Number of members, 1 for a single element or an interaction.

CollisionKernel(dimensions, spacings, cross_section, angles=None)

Bases: BaseKernel

Binary collision integral of a density over two uniform velocity axes, by the method of Mouhot and Pareschi.

The integral is Q(f, f)(v) = int int B(|g|, cos t) (f(v') f(v'*) - f(v) f(v*)) ds dv* in the Carleman form, an integral over the two orthogonal displacements x = v' - v and y = v'* - v, each up to twice the half-width of the grid. The weight of the pair (|x|, |y|) is split into terms of |x| times terms of |y| by a singular value decomposition, one term for Maxwell molecules, and the directions take a trapezoidal rule, so the integral is a set of FFT products on a grid padded by (3 + sqrt(2)) / 2. The mean rate is removed at every position, which conserves the density exactly.

Parameters:

Name Type Description Default
dimensions tuple[int, ...]

The number of points of every position axis and of the two velocity axes, the velocity axes last.

required
spacings tuple[float, float]

The widths of the cells of the two velocity axes.

required
cross_section Callable[[Array, Array], object]

The function B(speed, cosine) of arrays of the relative speed and the cosine of the deflection angle.

required
angles int | None

The number of directions of the displacements, or None for one per two velocity points of an axis, at least 16.

None

Attributes:

Name Type Description
padded_ tuple[int, int]

The shape of the padded velocity grid.

gains_ tuple[Array, Array]

The Fourier weights of x and of y of every direction and term, stacked over the directions and terms.

loss_ Array

The Fourier weight of the loss term.

values_ Array

The collision integral of the last run at every site.

Examples:

>>> import numpy as np
>>> from skcomplex.engine.problems import CollisionKernel
>>> from skcomplex.specs import RunStateSpec
>>> kernel = CollisionKernel((8, 8), (1.0, 1.0), lambda speed, cosine: 1 / (2 * np.pi)).check().prepare()
>>> f = np.exp(-(np.indices((8, 8)) - 3.5) ** 2).prod(axis=0)
>>> rates = kernel.run(RunStateSpec(0.0, f.ravel(), np.zeros(0))).values_
>>> abs(float(rates.sum())) < 1e-12
True
Source code in src/skcomplex/engine/problems/_collision_kernel.py
75
76
77
78
79
80
81
82
83
84
85
def __init__(
    self,
    dimensions: tuple[int, ...],
    spacings: tuple[float, float],
    cross_section: Callable[[Array, Array], object],
    angles: int | None = None,
) -> None:
    self.dimensions = dimensions
    self.spacings = spacings
    self.cross_section = cross_section
    self.angles = angles

CompiledRequestSpec(request, owner, network, rows, condition=None, toward=None) dataclass

Bases: BaseSpec

Topology request of a law with the rows it runs for and the kernels that decide and direct it.

Parameters:

Name Type Description Default
request TopologyRequestSpec

The request.

required
owner str

The path of the component whose law requests it.

required
network NetworkRowsSpec

The variable groups of the network it changes.

required
rows str

'node' for a law of the nodes, 'edge' for a law of the edges of a layer, or 'single' for a law that runs once.

required
condition LawKernel | None

The kernel of the probability that an update law fires the request for every row, or None for a jump.

None
toward LawKernel | None

The kernel of the weight of every node row as a new end, or None for equal weights.

None

DeclarationsSpec(time, states=(), parameters=(), initial_states=(), initial_parameters=(), blocks=dict(), processes=dict(), handles=dict(), agents=dict()) dataclass

Bases: BaseSpec

Entries, initial values, and blocks declared for the components of a system.

Parameters:

Name Type Description Default
time Expression

The time symbol.

required
states tuple[EntrySpec, ...]

The entries of the states, in the order of the state vector.

()
parameters tuple[EntrySpec, ...]

The entries of the parameters, in the order of the parameter vector.

()
initial_states tuple[Array, ...]

The flat initial value of every state entry.

()
initial_parameters tuple[Array, ...]

The flat initial value of every parameter entry.

()
blocks Mapping[str, BlockSpec]

The block of every element, group, and interaction by path.

dict()
processes Mapping[str, BlockSpec]

The block of every Wiener process that the noise roles of interactions bind, by path.

dict()
handles Mapping[Expression, tuple[str, int]]

The path of the Wiener process and the component of every handle symbol.

dict()
agents Mapping[Expression, tuple[str, str]]

The path of the agent group and the variable of every symbol of agents.

dict()

capacity cached property

Blocks of the variable groups on the sites or edges of networks, by path.

ends property

Sizes of the state vector, under True, and of the parameter vector, under False.

groups cached property

Paths of the declared groups.

members property

Number of members of the groups, leaving out agents, which chooses a default backend.

wieners property

Wiener processes that the noise roles of interactions bind, by path.

changing_edges(space)

Return the variable edge group of every layer of a network whose edges change.

Parameters:

Name Type Description Default
space object

The network space.

required

Returns:

Name Type Description
groups dict[str | None, BlockSpec]

The block of the variable edge group by layer.

Source code in src/skcomplex/engine/problems/_declarations_spec.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def changing_edges(self, space: object) -> dict[str | None, BlockSpec]:
    """Return the variable edge group of every layer of a network whose edges change.

    Args:
        space:
            The network space.

    Returns:
        groups:
            The block of the variable edge group by layer.
    """
    return {
        getattr(block.node.component, 'layer', None): block
        for block in self.capacity.values()
        if getattr(block.node.component, 'on', None) == 'edges' and block.node.space is space
    }

changing_entries(space)

Return the source, target, and activity entries of the variable edge group of every changing layer.

Parameters:

Name Type Description Default
space object

The network space.

required

Returns:

Name Type Description
entries dict[str | None, tuple[EntrySpec, EntrySpec, EntrySpec]]

The entries by layer.

Source code in src/skcomplex/engine/problems/_declarations_spec.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def changing_entries(self, space: object) -> dict[str | None, tuple[EntrySpec, EntrySpec, EntrySpec]]:
    """Return the source, target, and activity entries of the variable edge group of every changing layer.

    Args:
        space:
            The network space.

    Returns:
        entries:
            The entries by layer.
    """
    return {
        layer: (block.entries[ENDS[0]], block.entries[ENDS[1]], block.entries[_ACTIVE])
        for layer, block in self.changing_edges(space).items()
    }

DiffusiveLimitSpec(coefficient, parameters, stiffness) dataclass

Bases: BaseSpec

Stability limit of explicit steps for the diffusion of a finite-volume field, at the current parameters.

Parameters:

Name Type Description Default
coefficient Callable[..., object]

The NumPy function of time and parameters that returns the diffusion coefficient.

required
parameters tuple[ReferenceSpec, ...]

The references of the parameters the coefficient reads, in argument order.

required
stiffness float

The largest eigenvalue magnitude of the unit Laplacian, sum(2 / h^2) over the axes.

required

step(p)

Return the largest stable step of explicit Euler for the diffusion at parameter values.

Parameters:

Name Type Description Default
p NDArray[float64]

The parameter vector.

required

Returns:

Name Type Description
step float

The step 1 / (stiffness * max |D|), infinite without diffusion.

Source code in src/skcomplex/engine/problems/_diffusive_limit_spec.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def step(self, p: npt.NDArray[np.float64]) -> float:
    """Return the largest stable step of explicit Euler for the diffusion at parameter values.

    Args:
        p:
            The parameter vector.

    Returns:
        step:
            The step `1 / (stiffness * max |D|)`, infinite without diffusion.
    """
    values = [reference.column_of(np.zeros(0), p) for reference in self.parameters]
    magnitude = float(np.max(np.abs(ExpressionPrinter._values_of(self.coefficient(0.0, *values))[0])))
    return math.inf if magnitude == 0 else 1 / (self.stiffness * magnitude)

ExpressionPrinter(dialect='numpy', *, time=None, names=None, prefix='_cse', shared=True, compiled=False)

Bases: BasePrinter[Sequence[Expression], Sequence[Expression]]

Printer of Sage expressions as code of NumPy, scalar Python for Numba, or LAMMPS Lepton.

prepare(symbols) takes the symbols the code reads, in order, and run(expressions) keeps in code_ the assignments of the repeated compound subexpressions, bound to numbered names, and the code of every expression. A compiled printer keeps instead the NumPy or JAX function f(t, *symbols) of the expressions. A NumPy function runs in a NumPy error state that ignores division, invalid, and overflow warnings when the expressions hold conditions.

Lepton has neither comparisons nor a remainder, so conditions become step and select and remainders floor.

Parameters:

Name Type Description Default
dialect str

The target language, 'numpy', 'scalar' for the scalar Python of compiled Numba loops, 'lepton', or 'jax' for JAX's NumPy, whose functions return new arrays.

'numpy'
time Expression | None

The time symbol, the first argument of a compiled function.

None
names Callable[[Expression], str] | None

The function that gives the code name of a symbol, or None for its printed name.

None
prefix str

The prefix of the names of bound subexpressions.

'_cse'
shared bool

Whether repeated subexpressions are bound to names, false for languages without assignments.

True
compiled bool

Whether run keeps a NumPy or JAX function of time and the symbols instead of code.

False

Attributes:

Name Type Description
code_ tuple[list[tuple[str, str]], list[str]] | Callable[..., list[ArrayLike]]

The assignments of the repeated subexpressions and the code of every expression of the last run, or the NumPy function f(t, *symbols) of the expressions for a compiled printer.

Examples:

>>> import numpy as np
>>> from skcomplex.engine.problems import ExpressionPrinter
>>> from skcomplex.engine._utils import symbol
>>> t, x = symbol('t'), symbol('x')
>>> ExpressionPrinter().check().prepare().run([(x + 1) ** 2, 3 * (x + 1) ** 2]).code_
([('_cse0', '(x + 1)**2')], ['_cse0', '3*_cse0'])
>>> printer = ExpressionPrinter(time=t, compiled=True).check().prepare([x])
>>> printer.run([t * x, x**2]).code_(2.0, np.array([1.0, 3.0]))
[array([2., 6.]), array([1., 9.])]
Source code in src/skcomplex/engine/problems/_expression_printer.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def __init__(
    self,
    dialect: str = 'numpy',
    *,
    time: Expression | None = None,
    names: Callable[[Expression], str] | None = None,
    prefix: str = '_cse',
    shared: bool = True,
    compiled: bool = False,
) -> None:
    self.dialect = dialect
    self.time = time
    self.names = names
    self.prefix = prefix
    self.shared = shared
    self.compiled = compiled

FrameSpec(step, time, state, parameters, problem, agents=dict(), reporters=dict(), observables=dict(), events=(), structures=dict()) dataclass

Bases: BaseSpec

State of a simulation at one point in time.

Parameters:

Name Type Description Default
step int

The index of the frame, starting at 0.

required
time float

The simulated time.

required
state Array

A copy of the state vector.

required
parameters Array

A copy of the parameter vector.

required
problem BaseProblemSpec

The compiled problem, which gives the names of the vector components.

required
agents Mapping[str, AgentSnapshotSpec]

A mapping from agent group path to the agents alive at this frame.

dict()
reporters Mapping[str, float]

A mapping from dotted reporter name to its value at this frame.

dict()
observables Mapping[str, ArrayLike]

A mapping from observable path to its value at this frame.

dict()
events tuple[BaseRecordSpec, ...]

The events, cascades, and renewals since the previous frame, in order.

()
structures Mapping[str, object]

A mapping from the path of a rewritten element to its word or patch at this frame.

dict()

lattice(name)

Return the values of a lattice state in lattice shape.

Parameters:

Name Type Description Default
name str

The dotted name of a state of a lattice group, such as cells.alive.

required

Returns:

Name Type Description
values AnyArray

The array of the lattice shape, with the components of a vector or a tensor state on a last axis.

Raises:

Type Description
KeyError

The name is not a state of a lattice group.

Source code in src/skcomplex/engine/problems/_frame_spec.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def lattice(self, name: str) -> AnyArray:
    """Return the values of a lattice state in lattice shape.

    Args:
        name:
            The dotted name of a state of a lattice group, such as `cells.alive`.

    Returns:
        values:
            The array of the lattice shape, with the components of a vector or a tensor state on a last axis.

    Raises:
        KeyError:
            The name is not a state of a lattice group.
    """
    shape = self.problem.lattice_shape(name)
    if shape is None:
        error_msg = f'{name!r} is not a state of a lattice group.'
        raise KeyError(error_msg)
    member = self.problem.states.entry(name).member_shape
    return np.reshape(self.value(name), (*shape, *member))

value(name)

Return the value of a state or a parameter by dotted name.

Parameters:

Name Type Description Default
name str

The dotted name, such as rabbits.n or growth.r.

required

Returns:

Name Type Description
value ArrayLike

A number for a scalar and an array for a vector.

Source code in src/skcomplex/engine/problems/_frame_spec.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def value(self, name: str) -> npt.ArrayLike:
    """Return the value of a state or a parameter by dotted name.

    Args:
        name:
            The dotted name, such as `rabbits.n` or `growth.r`.

    Returns:
        value:
            A number for a scalar and an array for a vector.
    """
    if self.problem.states.has(name):
        return self.problem.states.read(self.state, name)
    if name in self.reporters:
        return self.reporters[name]
    if name in self.observables:
        return self.observables[name]
    return self.problem.parameters.read(self.parameters, name)

KernelSpec(owner, namespace, references, rows, pairs=None, roles=dict(), group=None, groups=dict(), sets=dict(), set_index=0, aligned=(), edges=None, hyperedge=None, reduce=None, counts=None, place=None) dataclass

Bases: BaseSpec

Namespace, references, and evaluation indices that the laws of one component are lowered with.

Parameters:

Name Type Description Default
owner str

The path of the component.

required
namespace SimpleNamespace

The namespace the laws of the component read.

required
references Mapping[Expression, ReferenceSpec]

The component of a state or parameter every symbol of the namespace stands for.

required
rows int

The number of rows the laws are evaluated over.

required
pairs PairsSearch | None

The search of the pairs an interaction over pairs or edges reads, or None.

None
roles dict[str, str | tuple[str, ...]]

The path, or the paths, every role binds to.

dict()
group str | None

The group whose members the laws are evaluated over, or None.

None
groups dict[str, int]

The size of every group the component reads member by member, by path.

dict()
sets dict[Expression, SetSymbolSpec]

The set symbol of every symbol that stands for one element of an element set.

dict()
set_index int

The index of the element of a set the spec is evaluated for.

0
aligned tuple[str, ...]

The groups of one size the laws read member by member, row i of each together.

()
edges KernelSpec | None

The spec of the edges of an interaction over neighbours, whose laws that read one end of an edge the spec evaluates once per member of the group on the nodes, or None.

None
hyperedge str | None

'member' for the laws of the focal member of every hyperedge of a hyperedge interaction, 'hyperedge' for its laws of the member of a group on hyperedges or edges, or None for other components.

None
reduce str | None

'sum' or 'mean' for the update laws of a grouping, which set every member to the sum or the mean of their values over the groups that hold it, or None.

None
counts Indices | None

The number of groups that hold the member of every row of a grouping, or None.

None
place FieldElement | None

The field on the nodes of an interaction over neighbours, whose laws per edge lower to the field's Laplacian, or on a lattice to whole-site shifted reads, or None.

None

KineticProblemSpec(form, states, parameters, initial_state, initial_parameters, time, equations, equation_symbols, has_callables, has_group_sums, model_rules=(), partitions=(), delays=(), events=(), breakpoints=_BreakpointsSpec(), history=dict(), lattice=(), stable_step=None, diffusive_limits=(), linear=(), observed=dict(), stream=RandomStream(), volumes=(), constrained=(lambda: np.zeros(0, dtype=np.intp))(), reductions=(), jacobian=None, unknowns=(lambda: np.zeros(0, dtype=np.intp))(), drift=None, sources=None, diffusion=None, noise_rows=(lambda: np.zeros(0, dtype=np.intp))(), noise_columns=(lambda: np.zeros(0, dtype=np.intp))(), n_processes=0, noise_blocks=(), propensities=None, jumps=(), update=None, boundary=None, draws=None, fractional=(), orders=(lambda: np.zeros(0))(), backend='numpy', backends=dict(), accelerated=None, encounters=(), *, symbols=dict(), elements=(), interactions=(), system=None, agents=(), agent_space=None, companions=dict(), renewals=(), matrices=(), transfer_network=None, topology=None, observables=()) dataclass

Bases: ProblemSpec

Problem whose drift holds the gain and loss rates of encounter laws, with the compiled encounters.

Parameters:

Name Type Description Default
encounters tuple[EncounterKernel, ...]

The encounter laws, whose gain and loss rates the drift holds and particle solvers sample.

()

LearningProblemSpec(form, states, parameters, initial_state, initial_parameters, time, inner=None, model='', training='', law=None, trained=(), hyperparameters=dict(), step=None, measures=dict(), *, symbols=dict(), elements=(), interactions=(), system=None, agents=(), agent_space=None, companions=dict(), renewals=(), matrices=(), transfer_network=None, topology=None, observables=()) dataclass

Bases: BaseProblemSpec

Problem of a system that trains the adaptive parameters of a nested system by gradient steps, once per epoch.

Its states are the trained parameters, named by their paths from the root, and its parameters are the hyperparameters of the optimizer. The nested system is compiled to its own problem, a discrete map or ordinary differential equations, which the solver unrolls over the sequences of the dataset.

Parameters:

Name Type Description Default
inner ProblemSpec | None

The problem of the nested system, compiled by the JAX backend.

None
model str

The path of the nested system.

''
training str

The path of the interaction that returns the law.

''
law GradientStepLaw | None

The gradient-step law.

None
trained tuple[str, ...]

The names of the trained parameters in the states of the inner problem, in the order of the states.

()
hyperparameters Mapping[str, str]

A mapping from the name of every hyperparameter of the optimizer to its name in the parameters.

dict()
step TracedFunction | None

The pure JAX function f(t, x, p) of the inner problem, its update or its drift.

None
measures Mapping[str, TracedFunction]

A mapping from 'loss' and the label of every metric to the pure JAX function o(t, x, p) of its value.

dict()

Examples:

>>> import numpy as np
>>> from skcomplex.types import FormKind
>>> from skcomplex.engine._utils import symbol
>>> from skcomplex.specs import LayoutSpec
>>> from skcomplex.engine.problems import LearningProblemSpec
>>> layout = LayoutSpec(())
>>> problem = LearningProblemSpec(FormKind.LEARNING, layout, layout, np.zeros(0), np.zeros(0), symbol('t'))
>>> problem.form.value, problem.trained
('learning by gradient steps', ())

LinearTermSpec(target, source, place, chain, coefficient, references) dataclass

Bases: BaseSpec

Term of the rate of a field state that applies a chain of operators to a state, times a constant coefficient.

Parameters:

Name Type Description Default
target ReferenceSpec

The component whose rate holds the term.

required
source ReferenceSpec

The component the operators act on.

required
place FieldElement

The field of the source.

required
chain Chain

The operators from outermost to innermost, with their axes.

required
coefficient Expression

The coefficient, an expression of parameters.

required
references Mapping[Expression, ReferenceSpec]

The references of the coefficient's symbols.

required

LoopTermSpec(kind, path, expression, references, source=None, sink=None) dataclass

Bases: BaseSpec

Node of the feedback graph of a system with the expression its value or its rate is computed from.

Parameters:

Name Type Description Default
kind str

'flow', or 'rate' for a rate law outside flows.

required
path str

The node of a flow, or the state a rate law targets.

required
expression Expression

The expression.

required
references Mapping[Expression, ReferenceSpec]

The references of the expression's symbols.

required
source str | None

The state a flow drains, or None.

None
sink str | None

The state a flow fills, or None.

None

LoweredKernelsSpec(kernels, jump_changes, laws, form=None, size=0, reductions=None, draws=None, stream=None) dataclass

Bases: BaseSpec

Kernels of every law kind, with the changes of every jump output and the law of every output.

Parameters:

Name Type Description Default
kernels defaultdict[type, list[LawKernel]]

The kernels of every law kind, in evaluation order.

required
jump_changes dict[int, list[tuple[ReferenceSpec, object]]]

The state changes of every jump output, keyed by the output's identity.

required
laws dict[int, tuple[object, BaseLaw]]

The kernel spec and the law every output was lowered from, keyed by the output's identity.

required
form FormKind | None

The form of the problem, which a backend writes the numeric functions of, or None before it is known.

None
size int

The size of the state vector.

0
reductions ReductionScheduleSpec | None

The reductions the kernels read, evaluated before them at every evaluation, or None.

None
draws DrawKernel | None

The random values a stochastic map draws at every step into the parameter vector extended after its parameters, or None for a deterministic problem.

None
stream RandomStream | None

The random stream the functions of the laws receive, or None.

None

ObservedSpec(namespace, references) dataclass

Bases: BaseSpec

Value of an observable that a role reads at the current state, with the references of its symbols.

OperatorProblemSpec(form, states, parameters, initial_state, initial_parameters, time, dimensions=(), subsystems=(), generator=(), dissipators=(), pure=True, namespaces=dict(), members=dict(), lowering=None, generator_operator=SR(0), cache=dict(), *, symbols=dict(), elements=(), interactions=(), system=None, agents=(), agent_space=None, companions=dict(), renewals=(), matrices=(), transfer_network=None, topology=None, observables=()) dataclass

Bases: BaseProblemSpec

Problem of the operator form, whose state is a pure state or a density matrix stored as complex pairs.

Parameters:

Name Type Description Default
dimensions tuple[int, ...]

The Hilbert space dimensions of the subsystems, in tensor product order.

()
subsystems tuple[str, ...]

The keys of the subsystems, such as chain[3], in tensor product order.

()
generator tuple[OperatorKernel, ...]

The terms of the generator.

()
dissipators tuple[tuple[OperatorKernel, ...], ...]

The terms of every collapse operator, each with the square root of its rate in the coefficients.

()
pure bool

Whether the state is a pure state vector rather than a density matrix.

True
namespaces Mapping[str, SimpleNamespace]

The namespace of every system, by its path, which observables receive.

dict()
members Mapping[str, tuple[SimpleNamespace, ...]]

The namespaces of the members of every operator element or group, by path.

dict()
lowering OperatorLowering | None

The lowering of operators to matrices and functions.

None
generator_operator Expression

The sum of the operators of the generator laws, a Sage expression of local operators.

SR(0)
cache dict[str, list[csr_array]]

The matrices of local operators computed so far.

dict()

OutputSpec(target, expression=None, function=None, arguments=None, component=0, components=1, process=None, combined=False, reduce=None, counts=None, relative=False, random=False, vectorized=False) dataclass

Bases: BaseSpec

Value a kernel produces for one target component.

Parameters:

Name Type Description Default
target ReferenceSpec | None

The target component, or None for the propensity of a jump.

required
expression Expression | None

The symbolic value, or None when a callable produces it.

None
function NumericFunction | Callable[..., object] | None

The callable or Call function that produces the values of every component of its target, or None.

None
arguments tuple[Expression, ...] | None

The argument expressions of a Call function, or None for a function of time and the named values.

None
component int

The component of the callable's result that this output holds.

0
components int

The number of components the callable returns per evaluation index.

1
process NoiseProcessSpec | None

The named Wiener process of a noise output, or None for one independent process per evaluation index.

None
combined bool

Whether an assigned output adds its change from the state to the value an earlier output set, so that several update laws of one state combine.

False
reduce str | None

'sum' or 'mean' for an assigned output that sets every member it writes to the sum or the mean of the values of the rows that write it, or None.

None
counts Indices | None

The number of rows, over every kernel, that write the member of every row of an output that reduces, or None.

None
relative bool

Whether a function reads the values keyed by the names of its component.

False
random bool

Whether a function receives the random stream as the keyword argument rng.

False
vectorized bool

Whether a Call function runs once per evaluation with arrays.

False

PartitionSpec(name, problem, owned, owners) dataclass

Bases: BaseSpec

Part of a co-simulation problem, compiled with the laws of its own elements.

The partition's problem has the state layout of the whole system. The states of other partitions have no laws in it, so they hold their values while it advances and act as its inputs.

Parameters:

Name Type Description Default
name str

The name of the partition, the path of its subsystem.

required
problem BaseProblemSpec

The problem of the partition.

required
owned NDArray[intp]

The flat state indices of the partition's own elements.

required
owners frozenset[str]

The paths of the partition's own elements and groups.

required

ProblemSpec(form, states, parameters, initial_state, initial_parameters, time, equations, equation_symbols, has_callables, has_group_sums, model_rules=(), partitions=(), delays=(), events=(), breakpoints=_BreakpointsSpec(), history=dict(), lattice=(), stable_step=None, diffusive_limits=(), linear=(), observed=dict(), stream=RandomStream(), volumes=(), constrained=(lambda: np.zeros(0, dtype=np.intp))(), reductions=(), jacobian=None, unknowns=(lambda: np.zeros(0, dtype=np.intp))(), drift=None, sources=None, diffusion=None, noise_rows=(lambda: np.zeros(0, dtype=np.intp))(), noise_columns=(lambda: np.zeros(0, dtype=np.intp))(), n_processes=0, noise_blocks=(), propensities=None, jumps=(), update=None, boundary=None, draws=None, fractional=(), orders=(lambda: np.zeros(0))(), backend='numpy', backends=dict(), accelerated=None, *, symbols=dict(), elements=(), interactions=(), system=None, agents=(), agent_space=None, companions=dict(), renewals=(), matrices=(), transfer_network=None, topology=None, observables=()) dataclass

Bases: BaseProblemSpec

System compiled to one mathematical form with flat state and parameter vectors.

Only the functions of the problem's form are set. drift, diffusion, and noise_rows belong to the ODE and SDE forms, propensities and jumps to the jump process form, and update to the discrete map form. boundary belongs to any form whose system has boundary laws.

Parameters:

Name Type Description Default
form FormKind | str

The mathematical form.

required
states LayoutSpec

The layout of the state vector.

required
parameters LayoutSpec

The layout of the parameter vector.

required
initial_state Array

The initial state vector.

required
initial_parameters Array

The initial parameter vector.

required
time Expression

The time symbol.

required
symbols Mapping[str, Expression | FreeModuleElement]

A mapping from dotted name to the symbol of a scalar or the column of symbols of a vector. The symbols of a group stand for one member.

dict()
equations Mapping[str, Expression]

A mapping from state component name to its symbolic drift, or to its next value for the discrete form. Group components are named with the member index i. Components with a callable law, a sum over group members, or a sum over pairs are left out.

required
equation_symbols Mapping[str, Expression]

A mapping from the names of equations to the symbol of the component.

required
stream RandomStream

The random stream the functions of the laws receive, which a solver binds when it prepares.

RandomStream()
has_callables bool

Whether any law is a callable.

required
has_group_sums bool

Whether any law sums over group members or pairs.

required
elements tuple[CompiledElementSpec, ...]

The compiled elements and groups in state vector order.

()
interactions tuple[CompiledInteractionSpec, ...]

The compiled interactions in tree order.

()
system System | None

The system the problem was compiled from.

None
agents tuple[CompiledAgentsSpec, ...]

The agent elements and groups of an agent-based problem.

()
model_rules tuple[tuple[str, BaseLaw], ...]

The rules of interactions of an agent-based problem, with the path of their interaction.

()
agent_space BaseSpace | None

The space the agents of an agent-based problem live in, or None.

None
companions Mapping[str, tuple[str, str, int]]

A mapping from the name of every companion state to its kind, 'derivative' or 'lag', the name of the state it belongs to, and the order of the derivative or the number of steps back.

dict()
partitions tuple[PartitionSpec, ...]

The partitions of a co-simulation problem, in system order.

()
delays tuple[_DelaySpec, ...]

The delayed states that laws read, which solvers fill from the run's history.

()
events tuple[EventKernel, ...]

The discrete events, with their guard and reset functions.

()
breakpoints _BreakpointsSpec

The times where time-dependent inputs jump, at which solvers restart.

_BreakpointsSpec()
history Mapping[str, object]

The values of states before the start, by dotted state path, as numbers or functions of time.

dict()
lattice tuple[BaseKernel, ...]

The steps that change the states of sites, such as the local rules of a lattice problem, in the order they apply within a step.

()
linear tuple[LinearTermSpec, ...]

The linear terms with constant coefficients of the rates of fields, which implicit and exponential solvers treat exactly while the rest of the rates is the nonlinear remainder.

()
observed Mapping[str, LawKernel]

The kernels of the observables of fields whose expressions apply field operators, such as a streamfunction inverted from a vorticity, by the path of the observable, each giving the expression at every site.

dict()
volumes tuple[FiniteVolumeKernel, ...]

The conservation laws of finite-volume fields, whose flux differences the drift includes.

()
constrained NDArray[intp]

The state indices held at their initial values, the fixed boundary nodes of finite element fields.

(lambda: zeros(0, dtype=intp))()
reductions tuple[ReductionKernel, ...]

The reductions that laws read, in the order they are evaluated before the kernels.

()
unknowns Indices

The positions in the state vector that the equations of a static problem solve for numerically.

(lambda: zeros(0, dtype=intp))()
jacobian JacobianKernel | None

The Jacobian of the drift of an ODE problem, derived from its laws: its sparsity pattern and, for laws with a symbolic form, its exact value. None for other forms.

None
stable_step float | None

The largest step of explicit fixed-step solvers for which the diffusion of fields is stable, h^2 / (degree * D), or None when the system has no diffusion with numeric coefficients.

None
diffusive_limits tuple[DiffusiveLimitSpec, ...]

The stability limits of the diffusion of finite-volume fields, which depend on the parameters.

()
drift VectorFunction | None

The function f(t, x, p) that returns the time derivative or the drift.

None
sources VectorFunction | None

The part of the drift without the flux differences of finite-volume fields, or None without fluxes.

None
diffusion VectorFunction | None

The function g(t, x, p) that returns every noise amplitude, one per state and process it couples.

None
noise_rows NDArray[intp]

The state index each amplitude of diffusion drives.

(lambda: zeros(0, dtype=intp))()
noise_columns NDArray[intp]

The Wiener process each amplitude of diffusion draws, which several amplitudes share for shared noise.

(lambda: zeros(0, dtype=intp))()
n_processes int

The number of standard normal increments drawn per step.

0
noise_blocks tuple[WienerBlockSpec, ...]

The blocks of correlated processes, whose increments their covariance factor scales.

()
propensities VectorFunction | None

The function a(t, x, p) that returns the rate of every jump channel.

None
jumps tuple[_JumpChannelsSpec, ...]

The channels of every jump law.

()
update Callable[..., Array] | None

The function F(t, x, p) that returns the next state vector, or F(t, x, p, rng) for a stochastic map.

None
boundary VectorFunction | None

The function B(t, x, p) that returns the state after the boundary laws.

None
draws DrawKernel | None

The random values the update draws at every step, or None for a deterministic map.

None
matrices tuple[MarkovKernel, ...]

The transition and emission matrices of the Markov laws, checked when their parameters change.

()
renewals tuple[RenewalKernel, ...]

The renewal laws, whose members renew on their own clocks.

()
fractional tuple[FractionalRowsSpec, ...]

The noise rows that fractional noise laws drive, with their Hurst indices.

()
orders Array

The order of the Caputo derivative that the drift gives for every state component of a problem of the fractional form, 1 for the ordinary states, and empty for the other forms.

(lambda: zeros(0))()
transfer_network TransferNetworkSpec | None

The quantities, flows, objectives, and feedback terms of the system, or None for partitioned and Mesa problems.

None
backend str

The name of the backend that produced the numeric functions.

'numpy'
backends Mapping[str, str]

The backend that runs every part of the problem: its form by the form's name, and the kernels of every law kind by the kind's name, 'python' for kernels that a compiling backend runs in Python.

dict()
accelerated AcceleratedSpec | None

The compiled stepper loops, or None when the backend has none for this problem.

None
topology TopologyKernel | None

The topology requests of the laws and the networks they change, which the stepper applies between steps, or None for a problem without them.

None

adaptive_parameters property

Declarations of the adaptive parameters of the elements and groups, by dotted state name.

parameter_symbols property

Symbols of the parameter components of interactions, single elements, and one member of every group.

partition_problems property

Problems of the partitions of a co-simulation problem, none for a problem of one form.

state_symbols property

Symbols of the state components of single elements and of one member of every group.

stochastic property

Whether the update draws random values, so it takes the random number generator.

stoichiometry property

Increments of the state vector, one row per jump channel.

lattice_shape(name)

Return the lattice shape of a state that lattice laws read or change, or None for other states.

Parameters:

Name Type Description Default
name str

The dotted name of the state.

required

Returns:

Name Type Description
shape tuple[int, ...] | None

The number of sites along every dimension.

Source code in src/skcomplex/engine/problems/_problem_spec.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def lattice_shape(self, name: str) -> tuple[int, ...] | None:
    """Return the lattice shape of a state that lattice laws read or change, or `None` for other states.

    Args:
        name:
            The dotted name of the state.

    Returns:
        shape:
            The number of sites along every dimension.
    """
    for step in self.lattice:
        for state in step.targets:
            if state.name == name:
                return state.shape
    owner = name.rpartition('.')[0]
    for element in self.elements:
        if element.path == owner and element.lattice_shape is not None:
            return element.lattice_shape
    return None

ReductionScheduleSpec(reductions=(), parameters=0, neighbourhoods=(), sequence=()) dataclass

Bases: BaseSpec

Reductions of a problem, evaluated in dependency order into the extended parameter vector.

Parameters:

Name Type Description Default
reductions tuple[ReductionKernel, ...]

The compiled reductions over groups, every one after the reductions it reads.

()
parameters int

The size of the parameter vector, where the reductions start.

0
neighbourhoods tuple[NeighborhoodKernel, ...]

The compiled reductions over the neighbours of every member.

()
sequence tuple[ReductionKernel | NeighborhoodKernel, ...]

Every reduction of both kinds in dependency order, or empty for the reductions over groups in their order.

()

extent property

Size of the extended parameter vector.

steps property

Every reduction in the order it is evaluated.

extend(t, x, p)

Return the parameter vector followed by the value of every reduction at a state.

Parameters:

Name Type Description Default
t float

The time.

required
x Array

The state vector.

required
p Array

The parameter vector.

required

Returns:

Name Type Description
extended Array

The extended parameter vector.

Source code in src/skcomplex/engine/problems/_reduction_schedule_spec.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def extend(self, t: float, x: Array, p: Array) -> Array:
    """Return the parameter vector followed by the value of every reduction at a state.

    Args:
        t:
            The time.

        x:
            The state vector.

        p:
            The parameter vector.

    Returns:
        extended:
            The extended parameter vector.
    """
    q = np.zeros(self.extent)
    given = min(p.size, self.parameters)
    q[:given] = p[:given]
    for reduction in self.steps:
        reduction.run(RunStateSpec(t, x, q))
    return q

wrap(function)

Return a function of the state and the parameters that reads the reductions at the state it receives.

Parameters:

Name Type Description Default
function Callable[[float, Array, Array], R] | None

A function f(t, x, q) of the extended parameter vector, or None.

required

Returns:

Name Type Description
wrapped Callable[[float, Array, Array], R] | None

The function f(t, x, p), or the function itself without reductions.

Source code in src/skcomplex/engine/problems/_reduction_schedule_spec.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def wrap[R](
    self,
    function: Callable[[float, Array, Array], R] | None,
) -> Callable[[float, Array, Array], R] | None:
    """Return a function of the state and the parameters that reads the reductions at the state it receives.

    Args:
        function:
            A function `f(t, x, q)` of the extended parameter vector, or `None`.

    Returns:
        wrapped:
            The function `f(t, x, p)`, or the function itself without reductions.
    """
    if function is None or not self.steps:
        return function

    def reduced(t: float, x: Array, p: Array) -> R:
        return function(t, x, self.extend(t, x, p))

    return reduced

ReferenceSpec(entry, is_state, column, source=ReadKind.SINGLE, neighbor=None) dataclass

Bases: BaseSpec

Component of a state or parameter that a symbol stands for.

Parameters:

Name Type Description Default
entry EntrySpec

The layout entry of the variable.

required
is_state bool

Whether the variable is a state, as opposed to a parameter.

required
column int

The component of the variable of one member, or for a single read of one group member, the component counted from the group's first component.

required
source ReadKind

How the component is read for each evaluation index.

SINGLE
neighbor NeighborReadSpec | FieldOperatorKernel | None

How a neighbour read finds the neighbour of every member, for the neighbour source, or the global operator of a derived read, for the derived source.

None

derived property

Global operator of the derived read.

Raises:

Type Description
ValidationError

The reference is not a derived read.

flat_indices property

Indices of the component of every member in the flat vector.

index cached property

Flat index of the component of a single read, or of the first member.

members cached property

Strided slice of the component of every member in the flat vector.

shifted property

How the neighbour read finds the neighbour of every member.

Raises:

Type Description
ValidationError

The reference is not a neighbour read.

column_of(x, p)

Return the component of every member as a view into the state or parameter vector.

Parameters:

Name Type Description Default
x Array

The state vector.

required
p Array

The parameter vector.

required

Returns:

Name Type Description
column Array

A view with one value per member, or with the one value of a single read.

Source code in src/skcomplex/engine/problems/_reference_spec.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def column_of(self, x: Array, p: Array) -> Array:
    """Return the component of every member as a view into the state or parameter vector.

    Args:
        x:
            The state vector.

        p:
            The parameter vector.

    Returns:
        column:
            A view with one value per member, or with the one value of a single read.
    """
    vector = x if self.is_state else p
    if self.source is ReadKind.SINGLE:
        return vector[self.index : self.index + 1]
    return vector[self.members]

SetSymbolSpec(canonical, references, label) dataclass

Bases: BaseSpec

Symbol of one element of an element set, with the same symbol of every element of the set.

SourceLoader(prefix, functions=None)

Bases: BaseConfig[str | Path | None, str]

Loader of generated source, imported from a file of the cache directory named by its digest.

prepare(cache_dir) sets the directory of the generated files, ~/.cache/skcomplex/kernels by default, and run(source) writes the source there once and imports it into module_. Source without injected functions is kept in sys.modules, so the same source is imported once. Source with injected functions is imported afresh, with the functions set on the module before its code runs.

Parameters:

Name Type Description Default
prefix str

The start of the name of the module, a Python identifier.

required
functions Mapping[str, object] | None

The functions the source reads by name, injected into its module, or None for none.

None

Attributes:

Name Type Description
directory_ Path

The directory of the generated files.

module_ ModuleType

The module of the last source run.

Examples:

>>> from skcomplex.engine.problems import SourceLoader
>>> loader = SourceLoader('skcomplex_example').check().prepare()
>>> loader.run('def double(x): return 2 * x').module_.double(3)
6
Source code in src/skcomplex/engine/problems/_source_loader.py
53
54
55
def __init__(self, prefix: str, functions: Mapping[str, object] | None = None) -> None:
    self.prefix = prefix
    self.functions = functions

SymmetrySpec(indices, shape, signs) dataclass

Bases: BaseSpec

Mirror symmetry of a field state on a periodic lattice, whose rates are projected onto odd or even parts.

Parameters:

Name Type Description Default
indices Indices

The state indices of the field state, one per lattice site.

required
shape tuple[int, ...]

The shape of the lattice.

required
signs tuple[tuple[int, int], ...]

The axes of the symmetry with their signs, -1 for an odd and 1 for an even part.

required

TransferNetworkSpec(quantities, flows=(), recorded=(), boundary=dict(), terms=(), discrete=False, delays=()) dataclass

Bases: BaseSpec

Quantities, flows, and recorded values of a compiled system.

Parameters:

Name Type Description Default
quantities Mapping[str, tuple[str, ...]]

The names of the states that hold each quantity.

required
flows tuple[_CompiledFlowSpec, ...]

The flows.

()
recorded tuple[_RecordedSpec, ...]

The flows and the running values of objectives that trajectories record.

()
boundary Mapping[str, str]

The hidden state that accumulates the net inflow from the environment of each quantity with boundary flows.

dict()
terms tuple[LoopTermSpec, ...]

The terms of the feedback graph.

()
discrete bool

Whether flow amounts are per step rather than rates.

False
delays tuple[_CompiledDelaySpec, ...]

The delays and smoothings of the laws, in the order the compiler replaced them by hidden states.

()

TransferStateSpec(quantities, born, flows, records, delays, terms) dataclass

Bases: BaseSpec

State of the lowering of flows between the pass that expands flows and the pass that lowers them.

Parameters:

Name Type Description Default
quantities Mapping[str, str]

The quantity of every tagged state by entry name.

required
born Mapping[int, _BornSpec]

The expanded flow of every law a flow became, by the identity of the law.

required
flows tuple[_BornSpec, ...]

The expanded flows, in the order they were expanded.

required
records tuple[_RecordSpec, ...]

The recorded expressions of the flows.

required
delays tuple[_CompiledDelaySpec, ...]

The delays and smoothings lowered to hidden states.

required
terms tuple[_FlowTermSpec, ...]

The flows and rates of every spec, with their sources and sinks, for the loop terms.

required

replaced(specs)

Return the state with replaced kernel specs, such as specs whose rows are settled.

Parameters:

Name Type Description Default
specs Mapping[int, KernelSpec]

The new spec of every replaced spec, by the identity of the replaced spec.

required

Returns:

Name Type Description
state TransferStateSpec

The state that holds the new specs.

Source code in src/skcomplex/engine/problems/_transfer_state_spec.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def replaced(self, specs: Mapping[int, KernelSpec]) -> TransferStateSpec:
    """Return the state with replaced kernel specs, such as specs whose rows are settled.

    Args:
        specs:
            The new spec of every replaced spec, by the identity of the replaced spec.

    Returns:
        state:
            The state that holds the new specs.
    """
    born = {id(item): replace(item, spec=specs.get(id(item.spec), item.spec)) for item in self.flows}
    return replace(
        self,
        born={key: born[id(item)] for key, item in self.born.items()},
        flows=tuple(born[id(item)] for item in self.flows),
        records=tuple(replace(record, spec=specs.get(id(record.spec), record.spec)) for record in self.records),
        terms=tuple(replace(term, spec=specs.get(id(term.spec), term.spec)) for term in self.terms),
    )