Skip to content

Remove NumPy matrix class #913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/doctest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
- name: Run doctest
shell: bash -l {0}
env:
PYTHON_CONTROL_ARRAY_AND_MATRIX: ${{ matrix.array-and-matrix }}
MPLBACKEND: ${{ matrix.mplbackend }}
working-directory: doc
run: |
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/python-package-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ jobs:
${{ matrix.slycot || 'no' }} Slycot;
${{ matrix.pandas || 'no' }} Pandas;
${{ matrix.cvxopt || 'no' }} CVXOPT
${{ matrix.array-and-matrix == 1 && '; array and matrix' || '' }}
${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }}
runs-on: ubuntu-latest

Expand All @@ -22,14 +21,12 @@ jobs:
pandas: [""]
cvxopt: ["", "conda"]
mplbackend: [""]
array-and-matrix: [0]
include:
- python-version: '3.11'
slycot: conda
pandas: conda
cvxopt: conda
mplbackend: QtAgg
array-and-matrix: 1

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -63,7 +60,6 @@ jobs:
- name: Test with pytest
shell: bash -l {0}
env:
PYTHON_CONTROL_ARRAY_AND_MATRIX: ${{ matrix.array-and-matrix }}
MPLBACKEND: ${{ matrix.mplbackend }}
run: pytest -v --cov=control --cov-config=.coveragerc control/tests

Expand Down
41 changes: 2 additions & 39 deletions control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

__all__ = ['defaults', 'set_defaults', 'reset_defaults',
'use_matlab_defaults', 'use_fbs_defaults',
'use_legacy_defaults', 'use_numpy_matrix']
'use_legacy_defaults']

