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

Add SparsePauliOp.sum #7202

Merged
merged 4 commits into from
Nov 2, 2021
Merged

Add SparsePauliOp.sum #7202

merged 4 commits into from
Nov 2, 2021

Conversation

t-imamichi
Copy link
Member

@t-imamichi t-imamichi commented Nov 1, 2021

Summary

Adds SparsePauliOp.sum as a specialized version the builtin sum for a list of SparsePauliOp.
The builtin sum calls SparsePauliOp._add N-1 times if there are N items in the list.
Each SparsePauliOp._add stacks arrays of z, x, phase and coeffs and it is carried out N-1 times.
On the other hand, the new method SparsePauliOp.sum stacks all arrays at once. So, it can reduce the overhead.

Details and comments

Jordan Wigner transformation takes a sum of a list of SparsePauliOp and it is one of the bottlenecks.
I proposed a workaround to reduce the overhead in this PR qiskit-community/qiskit-nature#397;
but, it is not intuitive.
The specialized method of this PR is more intuitive and more efficient than the workaround.

# workaround 
        def _sum(ops: List[SparsePauliOp]) -> SparsePauliOp:
            while len(ops) > 1:
                ops.append(ops[0] + ops[1])
                ops = ops[2:]
            return ops[0]

Microbenchmark of JW transformation with Qiskit nature. (same as one in qiskit-community/qiskit-nature#397)

import random
from timeit import timeit

from qiskit_nature.converters.second_quantization import QubitConverter
from qiskit_nature.mappers.second_quantization import JordanWignerMapper
from qiskit_nature.operators.second_quantization import FermionicOp

random.seed(123)
k = 10
n = 100

op = sum(
    (FermionicOp(''.join(random.choices('+-ENI', k=k)), display_format="dense") * random.random())
    for _ in range(n))
mapper = JordanWignerMapper()
qubit_conv = QubitConverter(mapper)
print(f"{timeit(lambda: qubit_conv.convert(op), number=1)} sec")

nature main (qiskit-community/qiskit-nature@7d12a13) + terra main

2.446942435002711 sec

nature qiskit-community/qiskit-nature#397 + terra main

1.5037378649976745 sec

nature qiskit-community/qiskit-nature#397 + this PR (replace _sum with SparsePauliOp.sum)

1.4083706309975241 sec

@t-imamichi t-imamichi requested review from chriseclectic and a team as code owners November 1, 2021 09:20
@jlapeyre jlapeyre mentioned this pull request Nov 1, 2021
- |
Added :meth:`~qiskit.quantum_info.SparsePauliOp.sum` as a specialized function of the builtin
function ``sum`` for a list of :class:`:meth:`~qiskit.quantum_info.SparsePauliOp`.
This method has less overhead of the array stacking of
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
This method has less overhead of the array stacking of
This method has less overhead than the array stacking of

@jlapeyre
Copy link
Contributor

jlapeyre commented Nov 1, 2021

LGTM!

jlapeyre
jlapeyre previously approved these changes Nov 2, 2021
jakelishman
jakelishman previously approved these changes Nov 2, 2021
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

Looks good to me - it's the improvement in asymptotic complexity that's the biggest win here!

In [1]: from qiskit.quantum_info import SparsePauliOp
   ...: import random
   ...:
   ...: def random_pauli(k):
   ...:     phase = "".join([
   ...:         random.choice(['', '+', '-']),
   ...:         random.choice(['', '1']),
   ...:         random.choice(['', 'i', 'j']),
   ...:     ])
   ...:     return phase + "".join(random.choices("IXYZ", k=k))
   ...:
   ...: def raw_sum(ops):
   ...:     out, *rest = ops
   ...:     for op in rest:
   ...:         out += op
   ...:     return out
   ...:
   ...: per_pauli = 50
   ...: qubits = 20
   ...: for n in [10, 30, 100, 300, 1000]:
   ...:     ops = [SparsePauliOp([random_pauli(qubits) for _ in [None]*per_pauli]) for _ in [None]*n]
   ...:     %timeit SparsePauliOp.sum(ops)
   ...:     %timeit raw_sum(ops)
   ...:     print()
   ...:
56.6 µs ± 1.17 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
265 µs ± 4.69 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

122 µs ± 1.12 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
866 µs ± 22 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

365 µs ± 6.08 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
3.46 ms ± 106 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

1.04 ms ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
15.9 ms ± 311 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

3.7 ms ± 96.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
109 ms ± 1.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

@jakelishman jakelishman added automerge Changelog: New Feature Include in the "Added" section of the changelog mod: quantum info Related to the Quantum Info module (States & Operators) performance labels Nov 2, 2021
@t-imamichi
Copy link
Member Author

Thank you for revising the reno.

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

(By the way, in most cases, you don't need to use the "update branch" button once a PR is tagged "automerge". The bot will take care of that for us, and it's generally more efficient at it as well. If the bot is blocked on a single transient test failure, Matthew, Kevin or I can re-run the failed job to avoid needing to re-run the entire CI suite. It's usually not really a problem to update the branch, just less efficient.)

@t-imamichi
Copy link
Member Author

Sorry. I updated the branch because I noticed the CI failure. I won't do that again.

@jakelishman
Copy link
Member

No worries! It's not super important, it just helps a bit, because we end up with fewer jobs running on CI in total (we can only have a fairly small number running at any given time).

@coveralls
Copy link

Pull Request Test Coverage Report for Build 1413484972

  • 14 of 14 (100.0%) changed or added relevant lines in 1 file are covered.
  • 5 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.004%) to 82.31%

Files with Coverage Reduction New Missed Lines %
qiskit/extensions/quantum_initializer/uc.py 2 88.65%
qiskit/pulse/library/waveform.py 3 90.38%
Totals Coverage Status
Change from base Build 1413483415: -0.004%
Covered Lines: 49275
Relevant Lines: 59865

💛 - Coveralls

@mergify mergify bot merged commit 418e0ba into Qiskit:main Nov 2, 2021
@t-imamichi t-imamichi deleted the spo_sum2 branch November 3, 2021 08:57
@kdk kdk added this to the 0.19 milestone Nov 15, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: quantum info Related to the Quantum Info module (States & Operators) performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants