Experiments
Study models through many runs: designs, scenarios, replicates, sensitivity analysis, and calibration.
BaseDesign
Bases: BaseConfig[Bounds, None], ABC
Base of designs, which give rows of values by factory argument or dotted path.
A design stores its settings, such as the number of rows, in __init__. Preparing it on a mapping from name to
what it samples sets its rows in rows_, which running it keeps:
- a sampled design reads a pair
(low, high)or a SciPy distribution per name; - a grid design reads a sequence of values per name;
- a rows design reads nothing, because its rows are its setting.
A child implements _prepare(bounds), which sets rows_.
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
One mapping from factory argument or dotted path to value per row, set by |
Examples:
>>> from skcomplex.experiments import BaseDesign
>>> class MidpointDesign(BaseDesign):
... def _prepare(self, bounds=None):
... self.rows_ = [{name: sum(bound) / 2 for name, bound in (bounds or {}).items()}]
>>> MidpointDesign().check().check().prepare({'r': (0.0, 1.0)}).run().rows_
[{'r': 0.5}]
BaseExperiment
Bases: BaseConfig[None, None], ABC
Base of experiments, which run a model, given by a factory of systems or a system, many times.
An experiment stores its source and settings in __init__, validates them in check, resolves the source and
what it runs in prepare, and runs in run, which keeps its results in the attributes that
end in _. A child implements _prepare and _run, and reads the model with the private methods of the base. A
model of the library is passed as its factory, or as a system that skcomplex.models.create returns, since
experiments do not import the model library.
Examples:
>>> from skcomplex.experiments import BaseExperiment
>>> from skcomplex.models.biology.ecology import logistic_growth
>>> class SystemExperiment(BaseExperiment):
... def __init__(self, source):
... self.source = source
...
... def _check(self):
... pass
...
... def _prepare(self, context):
... self.factory_ = self._check_source(self.source)
...
... def _run(self, inputs):
... self.system_ = self.factory_()
>>> len(SystemExperiment(logistic_growth).check().prepare().run().system_.elements)
1
BasinStabilityExperiment(source, bounds, classifier, *, n=500, until=None, dt=None, random_state=None, executor='serial', vectorized=False)
Bases: BaseExperiment
Fractions of random initial states, drawn uniformly within bounds, that end in each attractor.
Every initial state is run to until and its trajectory is labelled by the classifier. With vectorized, all
initial states run as the members of one simulation of the replicated system, and the classifier receives every
member as a trajectory of the original system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
A factory of systems, or a system, such as one that |
required |
bounds
|
Mapping[str, tuple[float, float]]
|
A mapping from dotted state path to the |
required |
classifier
|
Callable[[Trajectory], object]
|
A function of a trajectory that returns the label of the attractor it ends in. |
required |
n
|
int
|
The number of initial states. |
500
|
until
|
float | None
|
The simulated time of every run, or |
None
|
dt
|
float | None
|
The time between frames, or |
None
|
random_state
|
int | None
|
The seed of the initial states and the runs. |
None
|
executor
|
str | Executor
|
|
'serial'
|
vectorized
|
bool
|
Whether to run every initial state as a member of one simulation of the replicated system. |
False
|
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
The initial states, one mapping from dotted path to value per run, set by |
|
labels_ |
The attractor label of every initial state. |
|
fractions_ |
The fraction of initial states per attractor label. |
|
errors_ |
The standard error of every fraction. |
Examples:
>>> from skcomplex.experiments import BasinStabilityExperiment
>>> from skcomplex.models.biology.ecology import logistic_growth
>>> experiment = BasinStabilityExperiment(
... logistic_growth, {'population.n': (0.1, 10)}, lambda trajectory: 'capacity', n=5, until=5
... )
>>> experiment.check().prepare().run().fractions_
{'capacity': 1.0}
Source code in src/skcomplex/experiments/_basin_stability.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
CalibrationExperiment(source, data, bounds, *, initial=None, loss='squares', replicates=1, dt=None, random_state=None, arguments=None)
Bases: BaseExperiment
Fit of parameters, given by dotted path, so that a model reproduces observed time series, with SciPy.
Every evaluation runs the model to the last data time and interpolates its states or observables at the data
times. Stochastic models average replicates runs with fixed streams, so the loss is smooth in the parameters.
Least squares use scipy.optimize.least_squares, and the Gaussian likelihood with an unknown variance uses
scipy.optimize.minimize.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
A factory of systems, or a system, such as one that |
required |
data
|
Mapping[str, tuple[ArrayLike, ArrayLike]]
|
A mapping from state or observable path to its observed |
required |
bounds
|
Mapping[str, tuple[float, float]]
|
A mapping from dotted parameter or initial-state path to its |
required |
initial
|
Mapping[str, float] | None
|
The starting values of the parameters, or |
None
|
loss
|
str
|
|
'squares'
|
replicates
|
int
|
The number of runs averaged per evaluation. |
1
|
dt
|
float | None
|
The time between frames, or |
None
|
random_state
|
int | None
|
The seed of the runs. |
None
|
arguments
|
Mapping[str, object] | None
|
The factory arguments of the model, or |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
estimates_ |
The fitted value of every parameter. |
|
covariance_ |
The covariance of the estimates from the Jacobian of the residuals, or |
|
r2_ |
The coefficient of determination over all data points. |
|
rmse_ |
The root mean squared error. |
|
mae_ |
The mean absolute error. |
|
mape_ |
The mean absolute percentage error, over data points that are not zero. |
|
simulated_ |
The simulated values at the data times, by path. |
|
success_ |
Whether the optimiser converged. |
Examples:
>>> import numpy as np
>>> from skcomplex.experiments import CalibrationExperiment
>>> from skcomplex.models.biology.ecology import logistic_growth
>>> times = np.arange(0, 21.0)
>>> values = 100 / (1 + 9 * np.exp(-0.5 * times))
>>> bounds = {'growth.r': (0.1, 2.0), 'growth.K': (50, 200)}
>>> calibration = CalibrationExperiment(logistic_growth, {'population.n': (times, values)}, bounds)
>>> estimates = calibration.check().prepare().run().estimates_
>>> round(estimates['growth.r'], 2), round(estimates['growth.K'])
(0.5, 100)
Source code in src/skcomplex/experiments/_calibration.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
Experiment(source, design=None, bounds=None, scenarios=None, replicates=1, *, analyses=None, until=None, dt=None, solver=None, outputs=None, warmup=0.0, random_state=None, executor='serial', observables=None)
Bases: BaseExperiment
Runs of a model over a design, scenarios, and replicates, with one random stream per design row and replicate.
The streams are spawned from random_state in a fixed order, so the results do not depend on the executor, and
every scenario of a design row and replicate shares its stream.
Every run clones every analysis, prepares the clone on the run's trajectory, runs it, and keeps the attributes that
its run sets as scalar outputs named <prefix>.<attribute>, without the trailing underscore. The prefix is the
lowercase class name of the analysis, such as entropyanalysis, numbered -1, -2, ... when several analyses
share a class, as scikit-learn names the steps of make_pipeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
A factory of systems, or a system, such as one that |
required |
design
|
BaseDesign | None
|
The design of factory arguments and dotted paths, or |
None
|
bounds
|
Bounds | None
|
What the design prepares on: a pair |
None
|
scenarios
|
Sequence[ScenarioSpec] | None
|
The scenarios to compare, or |
None
|
replicates
|
int
|
The number of runs per design row and scenario. |
1
|
analyses
|
Sequence[BaseEstimator] | None
|
The unprepared analyses of every run, or |
None
|
until
|
float | None
|
The simulated time of every run, or |
None
|
dt
|
float | None
|
The time between frames, or |
None
|
solver
|
BaseSolver | None
|
The solver, or |
None
|
outputs
|
Sequence[Output] | None
|
The state and observable paths to keep as series, and functions of a trajectory that return a scalar or a
mapping of scalars. |
None
|
warmup
|
float
|
The simulated time before which kept series are discarded. |
0.0
|
random_state
|
int | None
|
The seed of the random streams. |
None
|
executor
|
str | Executor
|
|
'serial'
|
observables
|
Sequence[ObservableSpec] | None
|
The observables of every run, or |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
tasks_ |
The runs of the experiment in their fixed order, set by |
|
result_ |
The results of every run, set by |
Examples:
>>> from skcomplex.experiments import Experiment, GridDesign
>>> from skcomplex.models.biology.ecology import logistic_growth
>>> bounds = {'r': [0.5, 1.0]}
>>> experiment = Experiment(logistic_growth, GridDesign(), bounds, until=5, outputs=['population.n'])
>>> len(experiment.check().prepare().run().result_.rows)
2
Source code in src/skcomplex/experiments/_experiment.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
ExperimentResult(runs)
Results of an experiment: one row per run, the kept series, ensembles, and summaries across replicates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runs
|
Sequence[_RunResultSpec]
|
The results of every run, in the experiment's order. |
required |
Examples:
>>> from skcomplex.experiments import Experiment, GridDesign
>>> from skcomplex.models.biology.epidemiology import sir
>>> experiment = Experiment(sir, GridDesign(), {'gamma': [0.1, 0.2]}, until=10).check().prepare().run()
>>> [row['gamma'] for row in experiment.result_.rows]
[0.1, 0.2]
Source code in src/skcomplex/experiments/_results.py
31 32 | |
rows
property
One row per run with its design values, scenario, replicate, seed, and scalar outputs.
column(name)
Return one column of the rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of a design value, |
required |
Returns:
| Name | Type | Description |
|---|---|---|
values |
NDArray[generic]
|
One value per run. |
Source code in src/skcomplex/experiments/_results.py
42 43 44 45 46 47 48 49 50 51 52 53 | |
ensemble(name)
Return a kept series of every run interpolated on the times of the shortest run, with the seeds of the runs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The path of the kept series. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
times |
NDArray[float64]
|
The common times. |
values |
NDArray[float64]
|
One row per run and one column per common time. |
seeds |
NDArray[int64]
|
The seed of the run of every row. |
Examples:
>>> from skcomplex.experiments import Experiment
>>> from skcomplex.models.biology.epidemiology import sir
>>> result = Experiment(sir, replicates=3, until=10, random_state=0).check().prepare().run().result_
>>> times, values, seeds = result.ensemble('I.n')
>>> values.shape == (3, times.size), seeds.tolist()
(True, [0, 1, 2])
Source code in src/skcomplex/experiments/_results.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
series(name)
Return a kept series of every run, stacked when all runs have the same number of frames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The path of the kept series. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
series |
NDArray[generic]
|
One row per run, or an object array of per-run series of different lengths. |
Source code in src/skcomplex/experiments/_results.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
summary(quantiles=(0.05, 0.5, 0.95))
Return the mean, standard deviation, and quantiles of every numeric scalar output per row and scenario.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
quantiles
|
Sequence[float]
|
The quantiles to report. |
(0.05, 0.5, 0.95)
|
Returns:
| Name | Type | Description |
|---|---|---|
summary |
list[dict[str, object]]
|
One row per design row and scenario, with |
Source code in src/skcomplex/experiments/_results.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
to_frame()
Return the rows as a pandas data frame.
Returns:
| Name | Type | Description |
|---|---|---|
frame |
DataFrame
|
The data frame. |
Source code in src/skcomplex/experiments/_results.py
141 142 143 144 145 146 147 148 | |
GridDesign
Bases: BaseDesign
Design of every combination of the values of every name, which prepares on a sequence of values per name.
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
One row per combination of the values, the last name varying fastest, set by |
Examples:
>>> from skcomplex.experiments import GridDesign
>>> GridDesign().check().check().prepare({'r': [0.5, 1.0], 'K': [100, 200]}).run().rows_
[{'r': 0.5, 'K': 100}, {'r': 0.5, 'K': 200}, {'r': 1.0, 'K': 100}, {'r': 1.0, 'K': 200}]
LatinHypercubeDesign(n=64, random_state=None)
Bases: BaseDesign
Design of a Latin hypercube sample, one sample in every stratum of every name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of rows. |
64
|
random_state
|
int | None
|
The seed of the samples. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
The |
Examples:
>>> from skcomplex.experiments import LatinHypercubeDesign
>>> rows = LatinHypercubeDesign(n=4, random_state=0).check().check().prepare({'r': (0.0, 1.0)}).run().rows_
>>> sorted(int(row['r'] * 4) for row in rows)
[0, 1, 2, 3]
Source code in src/skcomplex/experiments/_latin_hypercube.py
33 34 35 | |
MorrisDesign(n=10, random_state=None)
Bases: BaseDesign
Design of the Morris trajectories of SALib on four levels, the rows a Morris sensitivity analysis reads.
Every trajectory starts at a random point of the grid of levels and moves one name at a time, n * (names + 1)
rows in all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of trajectories. |
10
|
random_state
|
int | None
|
The seed of the trajectories. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
The |
Examples:
>>> from skcomplex.experiments import MorrisDesign
>>> design = MorrisDesign(n=4, random_state=0).check().check().prepare({'r': (0.0, 1.0), 'K': (50, 150)})
>>> len(design.run().rows_)
12
Source code in src/skcomplex/experiments/_morris.py
36 37 38 | |
OneAtATimeDesign
Bases: BaseDesign
Design that moves every name from its lower to its upper bound with the others at their midpoints.
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
Two rows per name, at its lower and then at its upper bound, set by |
Examples:
>>> from skcomplex.experiments import OneAtATimeDesign
>>> OneAtATimeDesign().check().check().prepare({'r': (0.0, 1.0), 'K': (50, 150)}).run().rows_
[{'r': 0.0, 'K': 100.0}, {'r': 1.0, 'K': 100.0}, {'r': 0.5, 'K': 50.0}, {'r': 0.5, 'K': 150.0}]
OrbitDiagramExperiment(source, parameter, values, observable, *, transient, keep, dt=None, decimals=6, random_state=None, executor='serial', vectorized=False)
Bases: BaseExperiment
Values an observable visits after a transient, for every value of a parameter.
Every value of the parameter is run to transient + (keep - 1) * dt, and the frames from transient on are kept.
With vectorized, all values run as the members of one simulation of the system replicated over them, which needs
a dotted path of a state or parameter of an element, and a system of single elements and their interactions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
A factory of systems, or a system, such as one that |
required |
parameter
|
str
|
The dotted path or factory argument to sweep. |
required |
values
|
Sequence[float]
|
The values of the parameter. |
required |
observable
|
str
|
The state or observable path whose values are kept. |
required |
transient
|
float
|
The simulated time discarded before keeping values. |
required |
keep
|
int
|
The number of frames kept after the transient. |
required |
dt
|
float | None
|
The time between frames, or |
None
|
decimals
|
int
|
The decimals the kept values are rounded to before counting the period of every orbit. |
6
|
random_state
|
int | None
|
The seed of stochastic runs. |
None
|
executor
|
str | Executor
|
|
'serial'
|
vectorized
|
bool
|
Whether to run every value as a member of one simulation of the replicated system. |
False
|
Attributes:
| Name | Type | Description |
|---|---|---|
points_ |
The kept values of the observable for every value of the parameter. |
|
periods_ |
The number of distinct kept values for every value of the parameter, the period of periodic orbits. |
Examples:
>>> from skcomplex.experiments import OrbitDiagramExperiment
>>> from skcomplex.models.mathematics.dynamical_systems import logistic_map
>>> diagram = OrbitDiagramExperiment(logistic_map, 'map.r', [2.9, 3.2], 'map.x', transient=1000, keep=20)
>>> diagram.check().prepare().run().periods_.tolist()
[1, 2]
Source code in src/skcomplex/experiments/_orbit_diagram.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
RandomDesign(n=64, random_state=None)
Bases: BaseDesign
Design of independent random samples, uniform between a pair of bounds or drawn from a SciPy distribution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of rows. |
64
|
random_state
|
int | None
|
The seed of the samples. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
The |
Examples:
>>> from skcomplex.experiments import RandomDesign
>>> len(RandomDesign(n=5, random_state=0).check().check().prepare({'r': (0.1, 1.0)}).run().rows_)
5
Source code in src/skcomplex/experiments/_random.py
32 33 34 | |
RowsDesign(rows)
Bases: BaseDesign
Design given by its rows, which prepares on no bounds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rows
|
Sequence[Mapping[str, object]]
|
One mapping from factory argument or dotted path to value per row. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
A copy of the rows, set by |
Examples:
>>> from skcomplex.experiments import RowsDesign
>>> RowsDesign([{'growth.r': 0.5}, {'growth.r': 1.0}]).check().check().prepare().run().rows_
[{'growth.r': 0.5}, {'growth.r': 1.0}]
Source code in src/skcomplex/experiments/_rows.py
30 31 | |
SaltelliDesign(n=1024, random_state=None)
Bases: BaseDesign
Design of Saltelli's sample of SALib, the rows a Sobol sensitivity analysis reads.
The rows are n base points of a Sobol sequence and, for every name, the same points with that name taken from a
second set of points, n * (names + 2) rows in all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of base points, best a power of 2. |
1024
|
random_state
|
int | None
|
The seed of the scrambling. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
The |
Examples:
>>> from skcomplex.experiments import SaltelliDesign
>>> design = SaltelliDesign(n=8, random_state=0).check().check().prepare({'r': (0.0, 1.0), 'K': (50, 150)})
>>> len(design.run().rows_)
32
Source code in src/skcomplex/experiments/_saltelli.py
36 37 38 | |
SensitivityAnalysis(method='sobol', output=None, random_state=None)
Bases: BaseAnalysis
Sensitivity of a scalar output of an experiment to the values of its design, with SALib.
The analysis prepares on the result of an experiment whose design matches the method, with one run per row:
'oat'reads aOneAtATimeDesignand gives the change of the output from the lower to the upper bound of every input, with the others at their midpoints.'morris'reads aMorrisDesignand gives the Morris elementary effects.'sobol'reads aSaltelliDesignand gives the first-order and total Sobol indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
|
'sobol'
|
output
|
str | None
|
The name of the scalar output, such as the path of a kept series, whose last value it is. |
None
|
random_state
|
int | None
|
The seed of the bootstrap confidence intervals. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
names_ |
The inputs, the names of the design values. |
|
inputs_ |
The design values of every run, one column per input. |
|
outputs_ |
The output of every run. |
|
effects_ |
The one-at-a-time effect of every input, for |
|
mu_star_, |
(mu_star_conf_, sigma_)
|
The mean absolute elementary effect, its confidence interval, and the standard deviation of the elementary
effects of every input, for |
first_order_, |
(first_order_conf_, total_order_, total_order_conf_)
|
The first-order and total Sobol indices of every input and their confidence intervals, for |
ranking_ |
(first_order_conf_, total_order_, total_order_conf_)
|
The inputs from the most to the least influential, by |
Examples:
>>> from skcomplex.experiments import Experiment, OneAtATimeDesign, SensitivityAnalysis
>>> from skcomplex.models.biology.ecology import logistic_growth
>>> bounds = {'growth.r': (0.1, 1.0), 'growth.K': (50, 150)}
>>> experiment = Experiment(logistic_growth, OneAtATimeDesign(), bounds, until=20, outputs=['population.n'])
>>> result = experiment.check().prepare().run().result_
>>> SensitivityAnalysis('oat', 'population.n').check().prepare(result).run().ranking_[0]
'growth.K'
Source code in src/skcomplex/experiments/_sensitivity.py
72 73 74 75 | |
SobolDesign(n=64, random_state=None)
Bases: BaseDesign
Design of a scrambled Sobol sequence, a low-discrepancy sample, best with n a power of 2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of rows. |
64
|
random_state
|
int | None
|
The seed of the scrambling. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
rows_ |
list[Row]
|
The |
Examples:
>>> from skcomplex.experiments import SobolDesign
>>> len(SobolDesign(n=8, random_state=0).check().check().prepare({'r': (0.0, 1.0)}).run().rows_)
8
Source code in src/skcomplex/experiments/_sobol.py
32 33 34 | |