import os, sys
import numpy as np
sys.path.append('.')
import c4dynamics as c4d
from numpy.typing import NDArray
from typing import Any
import warnings
[docs]
class state:
'''
Custom state object.
A state object represents a state vector and other attributes
that form an entity of a physical (dynamic) system.
A custom state object means any set of state variables is possible,
while pre-defined states from the :mod:`state library<c4dynamics.states.lib>`
are ready to use out of the box.
Keyword Arguments
=================
**kwargs : float or int
Keyword arguments representing the variables and their initial conditions.
Each key is a variable name and each value is its initial condition.
For example: :code:`s = c4d.state(x = 0, theta = 3.14)`
See Also
========
.states
Examples
========
``Pendulum``
.. code::
>>> s = c4d.state(theta = 10 * c4d.d2r, omega = 0) # doctest: +IGNORE_OUTPUT
[ θ ω ]
``Strapdown navigation system``
.. code::
>>> s = c4d.state(x = 0, y = 0, z = 0, vx = 0, vy = 0, vz = 0, q0 = 0, q1 = 0, q2 = 0, q3 = 0, bax = 0, bay = 0, baz = 0) # doctest: +IGNORE_OUTPUT
[ x y z vx vy vz q0 q1 q2 q3 bax bay baz ]
``Objects tracker``
.. code::
>>> s = c4d.state(x = 960, y = 540, w = 20, h = 10) # doctest: +IGNORE_OUTPUT
[ x y w h ]
``Aircraft``
.. code::
>>> s = c4d.state(x = 0, y = 0, z = 0, vx = 0, vy = 0, vz = 0, phi = 0, theta = 0, psi = 0, p = 0, q = 0, r = 0) # doctest: +IGNORE_OUTPUT
[ x y z vx vy vz φ θ Ψ p q r ]
``Self-driving car``
.. code::
>>> s = c4d.state(x = 0, y = 0, v = 0, theta = 0, omega = 0) # doctest: +IGNORE_OUTPUT
[ x y v θ ω ]
``Robot arm``
.. code::
>>> s = c4d.state(theta1 = 0, theta2 = 0, omega1 = 0, omega2 = 0) # doctest: +IGNORE_OUTPUT
[ θ1 θ2 ω1 ω2 ]
'''
# Α α # Β β # Γ γ # Δ δ # Ε ε # Ζ ζ # Η η # Θ θ # Ι ι
# Κ κ # Λ λ # Μ μ # Ν ν # Ξ ξ # Ο ο # Π π # Ρ ρ # Σ σ/ς
# Τ τ # Υ υ # Φ φ # Χ χ # Ψ ψ # Ω ω
_greek_unicode = (
('alpha', '\u03B1'), ('beta', '\u03B2'), ('gamma', '\u03B3'), ('delta', '\u03B4'),
('epsilon', '\u03B5'), ('zeta', '\u03B6'), ('eta', '\u03B7'), ('theta', '\u03B8'),
('iota', '\u03B9'), ('kappa', '\u03BA'), ('lambda', '\u03BB'), ('mu', '\u03BC'),
('nu', '\u03BD'), ('xi', '\u03BE'), ('omicron', '\u03BF'), ('pi', '\u03C0'),
('rho', '\u03C1'), ('sigma', '\u03C3'), ('final_sigma', '\u03C2'), ('tau', '\u03C4'),
('upsilon', '\u03C5'), ('phi', '\u03C6'), ('chi', '\u03C7'), ('psi', '\u03C8'),
('omega', '\u03C9'), ('Alpha', '\u0391'), ('Beta', '\u0392'), ('Gamma', '\u0393'),
('Delta', '\u0394'), ('Epsilon', '\u0395'), ('Zeta', '\u0396'), ('Eta', '\u0397'),
('Theta', '\u0398'), ('Iota', '\u0399'), ('Kappa', '\u039A'), ('Lambda', '\u039B'),
('Mu', '\u039C'), ('Nu', '\u039D'), ('Xi', '\u039E'), ('Omicron', '\u039F'),
('Pi', '\u03A0'), ('Rho', '\u03A1'), ('Sigma', '\u03A3'), ('Tau', '\u03A4'),
('Upsilon', '\u03A5'), ('Phi', '\u03A6'), ('Chi', '\u03A7'), ('Psi', '\u03A8'), ('Omega', '\u03A9'))
#
_reserved_keys = ('X', 'X0', 'P', 'V')
def __init__(self, **kwargs):
# TODO enable providing type for the setter.X output.
# the problem with this is it cannot be used with seeker and other c4d objects
# that takes datapoint objects.
# beacuse sometimes it misses attributes such as y, z that are necessary for poistion etc
# alternatives:
# 1. all the attributes always exist but they are muted and most importantly not reflected
# in the state vector when doing dp.X
# 2. they will not be used in this fucntions.
# 3. the user must provide his implementations for poisiton velcity etc.. like used in the P() function that there i
# encountered the problem.
self._data = [] # for permanent class variables (t, x, y .. )
self._prmdata = {} # for user additional variables
self._didx = {'t': 0}
for i, (k, v) in enumerate(kwargs.items()):
if k in self._reserved_keys:
raise ValueError(f"{k} is a reserved key. Keys {self._reserved_keys} cannot use as variable names.")
setattr(self, k, v)
setattr(self, k + '0', v)
self._didx[k] = 1 + i
def __str__(self):
self_str = '[ '
for i, s in enumerate(self._didx.keys()):
if s == 't': continue
s = dict(self._greek_unicode).get(s, s)
if i < len(self._didx.keys()) - 1:
self_str += s + ' '
else:
self_str += s + ' ]'
return self_str
def __repr__(self):
# NOTE i think maybe to switch repr and str so
# when i print >>> s it show the variables and when
# i do >>> print(s) it show the entire description.
# but then i need to iterate all the examples and remove the print from state presentations
param_names = ", ".join(self._prmdata.keys())
return (f"<state object>\n"
f"State Variables: {self.__str__()}\n"
f"Initial Conditions (X0): {self.X0}\n"
f"Current State Vector (X): {self.X}\n"
f"Parameters: {param_names if param_names else 'None'}")
# def __setattr__(self, param, value):
# if param in self._reserved_keys:
# raise AttributeError(f"{param} is a reserved key. Keys {self._reserved_keys} cannot use as parameter names.")
# else:
# super().__setattr__(param, value)
#
# state operations
##
@property
def X(self) -> NDArray[Any]:
'''
Gets and sets the state vector variables.
Parameters
----------
x : array_like
Values vector to set the variables of the state.
Returns
-------
out : numpy.array
Values vector of the state.
Examples
--------
Getter:
.. code::
>>> s = c4d.state(x1 = 0, x2 = -1)
>>> s.X # doctest: +NUMPY_FORMAT
[0 -1]
Setter:
.. code::
>>> s = c4d.state(x1 = 0, x2 = -1)
>>> s.X += [0, 1] # equivalent to: s.X = s.X + [0, 1]
>>> s.X # doctest: +NUMPY_FORMAT
[0 0]
:class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>` getter - setter:
.. code::
>>> dp = c4d.datapoint()
>>> dp.X # doctest: +NUMPY_FORMAT
[0 0 0 0 0 0]
>>> # x y z vx vy vz
>>> dp.X = [1000, 100, 0, 0, 0, -100]
>>> dp.X # doctest: +NUMPY_FORMAT
[1000 100 0 0 0 -100]
'''
xout = []
for k in self._didx.keys():
if k == 't': continue
# the alteast_1d() + the flatten() is necessary to
# cope with non-homogenuous array
xout.append(np.atleast_1d(eval('self.' + k)))
#
# XXX why float64? maybe it's just some default unlsess anything else required.
# pixelpoint:override the .X property: will distance the devs from a datapoint class.
# con: much easier
#
# return np.array(xout).flatten().astype(np.float64)
return np.array(xout).ravel().astype(np.float64)
@X.setter
def X(self, Xin):
# Xin = np.atleast_1d(Xin).flatten()
# i think to replace flatten() with ravel() is
# safe also here because Xin is iterated over its elements which are
# mutable. but lets keep tracking.
Xin = np.atleast_1d(Xin).ravel()
xlen = len(Xin)
Xlen = len(self.X)
if xlen < Xlen:
raise ValueError(f'Partial vector assignment, len(Xin) = {xlen}, len(X) = {Xlen}', 'r')
elif xlen > Xlen:
raise ValueError(f'The length of the input state is bigger than X, len(Xin) = {xlen}, len(X) = {Xlen}')
for i, k in enumerate(self._didx.keys()):
if k == 't': continue
if i > xlen: break
setattr(self, k, Xin[i - 1])
@property
def X0(self):
'''
Returns the initial conditions of the state vector.
The initial conditions are determined at the stage of constructing
the state object.
Modifying the initial conditions is possible by direct assignment
of the state variable with a '0' suffix. For a state variable
:math:`s.x`, its initial condition is modifyied by:
:code:`s.x0 = x0`, where :code:`x0` is an arbitrary parameter.
Returns
-------
out : numpy.array
An array representing the initial values of the state variables.
Examples
--------
.. code::
>>> s = c4d.state(x1 = 0, x2 = -1)
>>> s.X += [0, 1]
>>> s.X0 # doctest: +NUMPY_FORMAT
[0 -1]
.. code::
>>> s = c4d.state(x1 = 1, x2 = 1)
>>> s.X0 # doctest: +NUMPY_FORMAT
[1 1]
>>> s.x10 = s.x20 = 0
>>> s.X0 # doctest: +NUMPY_FORMAT
[0 0]
'''
xout = []
for k in self._didx.keys():
if k == 't': continue
xout.append(eval('self.' + k + '0'))
return np.array(xout)
[docs]
def addvars(self, **kwargs):
'''
Add state variables.
Adding variables to the state outside the :class:`state <c4dynamics.states.state.state>`
constructor is possible by using :meth:`addvars() <c4dynamics.states.state.state.addvars>`.
Parameters
----------
**kwargs : float or int
Keyword arguments representing the variables and their initial conditions.
Each key is a variable name and each value is its initial condition.
Note
----
If :meth:`store() <c4dynamics.states.state.state.store>` is called before
adding the new variables, then the time histories of the new states
are filled with zeros to maintain the same size as the other state variables.
Examples
--------
.. code::
>>> s = c4d.state(x = 0, y = 0)
>>> print(s)
[ x y ]
>>> s.addvars(vx = 0, vy = 0)
>>> print(s)
[ x y vx vy ]
calling :meth:`store() <c4dynamics.states.state.state.store>` before
adding the new variables:
.. code::
>>> s = c4d.state(x = 1, y = 1)
>>> s.store()
>>> s.store()
>>> s.store()
>>> s.addvars(vx = 0, vy = 0)
>>> s.data('x')[1] # doctest: +NUMPY_FORMAT
[1 1 1]
>>> s.data('vx')[1] # doctest: +NUMPY_FORMAT
[0 0 0]
'''
b0 = len(self._didx)
for i, (k, v) in enumerate(kwargs.items()):
setattr(self, k, v)
setattr(self, k + '0', v)
self._didx[k] = b0 + i
if self._data:
# add zero columns at the size of the new vars to avoid wrong broadcasting.
dataarr = np.array(self._data)
dataarr = np.hstack((dataarr, np.zeros((dataarr.shape[0], b0))))
self._data = dataarr.tolist()
#
# data management operations
##
[docs]
def store(self, t = -1):
'''
Stores the current state.
The current state is defined by the vector of variables
as given by :attr:`state.X <c4dynamics.states.state.state.X>`.
:meth:`store() <c4dynamics.states.state.state.store>` is used to store the
instantaneous state variables.
Parameters
----------
t : float or int, optional
Time stamp for the stored state.
Note
----
1. Time `t` is an optional parameter with a default value of :math:`t = -1`.
The time is always appended at the head of the array to store. However,
if `t` is not given, default :math:`t = -1` is stored instead.
2. The method :meth:`store() <c4dynamics.states.state.state.store>` goes together with
the methods :meth:`data() <c4dynamics.states.state.state.data>`
and :meth:`timestate() <c4dynamics.states.state.state.timestate>` as input and outputs.
3. :meth:`store() <c4dynamics.states.state.state.store>` only stores
state variables (those construct :attr:`state.X <c4dynamics.states.state.state.X>`).
For other parameters, use :meth:`storeparams() <c4dynamics.states.state.state.storeparams>`.
Examples
--------
.. code::
>>> s = c4d.state(x = 1, y = 0, z = 0)
>>> s.store()
**Store with time stamp:**
.. code::
>>> s = c4d.state(x = 1, y = 0, z = 0)
>>> s.store(t = 0.5)
**Store in a for-loop:**
.. code::
>>> s = c4d.state(x = 1, y = 0, z = 0)
>>> for t in np.linspace(0, 1, 3):
... s.X = np.random.rand(3)
... s.store(t)
Usage of :meth:`store() <c4dynamics.states.state.state.store>`
inside a program with a :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>`
from the :mod:`states library <c4dynamics.states.lib>`:
.. code::
>>> t = 0
>>> dt = 1e-3
>>> h0 = 100
>>> dp = c4d.datapoint(z = h0)
>>> while dp.z >= 0:
... dp.inteqm([0, 0, -c4d.g_ms2], dt) # doctest: +IGNORE_OUTPUT
... t += dt
... dp.store(t)
>>> for z in dp.data('z'): # doctest: +IGNORE_OUTPUT
... print(z)
99.9999950
99.9999803
99.9999558
...
0.00033469
-0.0439570
'''
self._data.append([t] + self.X.tolist())
[docs]
def storeparams(self, params, t = -1.0):
'''
Stores parameters.
Parameters are data attributes which are not part of the state vector.
:meth:`storeparams() <c4dynamics.states.state.state.storeparams>` is
used to store the instantaneous parameters.
Parameters
----------
params : str or list of str
Name or names of the parameters to store.
t : float or int, optional
Time stamp for the stored state.
Note
----
1. Time `t` is an optional parameter with a default value of :math:`t = -1`.
The time is always appended at the head of the array to store. However,
if `t` is not given, default :math:`t = -1` is stored instead.
2. The method :meth:`storeparams() <c4dynamics.states.state.state.storeparams>`
goes together with the method :meth:`data() <c4dynamics.states.state.state.data>`
as input and output.
Examples
--------
.. code::
>>> s = c4d.state(x = 100, vx = 10)
>>> s.mass = 25
>>> s.storeparams('mass')
>>> s.data('mass')[1] # doctest: +NUMPY_FORMAT
[25]
**Store with time stamp:**
.. code::
>>> s = c4d.state(x = 100, vx = 10)
>>> s.mass = 25
>>> s.storeparams('mass', t = 0.1)
>>> s.data('mass')
(array([0.1]), array([25.]))
**Store multiple parameters:**
.. code::
>>> s = c4d.state(x = 100, vx = 10)
>>> s.x_std = 5
>>> s.vx_std = 10
>>> s.storeparams(['x_std', 'vx_std'])
>>> s.data('x_std')[1] # doctest: +NUMPY_FORMAT
[5]
>>> s.data('vx_std')[1] # doctest: +NUMPY_FORMAT
[10]
**Objects classification:**
.. code::
>>> s = c4d.state(x = 25, y = 25, w = 20, h = 10)
>>> np.random.seed(44)
>>> for i in range(3):
... s.X += 1
... s.w, s.h = np.random.randint(0, 50, 2)
... if s.w > 40 or s.h > 20:
... s.class_id = 'truck'
... else:
... s.class_id = 'car'
... s.store() # stores the state
... s.storeparams('class_id') # store the class_id parameter
>>> print(' x y w h class') # doctest: +IGNORE_OUTPUT
>>> print(np.hstack((s.data()[:, 1:].astype(int), np.atleast_2d(s.data('class_id')[1]).T))) # doctest: +IGNORE_OUTPUT
x y w h class
26 26 20 35 truck
27 27 49 45 car
28 28 3 32 car
The `morphospectra` implements a custom method `getdim` to update
the dimension parameter `dim` with respect to the position coordinates:
.. code::
>>> import types
>>> #
>>> def getdim(s):
... if s.X[2] != 0:
... # z
... s.dim = 3
... elif s.X[1] != 0:
... # y
... s.dim = 2
... elif s.X[0] != 0:
... # x
... s.dim = 1
... else:
... # none
... s.dim = 0
>>> #
>>> morphospectra = c4d.state(x = 0, y = 0, z = 0)
>>> morphospectra.dim = 0
>>> morphospectra.getdim = types.MethodType(getdim, morphospectra)
>>> #
>>> for r in range(10):
... morphospectra.X = np.random.choice([0, 1], 3)
... morphospectra.getdim()
... morphospectra.store()
... morphospectra.storeparams('dim')
>>> #
>>> print('x y z | dim') # doctest: +IGNORE_OUTPUT
>>> print('------------') # doctest: +IGNORE_OUTPUT
>>> for x, dim in zip(morphospectra.data().astype(int)[:, 1 : 4].tolist(), morphospectra.data('dim')[1].tolist()): # doctest: +IGNORE_OUTPUT
... print(*(x + [' | '] + [dim]))
x y z | dim
------------
0 1 0 | 2
1 1 0 | 2
1 0 0 | 1
0 1 1 | 3
1 0 0 | 1
1 1 1 | 3
1 1 0 | 2
1 1 0 | 2
1 0 1 | 3
1 0 1 | 3
'''
# TODO show example of multiple vars
# maybe the example of the kalman variables store
# from the detect track exmaple.
# TODO add test if the params is not 0 or 1 dim throw error or warning. why?
# TODO document about the two if's down here: nonscalar param and empty param.
from functools import reduce
lparams = params if isinstance(params, list) else [params]
for p in lparams:
if p not in self._prmdata:
self._prmdata[p] = []
vval = np.atleast_1d(reduce(getattr, p.split('.'), self)).flatten()
if len(vval) == 0: # empty, convert to none to keep homogenuous array
vval = np.atleast_1d(np.nan)
elif len(vval) > 1:
# c4d.cprint(f'{p} is not a scalar. only first item is stored', 'r')
warnings.warn(f'{p} is not a scalar. Only first item is stored', c4d.c4warn)
vval = vval[:1]
self._prmdata[p].append([t] + vval.tolist())
[docs]
def data(self, var = None, scale = 1.):
'''
Returns arrays of stored time and data.
:meth:`data() <c4dynamics.states.state.state.data>` returns a tuple containing two numpy arrays:
the first consists of timestamps, and the second
contains the values of a `var` corresponding to those timestamps.
`var` may be each one of the state variables or the parameters.
If `var` is not introduced, :meth:`data() <c4dynamics.states.state.state.data>`
returns a single array of the entire state histories.
If data were not stored, :meth:`data() <c4dynamics.states.state.state.data>`
returns an empty array.
Parameters
----------
var : str
The name of the variable or parameter of the required histories.
scale : float or int, optional
A scaling factor to apply to the variable values, by default 1.
Returns
-------
out : array or tuple of numpy arrays
if `var` is introduced, `out` is a tuple of a timestamps array
and an array of `var` values corresponding to those timestamps.
If `var` is not introduced, then :math:`n \\times m+1` numpy array is returned,
where `n` is the number of stored samples, and `m+1` is the
number of state variables and times.
Examples
--------
Get all stored data:
.. code::
>>> np.random.seed(100) # to reproduce results
>>> s = c4d.state(x = 1, y = 0, z = 0)
>>> for t in np.linspace(0, 1, 3):
... s.X = np.random.rand(3)
... s.store(t)
>>> s.data() # doctest: +NUMPY_FORMAT
[[0. 0.543 0.278 0.424]
[0.5 0.845 0.005 0.121]
[1. 0.671 0.826 0.137]]
Data of a variable:
.. code::
>>> time, x_data = s.data('x')
>>> time # doctest: +NUMPY_FORMAT
[0. 0.5 1.]
>>> x_data # doctest: +NUMPY_FORMAT
[0.543 0.845 0.671]
>>> s.data('y')[1] # doctest: +NUMPY_FORMAT
[0.278 0.005 0.826]
Get data with scaling:
.. code::
>>> s = c4d.state(phi = 0)
>>> for p in np.linspace(0, c4d.pi):
... s.phi = p
... s.store()
>>> s.data('phi', c4d.r2d)[1] # doctest: +IGNORE_OUTPUT
[0 3.7 7.3 ... 176.3 180]
Data of a parameter
.. code::
>>> s = c4d.state(x = 100, vx = 10)
>>> s.mass = 25
>>> s.storeparams('mass', t = 0.1)
>>> s.data('mass')
(array([0.1]), array([25.]))
'''
if var is None:
# return all
# XXX not sure this is applicable to the new change where arrays\ matrices
# are also stored.
# in fact the matrices are not stored in _data but in _prmdata
return np.array(self._data)
idx = self._didx.get(var, -1)
if idx >= 0:
if not self._data:
# empty array
# c4d.cprint('Warning: no history of state samples.', 'r')
warnings.warn(f"""No history of state samples.""" , c4d.c4warn)
return np.array([])
if idx == 0:
return np.array(self._data)[:, 0]
return (np.array(self._data)[:, 0], np.array(self._data)[:, idx] * scale)
# else \ user defined variables
if var not in self._prmdata:
# c4d.cprint('Warning: no history samples of ' + var + '.', 'r')
warnings.warn(f"""No history samples of {var}.""" , c4d.c4warn)
return np.array([])
# if the var is text, dont multiply by scale
if np.issubdtype(np.array(self._prmdata[var])[:, 1].dtype, np.number):
return (np.array(self._prmdata[var])[:, 0], np.array(self._prmdata[var])[:, 1] * scale)
else:
return (np.array(self._prmdata[var])[:, 0], np.array(self._prmdata[var])[:, 1])
[docs]
def timestate(self, t):
'''
Returns the state as stored at time `t`.
The method searches the closest time
to time `t` in the sampled histories and
returns the state that stored at the time.
If data were not stored returns None.
Parameters
----------
t : float or int
The time at the required sample.
Returns
-------
X : numpy.array
An array of the state vector
:attr:`state.X <c4dynamics.states.state.state.X>`
at time `t`.
Examples
--------
.. code::
>>> s = c4d.state(x = 0, y = 0, z = 0)
>>> for t in np.linspace(0, 1, 3):
... s.X += 1
... s.store(t)
>>> s.timestate(0.5) # doctest: +NUMPY_FORMAT
[2 2 2]
.. code::
>>> s = c4d.state(x = 1, y = 0, z = 0)
>>> s.timestate(0.5) # doctest: +IGNORE_OUTPUT
Warning: no history of state samples.
None
'''
# TODO what about throwing a warning when dt is too long?
# \\ what is dt and so what if its long?
times = self.data('t')
if len(times) == 0:
X = None
else:
idx = min(range(len(times)), key = lambda i: abs(times[i] - t))
X = np.array(self._data[idx][1:])
return X
[docs]
def plot(self, var, scale = 1, ax = None, filename = None, darkmode = True, **kwargs):
'''
Draws plots of variable evolution over time.
This method plots the evolution of a state variable over time.
The resulting plot can be saved to a directory if specified.
Parameters
----------
var : str
The name of the variable or parameter to be plotted.
scale : float or int, optional
A scaling factor to apply to the variable values. Defaults to `1`.
ax : matplotlib.axes.Axes, optional
An existing Matplotlib axis to plot on.
If None, a new figure and axis will be created, by default None.
filename : str, optional
Full file name to save the plot image.
If None, the plot will not be saved, by default None.
darkmode : bool, optional
Directory path to save the plot image.
If None, the plot will not be saved, by default None.
**kwargs : dict, optional
Additional key-value arguments passed to `matplotlib.pyplot.plot`.
These can include any keyword arguments accepted by `plot`,
such as `color`, `linestyle`, `marker`, etc.
Returns
-------
ax : matplotlib.axes.Axes.
Matplotlib axis of the derived plot.
Note
----
- The default `color` is set to `'m'` (magenta).
- The default `linewidth` is set to `1.2`.
Examples
--------
Import required packages:
.. code::
>>> import c4dynamics as c4d
>>> from matplotlib import pyplot as plt
>>> import numpy as np
Plot an arbitrary state variable and save:
.. code::
>>> s = c4d.state(x = 0, y = 0)
>>> s.store()
>>> for _ in range(100):
... s.x = np.random.randint(0, 100, 1)
... s.store()
>>> s.plot('x', filename = 'x.png') # doctest: +IGNORE_OUTPUT
>>> plt.show()
.. figure:: /_examples/state/plot_x.png
**Interactive mode:**
.. code::
>>> plt.switch_backend('TkAgg')
>>> s.plot('x') # doctest: +IGNORE_OUTPUT
>>> plt.show(block = True)
**Dark mode off:**
>>> s = c4d.state(x = 0)
>>> s.xstd = 0.2
>>> for t in np.linspace(-2 * c4d.pi, 2 * c4d.pi, 1000):
... s.x = c4d.sin(t) + np.random.randn() * s.xstd
... s.store(t)
>>> s.plot('x', darkmode = False) # doctest: +IGNORE_OUTPUT
>>> plt.show()
.. figure:: /_examples/state/plot_darkmode.png
**Scale plot:**
.. code::
>>> s = c4d.state(phi = 0)
>>> for y in c4d.tan(np.linspace(-c4d.pi, c4d.pi, 500)):
... s.phi = c4d.atan(y)
... s.store()
>>> s.plot('phi', scale = c4d.r2d) # doctest: +IGNORE_OUTPUT
>>> plt.gca().set_ylabel('deg') # doctest: +IGNORE_OUTPUT
>>> plt.show()
.. figure:: /_examples/state/plot_scale.png
**Given axis:**
.. code::
>>> plt.subplots(1, 1) # doctest: +IGNORE_OUTPUT
>>> plt.plot(np.linspace(-c4d.pi, c4d.pi, 500) * c4d.r2d, 'm') # doctest: +IGNORE_OUTPUT
>>> s.plot('phi', scale = c4d.r2d, ax = plt.gca(), color = 'c') # doctest: +IGNORE_OUTPUT
>>> plt.gca().set_ylabel('deg') # doctest: +IGNORE_OUTPUT
>>> plt.legend(['θ', 'φ']) # doctest: +IGNORE_OUTPUT
>>> plt.show()
.. figure:: /_examples/state/plot_axis.png
Top view + side view - options of :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>`
and :class:`rigidbody <c4dynamics.states.lib.rigidbody.rigidbody>` objects:
.. code::
>>> dt = 0.01
>>> floating_balloon = c4d.datapoint(vx = 10 * c4d.k2ms)
>>> floating_balloon.mass = 0.1
>>> for t in np.arange(0, 10, dt):
... floating_balloon.inteqm(forces = [0, 0, .05], dt = dt) # doctest: +IGNORE_OUTPUT
... floating_balloon.store(t)
>>> floating_balloon.plot('side')
>>> plt.gca().invert_yaxis()
>>> plt.show()
.. figure:: /_examples/state/plot_dp_inteqm3.png
'''
from matplotlib import pyplot as plt
#
# points to consider
# -------------------
#
# figsize
# -------
#
# i think the challenge here is to get view + save images with
# 1920 x 1080 pixels (Full HD)
# 72 DPI (the standard for web images). no way. 72dpi is poor res. at least 300.
# --> alternative: 960x540 600dpi. #
#
# get the screen dpi to get the desired resolution.
#
#
#
# backends
# --------
#
# non-interactive backends: Agg, SVG, PDF:
# When saving plots to files.
# include plt.show()
# interactive backends:
# TkAgg, Qt5Agg, etc.
# dont include plt.show()
#
# Check Backend: matplotlib.get_backend().
# avoid hardcoding backend settings.
# Avoid using features that are specific to certain backends.
# Users should be able to override backend settings.
#
# Test your plotting functions across different backends.
#
# finally i think the best soultion regardint backends is not
# to do anythink and let the user select the backend from outside.
##
if var not in self._didx:
warnings.warn(f"""{var} is not a state variable.""" , c4d.c4warn)
return None
if not self._data:
warnings.warn(f"""No stored data for {var}.""" , c4d.c4warn)
return None
if darkmode:
plt.style.use('dark_background')
else:
plt.style.use('default')
# plt.switch_backend('TkAgg')
# plt.switch_backend('TkAgg')
# try:
# from IPython import get_ipython
# if get_ipython() is None:
# return False
# else:
# return True
# except ImportError:
# return False
# Set default values in kwargs only if the user hasn't provided them
kwargs.setdefault('color', 'm')
kwargs.setdefault('linewidth', 1.2)
if ax is None:
# factorsize = 4
# aspectratio = 1080 / 1920
# _, ax = plt.subplots(1, 1, dpi = 200
# , figsize = (factorsize, factorsize * aspectratio)
# , gridspec_kw = {'left': 0.15, 'right': .9
# , 'top': .9, 'bottom': .2})
_, ax = c4d._figdef()
if not len(np.flatnonzero(self.data('t') != -1)): # values for t weren't stored
x = range(len(self.data('t'))) # t is just indices
xlabel = 'Samples'
else:
x = self.data('t')
xlabel = 'Time'
y = np.array(self._data)[:, self._didx[var]] * scale if self._data else np.empty(1) # used selection
if dict(self._greek_unicode).get(var, '') != '':
title = '$\\' + var + '$'
else:
title = var
ax.plot(x, y, **kwargs)
c4d.plotdefaults(ax, title, xlabel, '', 8)
if filename:
# plt.tight_layout(pad = 0)
plt.savefig(filename, bbox_inches = 'tight', pad_inches = .2, dpi = 600)
# plt.show(block = True)
return ax
#
# math operations
##
@property
def norm(self):
'''
Returns the Euclidean norm of the state vector.
Returns
-------
out : float
The computed norm of the state vector. The return type specifically is a numpy.float64.
Examples
--------
.. code::
>>> s = c4d.state(x1 = 1, x2 = -1)
>>> s.norm # doctest: +ELLIPSIS
1.414...
'''
return np.linalg.norm(self.X)
@property
def normalize(self):
'''
Returns a unit vector representation of the state vector.
Returns
-------
out : numpy.array
A normalized vector of the same direction and shape as `self.X`, where the norm of the vector is `1`.
Examples
--------
.. code::
>>> s = c4d.state(x = 1, y = 2, z = 3)
>>> s.normalize # doctest: +NUMPY_FORMAT
[0.267 0.534 0.801]
'''
return self.X / np.linalg.norm(self.X)
# cartesian operations
@property
def position(self):
'''
Returns a vector of position coordinates.
If the state doesn't include any position coordinate (x, y, z),
an empty array is returned.
Note
----
In the context of :attr:`position <c4dynamics.states.state.state.position>`,
only x, y, z, (case sensitive) are considered position coordinates.
Returns
-------
out : numpy.array
A vector containing the values of three position coordinates.
Examples
--------
.. code::
>>> s = c4d.state(theta = 3.14, x = 1, y = 2)
>>> s.position # doctest: +NUMPY_FORMAT
[1 2 0]
.. code::
>>> s = c4d.state(theta = 3.14, x = 1, y = 2, z = 3)
>>> s.position # doctest: +NUMPY_FORMAT
[1 2 3]
.. code::
>>> s = c4d.state(theta = 3.14, z = -100)
>>> s.position # doctest: +NUMPY_FORMAT
[0 0 -100]
.. code::
>>> s = c4d.state(theta = 3.14)
>>> s.position # doctest: +IGNORE_OUTPUT
Position is valid when at least one cartesian coordinate variable (x, y, z) exists...
[]
'''
if not self.cartesian():
# c4d.cprint('Warning: position is valid when at least one cartesian'
# ' coordinate variable (x, y, z) exists.', 'm')
warnings.warn(f"""Position is valid when at least one cartesian """
"""coordinate variable (x, y, z) exists.""" , c4d.c4warn)
return np.array([])
return np.array([getattr(self, var, 0) for var in ['x', 'y', 'z']])
@property
def velocity(self):
'''
Returns a vector of velocity coordinates.
If the state doesn't include any velocity coordinate (vx, vy, vz),
an empty array is returned.
Note
----
In the context of :attr:`velocity <c4dynamics.states.state.state.velocity>`,
only vx, vy, vz, (case sensitive) are considered velocity coordinates.
Returns
-------
out : numpy.array
A vector containing the values of three velocity coordinates.
Examples
--------
.. code::
>>> s = c4d.state(x = 100, y = 0, vx = -10, vy = 5)
>>> s.velocity # doctest: +NUMPY_FORMAT
[-10 5 0]
.. code::
>>> s = c4d.state(x = 100, vz = -100)
>>> s.velocity # doctest: +NUMPY_FORMAT
[0 0 -100]
.. code::
>>> s = c4d.state(z = 100)
>>> s.velocity # doctest: +IGNORE_OUTPUT
Warning: velocity is valid when at least one velocity coordinate variable (vx, vy, vz) exists.
[]
'''
if self.cartesian() < 2:
# c4d.cprint('Warning: velocity is valid when at least one velocity '
# 'coordinate variable (vx, vy, vz) exists.', 'm')
warnings.warn(f"""Velocity is valid when at least one velocity """
"""coordinate variable (vx, vy, vz) exists.""" , c4d.c4warn)
return np.array([])
return np.array([getattr(self, var, 0) for var in ['vx', 'vy', 'vz']])
[docs]
def P(self, state2 = None):
'''
Euclidean distance.
Calculates the Euclidean distance between the self state object and
a second object `state2`. If `state2` is not provided, then the self
Euclidean distance is calculated.
When a second state object is provided:
.. math::
P = \\sum_{k=x,y,z} (self.k - state2.k)^2
Otherwise:
.. math::
P = \\sum_{k=x,y,z} self.k^2
Raises
------
TypeError
If the states don't include any position coordinate (x, y, z).
Note
----
1. The provided states must have at least oneposition coordinate (x, y, z).
2. In the context of :meth:`P() <c4dynamics.states.state.state.P>`,
x, y, z, (case sensitive) are considered position coordinates.
Parameters
----------
state2 : :class:`state <c4dynamics.states.state.state>`
A second state object for which the relative distance is calculated.
Returns
-------
out : float
Euclidean norm of the distance vector. The return type specifically is a numpy.float64.
Examples
--------
.. code::
>>> import c4dynamics as c4d
.. code::
>>> s = c4d.state(theta = 3.14, x = 1, y = 1)
>>> s.P() # doctest: +ELLIPSIS
1.414...
.. code::
>>> s = c4d.state(theta = 3.14, x = 1, y = 1)
>>> s2 = c4d.state(x = 1)
>>> s.P(s2)
1.0
.. code::
>>> s = c4d.state(theta = 3.14, x = 1, y = 1)
>>> s2 = c4d.state(z = 1)
>>> s.P(s2) # doctest: +ELLIPSIS
1.73...
For final example, import required packages:
.. code::
>>> import numpy as np
>>> from matplotlib import pyplot as plt
Settings and initial conditions:
.. code::
>>> camera = c4d.state(x = 0, y = 0)
>>> car = c4d.datapoint(x = -100, vx = 40, vy = -7)
>>> dist = []
>>> time = np.linspace(0, 10, 1000)
Main loop:
.. code::
>>> for t in time:
... car.inteqm(np.zeros(3), time[1] - time[0]) # doctest: +IGNORE_OUTPUT
... dist.append(camera.P(car))
Show results:
.. code::
>>> plt.plot(time, dist, 'm') # doctest: +IGNORE_OUTPUT
>>> c4d.plotdefaults(plt.gca(), 'Distance', 'Time (s)', '(m)')
>>> plt.show()
.. figure:: /_examples/states/state_P.png
'''
if not self.cartesian():
raise TypeError('state must have at least one position coordinate (x, y, or z)')
if state2 is None:
state2 = c4d.datapoint()
else:
if not hasattr(state2, 'cartesian') or not state2.cartesian():
raise TypeError('state2 must be a state object with at least one position coordinate (x, y, or z)')
dist = 0
for var in ['x', 'y', 'z']:
dist += (getattr(self, var, 0) - getattr(state2, var, 0))**2
return np.sqrt(dist)
[docs]
def V(self):
'''
Velocity Magnitude.
Calculates the magnitude of the object velocity :
.. math::
V = \\sum_{k=v_x,v_y,v_z} self.k^2
If the state doesn't include any velocity coordinate (vx, vy, vz),
a `ValueError` is raised.
Returns
-------
out : float
Euclidean norm of the velocity vector. The return type specifically is a numpy.float64.
Raises
------
TypeError
If the state does not include any velocity coordinate (vx, vy, vz).
Note
----
In the context of :meth:`V() <c4dynamics.states.state.state.V>`,
vx, vy, vz, (case sensitive) are considered velocity coordinates.
Examples
--------
.. code::
>>> s = c4d.state(vx = 7, vy = 24)
>>> s.V()
25.0
.. code::
>>> s = c4d.state(x = 100, y = 0, vx = -10, vy = 7)
>>> s.V() # doctest: +ELLIPSIS
12.2...
Uncommenting the following line throws a type error:
.. code::
>>> s = c4d.state(x = 100, y = 0)
>>> # s.V()
TypeError: state must have at least one velocity coordinate (vx, vy, or vz)
'''
if self.cartesian() < 2:
raise TypeError('state must have at least one velocity coordinate (vx, vy, or vz)')
return np.linalg.norm(self.velocity)
def cartesian(self):
# TODO document!
if any([var for var in ['vx', 'vy', 'vz'] if hasattr(self, var)]):
return 2
elif any([var for var in ['x', 'y', 'z'] if hasattr(self, var)]):
return 1
else:
return 0
if __name__ == "__main__":
import doctest, contextlib
from c4dynamics import IgnoreOutputChecker, cprint
# Register the custom OutputChecker
doctest.OutputChecker = IgnoreOutputChecker
tofile = False
optionflags = doctest.FAIL_FAST
if tofile:
with open(os.path.join('tests', '_out', 'output.txt'), 'w') as f:
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
result = doctest.testmod(optionflags = optionflags)
else:
result = doctest.testmod(optionflags = optionflags)
if result.failed == 0:
cprint(os.path.basename(__file__) + ": all tests passed!", 'g')
else:
print(f"{result.failed}")