# Package level default values
_control_defaults = {
Expand Down Expand Up @@ -202,7 +202,6 @@ def use_matlab_defaults():
The following conventions are used:
* Bode plots plot gain in dB, phase in degrees, frequency in
rad/sec, with grids
* State space class and functions use Numpy matrix objects

Examples
--------
Expand All @@ -211,7 +210,6 @@ def use_matlab_defaults():

"""
set_defaults('freqplot', dB=True, deg=True, Hz=False, grid=True)
set_defaults('statesp', use_numpy_matrix=True)


# Set defaults to match FBS (Astrom and Murray)
Expand All @@ -233,41 +231,6 @@ def use_fbs_defaults():
set_defaults('nyquist', mirror_style='--')


# Decide whether to use numpy.matrix for state space operations
def use_numpy_matrix(flag=True, warn=True):
"""Turn on/off use of Numpy `matrix` class for state space operations.

Parameters
----------
flag : bool
If flag is `True` (default), use the deprecated Numpy
`matrix` class to represent matrices in the `~control.StateSpace`
class and functions. If flat is `False`, then matrices are
represented by a 2D `ndarray` object.

warn : bool
If flag is `True` (default), issue a warning when turning on the use
of the Numpy `matrix` class. Set `warn` to false to omit display of
the warning message.

Notes
-----
Prior to release 0.9.x, the default type for 2D arrays is the Numpy
`matrix` class. Starting in release 0.9.0, the default type for state
space operations is a 2D array.

Examples
--------
>>> ct.use_numpy_matrix(True, False)
>>> # do some legacy calculations using np.matrix

"""
if flag and warn:
warnings.warn("Return type numpy.matrix is deprecated.",
stacklevel=2, category=DeprecationWarning)
set_defaults('statesp', use_numpy_matrix=flag)


def use_legacy_defaults(version):
""" Sets the defaults to whatever they were in a given release.

Expand Down Expand Up @@ -331,7 +294,7 @@ def use_legacy_defaults(version):
# Version 0.9.0:
if major == 0 and minor < 9:
# switched to 'array' as default for state space objects
set_defaults('statesp', use_numpy_matrix=True)
warnings.warn("NumPy matrix class no longer supported")

# switched to 0 (=continuous) as default timestep
set_defaults('control', default_dt=None)
Expand Down
2 changes: 0 additions & 2 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -2227,8 +2227,6 @@ def ss(*args, **kwargs):
y[k] &= C x[k] + D u[k]

The matrices can be given as *array like* data types or strings.
Everything that the constructor of :class:`numpy.matrix` accepts is
permissible here too.

``ss(args, inputs=['u1', ..., 'up'], outputs=['y1', ..., 'yq'], states=['x1', ..., 'xn'])``
Create a system with named input, output, and state signals.
Expand Down
22 changes: 1 addition & 21 deletions control/mateqn.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,9 @@ def lyap(A, Q, C=None, E=None, method=None):

Returns
-------
X : 2D array (or matrix)
X : 2D array
Solution to the Lyapunov or Sylvester equation

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

"""
# Decide what method to use
method = _slycot_or_scipy(method)
Expand Down Expand Up @@ -260,11 +255,6 @@ def dlyap(A, Q, C=None, E=None, method=None):
X : 2D array (or matrix)
Solution to the Lyapunov or Sylvester equation

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

"""
# Decide what method to use
method = _slycot_or_scipy(method)
Expand Down Expand Up @@ -395,11 +385,6 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
G : 2D array (or matrix)
Gain matrix

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

"""
# Decide what method to use
method = _slycot_or_scipy(method)
Expand Down Expand Up @@ -554,11 +539,6 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
G : 2D array (or matrix)
Gain matrix

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

"""
# Decide what method to use
method = _slycot_or_scipy(method)
Expand Down
2 changes: 1 addition & 1 deletion control/matlab/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def bode(*args, **kwargs):
--------
>>> from control.matlab import ss, bode

>>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
>>> sys = ss([[1, -2], [3, -4]], [[5], [7]], [[6, 8]], 9)
>>> mag, phase, omega = bode(sys)

.. todo::
Expand Down
43 changes: 4 additions & 39 deletions control/statefbk.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ def place(A, B, p):
The algorithm will not place poles at the same location more
than rank(B) times.

The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

References
----------
.. [1] A.L. Tits and Y. Yang, "Globally convergent algorithms for robust
Expand Down Expand Up @@ -193,11 +190,6 @@ def place_varga(A, B, p, dtime=False, alpha=None):
[1] Varga A. "A Schur method for pole assignment." IEEE Trans. Automatic
Control, Vol. AC-26, pp. 517-519, 1981.

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

Examples
--------
>>> A = [[-1, -1], [0, 1]]
Expand Down Expand Up @@ -279,10 +271,6 @@ def acker(A, B, poles):
K : 2D array (or matrix)
Gains such that A - B K has given eigenvalues

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.
"""
# Convert the inputs to matrices
a = _ssmatrix(A)
Expand Down Expand Up @@ -366,13 +354,10 @@ def lqr(*args, **kwargs):

Notes
-----
1. If the first argument is an LTI object, then this object will be used
to define the dynamics and input matrices. Furthermore, if the LTI
object corresponds to a discrete time system, the ``dlqr()`` function
will be called.

2. The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.
If the first argument is an LTI object, then this object will be used
to define the dynamics and input matrices. Furthermore, if the LTI
object corresponds to a discrete time system, the ``dlqr()`` function
will be called.

Examples
--------
Expand Down Expand Up @@ -514,11 +499,6 @@ def dlqr(*args, **kwargs):
--------
lqr, lqe, dlqe

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

Examples
--------
>>> K, S, E = dlqr(dsys, Q, R, [N]) # doctest: +SKIP
Expand Down Expand Up @@ -971,11 +951,6 @@ def ctrb(A, B):
C : 2D array (or matrix)
Controllability matrix

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

Examples
--------
>>> G = ct.tf2ss([1], [1, 2, 3])
Expand Down Expand Up @@ -1010,11 +985,6 @@ def obsv(A, C):
O : 2D array (or matrix)
Observability matrix

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

Examples
--------
>>> G = ct.tf2ss([1], [1, 2, 3])
Expand Down Expand Up @@ -1063,11 +1033,6 @@ def gram(sys, type):
if slycot routine sb03md cannot be found
if slycot routine sb03od cannot be found

Notes
-----
The return type for 2D arrays depends on the default class set for
state space operations. See :func:`~control.use_numpy_matrix`.

Examples
--------
>>> G = ct.rss(4)
Expand Down
54 changes: 17 additions & 37 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@

# Define module default parameter values
_statesp_defaults = {
'statesp.use_numpy_matrix': False, # False is default in 0.9.0 and above
'statesp.remove_useless_states': False,
'statesp.latex_num_format': '.3g',
'statesp.latex_repr_type': 'partitioned',
Expand All @@ -104,14 +103,8 @@ def _ssmatrix(data, axis=1):
arr : 2D array, with shape (0, 0) if a is empty

"""
# Convert the data into an array or matrix, as configured
# If data is passed as a string, use (deprecated?) matrix constructor
if config.defaults['statesp.use_numpy_matrix']:
arr = np.matrix(data, dtype=float)
elif isinstance(data, str):
arr = np.array(np.matrix(data, dtype=float))
else:
arr = np.array(data, dtype=float)
# Convert the data into an array
arr = np.array(data, dtype=float)
ndim = arr.ndim
shape = arr.shape

Expand Down Expand Up @@ -205,12 +198,7 @@ class StateSpace(LTI):
-----
The main data members in the ``StateSpace`` class are the A, B, C, and D
matrices. The class also keeps track of the number of states (i.e.,
the size of A). The data format used to store state space matrices is
set using the value of `config.defaults['use_numpy_matrix']`. If True
(default), the state space elements are stored as `numpy.matrix` objects;
otherwise they are `numpy.ndarray` objects. The
:func:`~control.use_numpy_matrix` function can be used to set the storage
type.
the size of A).

