-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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 an efficient circuit synthesis for a special pattern exp(-it (IZZ + ZZI + ZZZ))
#13285
Comments
This seems like a very nice optimisation for this kind of class of for any single Pauli As in: I feel like a similar trick would work if these were all X rotations instead of Z, it should be impervious to commuting linear terms being in the mix, and it clearly should be valid under permutation of the qubits. If we add this case, I feel like we should make sure it validly recognises all those additional places too. That might not mean putting in separate catches for the linear terms - maybe the synthesis works out cleaner if we separate those out into a prefix (since they all trivially commute with all other operators). Going further, is it possible that for evolution of any polynomial of the same Pauli to use this cubic case as a decomposition to lift the synthesis to higher orders? It might not be immediately useful, but it feels like there might be some sort of decomposition rule possible here, since that kind of polynomial is fairly trivially detectable as "everything commutes". |
This comment has been minimized.
This comment has been minimized.
how do I start helping? |
Thank you for your idea of generalization, @jakelishman. It sounds great.
import numpy as np
from qiskit import QuantumCircuit, generate_preset_pass_manager
from qiskit.circuit import Parameter
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.quantum_info import SparsePauliOp, Statevector, Operator
# PauliEvolutionGate
qc = QuantumCircuit(3)
x = Parameter("x")
op = SparsePauliOp(["IXX", "XXI", "XXX"], coeffs=[1, 2, 3])
evo = PauliEvolutionGate(op, x)
qc.append(evo, qargs=qc.qregs[0])
print("PauliEvolutionGate")
print(qc)
print("PauliEvolutionGate + decompose")
print(qc.decompose())
pm = generate_preset_pass_manager(optimization_level=3, basis_gates=["cx", "rz", "h"])
qc = pm.run(qc)
print("PauliEvolutionGate + optimization_level=3")
print(qc)
# Hand-optimized
qc2 = QuantumCircuit(3)
qc2.rxx(2.0 * x, 0, 1)
# RXXX(6 * x, 0, 1, 2)
qc2.h([0, 1, 2])
qc2.cx(0, 1)
qc2.rzz(6.0 * x, 2, 1)
qc2.cx(0, 1)
qc2.h([0, 1, 2])
#
qc2.rxx(4.0 * x, 2, 1)
print("Hand-optimized")
print(qc2)
print("Hand-optimization + optimization_level=3")
qc2 = pm.run(qc2)
print(qc2)
for h in np.linspace(-1, 1, 10):
op = Operator(qc.assign_parameters([h]))
op2 = Operator(qc2.assign_parameters([h]))
assert np.allclose(op.to_matrix(), op2.to_matrix())
print("passed") |
Ah, thanks for checking Imamichi-san. Yeah, I guess I should have applied a couple of seconds' actual thought while typing all that... It's obvious when you draw it out that we can just rotate each individual single-qubit axis round to ZZZ, do the same trick, and rotate back. We ought to be able to handle the case that each qubit is being rotated on a different axis as well, right, e.g. if the cubic term is This is still all for CX-based synthesis, but I think recognising these transformations means that we ought to be able to formulate the synthesis neatly for |
Thank you for the detailed explanation. I got how we can handle cubic term |
I hand-optimized 4-body terms in the similar way as follows. The number of two-qubit gates become half (16 -> 8).
script to reproduce import numpy as np
from qiskit import QuantumCircuit, generate_preset_pass_manager
from qiskit.circuit import Parameter
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.quantum_info import SparsePauliOp, Operator
# PauliEvolutionGate
qc = QuantumCircuit(4)
x = Parameter("x")
op = SparsePauliOp(["ZZII", "IZZI", "IIZZ", "IZZZ", "ZZZI", "ZZZZ"], coeffs=[1, 2, 3, 4, 5, 6])
evo = PauliEvolutionGate(op, x)
qc.append(evo, qargs=qc.qregs[0])
print("PauliEvolutionGate")
print(qc)
print("PauliEvolutionGate + decompose")
print(qc.decompose())
pm = generate_preset_pass_manager(optimization_level=3, basis_gates=["cx", "rz", "h"])
qc = pm.run(qc)
print("PauliEvolutionGate + optimization_level=3")
print(qc)
print(qc.count_ops())
# Hand-optimized
qc2 = QuantumCircuit(4)
# IZZI
qc2.rzz(4 * x, 2, 1)
# IZZZ
qc2.cx(0, 1)
qc2.rzz(8 * x, 2, 1)
qc2.cx(0, 1)
# IIZZ
qc2.rzz(6 * x, 0, 1)
# ZZZZ
qc2.cx(3, 2)
qc2.cx(2, 1)
qc2.rzz(12 * x, 0, 1)
qc2.cx(2, 1)
qc2.cx(3, 2)
# ZZZI
qc2.cx(3, 2)
qc2.rzz(10 * x, 2, 1)
qc2.cx(3, 2)
# ZZII
qc2.rzz(2 * x, 3, 2)
print("Hand-optimized")
print(qc2)
print("Hand-optimization + optimization_level=3")
qc2 = pm.run(qc2)
print(qc2)
print(qc2.count_ops())
for h in np.linspace(-1, 1, 10):
op = Operator(qc.assign_parameters([h]))
op2 = Operator(qc2.assign_parameters([h]))
assert op.equiv(op2)
print("passed") |
@itoko -san taught me that circuit synthesis of only CNOT and RZ is called phase polynomial and qiskit has a heuristic for all-to-all connectivity https://docs.quantum.ibm.com/api/qiskit/synthesis#synth_cnot_phase_aam. |
Here is a small head-up: @Cryoris and I are actively working on integrating Simon Martiel's Rustiq synthesis code (see https://github.com/smartiel/rustiq) natively within Qiskit. This is a clever heuristic algorithm for all-to-all connectivity, and in particular exploits existing commutativity relations between Paulis: in both examples above all the Pauli terms commute and hence can be arbitrarily rearranged during synthesis (I am not sure if this is what makes the difference but it might). Here are the results for the two examples above:
The hand-crafted circuits are still slightly better, but only marginally so. |
Thanks, @alexanderivrii. It's a great news. It would be nice if Rustiq can include the hand-optimized pattern too. |
@alexanderivrii I tried rustiq following the reno, but 2q count by rustiq is larger than your experiment (count_2q = 5). from qiskit import QuantumCircuit, generate_preset_pass_manager
from qiskit.circuit import Parameter
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.passes import HLSConfig
# PauliEvolutionGate
qc = QuantumCircuit(3)
x = Parameter("x")
op = SparsePauliOp(["IZZ", "ZZI", "ZZZ"], coeffs=[1, 2, 3])
evo = PauliEvolutionGate(op, x)
qc.append(evo, qargs=qc.qregs[0])
print("PauliEvolutionGate")
print(qc)
print("PauliEvolutionGate + decompose")
print(qc.decompose())
pm = generate_preset_pass_manager(optimization_level=3, basis_gates=["cx", "rz"])
tqc = pm.run(qc)
print("PauliEvolutionGate + optimization_level=3")
print(tqc)
print(tqc.count_ops(), "\n")
config = HLSConfig(PauliEvolution=[("rustiq", {"upto_phase": False})])
pm = generate_preset_pass_manager(optimization_level=3, basis_gates=["cx", "rz"], hls_config=config)
tqc = pm.run(qc)
print("PauliEvolutionGate + rustiq + optimization_level=3")
print(tqc)
print(tqc.count_ops(), "\n")
# Hand-optimized
qc2 = QuantumCircuit(3)
qc2.rzz(2.0 * x, 0, 1)
# RZZZ(6 * x, 0, 1, 2)
qc2.cx(0, 1)
qc2.rzz(6.0 * x, 2, 1)
qc2.cx(0, 1)
#
qc2.rzz(4.0 * x, 2, 1)
print("Hand-optimized")
print(qc2)
print("Hand-optimization + optimization_level=3")
tqc2 = pm.run(qc2)
print(tqc2)
print(tqc2.count_ops(), "\n")
|
Thanks @t-imamichi! We have indeed encountered a certain complication when integrating Rustiq's synthesis algorithm inside Qiskit. The default algorithm in the One other point is that Rustiq is a greedy algorithm and at each step it chooses the "move" that minimizes a certain cost function. However, there are usually multiple best moves available. This is all to say that Rustiq's behavior might change from one version to another and in particular the number of CX-gates could have also changed due to some recent changes to Rustiq, but this is actually not the case for the example above. Within the same Qiskit version the behavior is deterministic. I am actually planning to study this example to see if we could modify Rustiq's heuristic to produce only 4 CX-gates. I am thinking that it would be worthwhile to implement the approach from the referenced paper with your and Jake's suggested extensions (in addition to having Rustiq). |
Thank you for your information. I tried |
What should we add?
[1] proposed an efficient way to synthesize the following special pattern of two-body terms (
IZZ
andZZI
) and three-body term (ZZZ
) (Fig. 2 [1]).It can halve the number of two-qubit gates (8 to 4).
This pattern is useful to handle QAOA with cubic terms as [1] did.
It would be nice if Qiskit can handle this special pattern of synthesis.
Target (
coeffs
can be arbitrary)Qiskit optimization_level=3
Proposed circuit by [1]
Reference
[1] Pelofske, E., Bärtschi, A., & Eidenbenz, S. (2024). Short-depth QAOA circuits and quantum annealing on higher-order ising models. Npj Quantum Information, 10(1), 30. https://doi.org/10.1038/s41534-024-00825-w
Script to reproduce
The text was updated successfully, but these errors were encountered: