Skip to content

Prepare for removal of dependency on numpy.matrix class #292

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

Closed
wants to merge 15 commits into from

Conversation

murrayrm
Copy link
Member

@murrayrm murrayrm commented Apr 16, 2019

This PR prepares for removal the dependency of the control package on the numpy.matrix class, which is planned for deprecation at some point in the future. To handle this, a number of changes have been made:

  • A new StateSpaceMatrix class, derived from numpy.ndarray, was created that is used to store the A, B, C, D matrices for a StateSpace system. This class maintains elements as a 2D matrix, so B[:,1] returns a column matrix (shape = (n, 1)). As an ndarray, this expression would normally return a 1D array. The StateSpaceMatrix class also overrides the * and ^ operators to give matrix multiplication and matrix powers (same as numpy.matlab does). This gives better compatibility with (user) code that was using those constructs to operate directly on matrices associated with a system. A new unit test file for the class is also included. There is also a function ssmatrix() that can be used to create a StateSpaceMatrix object (similar to the use of numpy.array to create an numpy.ndarray object).

  • Functions that used to return numpy.matrix objects now return a user-configurable type, set using the use_numpy_matrix or set_ss_matrix_type functions. By default the return type is currently numpy.matrix (for backward compatibility), but calling use_numpy_matrix(False) changes the return type to StateSpaceMatrix. Using set_ss_matrix_type, it is possible to use other subclasses derived from numpy.ndarray (including ndarray itself). A UserWarning is issued whenever numpy.matrix is set or used as the output type.

  • The code in the package as been updated to remove unneeded dependencies on the numpy.matrix class. This mainly involved changing numpy.matrix to numpy.array, but in some cases some additional changes were needed because of the differences between matrices and numpy. With the exception of extracting elements of submatrices, the main package code does not explicitly use the functionality of the StateSpaceMatrix class, so the main use of this class is to provide some backward compatibility for user code that users * and ^ operators (which is fairly common in my own code).

  • All unit test code (in control/tests) has been scrubbed of unnecessary dependence on the numpy.matrix class (except for testing that warning messages are generated and a few other checks for backward compatibility).

  • I did some PEP8 cleaning on statesp.py since there were lots of changes in this file already.

The changes were tested with the old unit test files, the new unit test files, and all examples and everything continues to work across all of those variants.

Net impact of this PR is that all numpy.matrix deprecation warnings can be removed by calling use_numpy_matrix(False), addressing issue #233. This behavior can be made the default in a future release or when numpy.matrix goes away.

There are two situations in which the numpy.matrix deprecation warning can still appear:

  • For backward compatibility, I set things up so that if you pass the StateSpaceMatrix constructor a string then it will call numpy.matrix. This allows all old examples to work without change (but does generate a warning message).

  • It's possible that if a user creates a numpy.matrix object to one of the python-control functions that it could end up generating a copy and that could trigger a warning message. I figure that is the user's fault -:).

(Note: this description was updated on 8 Jun 2019 to reflect the changes made in response to comments and responses below.)

@coveralls
Copy link

coveralls commented Apr 16, 2019

Coverage Status

Coverage increased (+0.3%) to 81.228% when pulling a5126f8 on murrayrm:np.matrix into 8a11cd3 on python-control:master.

@moorepants
Copy link

One overall comment: If control functions, methods, and attributes now return numpy arrays instead of numpy matrices, this change could break lots of code in the wild. If a user gets these matrix outputs and then operates on them with operations that assume a matrix type (e.g. overloaded *), then their code will now break or return incorrect results. It would probably be best to deprecate this in the control package so that users see warnings that output types will change in the next release. A 6 month deprecation time might be the minimum for this to register for average users.

Co-Authored-By: murrayrm <murray@cds.caltech.edu>
@murrayrm
Copy link
Member Author

@moorepants Good point. Most of these changes are internal and so shouldn't affect any user outputs. But I'll go through and check to see whether the output of any function actually changed pre/post PR. The only think that might have changed are some of the functions that return matrices (eg, Gramians).

Note also that the StateSpaceMatrix class does define the "standard" overloaded operators (*,^, [,]) so user code operating on state space matrices should function normally. But I completely agree this is something we want to be careful about.

@murrayrm
Copy link
Member Author

I checked through the code. Most functions that return matrices were already returning ndarrays (typically via a call to scipy.linalg.*). The one set that I found that could have compatibility problems is:

  • ctrb and obsv used to return matrix objects and now return ndarray objects.

Thoughts on the best approach to handling this? We could:

  • Keep the return type as matrix for now, with a deprecation warning. Change to ndarray in a future release.

  • Return a StateSpaceMatrix object, which would be compatible for most (all?) operations and allow matrix-like manipulation of the observability or controllability gramians.

  • Return an ndarray object and let people rewrite their code as they discover the change.

We could also allow a subtype keyword for the function and return an ndarray of the listed subtype (with a default to either matrix, StateSpaceMatrix, or ndarray, depending on how we resolve the issue).

@murrayrm
Copy link
Member Author

Updated the code so that all functions that used to return an ndarray of subtype numpy.matrix return that type by default, with a user warning that this will be deprecated in the future. This can be overridden using the return_type keyword.

Once we are OK with the code changing (perhaps in the next major or minor release), we can change the code to return numpy.ndarray by default (user will still be able to override using return_type keyword).

@murrayrm
Copy link
Member Author

