-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
dict-based SparsePauliOp.simplify
#7656
Conversation
Some unit tests failed due to #7657, |
Pull Request Test Coverage Report for Build 1997872381
💛 - Coveralls |
e7dab10
to
fc55a61
Compare
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.
It would be interesting to get some more in-depth profiling on your microbenchmarks, especially on smaller numbers (perhaps even smaller than you took). It looks like constant factors might be having quite an effect at small numbers.
Dictionary-based is asymptotically better, but we should make sure that we don't have too big an impact on the (more common) small-scale objects too.
def __eq__(self, other): | ||
"""Check if two SparsePauliOp operators are equal""" | ||
return ( | ||
super().__eq__(other) | ||
and np.allclose(self.coeffs, other.coeffs) | ||
and self.paulis == other.paulis | ||
) | ||
if not super().__eq__(other): | ||
return False | ||
return np.allclose((self - other).simplify().coeffs, [0]) |
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.
It would be good to check the performance characteristics of this. The dict-based simplify
has better asymptotic scaling than it used to, so this becomes more feasible, but this makes a relatively simple operation involve probably 3 SparsePauliOp
allocations (in -other
, self._add(-other)
and simplify
).
f34fca0
to
f96b71f
Compare
Thank you for your comments. I tried my microbenchmarks with smaller qubits. I see the current code runs faster up to 16 qubits. I think import random
from timeit import timeit
from qiskit.quantum_info import SparsePauliOp
def bench(k, n):
random.seed(123)
op = SparsePauliOp.from_list([(''.join(random.choices('IXYZ', k=k)), 1) for _ in range(n)])
op2 = SparsePauliOp.from_list([(''.join(random.choices('IXYZ', k=k)), 1) for _ in range(n)])
op3 = op.compose(op2)
op4 = op3.simplify()
print(f'{k}, {n} (1st pass): {timeit(lambda: op3.simplify(), number=5)} sec')
print(f'{k}, {n} (2nd pass): {timeit(lambda: op4.simplify(), number=5)} sec')
print()
for i in range(8, 33, 8):
bench(i, 1000) main
this PR
|
Hmm, that poses some interesting questions, especially because the current version is significantly better on close-to-simplified forms. I suspect that it's to do with being close-to-sorted as well, so shuffling the order of the operators in the output of a The speed-boost is significant at high numbers of qubits (of course, because of the asymptotic scaling removing a factor of That said, actually the best solution might be to move the backing of |
Thank you for your feedback. I don't think the current use cases require >50 qubits mostly. So, I agree to leave the current sort-based |
f96b71f
to
441c364
Compare
I implemented a rust version of comparison with main import random
from timeit import timeit
from qiskit.quantum_info import SparsePauliOp
def bench(k, n):
random.seed(123)
op = SparsePauliOp.from_list([(''.join(random.choices('IXYZ', k=k)), 1) for _ in range(n)])
op2 = SparsePauliOp.from_list([(''.join(random.choices('IXYZ', k=k)), 1) for _ in range(n)])
op3 = op.compose(op2)
op4 = op3.simplify()
print(f'{k}, {n} (1st pass): {timeit(lambda: op3.simplify(), number=1)} sec')
print(f'{k}, {n} (2nd pass): {timeit(lambda: op4.simplify(), number=1)} sec')
print()
for i in range(8, 41, 8):
bench(i, 1000) main
this PR
|
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.
I'm excited to see more Rust usage, I just left some quick inline suggestions on how to potentially improve the performance of the rust code. Although, I didn't benchmark any of the suggestions so not sure how big a difference it will make in practice.
Thank you for your feedback, @mtreinish! I will update my rust code based on your comments. I'm really impressed with the rust framework for terra. I feel it is easier than cython. Kudos to you. |
Thanks to @mtreinish's comment,
|
Thanks. The rust version looks good, so I will finalize this PR. TODOs
|
# Conflicts: # src/lib.rs
# Conflicts: # qiskit/__init__.py # src/lib.rs
aa136a6
to
8ce0a23
Compare
FYI: I updated the summary of this PR. |
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.
Overall this LGTM. The rust code looks fine and I'm fine along with the other changes. I think the distinction between __eq__
and equiv()
also makes sense here and is consistent with other classes. Just a couple inline comments/questions but nothing major
self.assertNotEqual(spp_op1 + spp_op2, spp_op2 + spp_op1) | ||
|
||
@combine(num_qubits=[1, 2, 3, 4]) | ||
def test_equiv(self, num_qubits): |
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.
Do you want to add a test here with a shuffled equiv pauli string? Like:
a = SparsePauliOp.from_list([('X', 1), ('Y', 2)])
b = SparsePauliOp.from_list([('Y', 2), ('X', 1)])
which was the example you used from the issue
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.
Thanks. I added two cases including your suggestion. a16a229
releasenotes/notes/add-sparsepauliop-equiv-7a8a1420117dba21.yaml
Outdated
Show resolved
Hide resolved
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
1685aeb
to
a16a229
Compare
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.
LGTM, thanks for updating
Summary
This PR replaces
np.unique
inSparsePauliOp.simplify
with a dict-based operation.This is faster thannp.unique
when the number of qubits is large and there are many duplicate operators.But, if the number of qubits is small or there is no duplicate operator, this PR can be slower.Moreover, the code gets more complicated than just applyingnp.unique
.So, I don't have a strong opinion to include this PR as Terra. I want feedback from Qiskit developers.Update:
This PR contains three updates:
I implemented a utility function
unordered_unique
in rust to replacenp.unique
inSparsePauliOp.simplify
.I added
SparsePauliOp.equiv
to check the equivalence of two operators whileSparsePauliOp.__eq__
checks elementwise equality. Based on Equality ofSparsePauliOp
s with different orders #7657E.g.,
SparsePauliOp.__eq__
raises an error if two operators have different numbers of coefficients.I fixed it and add unit tests.
Closes #7657
Details and comments
I attach a microbenchmark. Because
op4 = op3.simplify()
,op4
does not contain any duplicate operator.The current approach with
np.unique
is expected to be faster in the 2nd pass because it is fast when operators are already sorted.main
this PR