Skip to content

Commit dff5206

Browse files
committed
allow legacy matrix representation
1 parent 3de4956 commit dff5206

1 file changed

Lines changed: 86 additions & 7 deletions

File tree

control/stochsys.py

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
__email__ = "murray@cds.caltech.edu"
1818

1919
import numpy as np
20+
import scipy as sp
21+
from math import sqrt
2022

2123
from .iosys import InputOutputSystem, NonlinearIOSystem
2224
from .lti import LTI, isctime, isdtime
2325
from .mateqn import care, dare, _check_shape
2426
from .statesp import StateSpace, _ssmatrix
2527
from .exception import ControlArgument, ControlNotImplemented
2628

27-
__all__ = ['lqe','dlqe', 'create_estimator_iosystem']
29+
__all__ = ['lqe','dlqe', 'create_estimator_iosystem', 'white_noise',
30+
'correlation']
2831

2932

3033
# contributed by Sawyer B. Fuller <minster@uw.edu>
@@ -409,18 +412,18 @@ def create_estimator_iosystem(
409412

410413
else:
411414
# Create an I/O system for the state feedback gains
415+
# Note: reshape various vectors into column vectors for legacy matrix
412416
def _estim_update(t, x, u, params):
413417
# See if we are estimating or predicting
414418
correct = params.get('correct', True)
415419

416-
# Get the state of the estimator
417-
x = np.array(x) # bug fix for python-control 0.9.1
418-
xhat = x[0:sys.nstates]
420+
# Get the state of the estimator
421+
xhat = x[0:sys.nstates].reshape(-1, 1)
419422
P = x[sys.nstates:].reshape(sys.nstates, sys.nstates)
420423

421424
# Extract the inputs to the estimator
422-
y = u[0:C.shape[0]]
423-
u = u[C.shape[0]:]
425+
y = u[0:C.shape[0]].reshape(-1, 1)
426+
u = u[C.shape[0]:].reshape(-1, 1)
424427

425428
# Compute the optimal again
426429
Reps_inv = np.linalg.inv(RN + C @ P @ C.T)
@@ -437,7 +440,7 @@ def _estim_update(t, x, u, params):
437440
dP -= A @ P @ C.T @ Reps_inv @ C @ P @ A.T
438441

439442
# Return the update
440-
return np.hstack([dxhat, dP.reshape(-1)])
443+
return np.hstack([dxhat.reshape(-1), dP.reshape(-1)])
441444

442445
def _estim_output(t, x, u, params):
443446
return x[0:sys.nstates]
@@ -447,3 +450,79 @@ def _estim_output(t, x, u, params):
447450
_estim_update, _estim_output, states=state_labels + covariance_labels,
448451
inputs=sys.output_labels + sys.input_labels, outputs=output_labels,
449452
dt=sys.dt)
453+
454+
455+
def white_noise(T, Q, dt=0):
456+
"""Generate a white noise signal with specified intensity.
457+
458+
This function generates a (multi-variable) white noise signal of
459+
specified intensity as either a sampled continous time signal or a
460+
discrete time signal. A white noise signal along a 1D array
461+
of linearly spaced set of times T can be computing using
462+
463+
V = ct.white_noise(T, Q, dt)
464+
465+
where Q is a positive definite matrix providing the noise intensity.
466+
467+
In continuous time, the white noise signal is scaled such that the
468+
integral of the covariance over a sample period is Q, thus approximating
469+
a white noise signal. In discrete time, the white noise signal has
470+
covariance Q at each point in time (without any scaling based on the
471+
sample time).
472+
473+
"""
474+
# Check the shape of the input arguments
475+
if len(T.shape) != 1:
476+
raise ValueError("Time vector T must be 1D")
477+
if len(Q.shape) != 2 or Q.shape[0] != Q.shape[1]:
478+
raise ValueError("Covariance matrix Q must be square")
479+
480+
# Figure out the time increment
481+
if dt != 0:
482+
# Discrete time system => white noise is not scaled
483+
dt = 1
484+
else:
485+
dt = T[1] - T[0]
486+
487+
# Make sure data points are equally spaced
488+
if not np.allclose(np.diff(T), T[1] - T[0]):
489+
raise ValueError("Time values must be equally spaced.")
490+
491+
# Generate independent white noise sources for each input
492+
W = np.array([
493+
np.random.normal(0, 1/sqrt(dt), T.size) for i in range(Q.shape[0])])
494+
495+
# Return a linear combination of the noise sources
496+
return sp.linalg.sqrtm(Q) @ W
497+
498+
def correlation(T, X, Y=None, dt=0, squeeze=True):
499+
T = np.atleast_1d(T)
500+
X = np.atleast_2d(X)
501+
Y = np.atleast_2d(Y) if Y is not None else X
502+
503+
# Check the shape of the input arguments
504+
if len(T.shape) != 1:
505+
raise ValueError("Time vector T must be 1D")
506+
if len(X.shape) != 2 or len(Y.shape) != 2:
507+
raise ValueError("Signals X and Y must be 2D arrays")
508+
if T.shape[0] != X.shape[1] or T.shape[0] != Y.shape[1]:
509+
raise ValueError("Signals X and Y must have same length as T")
510+
511+
# Figure out the time increment
512+
if dt != 0:
513+
raise NotImplementedError("Discrete time systems not yet supported")
514+
else:
515+
dt = T[1] - T[0]
516+
517+
# Make sure data points are equally spaced
518+
if not np.allclose(np.diff(T), T[1] - T[0]):
519+
raise ValueError("Time values must be equally spaced.")
520+
521+
# Compute the correlation matrix
522+
R = np.array(
523+
[[sp.signal.correlate(X[i], Y[j])
524+
for i in range(X.shape[0])] for j in range(Y.shape[0])]
525+
) * dt / (T[-1] - T[0])
526+
tau = sp.signal.correlation_lags(len(X[0]), len(Y[0])) * dt
527+
528+
return tau, R.squeeze() if squeeze else R

0 commit comments

Comments
 (0)