Qiskit-Extensions / circuit-knitting-toolbox

Tools for knitting quantum circuits with Qiskit
https://qiskit-extensions.github.io/circuit-knitting-toolbox/
Apache License 2.0
74 stars 25 forks source link

IndexError: tuple index out of range #508

Closed sirgeorgesawcon closed 5 months ago

sirgeorgesawcon commented 5 months ago

Steps to Recreate

Do the necessary Imports

# useful additional packages
import matplotlib.pyplot as plt
import numpy as np
import networkx as nx

from qiskit.tools.visualization import plot_histogram
from qiskit.circuit.library import TwoLocal
from qiskit_optimization.applications import Maxcut, Tsp
from qiskit_algorithms import SamplingVQE, NumPyMinimumEigensolver
from qiskit_algorithms.optimizers import SPSA
from qiskit_algorithms.utils import algorithm_globals
from qiskit.primitives import Sampler
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import Aer, execute
from qiskit.circuit import Parameter
# General imports
import numpy as np
import warnings
warnings.filterwarnings("ignore")

# Pre-defined ansatz circuit, operator class and visualization tools
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
from qiskit.visualization import plot_distribution

# Qiskit Runtime
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Estimator, Sampler, Session, Options

# SciPy minimizer routine
from scipy.optimize import minimize

# rustworkx graph library
import rustworkx as rx
from rustworkx.visualization import mpl_draw

Create an QAOA ansatz with the help of a simple max cut instance:

# Generating a graph of 4 nodes

seed = 1
num_nodes = 8
G = nx.random_regular_graph(d=3, n=num_nodes, seed=seed)
nx.draw(G, with_labels=True, pos=nx.spring_layout(G, seed=seed))

maxcut = Maxcut(G)
problem = maxcut.to_quadratic_program()
print(problem.prettyprint())

Made Ising Hamiltonian

qubitOp, offset = problem.to_ising()
print("Offset:", offset)
print("Ising Hamiltonian:")
print(str(qubitOp))

Make a QAOA Ansatz out of it

# QAOA ansatz circuit
ansatz = QAOAAnsatz(qubitOp, reps=1)

# the reps in decompose means, the number of times the circuit should be decomposed
# for example reps = 2 means circuit.decompise().decompose()
ansatz.decompose(reps=3).draw(output="mpl", style="iqp",fold=-1)

gee

Transpile it and then look for best cuts

circuit_basis = transpile(ansatz, basis_gates=['u3', 'cx'], optimization_level=3)
from circuit_knitting.cutting import partition_problem
from circuit_knitting.cutting.cut_finding.cco_utils import qc_to_cco_circuit
circuit_ckt = qc_to_cco_circuit(circuit_basis)

import numpy as np
from circuit_knitting.cutting.cut_finding.circuit_interface import SimpleGateList
from circuit_knitting.cutting.cut_finding.lo_cuts_optimizer import LOCutsOptimizer
from circuit_knitting.cutting.cut_finding.optimization_settings import (
    OptimizationSettings,
)
from circuit_knitting.cutting.cut_finding.quantum_device_constraints import (
    DeviceConstraints,
)

settings = OptimizationSettings(rand_seed=12345)

settings.set_engine_selection("CutOptimization", "BestFirst")

qubits_per_QPU = 6
num_QPUs = 3

for num_qpus in range(num_QPUs, 1, -1):
    for qpu_qubits in range(qubits_per_QPU, 1, -1):
        print(f"\n\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------")

        constraint_obj = DeviceConstraints(qubits_per_QPU=qpu_qubits, num_QPUs=num_QPUs)

        interface = SimpleGateList(circuit_ckt)

        op = LOCutsOptimizer(interface, settings, constraint_obj)

        out = op.optimize()

        print(
            " Gamma =",
            None if (out is None) else out.upper_bound_gamma(),
            ", Min_gamma_reached =",
            op.minimum_reached(),
        )
        if out is not None:
            out.print(simple=True)
        else:
            print(out)

        print(
            "Subcircuits:",
            interface.export_subcircuits_as_string(name_mapping="default"),
            "\n",
        )

The top two results from these are :

---------- 6 Qubits per QPU, 3 QPUs ---------- Gamma = 64.0 , Min_gamma_reached = True [OneWireCutIdentifier(cut_action='CutLeftWire', wire_cut_location=WireCutLocation(instruction_id=22, gate_name='cx', input=1)), OneWireCutIdentifier(cut_action='CutLeftWire', wire_cut_location=WireCutLocation(instruction_id=30, gate_name='cx', input=1)), OneWireCutIdentifier(cut_action='CutRightWire', wire_cut_location=WireCutLocation(instruction_id=47, gate_name='cx', input=2))] Subcircuits: AAABAABBBAB

and

