Reference
Derived systems¶
Wrappers that re-present an existing system through a new lens while keeping the
System protocol intact, so every analysis composes with them
transparently — and so you run them exactly as you ran the original. Prose
introduction: the mental model.
Two of them are reached by a verb on the system, because they speak the same vocabulary the class does:
# skip-doctest — the two verbs, for reference (`sys` is any flow)
sys.poincare("y", 0.0) # a PoincareMap — a section plane
sys.poincare(period=4.488) # a StroboscopicMap — the phase circle
sys.ensemble(states) # an Ensemble — many copies, one object
The other two are Lyapunov and projection machinery rather than everyday verbs,
so they are constructed at their own address
(ts.derived.TangentSystem(system, k=2), ts.derived.ProjectedSystem(system, (0, 2))).
PoincareMap
¶
PoincareMap(
system: Any,
plane: tuple[Any, ...] | None = None,
*,
direction: int | str = +1,
dt: float = 0.01,
max_time: float = 10000.0,
)
Bases: DerivedSystem
Present a flow as the discrete map of its crossings through a hyperplane.
One step() advances the underlying system until the trajectory
crosses the section plane in the chosen direction, refines the crossing
point by cubic Hermite interpolation of the bracketing samples (using the
system's numeric RHS for endpoint derivatives — O(dt⁴) accuracy), and
returns the full-dimensional crossing state.
Because a PoincareMap is a discrete system, everything written for
maps applies to flows through it — e.g. an orbit diagram over a
PoincareMap is a bifurcation diagram of the flow.
| PARAMETER | DESCRIPTION |
|---|---|
system
|
A continuous-time system (ODE or DDE).
TYPE:
|
plane
|
The section, in any of three spellings: Omit it and one is chosen — see :func:
TYPE:
|
direction
|
Count only crossings with
TYPE:
|
dt
|
March step used for crossing detection. The refinement makes the
crossing itself far more accurate than
TYPE:
|
max_time
|
Raise if no crossing is found within this much time (e.g. the plane misses the attractor).
TYPE:
|
Examples:
>>> pmap = PoincareMap(Rossler(), plane=("x", 0.0, "up"))
>>> section = pmap.run(500) # 500 crossings → PoincareSection
>>> section.y.shape
(500, 3)
>>> pmap = PoincareMap(Lorenz()) # no plane named → one is chosen
>>> pmap.plane # ...and the choice is readable
(2, ...)
Source code in src/tsdynamics/derived/poincare.py
plane
property
writable
¶
The section this map actually marches — read-only.
The resolved reading: (component_index, offset) for an axis-aligned
section, or (normal_vector, offset) for an arbitrary normal. A
direction word given inside the constructor's plane argument has been
peeled off into :attr:direction, and repr(section) renders the whole
thing back in the words it was asked for (y = 0 up).
Read-only because writing it was a provenance lie
(CONTRACT.md §11.6 defect 1): the unit normal and offset the marcher
actually uses are derived from this tuple at construction, so
pmap.plane = ("z", 27.0) relabelled a section it never crossed —
every later run() kept returning crossings of the old plane, under
the new plane's name. To section somewhere else, build the map you mean::
pmap = system.poincare("z", 27.0)
| RETURNS | DESCRIPTION |
|---|---|
tuple
|
|
family
property
¶
family: str
"map" — a Poincaré map is a DISCRETE view of the flow.
v6 replaced is_discrete with family, and the wrapper's override
did not come across: :class:~tsdynamics.derived._base.DerivedSystem
answers with the inner system's word, so a section of a flow called
itself "ode". False/"ode" are both legal values, so every
reader that routes on it — the 0–1 test's observable, the sweep's
horizon word — went quietly wrong rather than raising. Its sibling
:class:~tsdynamics.derived.stroboscopic.StroboscopicMap already says
"map"; the two are the same concept and must agree.
step
¶
Advance to the n-th next crossing and return it (full-dim coords).
Source code in src/tsdynamics/derived/poincare.py
set_state
¶
set_state(u: Any) -> None
Overwrite the inner flow state and reset crossing bookkeeping.
as_events
¶
as_events() -> list[Event]
Return the section as a one-element [Event] for system.run(events=...).
A Poincaré section is an event: the crossing of g(u) = normal·u −
offset in the map's :attr:direction. This exposes it as an
:class:~tsdynamics._engine.run.Event so the general events= API
reproduces the section — PoincareMap is one consumer of the same
wired engine seam (stream WS-EVENTSAPI / WS-CROSSKERNEL). Driven at the
same fixed-step march (method="rk4" at this map's dt) from the
same initial condition, the crossings of
inner.run(events=pmap.as_events(), ...) match
:meth:run to the engine's refinement accuracy.
Examples:
>>> pmap = PoincareMap(Rossler(), plane=("y", 0.0, "up"), dt=0.01)
>>> sol = Rossler().run(final_time=400, dt=0.01, solver="rk4",
... events=pmap.as_events())
>>> sol.meta["y_events"][0][:5].shape # the same crossing states
(5, 3)
Notes
You never have to build an Event yourself: every events= door
also accepts a plain plane tuple (("y", 0.0, "up")) or a bare
g(y, t) callable, which is why the type is not exported. This is the
one place the library hands one back.
Source code in src/tsdynamics/derived/poincare.py
reinit
¶
Restart the inner flow and clear crossing bookkeeping.
Source code in src/tsdynamics/derived/poincare.py
run
¶
run(
steps: int = 100,
*,
ic: Any | None = None,
transient: int = 0,
backend: str | None = None,
**unknown: Any,
) -> PoincareSection
Collect crossings as a :class:PoincareSection — from a fresh start.
t holds the continuous crossing times; y the full-dimensional
crossing states. transient crossings are discarded first. The
returned :class:PoincareSection is a :class:~tsdynamics.data.Trajectory
carrying section intent (so a renderer draws the in-plane scatter) plus a
repr / .to_dict() readout.
For an ordinary (non-stiff) ODE on the compiled engine this marches the
whole attractor and refines every crossing in one engine call (the
wired Rust integrate_events, stream WS-CROSSKERNEL) — ~100× faster than
the per-dt Python loop it replaces. DDEs, systems without a numeric
RHS, stiff defaults, and backend="reference" keep the Python loop. The
engine path is answer-identical to that loop's fixed-step (rk4)
refinement; see :mod:tsdynamics.derived._crossings.
| PARAMETER | DESCRIPTION |
|---|---|
steps
|
Number of crossings to collect — the horizon word, counted in crossings (a Poincaré map's iterate is a crossing).
TYPE:
|
ic
|
Start state for the inner flow —
TYPE:
|
transient
|
Leading stretch to discard, in this view's own horizon unit:
crossings. (The free function
:func:
TYPE:
|
backend
|
Engine evaluator for the fast path; defaults to the inner system's
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
PoincareSection
|
The collected crossings (continuous times in |
| RAISES | DESCRIPTION |
|---|---|
ConvergenceError
|
If the inner flow diverges (non-finite state) or no crossing is found
within |
Notes
Live-cursor semantics. run advances the inner system as
a side effect, so a subsequent :meth:step continues forward rather than
re-yielding the crossings just collected. The two collection paths leave
the inner cursor at slightly different places: the Python loop stops just
past the last collected crossing, while the engine path stops at the
span end it marched to (which can be a little beyond the last
crossing). Both invariants — that step() resumes after the
collected crossings — hold; do not rely on the exact cursor offset. Call
:meth:reinit first if you need a deterministic restart point.
Source code in src/tsdynamics/derived/poincare.py
926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 | |
StroboscopicMap
¶
Bases: DerivedSystem
Present a forced flow as the discrete map of once-per-period samples.
One step() advances the underlying continuous system by exactly one
forcing period and returns the new state. Orbit diagrams over a
StroboscopicMap are the standard way to study forced oscillators
(Duffing, forced van der Pol, ...).
| PARAMETER | DESCRIPTION |
|---|---|
system
|
A continuous-time system.
TYPE:
|
period
|
Sampling period (the forcing period).
TYPE:
|
Examples:
>>> smap = StroboscopicMap(ForcedVanDerPol(), period=2 * np.pi / 0.63)
>>> samples = smap.run(300, transient=100)
Source code in src/tsdynamics/derived/stroboscopic.py
step
¶
Advance n forcing periods (default 1) and return the new state.
n_or_dt is a period count, not a time increment — the wrapper
presents a discrete map, so the argument is coerced to an integer with
int() (a float is truncated toward zero, matching the discrete-view
convention; pass a whole number to be explicit).
| PARAMETER | DESCRIPTION |
|---|---|
n_or_dt
|
Number of forcing periods to advance.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
The full-dimensional state after |
Source code in src/tsdynamics/derived/stroboscopic.py
run
¶
run(
steps: int = 100,
*,
ic: Any | None = None,
transient: int = 0,
**unknown: Any,
) -> Trajectory
Collect steps once-per-period samples — from a fresh start.
Sampling restarts the inner flow and advances by exactly one forcing
:attr:period per sample; transient leading periods are stepped
through and discarded first. run() twice returns the same data;
step() is the verb that continues.
| PARAMETER | DESCRIPTION |
|---|---|
steps
|
Number of once-per-period samples to collect — the horizon word, counted in forcing periods.
TYPE:
|
ic
|
Start state for the inner flow —
TYPE:
|
transient
|
Leading stretch to discard, in this view's own horizon unit: forcing periods.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Trajectory
|
The strobed samples — continuous sample times in |
Source code in src/tsdynamics/derived/stroboscopic.py
TangentSystem
¶
Bases: DerivedSystem
Evolve a system together with k deviation (tangent) vectors.
Each step() advances the state and the deviation vectors, then
QR-reorthonormalises; :meth:exponents is their running time-average — the
Lyapunov spectrum estimate — and :meth:deviations is the live orthonormal
frame. ts.analysis.lyapunov_spectrum wraps that into the standard
burn-in + time-averaged estimate, and is the single implementation every
family's Lyapunov answer delegates to.
Two members are withheld from dir() (CONTRACT.md §11, T2) and remain
fully callable:
convergence(steps, ...)
The Lyapunov estimates settling over time. Measured, it returns exactly
run(steps, ...).unpack() — the same two arrays, element-for-element —
so it is a second spelling of the one trajectory verb (corollary C3).
The picture it exists for has a front door of its own::
ts.plot(system, "lyapunov_convergence", steps=4000)
growths()
The most recent step's log|diag R|. A per-step internal of the
Benettin accumulation, meaningful only while you are driving step()
yourself; the quantity users want is :meth:exponents, its running
average.
Implementation per family
- Maps: pure NumPy —
W ← J(x)·Wwith_jacobianevaluated at the pre-image (the correct tangent-map convention), then QR. - ODEs: the extended ODE (state ⊕
ktangent vectors, see :mod:tsdynamics.derived._variational) is lowered to an engine tape and integrated per step on the Rust engine (or the pure-Python reference oracle), then QR-reorthonormalised here. Select the variational backend withbackend=:"jit"(default),"interp", or"reference". - DDEs: not supported — tangent dynamics of a DDE lives in an
infinite-dimensional history space; use
DelaySystem.lyapunov_spectrum(the engine DDE Lyapunov estimator).
| PARAMETER | DESCRIPTION |
|---|---|
system
|
The base system whose tangent dynamics to evolve.
TYPE:
|
k
|
Number of deviation vectors (
TYPE:
|
backend
|
ODE variational backend (ignored for maps): .. versionchanged:: 6.0
Default moved from
TYPE:
|
Examples:
>>> tang = TangentSystem(Henon(), k=2)
>>> tang.reinit([0.1, 0.1])
>>> for _ in range(5000):
... tang.step()
>>> tang.exponents() # ≈ [0.42, -1.62]
Source code in src/tsdynamics/derived/tangent.py
k
property
writable
¶
k: int
How many deviation vectors ride along — the number of exponents.
Writable, and validated on the way in against the same rule the
constructor applies (1 <= k <= system.dim). It used to be a plain
attribute, so tang.k = 7 on a 3-D flow was accepted and the next
step() died inside NumPy with cannot reshape array of size 6 into
shape (7,3) — an error about an array the caller never saw, from a
rule the constructor had already written down (CONTRACT.md §11.3 T2,
§11.6 defect 1).
Changing it re-arms the estimator: the orthonormal frame and the
accumulated growth records are shaped by k, so they are dropped and
the next :meth:reinit starts a fresh average. Assigning the value it
already has is a no-op and keeps a running average intact.
run
¶
run(
steps: int = 2000,
n_or_dt: float | None = None,
*,
ic: Any | None = None,
) -> Trajectory
Record the running Lyapunov estimates as a :class:Trajectory.
A tangent system's trajectory is not a state orbit — it is the growth
of the deviation vectors, i.e. each exponent's running estimate settling
as the time-average accumulates. That is what this records, one column
per exponent, named lambda1 … lambdak, so it plots, tabulates and
indexes like any other trajectory::
tang = ts.derived.TangentSystem(lor, k=3)
conv = tang.run(steps=2000)
conv["lambda1"][-1] # the leading exponent at the horizon
ts.plot(conv) # the convergence read-out
.. versionchanged:: 6.0
run was inherited from
:class:~tsdynamics.derived._base.DerivedSystem and raised a
NotImplementedError with an empty message, while hasattr
and isinstance(tang, System) both answered True — a member
that lies. The numbers it returns are exactly :meth:convergence's,
which it calls.
| PARAMETER | DESCRIPTION |
|---|---|
steps
|
Number of tangent steps to record. Default
TYPE:
|
n_or_dt
|
Per-step increment — iterations for a map, a
TYPE:
|
ic
|
Start state for the base system;
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Trajectory
|
|
Source code in src/tsdynamics/derived/tangent.py
reinit
¶
reinit(
u: Any | None = None,
*,
t: float | None = None,
params: dict[str, Any] | None = None,
**kwargs: Any,
) -> None
Restart state, deviation vectors, and accumulated growth sums.
Source code in src/tsdynamics/derived/tangent.py
step
¶
Advance state + deviation vectors and reorthonormalise.
For maps n_or_dt is the number of iterations (QR after each);
for ODEs it is the time increment (default 0.1).
Returns the new state.
Source code in src/tsdynamics/derived/tangent.py
state
¶
state() -> ndarray
Return a copy of the current base-system state.
Source code in src/tsdynamics/derived/tangent.py
set_state
¶
set_state(u: Any) -> None
Overwrite the base state (map mode only — ODE tangent vectors would desync).
Source code in src/tsdynamics/derived/tangent.py
deviations
¶
deviations() -> ndarray
Return the current orthonormal deviation vectors, shape (dim, k).
The live tangent frame: column j is the direction the j-th
Lyapunov exponent is currently being measured along, reorthonormalised at
every step(). Columns are ordered by decreasing growth, so
deviations()[:, 0] is the most unstable direction at this point on
the orbit — the covariant-direction estimate you want in order to
perturb a trajectory where it will actually separate, to seed a control
scheme, or to see how the stable/unstable splitting turns as the orbit
moves.
This is the only route to that frame, which is why it stays on the tab
surface while :meth:growths (a per-step scalar of the same accumulation)
does not: the spectrum is available from
ts.analysis.lyapunov_spectrum, the directions are available here and
nowhere else.
Available on every supported backend — maps and the ODE engine backends both carry the deviation matrix explicitly.
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
Shape |
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
Before the first :meth: |
Examples:
>>> tang = TangentSystem(Henon(), k=2)
>>> tang.reinit([0.1, 0.1])
>>> for _ in range(100):
... tang.step()
>>> tang.deviations()[:, 0] # the unstable direction here
...
Source code in src/tsdynamics/derived/tangent.py
growths
¶
growths() -> ndarray
Return the log stretch factors log|diag R| from the most recent step.
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
In map mode, after a batch engine |
Source code in src/tsdynamics/derived/tangent.py
exponents
¶
exponents() -> ndarray
Return the running Lyapunov-spectrum estimate (accumulated growths / elapsed).
convergence
¶
convergence(
steps: int = 2000,
n_or_dt: float | None = None,
*,
ic: Any | None = None,
) -> tuple[ndarray, ndarray]
Record the running Lyapunov estimates as they converge.
Reinitialises the tangent frame and steps it steps times, capturing
the running :meth:exponents estimate after every step. The estimates
settle as the time-average accumulates — the curve a user inspects to
judge whether a Lyapunov run has converged.
| PARAMETER | DESCRIPTION |
|---|---|
steps
|
Number of tangent steps to record. Default
TYPE:
|
n_or_dt
|
Per-step increment (iterations for a map,
TYPE:
|
ic
|
Initial condition;
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
(times, estimates)
|
|
Source code in src/tsdynamics/derived/tangent.py
Ensemble
¶
Many copies of one system, advanced synchronously from different states.
Used for two-trajectory Lyapunov estimates, basin sampling, and ensemble statistics. Members are independent copies — parameters are shared at construction, states are per-member.
Two members are withheld from dir() (CONTRACT.md §11, T2) and stay
callable. Both are second spellings of the System protocol this class
already answers in the protocol's own words:
states()
Identical to :meth:state — the very body of state() is return
self.states(). state() is the word every other view in the library
uses, so it is the one that stays listed (corollary C3).
set_states(states)
The plural of set_state, which is a capability rather than a
protocol member since v6. Reach it with hasattr like the others, or
build the ensemble you want: system.ensemble(new_states) is one call
and cannot desynchronise the member clocks.
| PARAMETER | DESCRIPTION |
|---|---|
system
|
The system to copy per member (the original is untouched). Readable
afterwards as :attr:
TYPE:
|
states
|
One initial state per member. |
| ATTRIBUTE | DESCRIPTION |
|---|---|
system |
The system every member is a copy of. .. versionchanged:: 6.0
Was
TYPE:
|
members |
The per-member copies, in construction order.
TYPE:
|
Examples:
>>> band = Lorenz().ensemble([[1, 1, 1], [1.001, 1, 1]])
>>> band.step(0.01)
array([[...], [...]])
>>> band.run(final_time=10.0).final
array([[...], [...]])
Source code in src/tsdynamics/derived/ensemble.py
reinit
¶
Restart every member (from u, or from its own construction state).
run
¶
run(
final_time: float | None = None,
dt: float | None = None,
*,
steps: int | None = None,
transient: float | None = None,
seed: int | None = None,
**run_kw: Any,
) -> TrajectoryBatch
Run every member and return a :class:TrajectoryBatch.
Takes the template family's own run vocabulary — final_time /
dt for a flow, steps for a map — and forwards it verbatim, with
each member's own initial state supplied as ic=::
band = lor.ensemble(np.random.rand(100, 3))
batch = band.run(final_time=10.0)
batch.final # the (100, 3) end states
| PARAMETER | DESCRIPTION |
|---|---|
final_time
|
Flow horizon, in time units.
TYPE:
|
dt
|
Output sampling interval, in time units.
TYPE:
|
steps
|
Map horizon, in iterations. A flow refuses it by name.
TYPE:
|
transient
|
Leading stretch discarded, in the template family's own horizon unit.
TYPE:
|
seed
|
For an SDE batch, member
TYPE:
|
**run_kw
|
Everything else the template family's
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
TrajectoryBatch
|
|
| RAISES | DESCRIPTION |
|---|---|
InvalidParameterError
|
If |
Source code in src/tsdynamics/derived/ensemble.py
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
step
¶
Advance every member synchronously and return the stacked states.
| PARAMETER | DESCRIPTION |
|---|---|
n_or_dt
|
The per-member increment (a |
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
The new states, shape |
Source code in src/tsdynamics/derived/ensemble.py
set_states
¶
set_states(states: Any) -> None
Overwrite every member's state.
| PARAMETER | DESCRIPTION |
|---|---|
states
|
One new state per member, in member order. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Source code in src/tsdynamics/derived/ensemble.py
collect
¶
Step every member steps times and stack the sampled states.
Advances the whole ensemble synchronously, recording each member's state
after every step. This is the trajectory collector the static fan chart
(:meth:__plot_spec__) summarises into a median line + percentile band.
This is a different numerical path from :meth:run, which is why it
stays on the tab surface rather than being folded into it. run
launches each member as its own independent integration, on the inner
family's own adaptive solver and output grid; collect drives the
live steppers in lockstep, so at every returned sample the members
share one clock. That is the reading an ensemble measurement needs — a
cross-member percentile at "time t" is only meaningful if every
member really is at t — and it is also what makes the returned block
rectangular in (sample, member, component). Use run when you want
per-member trajectories; use collect when you want the ensemble's
spread through time.
| PARAMETER | DESCRIPTION |
|---|---|
steps
|
Number of samples to collect (one per step).
TYPE:
|
n_or_dt
|
The per-step increment forwarded to each member's |
| RETURNS | DESCRIPTION |
|---|---|
EnsembleSamples
|
A named pair — |
Source code in src/tsdynamics/derived/ensemble.py
plot
¶
Draw the ensemble — the fan chart by default.
ProjectedSystem
¶
ProjectedSystem(
system: Any,
components: Any,
*,
complete: Callable[[ndarray], Any] | None = None,
)
Bases: DerivedSystem
View a system through a subset of its components.
The full system is stepped underneath; only state()/step()
outputs are projected. set_state needs the inverse direction and
therefore requires a complete callable mapping a projected state back
to a full state.
complete is withheld from dir() (CONTRACT.md §11, T2): it is a
constructor argument echoed back, not a measurement — you already hold the
callable you passed, and reading it off the view answers nothing the code
that built the view does not already know. It remains bound, and
:attr:components (which the constructor resolves, from names to indices)
stays listed for exactly the opposite reason.
| PARAMETER | DESCRIPTION |
|---|---|
system
|
The full system.
TYPE:
|
components
|
Component indices (or names, when the system declares
TYPE:
|
complete
|
TYPE:
|
Examples:
Source code in src/tsdynamics/derived/projected.py
components
property
writable
¶
Which components of the inner system this view hands you, in order.
This is the projection, so it is writable — re-aiming a view is a
legitimate customization, and it accepts the same names-or-indices
grammar the constructor does (proj.components = ["x", "z"]). The
write is validated for the same reason the constructor's is: an
out-of-range index used to be stored verbatim and then surface on the
next step() as NumPy's index 99 is out of bounds, an error about
the state array rather than about the selection that was written.
Note that :attr:dim and :attr:variables both follow it.
variables
property
¶
Component names of the projected view (the inner names, subset).
Overrides :class:DerivedSystem's pass-through, which would return the
inner system's full names and mislabel the projected columns.
step
¶
Advance the full system; return the projected new state.
set_state
¶
set_state(u: Any) -> None
Overwrite the state (projected inputs need a complete callable).
reinit
¶
Restart the full system (projected inputs need a complete callable).
Source code in src/tsdynamics/derived/projected.py
run
¶
run(*args: Any, **run_kw: Any) -> Trajectory
Run the full system and keep only the projected columns.
Takes the inner family's own run vocabulary verbatim — final_time
/ dt for a flow, steps for a map — so an unknown keyword is
refused by the family that owns the word, with its reason.
| RETURNS | DESCRIPTION |
|---|---|
Trajectory
|
|
Source code in src/tsdynamics/derived/projected.py
WrappedSystem
¶
WrappedSystem(
step_fn: Callable[[ndarray, float], Any],
*,
dim: int,
family: str = "map",
ic: Any | None = None,
default_dt: float = 1.0,
variables: tuple[str, ...] | None = None,
**renamed: Any,
)
Bases: DeriveMixin, SystemPlottable
Wrap any external stepping rule as a first-class :class:System.
Give it a step_fn(state, n_or_dt) -> new_state and a dimension, and the
whole analysis toolkit (orbit diagrams, Lyapunov-from-rescaling, Poincaré
sections, ensembles, basins) applies to your own simulation code — a
foreign ODE solver, an agent-based model, a hardware-in-the-loop rig,
anything that advances a state vector.
| PARAMETER | DESCRIPTION |
|---|---|
step_fn
|
TYPE:
|
dim
|
State-space dimension.
TYPE:
|
family
|
TYPE:
|
ic
|
Default initial state, used when
TYPE:
|
default_dt
|
Amount a bare
TYPE:
|
variables
|
Component names, enabling
TYPE:
|
Examples:
>>> # a plain logistic map written by hand
>>> import numpy as np
>>> def step(u, n):
... x = u[0]
... for _ in range(int(n)):
... x = 3.9 * x * (1 - x)
... return [x]
>>> sysm = WrappedSystem(step, dim=1, family="map", ic=[0.5])
>>> traj = sysm.run(500)
>>> import tsdynamics as ts
>>> float(ts.analysis.lyapunov_spectrum(sysm, k=1, ic=[0.3])[0]) > 0 # chaotic
True
Source code in src/tsdynamics/families/wrapped.py
dim
property
¶
dim: int
State-space dimension — read-only, as on every other family.
It used to be a plain attribute, so w.dim = 7 was accepted and left
the wrapper describing a width its stepper does not produce.
reinit
¶
reinit(
u: Any | None = None,
*,
t: float | None = None,
params: dict[str, Any] | None = None,
) -> None
Restart from state u (falls back to ic, then zeros).
Source code in src/tsdynamics/families/wrapped.py
step
¶
Advance by n_or_dt (default default_dt) and return the new state.
Source code in src/tsdynamics/families/wrapped.py
copy
¶
copy() -> WrappedSystem
Return a fresh wrapper sharing the same step rule (independent state).
Source code in src/tsdynamics/families/wrapped.py
run
¶
run(
steps: int | None = None,
*,
transient: float = 0.0,
ic: Any | None = None,
final_time: float | None = None,
dt: float | None = None,
) -> Trajectory
Step the wrapper repeatedly (after transient) and collect a Trajectory.
A wrapper stands in for either family, so it accepts both horizon
words and you give exactly one: steps (a sample count, the map
spelling) or final_time (the flow spelling, with the sample count
round(final_time / dt)). The per-step increment is dt, which
defaults to default_dt for both families.
| PARAMETER | DESCRIPTION |
|---|---|
steps
|
Number of samples to collect — the map horizon word, counted in
iterations. Mutually exclusive with
TYPE:
|
transient
|
Leading stretch to discard (stepped but not recorded), in this
wrapper's own horizon unit:
TYPE:
|
ic
|
Initial state for the run (falls back to the constructor's
TYPE:
|
final_time
|
Integration horizon in time units — the flow horizon word.
The sample count is
TYPE:
|
dt
|
Per-step increment, in this wrapper's own unit (time for
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Trajectory
|
|
| RAISES | DESCRIPTION |
|---|---|
InvalidInputError
|
If neither |
Examples:
>>> import numpy as np
>>> flow = lambda u, dt: [u[0] * np.exp(0.5 * dt)]
>>> w = WrappedSystem(flow, dim=1, family="ode", default_dt=0.1)
>>> traj = w.run(final_time=1.0, dt=0.1) # the one trajectory verb
>>> traj.y.shape
(10, 1)
Source code in src/tsdynamics/families/wrapped.py
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 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 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
DerivedSystem
¶
DerivedSystem(system: Any)
Base for wrappers that present an existing system through a new lens.
A derived system implements the :class:~tsdynamics.families.System protocol
by delegating to a wrapped system, transforming what "one step" or "the
state" means (Poincaré crossings, stroboscopic samples, projections...).
Parameters and metadata are forwarded to the wrapped system, and
with_params re-parametrizes the inner system and rebuilds the
wrapper, so parameter sweeps compose: an orbit diagram over a
PoincareMap is a bifurcation diagram of the underlying flow.
Source code in src/tsdynamics/derived/_base.py
dim
property
¶
dim: int
Dimension of the state this view hands you.
The inner system's, by default — a Poincaré crossing and a stroboscopic
sample are both full states of the flow. The wrappers that genuinely
change the state's width override it
(:class:~tsdynamics.derived.ProjectedSystem returns the number of
surviving components, :class:~tsdynamics.derived.TangentSystem the
extended state's).
Documented because it was not: help(DerivedSystem.dim) printed three
blank lines, and on an instance help(pmap.dim) resolved the value
and printed int([x]) -> integer — the builtin's reference, about a
different subject entirely (CONTRACT.md §11.6 defect 2).
params
property
¶
params: ParamSet
The inner system's parameters — an empty set when it declares none.
Blindly forwarding self.system.params made PoincareMap(wrapped)
march correctly and then die with a raw AttributeError from this
line, because a :class:~tsdynamics.families.WrappedSystem has no
parameter set at all.
with_params
¶
with_params(**overrides: Any) -> DerivedSystem
Return a new wrapper of the same kind around a re-parametrized copy.
copy
¶
copy() -> DerivedSystem
state
¶
state() -> ndarray
Return the current state, as this view reports it.
The four members below are the :class:~tsdynamics.families.System
protocol, delegated to the inner system. Each wrapper overrides
whichever of them its lens actually changes — a
:class:~tsdynamics.derived.ProjectedSystem returns the projected
columns, a :class:~tsdynamics.derived.TangentSystem the state ⊕
deviation vectors — so reading the protocol off the wrapper always
answers in the wrapper's own coordinates.
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
The current state vector. On a cold view this triggers an implicit
:meth: |
Source code in src/tsdynamics/derived/_base.py
set_state
¶
set_state(u: Any) -> None
Overwrite the live state without restarting or resetting time.
A capability, not a protocol member: it left the System protocol in
v6 because a :class:~tsdynamics.families.DelaySystem's state is a
history function, not a point. Ask for it with hasattr before
calling it on an arbitrary view.
| PARAMETER | DESCRIPTION |
|---|---|
u
|
The new state, in the inner system's coordinates.
TYPE:
|
See Also
reinit : restart from a state — the one that also resets time.
Source code in src/tsdynamics/derived/_base.py
time
¶
time() -> float
Return the current time — iteration count for a discrete view.
| RETURNS | DESCRIPTION |
|---|---|
float
|
Elapsed time of the inner system. Note that for a
:class: |
Source code in src/tsdynamics/derived/_base.py
reinit
¶
Restart the view from state u (or from the system's own default).
The way to pick up a parameter change: a live stepper holds a lowered
tape, so edits to params do not reach it until you reinitialise.
| PARAMETER | DESCRIPTION |
|---|---|
u
|
Initial state.
TYPE:
|
**kwargs
|
Forwarded verbatim to the inner system's
TYPE:
|
Source code in src/tsdynamics/derived/_base.py
run
¶
run(*args: Any, **kwargs: Any) -> Trajectory
Produce this derived view's trajectory — always from a fresh start.
The one trajectory verb, on a wrapper too::
section = Rossler().poincare("y", 0.0).run(steps=500)
Before v6 a wrapper's run continued the live stepper, so
pmap.run(steps=5) twice returned different data. One rule now, on
every family and every wrapper: run() is a fresh run from ic, and
step() is the one that continues.
Source code in src/tsdynamics/derived/_base.py
plot
¶
Draw this derived view.
All five wrappers used to answer hasattr(pm, "plot") -> False while
the plot seam was True: the seam was there and the verb was not.