-
Notifications
You must be signed in to change notification settings - Fork 621
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
Changed __mul__ behviour to be more flexible and consistent. #1620
Conversation
@AGaliciaMartinez Perhaps it would be better to check for whether |
There are two reasons I did not go with
I must admit that the try except with complex also seems not ideal to me. But I could not find a better implementation that fitted the requirements. I mean, we do use this same code pattern at some other points in the code (in arrray = np.array([1,2]) # length 2 array
qobj*array This will return
|
We don't want the Setting |
In my previous GSoC meeting we discussed what to do with the changes proposed in this PR. Currently (in this PR) we return NotImplemented if
It seems that the discussion steers to what a scalar is (which classes are scalars) and at what level do we define it (who is responsible for returning NotImplemented). Proposed solutions:
I think that 3.i is the closest to an ideal solution. |
This all seems incredibly complicated. How about all specialisations stored in the dispatcher take @functools.singledispatch
def _scalar_properties(x):
return {
# if the matrix was Hermitian, is the output Hermitian (True), or not known (False)
'preserves_hermicity': bool,
# regardless of the input matrix, is the output Hermitian (True), or not known (False)
# (i.e. was the scalar zero-like).
'forces_hermicity': bool,
# if the matrix was unitary, is it still unitary (True), or not known (False)
'preserves_unitarity': Optional[bool],
} Then just register types with the functools dispatcher? So then def __mul__(self, other):
if isinstance(other, (Qobj, QobjEvo)):
return self.__matmul__(other)
try:
out = _data.mul(self.data, other)
except TypeError:
return NotImplemented
properties = _scalar_properties(other)
isherm = properties['forces_hermicity'] or (self.isherm and properties['preserves_hermicity'] or None)
isunitary = self.isunitary and properties['preserves_unitarity'] or None
return Qobj(
out, copy=False, dims=self.dims, isherm=isherm, isunitary=isunitary,
) (I've probably not got the property truthiness checking exactly right, but that's the principle.) |
If you really wanted, you could even have it so that the callable = _data.mul.get(self.data, other)
out = callable(self.data, other)
properties = callable.base_operation.scalar_properties(other) (the The properties in my example really correspond to the booleans |
I am actually fine with using a try/except with Regarding to |
The upshot is that magic operator methods should never raise for invalid types, only return I don't remember saying that the dispatched specialisations shouldn't raise at all - if I did, there may be more considerations that I can't think of right now, but I don't think it's necessarily the case; they already raise on badly shaped inputs, or things like that. |
- try mul and return NotImplemented if TypeError is raised by mul. - try complex(other) to infer isherm and isunitary if possible.
I After the discussion in the GSoC meeting I changed the implementation to:
I think this is the simplest solution. @jakelishman are you ok with this implementation? I think the ideas of numpy broadcasting Qobj as scalar and |
Fine by me. You lose some additional cases where you would know that the multiplication maintains hermicity or whatever, but that can easily be handled when it's actually needed. |
- Removed blank line - Adapted tests to nor include NoComplexClass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left two small suggestions.
Do we have any tests for whether the isherm
and isunitary
preserving logic is correct? If not, could we add them in this PR? I know it's not related directly to the fix, but we are changing the code and we might as well add tests while we're looking at it and know what it is supposed to do.
I don't know if the test failure is related or not -- it looks unrelated but it is a data layer test, so we should probably check a bit carefully. |
Co-authored-by: Simon Cross <hodgestar+github@gmail.com>
@AGaliciaMartinez Could you also update the changelog entry in the PR description to say something about the change? |
We do have a few tests, see for instance |
Perfect. Thank you!
Happy to leave this for another PR. |
Description
I changed mul behaviour following the discussion of issue #1607. However, the implementation details differ slightly from what was discussed there. The current behaviout of
__mul__(self, other)
is:Qobj
: dispatch to__matmul__
NotImplemented
ifTypeError
is returned.other
instead ofcomplex(other)
. This is more flexible and allows specialisations to handle arbitrary scalar like objects (something extremely useful for qutip-tensorflow).__rmul__
now directly dispatches to__mul__
. Any necessary check (casting to complex included) is done in__mul__
.No changes were required to
mul_dense
andmul_csr
asadd_dense(data, value=np.array(1))
works. It internally triescomplex(np.array(1))
which is guaranteed to work. This is something I was very happy to find as I do not think that specialisations should returnNotImplemented
(although they still can if required).Related issues or PRs
Fixes issue #1611
Changelog
__mul__
now handles consistently right and left multiplications of an arbitrary python object.__mul__
is now more flexible passingother
to the dispatcher instead ofcomplex(other)
.qobj*np.array([1,2])
andqobj*np.array([1,2])
(or any other numpy array that does not represent an scalar) now raise TypeError. This change is not backwards compatible (!!).Edit: changed changelog and description