---------- 5 Qubits per QPU, 3 QPUs ---------- Gamma = 144.0 , Min_gamma_reached = True [CutIdentifier(cut_action='CutTwoQubitGate', gate_cut_location=GateCutLocation(instruction_id=10, gate_name='cx')), CutIdentifier(cut_action='CutTwoQubitGate', gate_cut_location=GateCutLocation(instruction_id=12, gate_name='cx')), OneWireCutIdentifier(cut_action='CutLeftWire', wire_cut_location=WireCutLocation(instruction_id=30, gate_name='cx', input=1)), OneWireCutIdentifier(cut_action='CutRightWire', wire_cut_location=WireCutLocation(instruction_id=47, gate_name='cx', input=2))] Subcircuits: AABAABBBAB

But then I make use of the following code:

from circuit_knitting.cutting import find_cuts

# Specify settings for the cut-finding optimizer
optimization_settings = {"rand_seed": 1157}

# Specify the size and number of the QPUs available
device_constraints = {"qubits_per_QPU": 5, "num_QPUs": 3}

cut_circuit, metadata = find_cuts(circuit_basis, optimization_settings, device_constraints)
print(
    f'Found solution using {len(metadata["cuts"])} cuts with a sampling '
    f'overhead of {metadata["sampling_overhead"]}.'
)

It gives the following Error:

IndexError: tuple index out of range

It works fine for (6,3) case, but always shows error on the (5,3) case. Given the cut is possible, why is it still not giving the resultant circuit.

I also tried the latest verison of the code, i.e

from circuit_knitting.cutting import (
    OptimizationParameters,
    DeviceConstraints,
    find_cuts,
)

# Specify settings for the cut-finding optimizer
optimization_settings = OptimizationParameters(seed=111)

# Specify the size of the QPUs available
device_constraints = DeviceConstraints(qubits_per_qpu=5)

cut_circuit, metadata = find_cuts(circuit_basis, optimization_settings, device_constraints)
print(
    f'Found solution using {len(metadata["cuts"])} cuts with a sampling '
    f'overhead of {metadata["sampling_overhead"]}.'
)
for cut in metadata["cuts"]:
    print(f"{cut[0]} at index {cut[1]}")
cut_circuit.draw("mpl", scale=0.8, fold=-1)

It still gives the same error for qubits_per_qpu=5. It works fine for other numbers. My Quantum Circuit is an 8 qubit one, given the (5,3) format adds extra qubit, do I have to manually add extra qubits before passing it on the code?

garrison commented 5 months ago

Thank you for the report. It appears you are using the branch of PR #471 -- is that correct? Ultimately, I expect we will expose only find_cuts as a public API.

sirgeorgesawcon commented 5 months ago

Yes, I am using that. In one of my earlier reported issues, I mentioned another problem with the same code as well. Now as I'm implementing a QAOA, I'm getting this error.

This was the previous error.

sirgeorgesawcon commented 5 months ago

I updated my code to show that detail.

On Thu, Mar 21, 2024 at 11:46 PM Ibrahim Shehzad @.***> wrote:

Trying to reproduce this, I'm assuming networkx is imported as nx? Also, where is Maxcut imported from?

— Reply to this email directly, view it on GitHub https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/issues/508#issuecomment-2012719354, or unsubscribe https://github.com/notifications/unsubscribe-auth/APBYKUSA6M7B3UC2BT5YNODYZL6GVAVCNFSM6AAAAABFA5EAPWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMJSG4YTSMZVGQ . You are receiving this because you authored the thread.Message ID: @.*** com>

ibrahim-shehzad commented 5 months ago

Thank you. After the latest push, (with commits 3305225 and 0c5d50f) this bug should now be fixed. Please follow up if it persists. Note also that this tutorial will eventually be deprecated and only the one here will be kept.

sirgeorgesawcon commented 5 months ago

Hi, it does work, but in the code of DeviceConstraint, we can only set the qubits_per_qpu, is the functionality where we can also provide number of qpu's as an argument deprecated?

class DeviceConstraints:
    """Specify the constraints (qubits per QPU) that must be respected."""

    qubits_per_qpu: int

    def __post_init__(self):
        """Post-init method for data class."""
        if self.qubits_per_qpu < 1:
            raise ValueError("qubits_per_QPU must be a positive definite integer.")

    def get_qpu_width(self) -> int:
        """Return the number of qubits supported on each individual QPU."""
        return self.qubits_per_qpu

That was helpful, since we can get even less overhead sampling if we can divide it into three different subcircuits!

ibrahim-shehzad commented 5 months ago

Thank you for your comment.

is the functionality where we can also provide number of qpu's as an argument deprecated?

Yes, in the way the code is desgined, that functionality is only relevant for LOCC, which is why it was taken out. We intend to clarify this further in the documentation but qubits_per_qpu is actually equivalent to "qubits per subcircuit" and so that can be used to assert that the input circuit be divided up into a given number of subcircuits.

ibrahim-shehzad commented 5 months ago

Follow up: this attribute has now been renamed to qubits_per_subcircuit to avoid confusion.

sirgeorgesawcon commented 5 months ago

Thanks @Ibrahim-Shehzad , that helped clear a lot of confusion.