Skip to content
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

ENH: Addition/multiplication operators for StateSpace systems #7483

Merged
merged 7 commits into from
Apr 8, 2018

Conversation

befelix
Copy link
Contributor

@befelix befelix commented Jun 11, 2017

This PR adds addition and multiplication operators for StateSpace systems, see also #2973. This allows for things like

s1 = StateSpace(1, 1, 1, 0)
s2 = SateSpace(2, 2, 2, 0)
new = s1 + s2
new = s1 - s2
new = s1 * s2
new = 2 * s2

These operations have a frequency-domain interpretation, where adding means the systems operate in parallel and their output is added, while multiplying s1 * s2 means that the output of s2 is applied as the input to system s1.

@rgommers rgommers added enhancement A new feature or improvement scipy.signal labels Jun 12, 2017
return StateSpace(a, b, c, d)

# A scalar/matrix is really just a static system (A=0, B=0, C=0)
return StateSpace(self.A, self.B, self.C, self.D + other)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not always do the right thing or give a sensible exception. You probably have to check what other is and raise an exception if it's not of the right type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't numpy complain on its own already if other is something that's not compatible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking about things that work when added to an array, e.g. if other is a masked array, or a pandas or xarray class. Or even just a higher-dimensional ndarray with a shape that D broadcasts to.

Anyway, not 100% sure you can write a sensible check, but worth thinking about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, yes broadcasting could cause some really strange bugs here - nice catch! I think

other = np.atleast_2d(other)
if self.D.shape !=  other.shape:
    raise Error

should cover almost all use cases anyways

@rgommers
Copy link
Member

Looks like a good idea. What did you decide on for the minimal realisations you suggested in #2973 (comment)?

@ilayn
Copy link
Member

ilayn commented Jun 12, 2017

I wouldn't automatically minreal it. There are many cases where people won't minreal things until the very end such as model reduction and also FEM based modeling. It should at least have an option to not to minreal it .

@befelix
Copy link
Contributor Author

befelix commented Jun 13, 2017

So I'm not doing anything in terms of minimal realisations. This seemed like the best approach - especially for a function that will typically not be called directly.

@befelix
Copy link
Contributor Author

befelix commented Jun 18, 2017

I've removed some functionality, such as __radd__ because it could lead to extremely weird behavior when dealing with numpy arrays and using __radd__ and __add__ is equivalent.

I've kept __rmul__ around because it is useful, but it does allow for things like this

from scipy import signal
sys = signal.StateSpace(1, 1, 1, 1)
np.array([1, 2]) * sys
Out[6]: 
array([ StateSpaceContinuous(
array([[1]]),
array([[1]]),
array([[1]]),
array([[1]]),
dt: None
),
       StateSpaceContinuous(
array([[1]]),
array([[1]]),
array([[2]]),
array([[2]]),
dt: None
)], dtype=object)

So you get back an object array with a system for each entry in the array. Maybe the best would be to strictly only allow multiplication of two StateSpace systems for now.

Down the road one could introduce a convenient classmethod for static systems. Then one could achieve the desired behavior with something like StateSpace.static(2) * sys.

Thoughts?

@ilayn
Copy link
Member

ilayn commented Jun 18, 2017

Oh you are opening a can of worms 😃 And even then, it is still not watertight thanks to NumPy's broadcasting behavior.

@shoyer
Copy link
Contributor

shoyer commented Jun 24, 2017

@befelix you need to set __array_priority__ to take precedence over numpy arrays in binary operations.

@pv
Copy link
Member

pv commented Jul 30, 2017

Some comments: the class should set __array_priority__ = some_large_value and __array_ufunc__ = None to avoid binary op issues caused by Numpy, as noted by shoyer above.

Also, the binary operations should be written in a way that they (i) check if they can deal with type(other) input type and return NotImplemented if not, and (ii) if the input is not handled for some other reason, raise an error.

Made those changes in 520b5cd (and pushed to contributor branch). Was there something else that's still needed for this PR before merging?

Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ilayn do you want to look again? LGTM aside from my comments about six.

@stefanv @e-q or @endolith you might also be interested in looking if you have time

@@ -26,6 +26,7 @@
# np.linalg.qr fails on some tests with LinAlgError: zgeqrf returns -7
# use scipy's qr until this is solved

import six
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be from scipy._lib.six, no? (And same for the tests code presumably.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, fixed.

H(s) = E1(s) + E2(s), means that applying H(s) to U(s)
is equivalent to applying E1(s) and E2(s) separately and adding up the
result.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this explanation is a tautology which already follows from LTIness of the systems. What is more informative might be mentioning that this is parallel connection behavior.

# A scalar/matrix is really just a static system (A=0, B=0, C=0)
return StateSpace(self.A, self.B, self.C, self.D + other)
else:
raise ValueError('Other needs to have compatible dimensions.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess users won't know what "other" means if they are not into class definitions in Python.

return NotImplemented

return (-self).__add__(other)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we are at it and if you agree; Quickly scaling the model with G/6.5 is also possible with __truediv__() method where you can simply test for float or int as you do for mult and then return self*(1/other). You can also explicitly reject __rtruediv__() with a more meaningful error message.

@pv
Copy link
Member

pv commented Aug 18, 2017

Updated with added __truediv__ (only for scalars -- let's not start guessing if the user meant matrix inversion, because / doesn't have that meaning elsewhere). There's no need for __rtruediv__, as the default error messages seem fine.

@pv
Copy link
Member

pv commented Oct 15, 2017

other comments? merge?

@larsoner
Copy link
Member

Nothing from me, let's merge Tuesday if nobody else wants to take a look.

@pv pv merged commit e9b861b into scipy:master Apr 8, 2018
@pv pv added this to the 1.1.0 milestone Apr 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement A new feature or improvement scipy.signal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants