Solvers
Advance compiled problems through time, with a default solver per form.
AgentView(agent, stepper)
Agent as rules see it: its states as attributes, its group's parameters, its neighbours, and its location.
The stepper gives every agent one view of the class of its group, whose states and parameters are properties, so
two views of an agent are the same object. Reading a state reads the agent, and setting it sets the agent, at
the end of the phase in a simultaneous phase. Parameters are read only, and an unknown name raises Python's own
AttributeError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent
|
Agent
|
The Mesa agent. |
required |
stepper
|
MesaSolver
|
The stepper that runs the model. |
required |
Source code in src/skcomplex/engine/solvers/_mesa.py
513 514 515 | |
alive
property
Whether the agent has not been removed.
group
property
Path of the agent's group.
group_label
property
Label of the agent's group, the last part of its path.
id
property
Unique id of the agent.
location
property
Grid coordinates, network node, or position of the agent.
location_key
property
Location of the agent as a key: a tuple of integers for a cell, a tuple of floats for a position, or a node.
The key equals the coordinates neighbor_cells gives for the agent's cell or node, so rules compare the cells
around an agent with the places of other agents.
agents_here(group=None)
Return the other agents in the agent's cell or node, of one group or of every group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group
|
str | None
|
The path of a group, or |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
agents |
list[AgentView]
|
The other agents at the same place. |
Source code in src/skcomplex/engine/solvers/_mesa.py
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 | |
create(group=None, location=None, **values)
Create an agent of a group, by default the agent's own group at the agent's location.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group
|
str | None
|
The path of the group, or |
None
|
location
|
object
|
The location of the new agent, or |
None
|
**values
|
object
|
The initial values of states, with the element type's defaults for the others. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
agent |
AgentView
|
The new agent. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
The group is not variable, so its size cannot change. |
Source code in src/skcomplex/engine/solvers/_mesa.py
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 | |
displacement_to(other)
Return the vector from the agent to another agent or a location, the shortest one on a torus.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
AgentView | object
|
An agent, or grid coordinates or a position. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
displacement |
NDArray[float64]
|
One component per axis of the space. |
Raises:
| Type | Description |
|---|---|
SpaceError
|
The agents live on a network or in no space. |
Source code in src/skcomplex/engine/solvers/_mesa.py
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 | |
distance_to(other)
Return the distance to another agent or a location, the number of hops on a network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
AgentView | object
|
An agent, or a network node, grid coordinates, or a position. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
distance |
float
|
The Euclidean length of the shortest displacement, or the length of the shortest path on a network. |
Raises:
| Type | Description |
|---|---|
SpaceError
|
The agents live in no space. |
Source code in src/skcomplex/engine/solvers/_mesa.py
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 | |
lags(state, window=None)
Return the past values of a state of an agent, the values at the ends of the last steps, the last one last.
An agent remembers a state declared with lags, StateSpec(lags=m): the agent stepper keeps its last m values
in the companion states <state>_lag1 to <state>_lag<m>, which rules may also read one by one. Before the
first steps the lags hold their initial values, the initial of the spec or else the initial value of the
state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The name of a state with lags. |
required |
window
|
int | None
|
The number of last values, or |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
values |
NDArray[float64]
|
The values, the oldest first. |
Raises:
| Type | Description |
|---|---|
KeyError
|
The state of the agent keeps no lags. |
Source code in src/skcomplex/engine/solvers/_mesa.py
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 | |
move_to(location)
Move the agent to a cell, a node, or a position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
location
|
object
|
The cell coordinates, network node, or position. |
required |
Source code in src/skcomplex/engine/solvers/_mesa.py
664 665 666 667 668 669 670 671 672 673 674 | |
neighbor_cells()
Return the coordinates of the cells or nodes around the agent's cell or node.
Returns:
| Name | Type | Description |
|---|---|---|
locations |
list[object]
|
The neighbouring cell coordinates or network nodes. |
Source code in src/skcomplex/engine/solvers/_mesa.py
608 609 610 611 612 613 614 615 | |
neighbors(radius=None, layer=None)
Return the agents in the cells or nodes around the agent, within a radius, or around it in a layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
radius
|
float | None
|
The distance within which agents are neighbours in a continuous space. |
None
|
layer
|
str | None
|
The layer of a network space whose edges join the agent to its neighbours, or |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
agents |
list[AgentView]
|
The neighbouring agents. |
Source code in src/skcomplex/engine/solvers/_mesa.py
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 | |
referenced(state)
Return the member of another group that a reference state of the agent names by its id.
A state declared with StateSpec(reference=group) holds the id of a member of the group, or a negative value
for none, such as the bank of a customer or the employer of a worker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
The name of a reference state. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
agent |
AgentView | None
|
The member the state names, or |
Raises:
| Type | Description |
|---|---|
KeyError
|
The state of the agent names no member of a group. |
Source code in src/skcomplex/engine/solvers/_mesa.py
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 | |
remove()
Remove the agent from the model, after checking that it holds none of a conserved quantity.
Raises:
| Type | Description |
|---|---|
ConservationError
|
A state tagged with a quantity is not zero, so removing the agent would destroy it. Settle the holdings with transfers first. |
SimulationError
|
The agent's group is not variable, so its size cannot change. |
Source code in src/skcomplex/engine/solvers/_mesa.py
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 | |
BaseSolver(*, random_state=None, until=math.inf)
Bases: BaseConfig[P, StepSpec]
Base of the solvers, which advance a problem of one form through time.
A solver stores its settings in its constructor, among them the random state of stochastic solvers and the end
time of the run it prepares for. prepare(problem) prepares it for a problem and run(step) advances the state
of a StepSpec from its time to its end time and keeps the state vector there in state_. The last call
run(step, final=True) releases what preparing acquired, such as an external process.
Subclasses implement the hooks _get_form, the form they solve, _get_support, what they solve, and _advance,
and solvers that keep events, agents, or other state outside the state vector also implement _collect. Solvers
that also solve other forms list them in _get_forms. Solvers that treat diffusion implicitly answer
_is_implicit, so they do not warn about the stability limit of explicit steps. Solvers that fire renewal laws
answer _is_renewing, solvers that integrate fractional noise _is_fractional, and solvers that read external
inputs _reads_inputs. A solver is generic over the problem it advances, so one written for the compiler's problem
reads its fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. A seed starts a stream spawned from
|
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped, which solvers that plan or draw a whole run read. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
problem_ |
P
|
The problem the solver is prepared to. |
rng_ |
Generator
|
The random number generator of stochastic solvers. |
initial_state_ |
Array
|
The state vector of the first frame, which solvers that derive states fill in. |
events_ |
list[BaseRecordSpec]
|
The events of the last run, in the order they happened. |
stopped_at_ |
float | None
|
The time at which a terminal event ended the run, or |
snapshot_ |
object | None
|
The state the solver keeps outside the state vector, such as agents, after the last run, or |
agents_ |
Mapping[str, list[object]]
|
The views of the agents alive after the last run, as rules receive them, by path of their agent group. |
state_ |
Array
|
The state vector at the end time of the last step. |
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
CaputoSolver(*, random_state=None, until=math.inf)
Bases: Solver
Base class of steppers of Caputo fractional differential equations on a fixed grid from the start of a run.
A state of order alpha follows D^alpha x = f(t, x), and a state of order 1 follows dx/dt = f(t, x). The
stepper keeps the history of the run on the grid t0 + k dt. A change of a fractional state between frames starts
a new history from the changed state, and a change of a state of order 1 carries on.
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
CoSimulationSolver(solvers=None, sync_dt=None, scheme='gauss-seidel', split=False, *, random_state=None, until=math.inf)
Bases: Solver
Scheduler that advances every partition of a co-simulation to each synchronisation point with its own solver.
Between synchronisation points a partition holds the states of the other partitions at their last values, so a
split adds an error of the order of sync_dt. A law of one partition that reads a reduction of the agents of
another, such as count(s.farmers), reads the value the scheduler computes at every synchronisation point and
holds until the next. Gauss-Seidel advances the partitions in system order, each reading the values the partitions
before it already advanced. Jacobi advances every partition from the same values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solvers
|
Mapping[str, BaseSolver] | None
|
The solver of a partition by its name, with the default solver of its form for the others. |
None
|
sync_dt
|
float | None
|
The time between synchronisation points, which must divide the frame duration, or |
None
|
scheme
|
str
|
|
'gauss-seidel'
|
split
|
bool
|
Whether to split a problem of one form into one partition per subsystem. |
False
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
steppers_ |
The stepper of every partition by partition name. |
Examples:
>>> from skcomplex.core.system import System
>>> from skcomplex.models.biology.common.populations import ExponentialGrowthInteraction, PopulationElement
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import CoSimulationSolver
>>> system = System(
... systems=[
... System(
... 'a',
... elements=[PopulationElement('x', n=1)],
... interactions=[ExponentialGrowthInteraction('g', target='x')],
... ),
... System(
... 'b',
... elements=[PopulationElement('y', n=1)],
... interactions=[ExponentialGrowthInteraction('g', target='y')],
... ),
... ],
... )
>>> simulation = Simulation(system, solver=CoSimulationSolver(split=True), dt=0.5)
>>> simulation = simulation.check().prepare().run(until=1)
>>> sorted(simulation.stepper_.steppers_)
['a', 'b']
Source code in src/skcomplex/engine/solvers/_cosimulation.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
CollocationSolver(objective=None, *, terminal=0.0, sense='min', nodes=101, scheme='hermite-simpson', method='SLSQP', tolerance=1e-10, max_iterations=1000, random_state=None, until=math.inf)
Bases: OptimalControlSolver
Optimal control by direct collocation, a transcription of the problem solved by nonlinear programming.
The states and the controls at the points of a grid are the variables of a nonlinear programme. The trapezoidal
scheme requires the states of neighbouring nodes to differ by the trapezoidal integral of their rates, and the
Hermite-Simpson scheme adds a midpoint to every interval, with a cubic interpolation of the states and Simpson's
integral. The cost is the quadrature of the running cost over the points plus the terminal cost. The controls lie
within their bounds and the states of nonnegative domains stay nonnegative. SciPy's minimize solves the programme
with the gradient of the cost and the Jacobian of the constraints derived symbolically. An objective to maximise is
minimised with its sign changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
objective
|
Cost | None
|
The running cost, a function of the namespace |
None
|
terminal
|
Cost
|
The terminal cost at the horizon, a function of the same namespace or a number. |
0.0
|
sense
|
str
|
|
'min'
|
nodes
|
int
|
The number of times of the grid, from the start to the horizon. |
101
|
scheme
|
str
|
|
'hermite-simpson'
|
method
|
str
|
The method of |
'SLSQP'
|
tolerance
|
float
|
The tolerance of the optimiser. |
1e-10
|
max_iterations
|
int
|
The largest number of iterations of the optimiser. |
1000
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
solution_ |
The optimal solution of the run. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
The sense, the scheme, or the method is unknown, or the grid has fewer than 2 nodes. |
SolverError
|
The solver has no objective, or the optimiser does not converge. |
Examples:
>>> from typing import Annotated
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import RateLaw
>>> from skcomplex.specs import ParameterSpec, StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import CollocationSolver
>>> class PlantElement(Element):
... def __init__(
... self,
... label,
... x: Annotated[float, StateSpec()] = 1.0,
... u: Annotated[float, ParameterSpec(adaptive=True, bounds=(-10, 10))] = 0.0,
... ):
... super().__init__(label)
... self.x = x
... self.u = u
...
... def _get_laws(self, s):
... return [RateLaw(s.x, s.u)]
>>> cost = lambda s: (s.plant.x**2 + s.plant.u**2) / 2
>>> solver = CollocationSolver(objective=cost, nodes=51, scheme='hermite-simpson')
>>> simulation = Simulation(System(elements=[PlantElement('plant')]), solver=solver)
>>> simulation = simulation.check().prepare().run(until=1.0)
>>> float(round(simulation.trajectory_.values('plant.u')[0], 3))
-0.762
Source code in src/skcomplex/engine/solvers/_collocation.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
CrankNicolsonSolver(dt=0.01, *, random_state=None, until=math.inf)
Bases: SplitSolver
Crank-Nicolson for the linear part with a Heun predictor-corrector for the remainder, of second order.
The predictor takes a Crank-Nicolson step with the remainder at the start, and the corrector repeats it with the remainder averaged over the start and the predicted end. For linear fields it is the trapezoidal rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. |
0.01
|
Examples:
>>> from skcomplex.engine.solvers import CrankNicolsonSolver
>>> CrankNicolsonSolver(dt=0.5).dt
0.5
Source code in src/skcomplex/engine/solvers/_split.py
84 85 86 87 88 89 90 91 92 93 | |
ETDRK4Solver(dt=0.01, *, random_state=None, until=math.inf)
Bases: SplitSolver
Fourth-order exponential time differencing Runge-Kutta of Cox and Matthews, for spectral fields.
The linear part is integrated exactly per Fourier mode and the remainder by a fourth-order scheme, with the coefficients computed by contour integrals as Kassam and Trefethen propose. Coupled states of a field use a small matrix exponential per mode. States outside spectral fields follow classical RK4.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. |
0.01
|
Examples:
>>> from skcomplex.engine.solvers import ETDRK4Solver
>>> ETDRK4Solver(dt=0.25).dt
0.25
Source code in src/skcomplex/engine/solvers/_split.py
84 85 86 87 88 89 90 91 92 93 | |
EulerMaruyamaSolver(dt=0.001, *, random_state=None, until=math.inf)
Bases: SDESolver
Euler-Maruyama integrator of stochastic differential equations with a fixed internal step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.001
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.system import System
>>> from skcomplex.models.economics.finance import AssetElement, GeometricBrownianMotionInteraction
>>> from skcomplex.engine.solvers import EulerMaruyamaSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(
... System(
... elements=[AssetElement('stock', price=100)],
... interactions=[GeometricBrownianMotionInteraction('gbm', target='stock', mu=0.05, sigma=0.2)],
... ),
... )
>>> stepper = EulerMaruyamaSolver(dt=1e-3, random_state=0).check().prepare(problem)
>>> bool(stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_[0] > 0)
True
Source code in src/skcomplex/engine/solvers/_sde.py
40 41 42 43 44 45 46 47 48 49 | |
ExternalSolver(*, random_state=None, until=math.inf)
Bases: Solver
Base class of solvers that translate a compiled problem into a third-party library.
Subclasses implement the hooks _get_name, _get_form, _unsupported, _build, and _advance, and list the
other forms the library runs in _get_forms. Before the first frame, prepare checks every element, interaction,
and law, and raises one error that lists everything the library cannot run.
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
FractionalABMSolver(dt=0.01, *, random_state=None, until=math.inf)
Bases: CaputoSolver
Fractional Adams-Bashforth-Moulton predictor-corrector of Diethelm, Ford, and Freed, of order 1 + alpha.
The scheme solves the equivalent Volterra equation x(t) = x_0 + (1 / Gamma(alpha)) int_0^t (t - s)^(alpha - 1)
f(s, x(s)) ds. A rectangle rule over the drifts of the whole run predicts the next state, and a trapezoidal rule
with the drift at the prediction corrects it. States of order 1 take Heun steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed step. Frames must fall on multiples of it. |
0.01
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
orders_ |
The order of every state component. |
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import RateLaw
>>> from skcomplex.specs import StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.engine.solvers import FractionalABMSolver
>>> from skcomplex.specs import StepSpec
>>> class RelaxingElement(Element):
... def __init__(self, label, x: Annotated[float, StateSpec(order=0.5)] = 1.0):
... super().__init__(label)
... self.x = x
...
... def _get_laws(self, s):
... return [RateLaw(s.x, -s.x)]
>>> problem = compile_system(System(elements=[RelaxingElement('a')]))
>>> stepper = FractionalABMSolver(dt=0.001, random_state=0).check().prepare(problem)
>>> step = StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)
>>> round(float(stepper.run(step).state_[0]), 2)
0.43
Source code in src/skcomplex/engine/solvers/_fractional_abm.py
61 62 63 64 65 66 67 68 69 70 | |
FractionalEulerMaruyamaSolver(dt=0.001, horizon=None, max_increments=_MAX_INCREMENTS, *, random_state=None, until=math.inf)
Bases: Solver
Euler-Maruyama integrator of stochastic differential equations driven by fractional and ordinary noise.
The stepper draws the correlated increments of every fractional noise row for the whole run when it first advances,
on the grid t0 + k dt up to the horizon, and adds the stored increment of each step times the amplitude.
Ordinary noise rows take independent Wiener increments. A simulation passes the end time of its run as the
horizon when the stepper has none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. Frames must fall on multiples of it. |
0.001
|
horizon
|
float | None
|
The last time the run reaches, or |
None
|
max_increments
|
int
|
The largest number of stored increments, one per step, fractional noise row, and group member. |
_MAX_INCREMENTS
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
horizon_ |
The horizon of the drawn increments. |
|
hurst_ |
The Hurst index of every noise row the increments were drawn with. |
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from skcomplex.core.elements import Element, GroupElement
>>> from skcomplex.core.laws import FractionalNoiseLaw
>>> from skcomplex.specs import StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.engine.solvers import FractionalEulerMaruyamaSolver
>>> from skcomplex.specs import StepSpec
>>> class WalkerElement(Element):
... def __init__(self, label, x: Annotated[float, StateSpec()] = 0.0):
... super().__init__(label)
... self.x = x
...
... def _get_laws(self, s):
... return [FractionalNoiseLaw(s.x, 1, hurst=0.75)]
>>> problem = compile_system(System(elements=[GroupElement('walkers', element=WalkerElement, size=500)]))
>>> stepper = FractionalEulerMaruyamaSolver(dt=0.01, horizon=1.0, random_state=0).check().prepare(problem)
>>> state = stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_
>>> round(float(np.var(state)), 1)
1.0
Source code in src/skcomplex/engine/solvers/_fractional_euler_maruyama.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 | |
GillespieSolver(*, random_state=None, until=math.inf)
Bases: Solver
Exact stochastic simulation of jump processes by the Gillespie direct method.
An event that would fire after until is discarded, which is exact because waiting times are memoryless.
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.system import System
>>> from skcomplex.core.elements import CompartmentElement
>>> from skcomplex.models.biology.epidemiology import RecoveryInteraction
>>> from skcomplex.engine.solvers import GillespieSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(
... System(
... elements=[CompartmentElement('I', n=10), CompartmentElement('R', n=0)],
... interactions=[RecoveryInteraction('rec', infected='I', recovered='R', gamma=1.0, stochastic=True)],
... ),
... )
>>> stepper = GillespieSolver(random_state=0).check().prepare(problem)
>>> stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 100.0)).state_.tolist()
[0.0, 10.0]
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
GradientDescentSolver(precision='float64', *, random_state=None, until=math.inf)
Bases: BaseSolver[LearningProblemSpec]
Solver of the learning form, which trains the parameters of a nested system by gradient steps on JAX and Optax.
Every unit of time is one epoch. The solver shuffles the sequences of the dataset with its random number generator,
splits them into batches, and for every batch unrolls the nested system from its initial states over each
sequence, writing the values of the dataset into its parameters before every inner step. A discrete map unrolls
with jax.lax.scan, and ordinary differential equations with one diffrax.diffeqsolve per inner step, with
Tsit5, a PID step size controller, and recursive checkpointing. The loss is the weighted mean of the loss
observable over the steps and sequences, its gradient comes from jax.value_and_grad, and an Optax optimizer
updates the trained parameters. Truncated backpropagation through time stops the gradient of the carried inner
states every truncation steps. The optimizer reads its hyperparameters from the parameter vector before every
frame, so a schedule takes effect at the next frame, and its state persists across frames until the solver is
prepared again. The solver records the mean batch loss of every epoch and every metric over the whole dataset at the
end of the epoch as reporters <interaction path>/loss and <interaction path>/<metric>.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
precision
|
str
|
|
'float64'
|
random_state
|
int | Generator | None
|
The seed or generator of the shuffling of the sequences. |
None
|
until
|
float
|
The end time of the run the solver prepares for. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
theta_ |
TracedArray
|
The trained parameters after the last epoch, as a JAX array. |
optimizer_state_ |
OptState
|
The state of the Optax optimizer, such as the moments of Adam. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
The precision is unknown. |
Examples:
>>> from skcomplex.engine.solvers import GradientDescentSolver
>>> GradientDescentSolver(precision='float32').check().precision
'float32'
Source code in src/skcomplex/engine/solvers/_gradient_descent.py
78 79 80 81 82 83 84 85 86 | |
GrunwaldLetnikovSolver(dt=0.01, memory=None, *, random_state=None, until=math.inf)
Bases: ConvolutionSolver
Explicit Grünwald-Letnikov scheme of Caputo fractional differential equations, of order 1.
A step sets dt^alpha f(t_(k-1), x_(k-1)) = sum_(j=0)^k w_j (x_(k-j) - x_0) with the weights
w_j = (-1)^j binomial(alpha, j). States of order 1 take explicit Euler steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed step. Frames must fall on multiples of it. |
0.01
|
memory
|
float | None
|
The length of time over which the history enters with its exact weights, or |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
orders_ |
The order of every state component. |
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import RateLaw
>>> from skcomplex.specs import StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.engine.solvers import GrunwaldLetnikovSolver
>>> from skcomplex.specs import StepSpec
>>> class RelaxingElement(Element):
... def __init__(self, label, x: Annotated[float, StateSpec(order=0.5)] = 1.0):
... super().__init__(label)
... self.x = x
...
... def _get_laws(self, s):
... return [RateLaw(s.x, -s.x)]
>>> problem = compile_system(System(elements=[RelaxingElement('a')]))
>>> stepper = GrunwaldLetnikovSolver(dt=0.001, random_state=0).check().prepare(problem)
>>> step = StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)
>>> round(float(stepper.run(step).state_[0]), 2)
0.43
Source code in src/skcomplex/engine/solvers/_convolution.py
32 33 34 35 36 37 38 39 40 41 42 43 | |
HeunSolver(dt=0.001, *, random_state=None, until=math.inf)
Bases: SDESolver
Predictor-corrector scheme for SDEs that averages the drift over the step and keeps the Itô noise.
The predictor X~ = X + f(X) h + g(X) dW gives the corrector X + (f(X) + f(X~)) h / 2 + g(X) dW, which has strong
order 1 for additive noise and integrates the Itô form for any noise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. |
0.001
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.models.economics.finance import geometric_brownian_motion
>>> from skcomplex.engine.solvers import HeunSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(geometric_brownian_motion())
>>> stepper = HeunSolver(dt=0.01, random_state=0).check().prepare(problem)
>>> bool(stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_[0] > 0)
True
Source code in src/skcomplex/engine/solvers/_sde.py
40 41 42 43 44 45 46 47 48 49 | |
HorizonError
Bases: SolverError
A stepper with a fixed horizon is asked to advance beyond it.
IMEX2Solver(dt=0.01, *, random_state=None, until=math.inf)
Bases: SplitSolver
Crank-Nicolson Adams-Bashforth scheme of second order: trapezoidal linear part and extrapolated remainder.
Every step solves (I - h L / 2) x_{n+1} = (I + h L / 2) x_n + h (3 N_n - N_{n-1}) / 2, starting with one
Crank-Nicolson step with an Euler remainder. A call that does not continue the time, state, and step of the
previous call starts the scheme again.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. |
0.01
|
Examples:
>>> from skcomplex.engine.solvers import IMEX2Solver
>>> IMEX2Solver(dt=0.1).dt
0.1
Source code in src/skcomplex/engine/solvers/_split.py
84 85 86 87 88 89 90 91 92 93 | |
IMEXEulerSolver(dt=0.01, *, random_state=None, until=math.inf)
Bases: SplitSolver
Implicit-explicit Euler: the linear part implicit and the nonlinear remainder explicit, of first order.
Every step solves (I - h L) x_{n+1} = x_n + h N(x_n), with sparse solves for finite differences and finite
elements and division per Fourier mode for spectral fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. |
0.01
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.models.physics.heat import heat_diffusion
>>> from skcomplex.engine.solvers import IMEXEulerSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(heat_diffusion((16, 16)))
>>> stepper = IMEXEulerSolver(dt=1.0, random_state=0).check().prepare(problem)
>>> state = stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 10.0)).state_
>>> bool(np.isclose(state.sum(), problem.initial_state.sum()))
True
Source code in src/skcomplex/engine/solvers/_split.py
84 85 86 87 88 89 90 91 92 93 | |
IterationSolver(*, random_state=None, until=math.inf)
Bases: Solver
Iteration of a discrete map, one application per unit of time.
A stochastic map takes its random draws from the stepper's random number generator at every step.
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.system import System
>>> from skcomplex.models.economics.markets import CobwebInteraction, MarketElement
>>> from skcomplex.engine.solvers import IterationSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(
... System(
... elements=[MarketElement('m', price=1)],
... interactions=[CobwebInteraction('c', target='m', a=10, b=2, c=1, d=1)],
... ),
... )
>>> stepper = IterationSolver(random_state=0).check().prepare(problem)
>>> stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_.tolist()
[4.0]
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
JumpTrajectoriesSolver(atol=1e-12, rtol=1e-10, nsteps=100000, max_dimension=DEFAULT_MAX_DIMENSION, *, random_state=None, until=math.inf)
Bases: OperatorSolver
Solver that samples the master equation by jumps of a pure state with QuTiP's MCSolver.
Every run is one trajectory, whose jumps are drawn from the simulation's random number generator, so a seeded run is reproducible. A problem stored as a density matrix records the projector on the pure state of every frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atol
|
float
|
The absolute tolerance of the integrator. |
1e-12
|
rtol
|
float
|
The relative tolerance of the integrator. |
1e-10
|
nsteps
|
int
|
The largest number of internal steps between two frames. |
100000
|
Raises:
| Type | Description |
|---|---|
SolverError
|
The initial state is a mixed density matrix. |
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from sage.all__sagemath_symbolics import CC, SR
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import DissipatorLaw, GeneratorLaw
>>> from skcomplex.core.laws.utils import local_operator
>>> from skcomplex.specs import HilbertSpec, ObservableSpec, StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import JumpTrajectoriesSolver
>>> operators = {'x': (np.array([[0, 1], [1, 0]]), True), 'z': (np.diag([1, -1]), True)}
>>> operators['minus'] = (np.array([[0, 0], [1, 0]]), False)
>>> QUBIT = HilbertSpec(2, operators, basis={'up': 0, 'down': 1})
>>> class TwoLevelElement(Element):
... def __init__(self, label, psi: Annotated[object, StateSpec(domain=CC**2)] = 'up', hilbert=QUBIT):
... super().__init__(label)
... self.psi = psi
... self.hilbert = hilbert
...
... def _get_laws(self, s):
... return [DissipatorLaw(local_operator(s.psi, SR.symbol('minus')), 1.0)]
>>> z = [ObservableSpec('z', element='q', variable='z')]
>>> system = System(elements=[TwoLevelElement('q')])
>>> solver = JumpTrajectoriesSolver()
>>> from skcomplex.engine.compile import OperatorCompiler
>>> simulation = Simulation(
... system, compiler=OperatorCompiler(), solver=solver, dt=1.0, random_state=0, observables=z,
... ).check().prepare().run(until=20.0)
>>> float(simulation.trajectory_.values('q.z')[-1])
-1.0
Source code in src/skcomplex/engine/solvers/_operator.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
KineticSolver(n=10000, dt=0.01, *, random_state=None, until=math.inf)
Bases: Solver
Direct simulation Monte Carlo of encounter laws with finite particles on activity grids.
Every density is held as counts of particles of one common mass, chosen so that the largest initial density has n
particles. Every step of length at most dt applies the encounter laws in turn. A candidate particle at activity
h encounters with probability 1 - exp(-lambda_h dt), where lambda_h = sum_k eta[h, k] g_k over the field
density g, and picks its field activity and outcome in proportion to eta[h, k] A[h, k, i] g_k. A proliferation
adds a Poisson number of particles and a destruction removes a binomial number. The state holds the empirical
densities, so the trajectories match those of the gain and loss equations, which the particles approach as n
grows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of particles of the largest initial density. |
10000
|
dt
|
float
|
The largest time step. |
0.01
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Examples:
>>> from skcomplex.engine.solvers import KineticSolver
>>> from skcomplex.core.laws import BilinearLaw, EncounterLaw
>>> KineticSolver(n=1000)._get_support().laws == {EncounterLaw, BilinearLaw}
True
Source code in src/skcomplex/engine/solvers/_kinetics.py
67 68 69 70 71 72 73 74 75 76 77 78 | |
L1Solver(dt=0.01, memory=None, *, random_state=None, until=math.inf)
Bases: ConvolutionSolver
Explicit L1 scheme of Caputo fractional differential equations, of order 1.
The L1 scheme interpolates the state linearly between grid points inside the Caputo integral. A step sets
Gamma(2 - alpha) dt^alpha f(t_(k-1), x_(k-1)) = sum_(j=0)^(k-1) b_j (x_(k-j) - x_(k-j-1)) with the weights
b_j = (j + 1)^(1 - alpha) - j^(1 - alpha). States of order 1 take explicit Euler steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed step. Frames must fall on multiples of it. |
0.01
|
memory
|
float | None
|
The length of time over which the history enters with its exact weights, or |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
orders_ |
The order of every state component. |
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import RateLaw
>>> from skcomplex.specs import StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.engine.solvers import L1Solver
>>> from skcomplex.specs import StepSpec
>>> class RelaxingElement(Element):
... def __init__(self, label, x: Annotated[float, StateSpec(order=0.5)] = 1.0):
... super().__init__(label)
... self.x = x
...
... def _get_laws(self, s):
... return [RateLaw(s.x, -s.x)]
>>> problem = compile_system(System(elements=[RelaxingElement('a')]))
>>> stepper = L1Solver(dt=0.001, random_state=0).check().prepare(problem)
>>> step = StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)
>>> round(float(stepper.run(step).state_[0]), 2)
0.43
Source code in src/skcomplex/engine/solvers/_convolution.py
32 33 34 35 36 37 38 39 40 41 42 43 | |
LammpsFixSpec(name, group, value, mass, seed)
dataclass
Bases: BaseSpec
Values the native fix of an interaction reads when the LAMMPS backend writes its command.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The unique name of the fix. |
required |
group
|
str
|
The LAMMPS group of the atoms the fix acts on. |
required |
value
|
Callable[[str], Array]
|
The function that returns the current value of a parameter of the interaction by name. |
required |
mass
|
Callable[[], float]
|
The function that returns the mass of the atoms of the interaction's target. |
required |
seed
|
Callable[[], int]
|
The function that draws a random seed. |
required |
LammpsSolver(dt=0.005, pairs=None, fixes=None, *, random_state=None, until=math.inf)
Bases: ExternalSolver
Solver that runs systems of points in LAMMPS, with LAMMPS velocity Verlet integration.
An element or group whose members declare a vector position state of order 2, such as PointElement, becomes an
atom type, whose atoms move with the first derivative of the position as their velocity. The mass of an atom type
is the parameter that the divisor of the interactions on its members names, and 1 without a divisor. Boxes become
LAMMPS boundaries and fix wall/reflect. A PairCouplingInteraction with a potential becomes the native pair
style pairs gives for its class, or a Lepton expression potential otherwise. An interaction whose class fixes
gives a native fix becomes that fix. Any other interaction that only changes the derivatives of the positions runs
through fix external, evaluated by the framework.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest LAMMPS time step. |
0.005
|
pairs
|
Mapping[type[BaseInteraction], tuple[str, PairArguments]] | None
|
The native LAMMPS pair style of pair potential classes, with the function of the parameter values that
returns the coefficients of |
None
|
fixes
|
Mapping[type[BaseInteraction], FixCommand] | None
|
The function that returns the native LAMMPS fix command of interaction classes from the values of the fix. |
None
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
lammps_ |
The LAMMPS instance, which the final run |
|
atom_elements_ |
The elements and groups of points, each a LAMMPS atom type. |
|
atom_numbers_ |
The LAMMPS atom type of every element and group of points, by path. |
|
position_indices_ |
The flat state indices of the position of every atom, of shape |
|
velocity_indices_ |
The flat state indices of the velocity of every atom, of shape |
|
atom_types_ |
The LAMMPS atom type of every atom. |
|
n_dim_ |
The number of dimensions of the space of the atoms. |
|
released_ |
Whether the LAMMPS instance is closed. |
Examples:
>>> from skcomplex.core.elements import GroupElement, PointElement
>>> from skcomplex.core.interactions import PairCouplingInteraction
>>> from skcomplex.core.spaces import ContinuousSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import LammpsSolver
>>> potential = lambda namespace, r: 4 * (r**-12 - r**-6)
>>> system = System(
... elements=[
... GroupElement('gas', element=PointElement, size=2, values={'x': [[0.0, 0.0], [1.5, 0.0]]}),
... ],
... interactions=[PairCouplingInteraction('lj', 'gas', 'gas', cutoff=2.5, potential=potential)],
... space=ContinuousSpace(n_dim=2),
... )
>>> solver = LammpsSolver(dt=0.005)
>>> trajectory = Simulation(system, solver=solver, dt=0.1).check().prepare().run(until=0.5).trajectory_
>>> positions = trajectory.values('gas.x')
>>> bool(positions[-1, 1, 0] - positions[-1, 0, 0] < 1.5)
True
Source code in src/skcomplex/engine/solvers/_lammps.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
LatticeSolver(*, random_state=None, until=math.inf)
Bases: Solver
Solver of lattice dynamics that applies every local rule, drive, cascade, and extremal law once per step.
Steps apply in the order the laws are declared, and the boundary laws apply after every step. Cascades record their size, duration, and area as events, and extremal laws record the site and the value of every application.
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.models.computer_science.automata import elementary_automaton
>>> from skcomplex.engine.solvers import LatticeSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(elementary_automaton(30, size=11))
>>> stepper = LatticeSolver(random_state=0).check().prepare(problem)
>>> step = StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)
>>> stepper.run(step).state_.astype(int).tolist()
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0]
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
LindbladSolver(atol=1e-12, rtol=1e-10, nsteps=100000, max_dimension=DEFAULT_MAX_DIMENSION, *, random_state=None, until=math.inf)
Bases: OperatorSolver
Solver of the Lindblad master equation with QuTiP's MESolver, and of pure closed systems with SESolver.
It is the default solver of the operator form. A system without dissipators that starts in a pure state follows the
linear evolution dx/dt = -i H x, which is the master equation without dissipators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atol
|
float
|
The absolute tolerance of the integrator. |
1e-12
|
rtol
|
float
|
The relative tolerance of the integrator. |
1e-10
|
nsteps
|
int
|
The largest number of internal steps between two frames. |
100000
|
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from sage.all__sagemath_symbolics import CC, SR
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import DissipatorLaw, GeneratorLaw
>>> from skcomplex.core.laws.utils import local_operator
>>> from skcomplex.specs import HilbertSpec, ObservableSpec, StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import LindbladSolver
>>> operators = {'x': (np.array([[0, 1], [1, 0]]), True), 'z': (np.diag([1, -1]), True)}
>>> operators['minus'] = (np.array([[0, 0], [1, 0]]), False)
>>> QUBIT = HilbertSpec(2, operators, basis={'up': 0, 'down': 1})
>>> class TwoLevelElement(Element):
... def __init__(self, label, psi: Annotated[object, StateSpec(domain=CC**2)] = 'up', hilbert=QUBIT):
... super().__init__(label)
... self.psi = psi
... self.hilbert = hilbert
...
... def _get_laws(self, s):
... return [DissipatorLaw(local_operator(s.psi, SR.symbol('minus')), 0.5)]
>>> z = [ObservableSpec('z', element='q', variable='z')]
>>> system = System(elements=[TwoLevelElement('q')])
>>> from skcomplex.engine.compile import OperatorCompiler
>>> compiler = OperatorCompiler()
>>> simulation = Simulation(system, compiler=compiler, solver=LindbladSolver(), dt=1.0, observables=z)
>>> simulation = simulation.check().prepare()
>>> values = simulation.run(until=2.0).trajectory_.values('q.z')
>>> bool(abs((1 + values[-1]) / 2 - np.exp(-1.0)) < 1e-6)
True
Source code in src/skcomplex/engine/solvers/_operator.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
MesaSolver(*, random_state=None, until=math.inf)
Bases: ExternalSolver
Mesa backend of agent-based problems, one Mesa step per unit of time.
The agents are the members of the elements and groups whose laws include RuleLaws, and of the variable groups.
Every step advances Mesa's clock by one, activates all agents in random order, or by the phases of the system's
discrete time, and each agent runs the rules of its element type in declaration order. Interaction rules run once
after the agents. Rules add and remove members of variable groups only. The class variable supports declares
what the backend runs, which validation checks systems against: discrete time, no space or a lattice, network, or
continuous space, rule, boundary, transfer, and matching laws, and elements and fixed and variable
groups with any domain but Hilbert spaces.
Attributes:
| Name | Type | Description |
|---|---|---|
model_ |
The Mesa model. |
|
members_ |
Every agent created in the run by its id, which reference states name. |
Examples:
>>> from typing import Annotated
>>> from skcomplex.core.elements import Element, GroupElement
>>> from skcomplex.core.laws import RuleLaw
>>> from skcomplex.specs import StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import MesaSolver
>>> class CounterElement(Element):
... def __init__(self, label, count: Annotated[float, StateSpec()] = 0.0):
... super().__init__(label)
... self.count = count
...
... def _get_laws(self, namespace):
... return [RuleLaw(self.tick)]
...
... def tick(self, agent, model):
... agent.count += 1
>>> system = System(elements=[GroupElement('counters', element=CounterElement, size=3)])
>>> trajectory = Simulation(system, solver=MesaSolver()).check().prepare().run(until=4).trajectory_
>>> trajectory.agents('counters', 4).values['count'].tolist()
[4.0, 4.0, 4.0]
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
MilsteinSolver(dt=0.001, *, random_state=None, until=math.inf)
Bases: SDESolver
Derivative-free Milstein scheme of Platen for SDEs with diagonal noise, of strong order 1.
It takes a supporting value Y = X + f h + g sqrt(h) and corrects the noise with
(g(Y) - g(X)) (dW^2 - h) / (2 sqrt(h)), which needs no derivative of the noise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The fixed internal step. |
0.001
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.models.economics.finance import geometric_brownian_motion
>>> from skcomplex.engine.solvers import MilsteinSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(geometric_brownian_motion())
>>> stepper = MilsteinSolver(dt=0.01, random_state=0).check().prepare(problem)
>>> bool(stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_[0] > 0)
True
Source code in src/skcomplex/engine/solvers/_sde.py
40 41 42 43 44 45 46 47 48 49 | |
ModelView(stepper, owner='', roles=None)
Model as rules see it: parameters, random numbers, time, agents, and the space.
Every rule receives the view scoped to the interaction or agent group that owns it, so the rule reads the owner's parameters by name and the groups bound to the owner's roles by role, also when its system is nested in another.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stepper
|
MesaSolver
|
The stepper that runs the model. |
required |
owner
|
str
|
The path of the interaction or agent group whose rules receive the view, or |
''
|
roles
|
Mapping[str, str | tuple[str, ...]] | None
|
The paths of the elements and groups the owner's roles bind to, by role. |
None
|
Source code in src/skcomplex/engine/solvers/_mesa.py
67 68 69 70 71 72 73 74 75 | |
dimensions
property
Extent of the agent space per axis, cells of a grid or lengths of a continuous space, None otherwise.
inputs
property
Values of the states of other partitions at the last synchronisation point, by dotted name.
parameters
property
Current values of every parameter by dotted name.
rng
property
Random number generator of the model, seeded by the simulation's random state.
round
property
Index of the current round of a repeated phase, 0 outside repeated phases.
storage
property
Mutable mapping private to the owner that persists across the steps of one run, such as an order book.
time
property
Time of Mesa's clock, k during the step that produces frame k, plus i / n at visit i of n.
torus
property
Whether the agent space wraps around at its edges.
agents(name, view=None)
agents(name: str) -> list[AgentView]
agents(name: str, view: type[V]) -> list[V]
Return the views of the agents of a group bound to a role of the owner, or of a group by path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A role of the owner, the path of a group relative to the owner's system, or the path of a group. |
required |
view
|
type[V] | None
|
The view class of the group, which its element names with |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
agents |
list[V] | list[AgentView]
|
The views of the agents alive in the group, or in every group of a role that binds several. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
No role or group has that name, or the views of a group are not of the view class. |
Source code in src/skcomplex/engine/solvers/_mesa.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
composition(name, state)
Return the number of agents of a group that hold every value of a state, for the values some agent holds.
The stepper keeps the counts as agents are created, removed, and change the state, so reading them costs no pass over the agents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A role of the owner, the path of a group relative to the owner's system, or the path of a group. |
required |
state
|
str
|
The name of a scalar state of the agents of the group. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
dict[Hashable, int]
|
The number of agents alive in the group, or in every group of a role that binds several, by value. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
No role or group has that name, or its agents have no such state. |
Source code in src/skcomplex/engine/solvers/_mesa.py
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | |
count(name)
Return the number of agents of a group bound to a role of the owner, or of a group by path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A role of the owner, the path of a group relative to the owner's system, or the path of a group. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
count |
int
|
The number of agents alive in the group, or in every group of a role that binds several. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
No role or group has that name. |
Source code in src/skcomplex/engine/solvers/_mesa.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
create(group, location=None, **values)
Create an agent of a group, at a location or at a random place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group
|
str
|
A role of the owner that binds the group, or the path of the group. |
required |
location
|
object
|
The cell coordinates, network node, or position, or |
None
|
**values
|
object
|
The initial values of states, with the element type's defaults for the others. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
agent |
AgentView
|
The new agent. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
The group is not variable, so its size cannot change. |
Source code in src/skcomplex/engine/solvers/_mesa.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | |
deposit(name, cell, amount)
Add an amount to a field at a cell at the next synchronisation point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The dotted path of the field state, such as |
required |
cell
|
object
|
The lattice coordinates of the cell or a network node. |
required |
amount
|
float
|
The amount to add. |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
The name is not a state of a field. |
Source code in src/skcomplex/engine/solvers/_mesa.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | |
field(name, cell)
Return the value of a field at a cell, as it was at the last synchronisation point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The dotted path of the field state, such as |
required |
cell
|
object
|
The lattice coordinates of the cell, which match the coordinates of a grid of the same shape, or a network node. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
value |
float
|
The value of the field there. |
Raises:
| Type | Description |
|---|---|
KeyError
|
The name is not a state of a field. |
Source code in src/skcomplex/engine/solvers/_mesa.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | |
groups(name)
Return the paths of the groups a role of the owner binds, or of a group by relative or absolute path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A role of the owner, the path of a group relative to the owner's system, or the path of a group. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
paths |
list[str]
|
The paths of the groups. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
No role or group has that name. |
Source code in src/skcomplex/engine/solvers/_mesa.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
history(name, window=None)
Return the past values of a reporter, or of an observable at the end of each earlier step, the last one last.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of a value the owner reports, the full name of any reporter, such as |
required |
window
|
int | None
|
The number of last values, or |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
values |
NDArray[float64]
|
The values, at most |
Source code in src/skcomplex/engine/solvers/_mesa.py
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | |
observable(name)
Return the current value of an observable of the system, such as an aggregate of a group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The path of the observable. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
value |
object
|
Its value for the current agents and parameters. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
The system has no such observable. |
Source code in src/skcomplex/engine/solvers/_mesa.py
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 | |
occupancy(name)
Return the number of agents of a group in every cell or node that holds one, by the key of the location.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A role of the owner, the path of a group relative to the owner's system, or the path of a group. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
dict[Hashable, int]
|
The number of agents alive in the group, or in every group of a role that binds several, by the
|
Raises:
| Type | Description |
|---|---|
SimulationError
|
No role or group has that name. |
Source code in src/skcomplex/engine/solvers/_mesa.py
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | |
parameter(name)
Return the current value of a parameter of the owner, or of one component as name[i].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of a scalar parameter, such as |
required |
Returns:
| Name | Type | Description |
|---|---|---|
value |
float
|
The value. |
Raises:
| Type | Description |
|---|---|
SimulationError
|
The owner has no such parameter, or the parameter has several components and no index. |
Source code in src/skcomplex/engine/solvers/_mesa.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
random_empty_cell()
Return the coordinates of a random empty cell of the grid or network.
Returns:
| Name | Type | Description |
|---|---|---|
location |
object
|
The cell coordinates or the network node. |
Source code in src/skcomplex/engine/solvers/_mesa.py
357 358 359 360 361 362 363 364 | |
report(name, value)
Publish a value of the owner as the reporter <owner>.<name> and add it to the reporter's history.
The owner declares the names it reports in its class variable reports, so every frame records them, NaN
before the first report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the value, such as |
required |
value
|
float
|
The value. |
required |
Raises:
| Type | Description |
|---|---|
SimulationError
|
The owner does not declare the name in |
Source code in src/skcomplex/engine/solvers/_mesa.py
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
set_parameter(name, value)
Set a parameter of any group or interaction during the run, recorded in the trajectory like any parameter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The dotted name of the parameter, such as |
required |
value
|
float
|
The new value. |
required |
Raises:
| Type | Description |
|---|---|
SimulationError
|
No parameter has that name. |
Source code in src/skcomplex/engine/solvers/_mesa.py
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |
MonteCarloSolver(*, random_state=None, until=math.inf)
Bases: LatticeSolver
Solver of Monte Carlo sampling that sweeps the lattice once per unit of time.
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.models.physics.statistical_mechanics import ising
>>> from skcomplex.engine.solvers import MonteCarloSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(ising(size=8, temperature=1.0))
>>> stepper = MonteCarloSolver(random_state=0).check().prepare(problem)
>>> state = stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 50.0)).state_
>>> bool(abs(state.mean()) > 0.9)
True
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
OperatorSolver(atol=1e-12, rtol=1e-10, nsteps=100000, max_dimension=DEFAULT_MAX_DIMENSION, *, random_state=None, until=math.inf)
Bases: BaseSolver[OperatorProblemSpec]
Base class of solvers that advance operator problems with a stepwise QuTiP solver.
The solver is created at the first frame and again whenever the parameters change, from the current state, so a parameter update or a schedule applies from the next frame on. Coefficients that depend on time stay functions of time, and the others are folded into constant matrices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atol
|
float
|
The absolute tolerance of the integrator. |
1e-12
|
rtol
|
float
|
The relative tolerance of the integrator. |
1e-10
|
nsteps
|
int
|
The largest number of internal steps between two frames. |
100000
|
max_dimension
|
int
|
The largest dimension of the Hilbert space the solver accepts, which compiling checks. |
DEFAULT_MAX_DIMENSION
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Source code in src/skcomplex/engine/solvers/_operator.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
OptimalControlSolver(*, random_state=None, until=math.inf)
Bases: Solver
Base class of solvers that compute the controls minimising or maximising an objective over a run of a system.
The solver reads the bounded adaptive parameters of the system as its controls. Its objective is the running cost
objective integrated over the run plus the terminal cost terminal at the horizon, the time until of the run.
Each cost is a function of the namespace s of the states, parameters, and time of the system by dotted name, such
as lambda s: s.plant.x**2 + s.plant.u**2, or a number. The solver minimises the objective for the sense 'min'
and maximises it for 'max'. It solves the problem over the whole run from the initial state and the initial
parameters at the start of the run, and then follows the solution frame by frame. Subclasses take objective,
terminal, and sense as arguments, implement _solve, and may implement _prepare.
Attributes:
| Name | Type | Description |
|---|---|---|
control_problem_ |
The optimal control problem of the compiled system. |
|
horizon_ |
The end of the run, the horizon of the objective. |
|
solution_ |
The optimal solution of the run. |
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
PontryaginSolver(objective=None, *, terminal=0.0, sense='min', nodes=201, relaxation=0.5, tolerance=1e-06, max_iterations=1000, random_state=None, until=math.inf)
Bases: OptimalControlSolver
Optimal control by Pontryagin's principle, with costates and the stationarity condition derived symbolically.
The solver builds the Hamiltonian H = L + λ · f from the running cost L and the rates f of the states, and
derives the costate equations λ' = -∂H/∂x with λ(T) = ∂φ/∂x at the horizon T for the terminal cost φ. The
controls solve ∂H/∂u = 0, projected onto their bounds, or take the bound that minimises H where H is linear
in them. A forward-backward sweep integrates the states forward and the costates backward with fourth-order
Runge-Kutta steps on a grid, and relaxes the controls towards their update until they converge. The weight of the
update halves whenever the distance of the controls to their update stops falling, which breaks the cycles of the
sweep. An objective to maximise is minimised with its sign changed. The solution holds the costates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
objective
|
Cost | None
|
The running cost, a function of the namespace |
None
|
terminal
|
Cost
|
The terminal cost at the horizon, a function of the same namespace or a number. |
0.0
|
sense
|
str
|
|
'min'
|
nodes
|
int
|
The number of times of the grid, from the start to the horizon. |
201
|
relaxation
|
float
|
The initial weight of the update of the controls at every sweep, between 0 and 1. |
0.5
|
tolerance
|
float
|
The largest relative change of the states, costates, and controls between sweeps at convergence. |
1e-06
|
max_iterations
|
int
|
The largest number of sweeps. |
1000
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
solution_ |
The optimal solution of the run. |
Raises:
| Type | Description |
|---|---|
SolverError
|
The solver has no objective, the sweep diverges or does not converge, or the stationarity condition has no unique explicit solution. |
ValidationError
|
The sense is unknown, the grid has fewer than 2 nodes, or the relaxation is not in |
Examples:
>>> from typing import Annotated
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import RateLaw
>>> from skcomplex.specs import ParameterSpec, StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import PontryaginSolver
>>> class PlantElement(Element):
... def __init__(
... self,
... label,
... x: Annotated[float, StateSpec()] = 1.0,
... u: Annotated[float, ParameterSpec(adaptive=True, bounds=(-10, 10))] = 0.0,
... ):
... super().__init__(label)
... self.x = x
... self.u = u
...
... def _get_laws(self, s):
... return [RateLaw(s.x, s.u)]
>>> solver = PontryaginSolver(objective=lambda s: (s.plant.x**2 + s.plant.u**2) / 2)
>>> simulation = Simulation(System(elements=[PlantElement('plant')]), solver=solver)
>>> simulation = simulation.check().prepare().run(until=1.0)
>>> float(round(simulation.trajectory_.values('plant.u')[0], 3))
-0.762
Source code in src/skcomplex/engine/solvers/_pontryagin.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
RK4Solver(dt=0.001, *, random_state=None, until=math.inf)
Bases: Solver
Classical fourth-order Runge-Kutta integrator of ordinary differential equations with a fixed internal step.
With the Numba backend every internal step of a frame runs in compiled code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.001
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.system import System
>>> from skcomplex.models.biology.common.populations import ExponentialGrowthInteraction, PopulationElement
>>> from skcomplex.engine.solvers import RK4Solver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(
... System(
... elements=[PopulationElement('a', n=1)],
... interactions=[ExponentialGrowthInteraction('g', target='a', r=1)],
... ),
... )
>>> stepper = RK4Solver(dt=0.001, random_state=0).check().prepare(problem)
>>> step = StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)
>>> round(float(stepper.run(step).state_[0]), 10)
2.7182818285
Source code in src/skcomplex/engine/solvers/_rk4.py
97 98 99 100 101 102 103 104 105 106 | |
RenewalSolver(*, random_state=None, until=math.inf)
Bases: Solver
Exact stepper of renewal processes, which jumps from renewal to renewal without a time grid.
Every member renews on its own clock, and a frame holds the states the last renewal at or before its time set.
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.specs import StateSpec
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import JumpLaw
>>> from skcomplex.core.laws.utils import draw
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.solvers import RenewalSolver
>>> from skcomplex.specs import StepSpec
>>> class CounterElement(Element):
... def __init__(self, label, n: Annotated[float, StateSpec()] = 0.0):
... super().__init__(label)
... self.n = n
...
... def _get_laws(self, s):
... return [JumpLaw(waiting=draw('wait', 'expon'), reset={s.n: s.n + 1})]
>>> problem = compile_system(System(elements=[GroupElement('counters', element=CounterElement, size=3)]))
>>> stepper = RenewalSolver(random_state=0).check().prepare(problem)
>>> state = stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 100.0)).state_
>>> bool(np.all(state[:3] > 50))
True
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
RootSolver(*, method='hybr', tolerance=1e-09, random_state=None, until=np.inf)
Bases: Solver
Solver of the equations of a static system, which it solves once for the one frame of the run.
The compiled solution gives the states when Sage finds a unique explicit one. Otherwise scipy.optimize.root solves
the residuals of the equations with their exact Jacobian, from the initial state as the guess. A system without
equations solves to its initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
The method of |
'hybr'
|
tolerance
|
float
|
The largest residual of a solution. |
1e-09
|
random_state
|
int | Generator | None
|
The seed or generator of the solver, which draws nothing. |
None
|
until
|
float
|
The end time of the run the solver prepares for, which a static system does not read. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
initial_state_ |
Array
|
The solved state vector, the one frame of the run. |
Examples:
>>> from sage.all__sagemath_symbolics import cos
>>> from typing import Annotated
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import EquationLaw
>>> from skcomplex.core.spaces import StaticTime
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.specs import StateSpec
>>> class FixedPointElement(Element):
... def __init__(self, label, x: Annotated[float, StateSpec()] = 1.0):
... super().__init__(label)
... self.x = x
...
... def _get_laws(self, s):
... return [EquationLaw(s.x == cos(s.x), target=s.x)]
>>> system = System(elements=[FixedPointElement('a')], time=StaticTime())
>>> trajectory = Simulation(system).check().prepare().run(1.0).trajectory_
>>> round(float(trajectory.values('a.x')[0]), 7)
0.7390851
Source code in src/skcomplex/engine/solvers/_root.py
65 66 67 68 69 70 71 72 73 74 75 | |
SDESolver(dt=0.001, *, random_state=None, until=math.inf)
Bases: Solver
Base class of fixed-step schemes of stochastic differential equations, which take one increment per step.
Subclasses set supports and implement _increment. The steps stop at breakpoints, read delays by linear
interpolation of the steps, and locate events in them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.001
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Source code in src/skcomplex/engine/solvers/_sde.py
40 41 42 43 44 45 46 47 48 49 | |
SSPRKSolver(order=3, cfl=0.4, dt=0.01, *, random_state=None, until=math.inf)
Bases: Solver
Strong-stability-preserving Runge-Kutta of order 2 or 3, with steps from the CFL condition of conservation laws.
The schemes of Shu and Osher are convex combinations of Euler steps, so they keep the total variation bound of a
limited finite-volume scheme. With finite-volume fields the step is the smaller of cfl * h / s, for the cell size
h and the largest wave speed s, and the stability limit of the Laplacian rates at the current parameters, and
otherwise dt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
order
|
int
|
2 or 3. |
3
|
cfl
|
float
|
The Courant number. |
0.4
|
dt
|
float
|
The largest step, and the step without conservation laws. |
0.01
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.models.physics.fluids import burgers
>>> from skcomplex.engine.solvers import SSPRKSolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(burgers(64))
>>> stepper = SSPRKSolver(random_state=0).check().prepare(problem)
>>> state = stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_
>>> bool(np.isclose(state.sum(), problem.initial_state.sum()))
True
Source code in src/skcomplex/engine/solvers/_ssprk.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
ScipyODESolver(method='auto', rtol=1e-06, atol=1e-09, jacobian='auto', *, random_state=None, until=math.inf)
Bases: Solver
Integrator of ordinary differential equations based on the step solvers of scipy.integrate.
One integrator runs across the frames of a run, and every frame reads its state from the dense output of the step that covers the frame's time, and it restarts at every frame when a condition on time jumps at a time that depends on parameters. With delays, steps are at most the shortest delay, or a hundredth of the frame when the delay is shorter, and a delay shorter than the step reads the extension of the previous step.
The implicit methods BDF, Radau, and LSODA receive the structure of the Jacobian that the compiler derives
from the laws, at every start and restart of the integrator. BDF and Radau estimate the Jacobian by finite
differences over groups of independent columns and factor it with sparse LU, which makes them the stiff path for
finite-difference and finite element fields. An explicit method that spans more than 1000 stable steps of the
diffusion of a field in one call warns once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
The integration method: |
'auto'
|
rtol
|
float
|
The relative tolerance. |
1e-06
|
atol
|
float
|
The absolute tolerance. |
1e-09
|
jacobian
|
str
|
How implicit methods obtain the Jacobian. |
'auto'
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
method_ |
The method the stepper integrates with, which |
|
jacobian_pattern_ |
The sparsity pattern of the Jacobian that an implicit method uses, or |
|
jacobian_options_ |
The keyword arguments of the pattern or the half bandwidths that the integrator receives. The exact
Jacobian is passed as |
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.system import System
>>> from skcomplex.models.biology.common.populations import ExponentialGrowthInteraction, PopulationElement
>>> from skcomplex.engine.solvers import ScipyODESolver
>>> from skcomplex.specs import StepSpec
>>> problem = compile_system(
... System(
... elements=[PopulationElement('a', n=1)],
... interactions=[ExponentialGrowthInteraction('g', target='a', r=1)],
... ),
... )
>>> stepper = ScipyODESolver(rtol=1e-8, random_state=0).check().prepare(problem)
>>> x = stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_
>>> round(float(x[0]), 4)
2.7183
Source code in src/skcomplex/engine/solvers/_scipy_ode.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | |
SemiLagrangianSolver(dt=0.01, *, random_state=None, until=math.inf)
Bases: Solver
Directional splitting of Cheng and Knorr, which shifts every slice of a field along each axis in turn.
Every flux of the field must be a linear drift a u, with a free of the state and constant along its own axis. A
shift along a periodic axis multiplies the Fourier modes of the slices through scipy.fft, which conserves the
total, and a shift along an interval evaluates a cubic spline through scipy.ndimage.map_coordinates. The solver is
stable at any Courant number. Rate laws advance by one midpoint step between two half transport steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.01
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Examples:
>>> import numpy as np
>>> from skcomplex.core.elements import DensityElement, FieldElement
>>> from skcomplex.core.interactions import AdvectionInteraction
>>> from skcomplex.core.laws.utils import coordinate
>>> from skcomplex.core.spaces import LatticeSpace, PhaseSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import SemiLagrangianSolver
>>> from skcomplex.specs import AxisSpec
>>> space = PhaseSpace(LatticeSpace((16,)), [AxisSpec('v', 8, -1.0, 1.0)]).check().prepare()
>>> start = np.outer(2 + np.sin(2 * np.pi * np.arange(16) / 16), np.ones(8))
>>> gas = FieldElement(
... 'gas', element=DensityElement, space=space, values={'f': start}, discretization='finite-volume'
... )
>>> stream = AdvectionInteraction('s', target='gas', state='f', drift=lambda s: [coordinate('v'), 0])
>>> system = System(elements=[gas], interactions=[stream])
>>> solver = SemiLagrangianSolver(dt=1.0)
>>> final = Simulation(system, solver=solver).check().prepare().run(until=2.0).trajectory_.values('gas.f')[-1]
>>> abs(float(final.sum() / start.sum()) - 1.0) < 1e-12
True
Source code in src/skcomplex/engine/solvers/_semi_lagrangian.py
87 88 89 90 91 92 93 94 95 96 | |
Solver(*, random_state=None, until=math.inf)
Bases: BaseSolver[ProblemSpec]
Base of the solvers of the compiler's problem, which run its events, renewals, delays, and boundary laws.
Every call to run receives the current parameters, so parameter changes apply at the next call, and applies the
boundary laws of the problem after every internal step.
Source code in src/skcomplex/engine/solvers/_base.py
87 88 89 | |
SplitSolver(dt=0.01, *, random_state=None, until=math.inf)
Bases: Solver
Base of steppers that treat the linear part of the rates of fields apart from the nonlinear remainder.
The linear part is held in Fourier blocks for spectral fields, a small matrix per Fourier mode, and in a sparse matrix over the whole state vector for the other fields. Every block is the shape of the field, the state indices of its states, and its matrices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.01
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
sparse_ |
The sparse matrix of the linear terms outside spectral fields, or |
|
blocks_ |
The spectral blocks. |
Source code in src/skcomplex/engine/solvers/_split.py
84 85 86 87 88 89 90 91 92 93 | |
StrangSplittingSolver(transport=None, collision=None, dt=0.01, *, random_state=None, until=math.inf)
Bases: Solver
Strang splitting of a half transport step, a full collision step, and a half transport step.
The transport solver advances the flux differences of the finite-volume fields and the collision solver the source rates, which the problem keeps apart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
Solver | None
|
The solver of the flux differences, or |
None
|
collision
|
Solver | None
|
The solver of the source rates, or |
None
|
dt
|
float
|
The largest internal time step, the length of one split step. |
0.01
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
transport_ |
The prepared solver of the flux differences. |
|
collision_ |
The prepared solver of the source rates. |
Examples:
>>> import numpy as np
>>> from skcomplex.core.elements import DensityElement, FieldElement
>>> from skcomplex.core.interactions import AdvectionInteraction, RelaxationInteraction
>>> from skcomplex.core.laws.utils import coordinate, maximum_entropy
>>> from skcomplex.core.spaces import LatticeSpace, PhaseSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import RK4Solver, SemiLagrangianSolver, StrangSplittingSolver
>>> from skcomplex.specs import AxisSpec
>>> space = PhaseSpace(LatticeSpace((8,)), [AxisSpec('v', 16, -6.0, 6.0)]).check().prepare()
>>> start = np.outer(2 + np.sin(2 * np.pi * np.arange(8) / 8), np.exp(-space.coordinates_['v'] ** 2))
>>> gas = FieldElement(
... 'gas', element=DensityElement, space=space, values={'f': start}, discretization='finite-volume'
... )
>>> stream = AdvectionInteraction('s', target='gas', state='f', drift=lambda s: [coordinate('v'), 0])
>>> relax = RelaxationInteraction(
... 'r', target='gas', state='f', goal=lambda s: maximum_entropy(s.target.f, axes=('v',)), time=0.5
... )
>>> system = System(elements=[gas], interactions=[stream, relax])
>>> solver = StrangSplittingSolver(SemiLagrangianSolver(dt=0.1), RK4Solver(dt=0.1), dt=0.1)
>>> final = Simulation(system, solver=solver).check().prepare().run(until=0.2).trajectory_.values('gas.f')[-1]
>>> abs(float(final.sum() / start.sum()) - 1.0) < 1e-12
True
Source code in src/skcomplex/engine/solvers/_strang_splitting.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
UnitarySolver(atol=1e-12, rtol=1e-10, nsteps=100000, max_dimension=DEFAULT_MAX_DIMENSION, *, random_state=None, until=math.inf)
Bases: OperatorSolver
Solver of the linear evolution dx/dt = -i H x of a pure state with QuTiP's SESolver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atol
|
float
|
The absolute tolerance of the integrator. |
1e-12
|
rtol
|
float
|
The relative tolerance of the integrator. |
1e-10
|
nsteps
|
int
|
The largest number of internal steps between two frames. |
100000
|
Examples:
>>> from typing import Annotated
>>> import numpy as np
>>> from sage.all__sagemath_symbolics import CC, SR
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.laws import DissipatorLaw, GeneratorLaw
>>> from skcomplex.core.laws.utils import local_operator
>>> from skcomplex.specs import HilbertSpec, ObservableSpec, StateSpec
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.engine.solvers import UnitarySolver
>>> operators = {'x': (np.array([[0, 1], [1, 0]]), True), 'z': (np.diag([1, -1]), True)}
>>> operators['minus'] = (np.array([[0, 0], [1, 0]]), False)
>>> QUBIT = HilbertSpec(2, operators, basis={'up': 0, 'down': 1})
>>> class TwoLevelElement(Element):
... def __init__(self, label, psi: Annotated[object, StateSpec(domain=CC**2)] = 'up', hilbert=QUBIT):
... super().__init__(label)
... self.psi = psi
... self.hilbert = hilbert
...
... def _get_laws(self, s):
... return [GeneratorLaw(local_operator(s.psi, SR.symbol('x')) / 2)]
>>> z = [ObservableSpec('z', element='q', variable='z')]
>>> system = System(elements=[TwoLevelElement('q')])
>>> from skcomplex.engine.compile import OperatorCompiler
>>> compiler = OperatorCompiler()
>>> simulation = Simulation(system, compiler=compiler, solver=UnitarySolver(), dt=np.pi, observables=z)
>>> simulation = simulation.check().prepare()
>>> float(round(simulation.run(until=np.pi).trajectory_.values('q.z')[-1], 6))
-1.0
Source code in src/skcomplex/engine/solvers/_operator.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
VelocityVerletSolver(dt=0.001, *, random_state=None, until=math.inf)
Bases: Solver
Velocity Verlet integrator of particle systems, whose positions move with their velocities.
Every step kicks the velocities by half a step, moves the positions, applies the boundary laws, and kicks the velocities again with the accelerations at the new positions, so a step costs one drift evaluation. It conserves energy over long runs of conservative forces. With the Numba backend every internal step of a frame runs in compiled code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.001
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Examples:
>>> import numpy as np
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.spaces import ContinuousSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.core.elements import PointElement
>>> from skcomplex.engine.solvers import VelocityVerletSolver
>>> from skcomplex.specs import StepSpec
>>> system = System(elements=[PointElement('ball', x_d1=2.0)], space=ContinuousSpace(n_dim=1))
>>> problem = compile_system(system)
>>> stepper = VelocityVerletSolver(dt=0.1, random_state=0).check().prepare(problem)
>>> stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_.round(10).tolist()
[2.0, 2.0]
Source code in src/skcomplex/engine/solvers/_velocity_verlet.py
72 73 74 75 76 77 78 79 80 81 | |
Yoshida4Solver(dt=0.001, *, random_state=None, until=math.inf)
Bases: Solver
Yoshida integrator of fourth order, three velocity Verlet steps over two blocks of states.
The solver splits the states into two blocks whose rates each read only the other block: a state of order 2 into
its position and velocity, and states of order 1 by the sparsity of the Jacobian of the drift, as the coordinates
and the momenta of a separable Hamiltonian split. Every step composes three velocity Verlet steps of the lengths
w1 h, w0 h, and w1 h, with w1 = 1 / (2 - 2^(1/3)) and w0 = 1 - 2 w1, which kick the second block by half
a step, drift the first, and kick the second again. The scheme is symplectic, so the energy of a separable
Hamiltonian stays bounded over long runs. With the Numba backend every internal step of a frame runs in compiled
code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
The largest internal time step. |
0.001
|
random_state
|
int | Generator | None
|
The seed or generator of stochastic solvers. |
None
|
until
|
float
|
The end time of the run the solver prepares for, infinite for a run that goes on until it is stopped. |
inf
|
Attributes:
| Name | Type | Description |
|---|---|---|
first_ |
The flat indices of the states of the block that drifts. |
|
second_ |
The flat indices of the states of the block that kicks. |
Raises:
| Type | Description |
|---|---|
SolverError
|
The problem is not an ODE, or its states admit no split into two blocks whose rates read only the other. |
Examples:
>>> from skcomplex.engine.compile import compile_system
>>> from skcomplex.core.spaces import ContinuousSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.core.elements import PointElement
>>> from skcomplex.engine.solvers import Yoshida4Solver
>>> from skcomplex.specs import StepSpec
>>> system = System(elements=[PointElement('ball', x_d1=2.0)], space=ContinuousSpace(n_dim=1))
>>> problem = compile_system(system)
>>> stepper = Yoshida4Solver(dt=0.1).check().prepare(problem)
>>> stepper.run(StepSpec(0.0, problem.initial_state, problem.initial_parameters, 1.0)).state_.round(10).tolist()
[2.0, 2.0]
Source code in src/skcomplex/engine/solvers/_yoshida4.py
89 90 91 92 93 94 95 96 97 98 | |