A discrete time system is created by specifying a nonzero 'timebase', dt
when the system is constructed:
Expand Down Expand Up @@ -358,10 +346,8 @@ def __init__(self, *args, init_namedio=True, **kwargs):
elif kwargs:
raise TypeError("unrecognized keyword(s): ", str(kwargs))

# Reset shapes (may not be needed once np.matrix support is removed)
# Reset shape if system is static
if self._isstatic():
# static gain
# matrix's default "empty" shape is 1x0
A.shape = (0, 0)
B.shape = (0, self.ninputs)
C.shape = (self.noutputs, 0)
Expand Down Expand Up @@ -467,10 +453,6 @@ def _remove_useless_states(self):
"""

# Search for useless states and get indices of these states.
#
# Note: shape from np.where depends on whether we are storing state
# space objects as np.matrix or np.array. Code below will work
# correctly in either case.
ax1_A = np.where(~self.A.any(axis=1))[0]
ax1_B = np.where(~self.B.any(axis=1))[0]
ax0_A = np.where(~self.A.any(axis=0))[-1]
Expand Down Expand Up @@ -502,12 +484,11 @@ def __str__(self):
return string

# represent to implement a re-loadable version
# TODO: remove the conversion to array when matrix is no longer used
def __repr__(self):
"""Print state-space system in loadable form."""
return "StateSpace({A}, {B}, {C}, {D}{dt})".format(
A=asarray(self.A).__repr__(), B=asarray(self.B).__repr__(),
C=asarray(self.C).__repr__(), D=asarray(self.D).__repr__(),
A=self.A.__repr__(), B=self.B.__repr__(),
C=self.C.__repr__(), D=self.D.__repr__(),
dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '')

def _latex_partitioned_stateless(self):
Expand Down Expand Up @@ -930,18 +911,17 @@ def horner(self, x, warn_infinite=True):
x_arr = np.atleast_1d(x).astype(complex, copy=False)

# return fast on systems with 0 or 1 state
if not config.defaults['statesp.use_numpy_matrix']:
if self.nstates == 0:
return self.D[:, :, np.newaxis] \
* np.ones_like(x_arr, dtype=complex)
if self.nstates == 1:
with np.errstate(divide='ignore', invalid='ignore'):
out = self.C[:, :, np.newaxis] \
/ (x_arr - self.A[0, 0]) \
* self.B[:, :, np.newaxis] \
+ self.D[:, :, np.newaxis]
out[np.isnan(out)] = complex(np.inf, np.nan)
return out
if self.nstates == 0:
return self.D[:, :, np.newaxis] \
* np.ones_like(x_arr, dtype=complex)
elif self.nstates == 1:
with np.errstate(divide='ignore', invalid='ignore'):
out = self.C[:, :, np.newaxis] \
/ (x_arr - self.A[0, 0]) \
* self.B[:, :, np.newaxis] \
+ self.D[:, :, np.newaxis]
out[np.isnan(out)] = complex(np.inf, np.nan)
return out

try:
out = self.slycot_laub(x_arr)
Expand Down
Loading