Merged changes from current version of master (including implementing updated ctrb and obsv computations in PR #300).

@billtubbs FYI. Perhaps worth waiting until this PR is merged before making any more changes to ctrb and obsv. I took your recent update and applied that same change here (but without using the matrix class).

@roryyorke
Copy link
Contributor

StateSpaceMatrix should probably have an __imul__ method.
Using __new__ here seems unusual to me; is it obvious why it's necessary?

Putting return_type=np.matrix arguably makes a long-term commitment to this interface (i.e., the "return_type" argument will be expected in the future); an alternative might be a config option for return types of the relevant functions. It also suggests more generality than is on offer; that is, a naive user might expect return_type=StateSpaceMatrix to work (it might, though I assume it's not what's intended) Perhaps a flag instead, "return_npmatrix=True"?

The code for turning Matlab-style matrix strings into nd.arrays is in numpy.matrixlib.defmatrix._convert_from_string -- it's not all that long or complicated, one could copy it if necessary.

Less importantly, if we didn't have to support Python 2.7 this refactoring could have used @ to be a bit tidier. Pity there's no tidy "matrix-power" operator.

@murrayrm
Copy link
Member Author

murrayrm commented Jun 3, 2019

Thanks for the comments @roryyorke. Responses (no actions yet):

  • The code for __new__ is based on the numpy.matrix.__new__ function. I'm pretty sure we need to use __new__ instead of __init__ because we are creating the object (via a call to np.ndarray.__new__) with the desired storage type, shape, etc. If we just call __init__ then that information will have already been created by numpy.ndarray (since __init__ gets called after __new__).

  • I'm pretty sure that return_type=StateSpaceMatrix will actually work. You can also use return_type=numpy.ndarray, if you wanted to make sure you got a NumPy array and not a StateSpace object. But I agree that we may want the simpler behavior of just turning off the matrix class. Need to think on that one.

  • I hadn't thought about what other operators to implement. In addition to __imul__ there are things like __iadd__ and __isub__ as well. Implementing all of them is tedious, but not implementing them means that you get the default numpy.ndarray behavior, which could be confusing. Another alternative would be to define all of those to raise a NotImplementedError exception, so at least you couldn't "accidentally" use it.

@roryyorke
Copy link
Contributor

np.matrix inherits most numeric dunder methods from np.ndarray, except mul and pow (see below). Raising NotImplementedError for unimplemented mul and pow methods is an easy, safe option.

prefixes = ['','r','i']                                                                                       
bases = ['add','sub','mul','matmul','truediv','floordiv','mod','divmod','pow','lshift','rshift','and','or','xor']
makemeth = lambda p, b: '__' + p + b + '__'
# there's no such thing as __idivmod__; otherwise all exist
methods = [makemeth(p,b) for p,b in itertools.product(prefixes, bases) if hasattr(np.ndarray, makemeth(p,b))]
matonly = [m for m in methods if getattr(np.matrix, m) is not getattr(np.ndarray, m)]                          
print(matonly)
# output: ['__mul__', '__pow__', '__rmul__', '__rpow__', '__imul__', '__ipow__']

@murrayrm
Copy link
Member Author

murrayrm commented Jun 8, 2019

Committed a new set of changes to address the issues raised by @roryyorke. Most recent updates:

  • The use of numpy.matrix as a return type is now set as a configuration option. The default is for functions in statesp.py and (hsvd in modelsimp.py) to return numpy.matrix objects (for backward compatibility), but the call use_numpy_matrix(False) will change the return type to StateSpaceMatrix. There is also a function set_ss_return_type that can set the return type to any subclass of numpy.ndarray (including ndarray itself).

  • New unit test code to test the functionality of use_numpy_matrix(False) and set_ss_return_type.

  • Added code to raise theNotImplementedError exception for dunder methods ipow and imul, so that they don't fall back to the (incompatible) numpy.ndarray methods.

In a future release, we can change the defaults in config.py to return either StateSpaceMatrix objects or numpy.ndarray objects and the dependence on numpy.matrix will be removed.

I decided not to copy the string processing code from numpy.matrixintopython-control though we may eventually want to put that into the matlab submodule. I think the main module should follow whatever Numpy does in terms of allowing string initialization (my guess is that it will go away).

@murrayrm murrayrm changed the title Remove dependency on numpy.matrix class Prepare for removal of dependency on numpy.matrix class Jun 8, 2019
@murrayrm
Copy link
Member Author

murrayrm commented Jun 8, 2019

In looking through the code just now, I see what we have some inconsistencies in how we return matrix objects. In some place we are returning matrix objects (now convertible to StateSpaceMatrix via changes in this PR) but in other place we return ndarray objects. For example, place and lqr return ndarray objects, but ctrb, obsv, and acker return matrix-like objects (either numpy.matrix or StateSpaceMatrix, depending on settings).

On the one hand, it seems useful to have functions that return a gain matrix sending back something that can be used like the numpy.matrix class, so that you can say (for example)

K = lqr(A, B, Q, R)
Acl = A - B * K

(note that this won't actually work in the current code, tthough if you used acker instead of lqr then it would!).

On the other hand, at some point we should probably just bite the bullet and use ndarray everywhere, so that we are compatible with Numpy. If we did that, we could just get rid of the new StateSpaceMatrix class in this PR completely.

Any thoughts?

@moorepants
Copy link

Will this work?

K = lqr(A, B, Q, R)
Acl = A - B @ K

Using @ should be what we move towards.

@murrayrm
Copy link
Member Author

murrayrm commented Jun 8, 2019

Yes, that will work (though not in python 2.7, of course).

@murrayrm
Copy link
Member Author

Following the discussion in issue #233, this PR is being abandoned in favor of PR #314, which is aligned with the approach in issue #233 (and is much more straightforward).

@murrayrm murrayrm closed this Jun 15, 2019
@murrayrm murrayrm deleted the np.matrix branch January 3, 2020 01:48
@murrayrm murrayrm added this to the 0.8.3 milestone Jan 4, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants