Remove deprecated BIPMapping pass (#10526)

* Remove deprecated BIPMapping pass

This commit removes the deprecated BIPMapping pass. It was deprecated in
qiskit-terra 0.24.0 and the minimum deprecation period has elapsed for
the Qiskit 0.45.0 release. This pass has been moved to an external
plugin that can be installed separately and integrated into the
transpiler more cleanly than a single pass and also separates the
optional dependency on the proprietary CPLEX into a separate package.

* Apply suggestions from code review

Co-authored-by: Kevin Hartman <kevin@hart.mn>

* Remove additional uses of BIPMapping

There were still two locations where the BIPMapping pass was being used.
First the dedicated unit tests, and then also import redirects for the
docs and easier access. This commit removes these locations so nothing
left is using the BIPMapping pass.

* Update optionals documentation

* Update install commands

---------

Co-authored-by: Kevin Hartman <kevin@hart.mn>
This commit is contained in:
Matthew Treinish 2023-09-12 11:44:25 -04:00 committed by GitHub
parent 884cfe6c66
commit 2ab1e547e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 21 additions and 1156 deletions

View File

@ -45,7 +45,6 @@ Routing
LookaheadSwap
StochasticSwap
SabreSwap
BIPMapping
Commuting2qGateRouter
Basis Change
@ -199,7 +198,6 @@ from .routing import LayoutTransformation
from .routing import LookaheadSwap
from .routing import StochasticSwap
from .routing import SabreSwap
from .routing import BIPMapping
from .routing import Commuting2qGateRouter
# basis change

View File

@ -17,6 +17,5 @@ from .layout_transformation import LayoutTransformation
from .lookahead_swap import LookaheadSwap
from .stochastic_swap import StochasticSwap
from .sabre_swap import SabreSwap
from .bip_mapping import BIPMapping
from .commuting_2q_gate_routing.commuting_2q_gate_router import Commuting2qGateRouter
from .commuting_2q_gate_routing.swap_strategy import SwapStrategy

View File

@ -1,497 +0,0 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Integer programming model for quantum circuit compilation."""
import copy
import logging
from functools import lru_cache
import numpy as np
from qiskit.transpiler.exceptions import TranspilerError, CouplingError
from qiskit.transpiler.layout import Layout
from qiskit.circuit.library.standard_gates import SwapGate
from qiskit.providers.models import BackendProperties
from qiskit.quantum_info import two_qubit_cnot_decompose
from qiskit.quantum_info.synthesis.two_qubit_decompose import (
TwoQubitWeylDecomposition,
trace_to_fid,
)
from qiskit.utils import optionals as _optionals
from qiskit.utils.deprecation import deprecate_func
logger = logging.getLogger(__name__)
@_optionals.HAS_DOCPLEX.require_in_instance
class BIPMappingModel:
"""Internal model to create and solve a BIP problem for mapping.
Attributes:
problem (Model):
A CPLEX problem model object, which is set by calling
:method:`create_cpx_problem`. After calling :method:`solve_cpx_problem`,
the solution will be stored in :attr:`solution`). None if it's not yet set.
"""
@deprecate_func(
since="0.24.0",
additional_msg="This has been replaced by a new transpiler plugin package: "
"qiskit-bip-mapper. More details can be found here: "
"https://github.com/qiskit-community/qiskit-bip-mapper",
) # pylint: disable=bad-docstring-quotes
def __init__(self, dag, coupling_map, qubit_subset, dummy_timesteps=None):
"""
Args:
dag (DAGCircuit): DAG circuit to be mapped
coupling_map (CouplingMap): Coupling map of the device on which the `dag` is mapped.
qubit_subset (list[int]): Sublist of physical qubits to be used in the mapping.
dummy_timesteps (int):
Number of dummy time steps, after each real layer of gates, to
allow arbitrary swaps between neighbors.
Raises:
MissingOptionalLibraryError: If docplex is not installed
TranspilerError: If size of virtual qubits and physical qubits differ, or
if coupling_map is not symmetric (bidirectional).
"""
self._dag = dag
self._coupling = copy.deepcopy(coupling_map) # reduced coupling map
try:
self._coupling = self._coupling.reduce(qubit_subset)
except CouplingError as err:
raise TranspilerError(
"The 'coupling_map' reduced by 'qubit_subset' must be connected."
) from err
self._coupling.make_symmetric()
self.global_qubit = qubit_subset # the map from reduced qubit index to global qubit index
self.problem = None
self.solution = None
self.num_vqubits = len(self._dag.qubits)
self.num_pqubits = self._coupling.size()
self._arcs = self._coupling.get_edges()
if self.num_vqubits != self.num_pqubits:
raise TranspilerError(
"BIPMappingModel assumes the same size of virtual and physical qubits."
)
self._index_to_virtual = dict(enumerate(dag.qubits))
# Construct internal circuit model
# Extract layers with 2-qubit gates
self._to_su4layer = []
self.su4layers = []
for lay in dag.layers():
laygates = []
for node in lay["graph"].two_qubit_ops():
i1 = self._dag.find_bit(node.qargs[0]).index
i2 = self._dag.find_bit(node.qargs[1]).index
laygates.append(((i1, i2), node))
if laygates:
self._to_su4layer.append(len(self.su4layers))
self.su4layers.append(laygates)
else:
self._to_su4layer.append(-1)
# Add dummy time steps inbetween su4layers. Dummy time steps can only contain SWAPs.
self.gates = [] # layered 2q-gates with dummy steps
for k, lay in enumerate(self.su4layers):
self.gates.append(lay)
if k == len(self.su4layers) - 1: # do not add dummy steps after the last layer
break
self.gates.extend([[]] * dummy_timesteps)
self.bprop = None # Backend properties to compute cx fidelities (set later if necessary)
self.default_cx_error_rate = (
None # Default cx error rate in case backend properties are not available
)
logger.info("Num virtual qubits: %d", self.num_vqubits)
logger.info("Num physical qubits: %d", self.num_pqubits)
logger.info("Model depth: %d", self.depth)
logger.info("Dummy steps: %d", dummy_timesteps)
@property
def depth(self):
"""Number of time-steps (including dummy steps)."""
return len(self.gates)
def is_su4layer(self, depth: int) -> bool:
"""Check if the depth-th layer is su4layer (layer containing 2q-gates) or not.
Args:
depth: Depth of the ordinary layer
Returns:
True if the depth-th layer is su4layer, otherwise False
"""
return self._to_su4layer[depth] >= 0
def to_su4layer_depth(self, depth: int) -> int:
"""Return the depth as a su4layer. If the depth-th layer is not a su4layer, return -1.
Args:
depth: Depth of the ordinary layer
Returns:
su4layer depth if the depth-th layer is a su4layer, otherwise -1
"""
return self._to_su4layer[depth]
# pylint: disable=invalid-name
def _is_dummy_step(self, t: int):
"""Check if the time-step t is a dummy step or not."""
return len(self.gates[t]) == 0
@_optionals.HAS_DOCPLEX.require_in_call
def create_cpx_problem(
self,
objective: str,
backend_prop: BackendProperties = None,
line_symm: bool = False,
depth_obj_weight: float = 0.1,
default_cx_error_rate: float = 5e-3,
):
"""Create integer programming model to compile a circuit.
Args:
objective:
Type of objective function to be minimized:
* ``'gate_error'``: Approximate gate error of the circuit, which is given as the sum of
negative logarithm of CNOT gate fidelities in the circuit. It takes into account
only the CNOT gate errors reported in ``backend_prop``.
* ``'depth'``: Depth (number of timesteps) of the circuit
* ``'balanced'``: Weighted sum of gate_error and depth
backend_prop:
Backend properties storing gate errors, which are required in computing certain
types of objective function such as ``'gate_error'`` or ``'balanced'``.
If this is not available, default_cx_error_rate is used instead.
line_symm:
Use symmetry breaking constrainst for line topology. Should
only be True if the hardware graph is a chain/line/path.
depth_obj_weight:
Weight of depth objective in ``'balanced'`` objective function.
default_cx_error_rate:
Default CX error rate to be used if backend_prop is not available.
Raises:
TranspilerError: if unknown objective type is specified or invalid options are specified.
MissingOptionalLibraryError: If docplex is not installed
"""
self.bprop = backend_prop
self.default_cx_error_rate = default_cx_error_rate
if self.bprop is None and self.default_cx_error_rate is None:
raise TranspilerError("BackendProperties or default_cx_error_rate must be specified")
from docplex.mp.model import Model
mdl = Model()
# *** Define main variables ***
# Add w variables
w = {}
for t in range(self.depth):
for q in range(self.num_vqubits):
for j in range(self.num_pqubits):
w[t, q, j] = mdl.binary_var(name=f"w_{t}_{q}_{j}")
# Add y variables
y = {}
for t in range(self.depth):
for ((p, q), _) in self.gates[t]:
for (i, j) in self._arcs:
y[t, p, q, i, j] = mdl.binary_var(name=f"y_{t}_{p}_{q}_{i}_{j}")
# Add x variables
x = {}
for t in range(self.depth - 1):
for q in range(self.num_vqubits):
for i in range(self.num_pqubits):
x[t, q, i, i] = mdl.binary_var(name=f"x_{t}_{q}_{i}_{i}")
for j in self._coupling.neighbors(i):
x[t, q, i, j] = mdl.binary_var(name=f"x_{t}_{q}_{i}_{j}")
# *** Define main constraints ***
# Assignment constraints for w variables
for t in range(self.depth):
for q in range(self.num_vqubits):
mdl.add_constraint(
sum(w[t, q, j] for j in range(self.num_pqubits)) == 1,
ctname=f"assignment_vqubits_{q}_at_{t}",
)
for t in range(self.depth):
for j in range(self.num_pqubits):
mdl.add_constraint(
sum(w[t, q, j] for q in range(self.num_vqubits)) == 1,
ctname=f"assignment_pqubits_{j}_at_{t}",
)
# Each gate must be implemented
for t in range(self.depth):
for ((p, q), _) in self.gates[t]:
mdl.add_constraint(
sum(y[t, p, q, i, j] for (i, j) in self._arcs) == 1,
ctname=f"implement_gate_{p}_{q}_at_{t}",
)
# Gate can be implemented iff both of its qubits are located at the associated nodes
for t in range(self.depth - 1):
for ((p, q), _) in self.gates[t]:
for (i, j) in self._arcs:
# Apply McCormick to y[t, p, q, i, j] == w[t, p, i] * w[t, q, j]
mdl.add_constraint(
y[t, p, q, i, j] >= w[t, p, i] + w[t, q, j] - 1,
ctname=f"McCormickLB_{p}_{q}_{i}_{j}_at_{t}",
)
# Stronger version of McCormick: gate (p,q) is implemented at (i, j)
# if i moves to i or j, and j moves to i or j
mdl.add_constraint(
y[t, p, q, i, j] <= x[t, p, i, i] + x[t, p, i, j],
ctname=f"McCormickUB1_{p}_{q}_{i}_{j}_at_{t}",
)
mdl.add_constraint(
y[t, p, q, i, j] <= x[t, q, j, i] + x[t, q, j, j],
ctname=f"McCormickUB2_{p}_{q}_{i}_{j}_at_{t}",
)
# For last time step, use regular McCormick
for ((p, q), _) in self.gates[self.depth - 1]:
for (i, j) in self._arcs:
# Apply McCormick to y[self.depth - 1, p, q, i, j]
# == w[self.depth - 1, p, i] * w[self.depth - 1, q, j]
mdl.add_constraint(
y[self.depth - 1, p, q, i, j]
>= w[self.depth - 1, p, i] + w[self.depth - 1, q, j] - 1,
ctname=f"McCormickLB_{p}_{q}_{i}_{j}_at_last",
)
mdl.add_constraint(
y[self.depth - 1, p, q, i, j] <= w[self.depth - 1, p, i],
ctname=f"McCormickUB1_{p}_{q}_{i}_{j}_at_last",
)
mdl.add_constraint(
y[self.depth - 1, p, q, i, j] <= w[self.depth - 1, q, j],
ctname=f"McCormickUB2_{p}_{q}_{i}_{j}_at_last",
)
# Logical qubit flow-out constraints
for t in range(self.depth - 1): # Flow out; skip last time step
for q in range(self.num_vqubits):
for i in range(self.num_pqubits):
mdl.add_constraint(
w[t, q, i]
== x[t, q, i, i] + sum(x[t, q, i, j] for j in self._coupling.neighbors(i)),
ctname=f"flow_out_{q}_{i}_at_{t}",
)
# Logical qubit flow-in constraints
for t in range(1, self.depth): # Flow in; skip first time step
for q in range(self.num_vqubits):
for i in range(self.num_pqubits):
mdl.add_constraint(
w[t, q, i]
== x[t - 1, q, i, i]
+ sum(x[t - 1, q, j, i] for j in self._coupling.neighbors(i)),
ctname=f"flow_in_{q}_{i}_at_{t}",
)
# If a gate is implemented, involved qubits cannot swap with other positions
for t in range(self.depth - 1):
for ((p, q), _) in self.gates[t]:
for (i, j) in self._arcs:
mdl.add_constraint(
x[t, p, i, j] == x[t, q, j, i], ctname=f"swap_{p}_{q}_{i}_{j}_at_{t}"
)
# Qubit not in gates can flip with their neighbors
for t in range(self.depth - 1):
q_no_gate = list(range(self.num_vqubits))
for ((p, q), _) in self.gates[t]:
q_no_gate.remove(p)
q_no_gate.remove(q)
for (i, j) in self._arcs:
mdl.add_constraint(
sum(x[t, q, i, j] for q in q_no_gate) == sum(x[t, p, j, i] for p in q_no_gate),
ctname=f"swap_no_gate_{i}_{j}_at_{t}",
)
# *** Define supplemental variables ***
# Add z variables to count dummy steps (supplemental variables for symmetry breaking)
z = {}
for t in range(self.depth):
if self._is_dummy_step(t):
z[t] = mdl.binary_var(name=f"z_{t}")
# *** Define supplemental constraints ***
# See if a dummy time step is needed
for t in range(self.depth):
if self._is_dummy_step(t):
for q in range(self.num_vqubits):
mdl.add_constraint(
sum(x[t, q, i, j] for (i, j) in self._arcs) <= z[t],
ctname=f"dummy_ts_needed_for_vqubit_{q}_at_{t}",
)
# Symmetry breaking between dummy time steps
for t in range(self.depth - 1):
# This is a dummy time step and the next one is dummy too
if self._is_dummy_step(t) and self._is_dummy_step(t + 1):
# We cannot use the next time step unless this one is used too
mdl.add_constraint(z[t] >= z[t + 1], ctname=f"dummy_precedence_{t}")
# Symmetry breaking on the line -- only works on line topology!
if line_symm:
for h in range(1, self.num_vqubits):
mdl.add_constraint(
sum(w[0, p, 0] for p in range(h))
+ sum(w[0, q, self.num_pqubits - 1] for q in range(h, self.num_vqubits))
>= 1,
ctname=f"sym_break_line_{h}",
)
# *** Define objevtive function ***
if objective == "depth":
objexr = sum(z[t] for t in range(self.depth) if self._is_dummy_step(t))
for t in range(self.depth - 1):
for q in range(self.num_vqubits):
for (i, j) in self._arcs:
objexr += 0.01 * x[t, q, i, j]
mdl.minimize(objexr)
elif objective in ("gate_error", "balanced"):
# We add the depth objective with coefficient depth_obj_weight if balanced was selected.
objexr = 0
for t in range(self.depth - 1):
for (p, q), node in self.gates[t]:
for (i, j) in self._arcs:
# We pay the cost for gate implementation.
pbest_fid = -np.log(self._max_expected_fidelity(node, i, j))
objexr += y[t, p, q, i, j] * pbest_fid
# If a gate is mirrored (followed by a swap on the same qubit pair),
# its cost should be replaced with the cost of the combined (mirrored) gate.
pbest_fidm = -np.log(self._max_expected_mirrored_fidelity(node, i, j))
objexr += x[t, q, i, j] * (pbest_fidm - pbest_fid) / 2
# Cost of swaps on unused qubits
for q in range(self.num_vqubits):
used_qubits = {q for (pair, _) in self.gates[t] for q in pair}
if q not in used_qubits:
for i in range(self.num_pqubits):
for j in self._coupling.neighbors(i):
objexr += x[t, q, i, j] * -3 / 2 * np.log(self._cx_fidelity(i, j))
# Cost for the last layer (x variables are not defined for depth-1)
for (p, q), node in self.gates[self.depth - 1]:
for (i, j) in self._arcs:
pbest_fid = -np.log(self._max_expected_fidelity(node, i, j))
objexr += y[self.depth - 1, p, q, i, j] * pbest_fid
if objective == "balanced":
objexr += depth_obj_weight * sum(
z[t] for t in range(self.depth) if self._is_dummy_step(t)
)
mdl.minimize(objexr)
else:
raise TranspilerError(f"Unknown objective type: {objective}")
self.problem = mdl
logger.info("BIP problem stats: %s", self.problem.statistics)
def _max_expected_fidelity(self, node, i, j):
return max(
gfid * self._cx_fidelity(i, j) ** k
for k, gfid in enumerate(self._gate_fidelities(node))
)
def _max_expected_mirrored_fidelity(self, node, i, j):
return max(
gfid * self._cx_fidelity(i, j) ** k
for k, gfid in enumerate(self._mirrored_gate_fidelities(node))
)
def _cx_fidelity(self, i, j) -> float:
# fidelity of cx on global physical qubits
if self.bprop is not None:
return 1.0 - self.bprop.gate_error("cx", [self.global_qubit[i], self.global_qubit[j]])
else:
return 1.0 - self.default_cx_error_rate
@staticmethod
@lru_cache()
def _gate_fidelities(node):
matrix = node.op.to_matrix()
target = TwoQubitWeylDecomposition(matrix)
traces = two_qubit_cnot_decompose.traces(target)
return [trace_to_fid(traces[i]) for i in range(4)]
@staticmethod
@lru_cache()
def _mirrored_gate_fidelities(node):
matrix = node.op.to_matrix()
swap = SwapGate().to_matrix()
targetm = TwoQubitWeylDecomposition(matrix @ swap)
tracesm = two_qubit_cnot_decompose.traces(targetm)
return [trace_to_fid(tracesm[i]) for i in range(4)]
@_optionals.HAS_CPLEX.require_in_call
def solve_cpx_problem(self, time_limit: float = 60, threads: int = None) -> str:
"""Solve the BIP problem using CPLEX.
Args:
time_limit:
Time limit (seconds) given to CPLEX.
threads:
Number of threads to be allowed for CPLEX to use.
Returns:
Status string that CPLEX returned after solving the BIP problem.
Raises:
MissingOptionalLibraryError: If CPLEX is not installed
"""
self.problem.set_time_limit(time_limit)
if threads is not None:
self.problem.context.cplex_parameters.threads = threads
self.problem.context.cplex_parameters.randomseed = 777
self.solution = self.problem.solve()
status = self.problem.solve_details.status
logger.info("BIP solution status: %s", status)
return status
def get_layout(self, t: int) -> Layout:
"""Get layout at time-step t.
Args:
t: Time-step
Returns:
Layout
"""
dic = {}
for q in range(self.num_vqubits):
for i in range(self.num_pqubits):
if self.solution.get_value(f"w_{t}_{q}_{i}") > 0.5:
dic[self._index_to_virtual[q]] = self.global_qubit[i]
layout = Layout(dic)
for reg in self._dag.qregs.values():
layout.add_register(reg)
return layout
def get_swaps(self, t: int) -> list:
"""Get swaps (pairs of physical qubits) inserted at time-step ``t``.
Args:
t: Time-step (<= depth - 1)
Returns:
List of swaps (pairs of physical qubits (integers))
"""
swaps = []
for (i, j) in self._arcs:
if i >= j:
continue
for q in range(self.num_vqubits):
if self.solution.get_value(f"x_{t}_{q}_{i}_{j}") > 0.5:
swaps.append((self.global_qubit[i], self.global_qubit[j]))
return swaps

View File

@ -1,273 +0,0 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Map a DAGCircuit onto a given ``coupling_map``, allocating qubits and adding swap gates."""
import copy
import logging
import math
from qiskit.circuit import QuantumRegister
from qiskit.circuit.library.standard_gates import SwapGate
from qiskit.dagcircuit import DAGCircuit, DAGOpNode
from qiskit.utils import optionals as _optionals
from qiskit.transpiler import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes.routing.algorithms.bip_model import BIPMappingModel
from qiskit.transpiler.target import target_to_backend_properties, Target
from qiskit.utils.deprecation import deprecate_func
from qiskit.transpiler.passes.layout import disjoint_utils
logger = logging.getLogger(__name__)
@_optionals.HAS_CPLEX.require_in_instance("BIP-based mapping pass")
@_optionals.HAS_DOCPLEX.require_in_instance("BIP-based mapping pass")
class BIPMapping(TransformationPass):
r"""Map a DAGCircuit onto a given ``coupling_map``, allocating qubits and adding swap gates.
The BIP mapper tries to find the best layout and routing at once by
solving a BIP (binary integer programming) problem as described in [1].
The BIP problem represents the layer-by-layer mapping of 2-qubit gates, assuming all the gates
in a layer can be run on the ``coupling_map``. In the problem, the variables :math:`w` represent
the layout of qubits for each layer and the variables :math:`x` represent which pair of qubits
should be swapped in between layers. Based on the values in the solution of the BIP problem,
the mapped circuit will be constructed.
The BIP mapper depends on ``docplex`` to represent the BIP problem and CPLEX (``cplex``)
to solve it. Those packages can be installed with ``pip install qiskit-terra[bip-mapper]``.
Since the free version of CPLEX can solve only small BIP problems, i.e. mapping of circuits
with less than about 5 qubits, the paid version of CPLEX may be needed to map larger circuits.
If you want to fix physical qubits to be used in the mapping (e.g. running Quantum Volume
circuits), you need to supply ``qubit_subset``, i.e. list of physical qubits to be used
within the ``coupling_map``.
Please do not use ``initial_layout`` for that purpose because the BIP mapper gracefully
ignores ``initial_layout`` (and tries to determines its best layout).
.. warning::
The BIP mapper does not scale very well with respect to the number of qubits or gates.
For example, it may not work with ``qubit_subset`` beyond 10 qubits because
the BIP solver (CPLEX) may not find any solution within the default time limit.
**References:**
[1] G. Nannicini et al. "Optimal qubit assignment and routing via integer programming."
`arXiv:2106.06446 <https://arxiv.org/abs/2106.06446>`_
"""
@deprecate_func(
since="0.24.0",
additional_msg="This has been replaced by a new transpiler plugin package: "
"qiskit-bip-mapper. More details can be found here: "
"https://github.com/qiskit-community/qiskit-bip-mapper",
) # pylint: disable=bad-docstring-quotes
def __init__(
self,
coupling_map,
qubit_subset=None,
objective="balanced",
backend_prop=None,
time_limit=30,
threads=None,
max_swaps_inbetween_layers=None,
depth_obj_weight=0.1,
default_cx_error_rate=5e-3,
):
"""BIPMapping initializer.
Args:
coupling_map (Union[CouplingMap, Target]): Directed graph represented a coupling map.
qubit_subset (list[int]): Sublist of physical qubits to be used in the mapping.
If None, all qubits in the coupling_map will be considered.
objective (str): Type of objective function to be minimized:
* ``'gate_error'``: Approximate gate error of the circuit, which is given as the sum of
negative logarithm of 2q-gate fidelities in the circuit. It takes into account only
the 2q-gate (CNOT) errors reported in ``backend_prop`` and ignores the other errors
in such as 1q-gates, SPAMs and idle times.
* ``'depth'``: Depth (number of 2q-gate layers) of the circuit.
* ``'balanced'``: [Default] Weighted sum of ``'gate_error'`` and ``'depth'``
backend_prop (BackendProperties): Backend properties object containing 2q-gate gate errors,
which are required in computing certain types of objective function
such as ``'gate_error'`` or ``'balanced'``. If this is not available,
default_cx_error_rate is used instead.
time_limit (float): Time limit for solving BIP in seconds
threads (int): Number of threads to be allowed for CPLEX to solve BIP
max_swaps_inbetween_layers (int):
Number of swaps allowed in between layers. If None, automatically set.
Large value could decrease the probability to build infeasible BIP problem but also
could reduce the chance of finding a feasible solution within the ``time_limit``.
depth_obj_weight (float):
Weight of depth objective in ``'balanced'`` objective. The balanced objective is the
sum of error_rate + depth_obj_weight * depth.
default_cx_error_rate (float):
Default CX error rate to be used if backend_prop is not available.
Raises:
MissingOptionalLibraryError: if cplex or docplex are not installed.
TranspilerError: if invalid options are specified.
"""
super().__init__()
if isinstance(coupling_map, Target):
self.target = coupling_map
self.coupling_map = self.target.build_coupling_map()
self.backend_prop = target_to_backend_properties(self.target)
else:
self.target = None
self.coupling_map = coupling_map
self.backend_prop = None
self.qubit_subset = qubit_subset
self.objective = objective
if backend_prop is not None:
self.backend_prop = backend_prop
self.time_limit = time_limit
self.threads = threads
self.max_swaps_inbetween_layers = max_swaps_inbetween_layers
self.depth_obj_weight = depth_obj_weight
self.default_cx_error_rate = default_cx_error_rate
if self.coupling_map is not None and self.qubit_subset is None:
self.qubit_subset = list(range(self.coupling_map.size()))
def run(self, dag):
"""Run the BIPMapping pass on `dag`, assuming the number of virtual qubits (defined in
`dag`) and the number of physical qubits (defined in `coupling_map`) are the same.
Args:
dag (DAGCircuit): DAG to map.
Returns:
DAGCircuit: A mapped DAG. If there is no 2q-gate in DAG or it fails to map,
returns the original dag.
Raises:
TranspilerError: if the number of virtual and physical qubits are not the same.
AssertionError: if the final layout is not valid.
"""
if self.coupling_map is None:
return dag
if len(dag.qubits) > len(self.qubit_subset):
raise TranspilerError("More virtual qubits exist than physical qubits.")
if len(dag.qubits) != len(self.qubit_subset):
raise TranspilerError(
"BIPMapping requires the number of virtual and physical qubits to be the same. "
"Supply 'qubit_subset' to specify physical qubits to use."
)
disjoint_utils.require_layout_isolated_to_component(
dag, self.coupling_map if self.target is None else self.target
)
original_dag = dag
dummy_steps = math.ceil(math.sqrt(dag.num_qubits()))
if self.max_swaps_inbetween_layers is not None:
dummy_steps = max(0, self.max_swaps_inbetween_layers - 1)
model = BIPMappingModel(
dag=dag,
coupling_map=self.coupling_map,
qubit_subset=self.qubit_subset,
dummy_timesteps=dummy_steps,
)
if len(model.su4layers) == 0:
logger.info("BIPMapping is skipped due to no 2q-gates.")
return original_dag
model.create_cpx_problem(
objective=self.objective,
backend_prop=self.backend_prop,
depth_obj_weight=self.depth_obj_weight,
default_cx_error_rate=self.default_cx_error_rate,
)
status = model.solve_cpx_problem(time_limit=self.time_limit, threads=self.threads)
if model.solution is None:
logger.warning("Failed to solve a BIP problem. Status: %s", status)
return original_dag
# Get the optimized initial layout
optimized_layout = model.get_layout(0)
# Create a layout to track changes in layout for each layer
layout = copy.deepcopy(optimized_layout)
# Construct the mapped circuit
canonical_qreg = QuantumRegister(self.coupling_map.size(), "q")
mapped_dag = self._create_empty_dagcircuit(dag, canonical_qreg)
interval = dummy_steps + 1
for k, layer in enumerate(dag.layers()):
if model.is_su4layer(k):
su4dep = model.to_su4layer_depth(k)
# add swaps between (su4dep-1)-th and su4dep-th su4layer
from_steps = max(interval * (su4dep - 1), 0)
to_steps = min(interval * su4dep, model.depth - 1)
for t in range(from_steps, to_steps): # pylint: disable=invalid-name
for (i, j) in model.get_swaps(t):
mapped_dag.apply_operation_back(
op=SwapGate(),
qargs=[canonical_qreg[i], canonical_qreg[j]],
)
# update layout, swapping physical qubits (i, j)
layout.swap(i, j)
# map gates in k-th layer
for node in layer["graph"].nodes():
if isinstance(node, DAGOpNode):
mapped_dag.apply_operation_back(
op=copy.deepcopy(node.op),
qargs=[canonical_qreg[layout[q]] for q in node.qargs],
cargs=node.cargs,
)
# TODO: double check with y values?
# Check final layout
final_layout = model.get_layout(model.depth - 1)
if layout != final_layout:
raise AssertionError(
f"Bug: final layout {final_layout} != the layout computed from swaps {layout}"
)
self.property_set["layout"] = self._to_full_layout(optimized_layout)
self.property_set["final_layout"] = self._to_full_layout(final_layout)
return mapped_dag
@staticmethod
def _create_empty_dagcircuit(source_dag: DAGCircuit, canonical_qreg: QuantumRegister):
target_dag = DAGCircuit()
target_dag.name = source_dag.name
target_dag._global_phase = source_dag._global_phase
target_dag.metadata = source_dag.metadata
target_dag.add_qreg(canonical_qreg)
for creg in source_dag.cregs.values():
target_dag.add_creg(creg)
return target_dag
def _to_full_layout(self, layout):
# fill layout with ancilla qubits (required by drawers)
idle_physical_qubits = [
q for q in range(self.coupling_map.size()) if q not in layout.get_physical_bits()
]
if idle_physical_qubits:
qreg = QuantumRegister(len(idle_physical_qubits), name="ancilla")
for idx, idle_q in enumerate(idle_physical_qubits):
layout[idle_q] = qreg[idx]
layout.add_register(qreg)
return layout

View File

@ -58,7 +58,8 @@ External Python Libraries
* - .. py:data:: HAS_CPLEX
- The `IBM CPLEX Optimizer <https://www.ibm.com/analytics/cplex-optimizer>`__ is a
high-performance mathematical programming solver for linear, mixed-integer and quadratic
programming. It is required by the :class:`.BIPMapping` transpiler pass.
programming. This is no longer by Qiskit, but it weas historically and the optional
remains for backwards compatibility.
* - .. py:data:: HAS_CVXPY
- `CVXPY <https://www.cvxpy.org/>`__ is a Python package for solving convex optimization
@ -68,7 +69,8 @@ External Python Libraries
* - .. py:data:: HAS_DOCPLEX
- `IBM Decision Optimization CPLEX Modelling
<http://ibmdecisionoptimization.github.io/docplex-doc/>`__ is a library for prescriptive
analysis. Like CPLEX, it is required for the :class:`.BIPMapping` transpiler pass.
analysis. Like CPLEX, this is no longer by Qiskit, but it weas historically and the
optional remains for backwards compatibility.
* - .. py:data:: HAS_FIXTURES
- The test suite has additional features that are available if the optional `fixtures
@ -246,13 +248,13 @@ HAS_CONSTRAINT = _LazyImportTester(
HAS_CPLEX = _LazyImportTester(
"cplex",
install="pip install 'qiskit-terra[bip-mapper]'",
install="pip install cplex",
msg="This may not be possible for all Python versions and OSes",
)
HAS_CVXPY = _LazyImportTester("cvxpy", install="pip install cvxpy")
HAS_DOCPLEX = _LazyImportTester(
{"docplex": (), "docplex.mp.model": ("Model",)},
install="pip install 'qiskit-terra[bip-mapper]'",
install="pip install docplex",
msg="This may not be possible for all Python versions and OSes",
)
HAS_FIXTURES = _LazyImportTester("fixtures", install="pip install fixtures")

View File

@ -0,0 +1,15 @@
---
upgrade:
- |
The deprecated transpiler routing pass, ``BIPMapping`` has been removed.
It was marked as deprecated in the Qiskit 0.43.0 release. It has been
replaced by an external plugin package: ``qiskit-bip-mapper``. Details for
this new package can be found at the package's github repository:
https://github.com/qiskit-community/qiskit-bip-mapper
The pass was made into a separate plugin package for two reasons, first
the dependency on CPLEX makes it harder to use and secondly the plugin
package more cleanly integrates with :func:`~.transpile`. The optional
extra ``bip-mapper`` to install the ``cplex`` and ``docplex`` to support
this pass has been removed as nothing in Qiskit optionally requires it anymore.

View File

@ -1,379 +0,0 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Test the BIPMapping pass"""
import unittest
from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister
from qiskit.circuit import Barrier
from qiskit.circuit.library.standard_gates import SwapGate, CXGate
from qiskit.converters import circuit_to_dag
from qiskit.test import QiskitTestCase
from qiskit.providers.fake_provider import FakeLima
from qiskit.transpiler import CouplingMap, Layout, PassManager, Target
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes import BIPMapping
from qiskit.transpiler.passes import CheckMap, Collect2qBlocks, ConsolidateBlocks, UnitarySynthesis
from qiskit.utils import optionals
@unittest.skipUnless(optionals.HAS_CPLEX, "cplex is required to run the BIPMapping tests")
@unittest.skipUnless(optionals.HAS_DOCPLEX, "docplex is required to run the BIPMapping tests")
class TestBIPMapping(QiskitTestCase):
"""Tests the BIPMapping pass."""
def test_empty(self):
"""Returns the original circuit if the circuit is empty."""
coupling = CouplingMap([[0, 1]])
circuit = QuantumCircuit(2)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit)
self.assertEqual(circuit, actual)
def test_no_two_qubit_gates(self):
"""Returns the original circuit if the circuit has no 2q-gates
q0:--[H]--
q1:-------
CouplingMap map: [0]--[1]
"""
coupling = CouplingMap([[0, 1]])
circuit = QuantumCircuit(2)
circuit.h(0)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit)
self.assertEqual(circuit, actual)
def test_trivial_case(self):
"""No need to have any swap, the CX are distance 1 to each other
q0:--(+)-[H]-(+)-
| |
q1:---.-------|--
|
q2:-----------.--
CouplingMap map: [1]--[0]--[2]
"""
coupling = CouplingMap([[0, 1], [0, 2]])
circuit = QuantumCircuit(3)
circuit.cx(1, 0)
circuit.h(0)
circuit.cx(2, 0)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit)
self.assertEqual(3, len(actual))
for inst, _, _ in actual.data: # there are no swaps
self.assertFalse(isinstance(inst, SwapGate))
def test_no_swap(self):
"""Adding no swap if not giving initial layout"""
coupling = CouplingMap([[0, 1], [0, 2]])
circuit = QuantumCircuit(3)
circuit.cx(1, 2)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit)
q = QuantumRegister(3, name="q")
expected = QuantumCircuit(q)
expected.cx(q[0], q[1])
self.assertEqual(expected, actual)
def test_ignore_initial_layout(self):
"""Ignoring initial layout even when it is supplied"""
coupling = CouplingMap([[0, 1], [0, 2]])
circuit = QuantumCircuit(3)
circuit.cx(1, 2)
property_set = {"layout": Layout.generate_trivial_layout(*circuit.qubits)}
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit, property_set)
q = QuantumRegister(3, name="q")
expected = QuantumCircuit(q)
expected.cx(q[0], q[1])
self.assertEqual(expected, actual)
def test_can_map_measurements_correctly(self):
"""Verify measurement nodes are updated to map correct cregs to re-mapped qregs."""
coupling = CouplingMap([[0, 1], [0, 2]])
qr = QuantumRegister(3, "qr")
cr = ClassicalRegister(2)
circuit = QuantumCircuit(qr, cr)
circuit.cx(qr[1], qr[2])
circuit.measure(qr[1], cr[0])
circuit.measure(qr[2], cr[1])
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit)
q = QuantumRegister(3, "q")
expected = QuantumCircuit(q, cr)
expected.cx(q[0], q[1])
expected.measure(q[0], cr[0]) # <- changed due to initial layout change
expected.measure(q[1], cr[1]) # <- changed due to initial layout change
self.assertEqual(expected, actual)
def test_can_map_measurements_correctly_with_target(self):
"""Verify measurement nodes are updated to map correct cregs to re-mapped qregs."""
target = Target()
target.add_instruction(CXGate(), {(0, 1): None, (0, 2): None})
qr = QuantumRegister(3, "qr")
cr = ClassicalRegister(2)
circuit = QuantumCircuit(qr, cr)
circuit.cx(qr[1], qr[2])
circuit.measure(qr[1], cr[0])
circuit.measure(qr[2], cr[1])
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(target)(circuit)
q = QuantumRegister(3, "q")
expected = QuantumCircuit(q, cr)
expected.cx(q[0], q[1])
expected.measure(q[0], cr[0]) # <- changed due to initial layout change
expected.measure(q[1], cr[1]) # <- changed due to initial layout change
self.assertEqual(expected, actual)
def test_never_modify_mapped_circuit(self):
"""Test that the mapping is idempotent.
It should not modify a circuit which is already compatible with the
coupling map, and can be applied repeatedly without modifying the circuit.
"""
coupling = CouplingMap([[0, 1], [0, 2]])
circuit = QuantumCircuit(3, 2)
circuit.cx(1, 2)
circuit.measure(1, 0)
circuit.measure(2, 1)
dag = circuit_to_dag(circuit)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
mapped_dag = BIPMapping(coupling).run(dag)
remapped_dag = BIPMapping(coupling).run(mapped_dag)
self.assertEqual(mapped_dag, remapped_dag)
def test_no_swap_multi_layer(self):
"""Can find the best layout for a circuit with multiple layers."""
coupling = CouplingMap([[0, 1], [1, 2], [2, 3]])
qr = QuantumRegister(4, name="qr")
circuit = QuantumCircuit(qr)
circuit.cx(qr[1], qr[0])
circuit.cx(qr[0], qr[3])
property_set = {}
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling, objective="depth")(circuit, property_set)
self.assertEqual(2, actual.depth())
CheckMap(coupling)(actual, property_set)
self.assertTrue(property_set["is_swap_mapped"])
def test_unmappable_cnots_in_a_layer(self):
"""Test mapping of a circuit with 2 cnots in a layer into T-shape coupling,
which BIPMapping cannot map."""
qr = QuantumRegister(4, "q")
cr = ClassicalRegister(4, "c")
circuit = QuantumCircuit(qr, cr)
circuit.cx(qr[0], qr[1])
circuit.cx(qr[2], qr[3])
circuit.measure(qr, cr)
coupling = CouplingMap([[0, 1], [1, 2], [1, 3]]) # {0: [1], 1: [2, 3]}
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling)(circuit)
# Fails to map and returns the original circuit
self.assertEqual(circuit, actual)
def test_multi_cregs(self):
"""Test for multiple ClassicalRegisters."""
# ┌───┐ ░ ┌─┐
# qr_0: ──■────────────┤ X ├─░─┤M├─────────
# ┌─┴─┐ ┌───┐└─┬─┘ ░ └╥┘┌─┐
# qr_1: ┤ X ├──■──┤ H ├──■───░──╫─┤M├──────
# └───┘┌─┴─┐└───┘ ░ ║ └╥┘┌─┐
# qr_2: ──■──┤ X ├───────────░──╫──╫─┤M├───
# ┌─┴─┐└───┘ ░ ║ ║ └╥┘┌─┐
# qr_3: ┤ X ├────────────────░──╫──╫──╫─┤M├
# └───┘ ░ ║ ║ ║ └╥┘
# c: 2/════════════════════════╩══╬══╩══╬═
# 0 ║ 1 ║
# ║ ║
# d: 2/═══════════════════════════╩═════╩═
# 0 1
qr = QuantumRegister(4, "qr")
cr1 = ClassicalRegister(2, "c")
cr2 = ClassicalRegister(2, "d")
circuit = QuantumCircuit(qr, cr1, cr2)
circuit.cx(qr[0], qr[1])
circuit.cx(qr[2], qr[3])
circuit.cx(qr[1], qr[2])
circuit.h(qr[1])
circuit.cx(qr[1], qr[0])
circuit.barrier(qr)
circuit.measure(qr[0], cr1[0])
circuit.measure(qr[1], cr2[0])
circuit.measure(qr[2], cr1[1])
circuit.measure(qr[3], cr2[1])
coupling = CouplingMap([[0, 1], [0, 2], [2, 3]]) # linear [1, 0, 2, 3]
property_set = {}
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling, objective="depth")(circuit, property_set)
self.assertEqual(5, actual.depth())
CheckMap(coupling)(actual, property_set)
self.assertTrue(property_set["is_swap_mapped"])
def test_swaps_in_dummy_steps(self):
"""Test the case when swaps are inserted in dummy steps."""
# ┌───┐ ░ ░
# q_0: ──■──┤ H ├─░───■────────░───■───────
# ┌─┴─┐├───┤ ░ │ ░ │
# q_1: ┤ X ├┤ H ├─░───┼────■───░───┼────■──
# └───┘├───┤ ░ │ ┌─┴─┐ ░ ┌─┴─┐ │
# q_2: ──■──┤ H ├─░───┼──┤ X ├─░─┤ X ├──┼──
# ┌─┴─┐├───┤ ░ ┌─┴─┐└───┘ ░ └───┘┌─┴─┐
# q_3: ┤ X ├┤ H ├─░─┤ X ├──────░──────┤ X ├
# └───┘└───┘ ░ └───┘ ░ └───┘
circuit = QuantumCircuit(4)
circuit.cx(0, 1)
circuit.cx(2, 3)
circuit.h([0, 1, 2, 3])
circuit.barrier()
circuit.cx(0, 3)
circuit.cx(1, 2)
circuit.barrier()
circuit.cx(0, 2)
circuit.cx(1, 3)
coupling = CouplingMap.from_line(4)
property_set = {}
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling, objective="depth")(circuit, property_set)
self.assertEqual(7, actual.depth())
CheckMap(coupling)(actual, property_set)
self.assertTrue(property_set["is_swap_mapped"])
# no swaps before the first barrier
for inst, _, _ in actual.data:
if isinstance(inst, Barrier):
break
self.assertFalse(isinstance(inst, SwapGate))
def test_different_number_of_virtual_and_physical_qubits(self):
"""Test the case when number of virtual and physical qubits are different."""
# q_0: ──■────■───────
# ┌─┴─┐ │
# q_1: ┤ X ├──┼────■──
# └───┘ │ ┌─┴─┐
# q_2: ──■────┼──┤ X ├
# ┌─┴─┐┌─┴─┐└───┘
# q_3: ┤ X ├┤ X ├─────
# └───┘└───┘
circuit = QuantumCircuit(4)
circuit.cx(0, 1)
circuit.cx(2, 3)
circuit.cx(0, 3)
circuit.cx(1, 2)
coupling = CouplingMap.from_line(5)
with self.assertRaises(TranspilerError):
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
BIPMapping(coupling)(circuit)
def test_qubit_subset(self):
"""Test if `qubit_subset` option works as expected."""
circuit = QuantumCircuit(3)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(0, 2)
coupling = CouplingMap([(0, 1), (1, 3), (3, 2)])
qubit_subset = [0, 1, 3]
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
actual = BIPMapping(coupling, qubit_subset=qubit_subset)(circuit)
# all used qubits are in qubit_subset
bit_indices = {bit: index for index, bit in enumerate(actual.qubits)}
for _, qargs, _ in actual.data:
for q in qargs:
self.assertTrue(bit_indices[q] in qubit_subset)
# ancilla qubits are set in the resulting qubit
idle = QuantumRegister(1, name="ancilla")
self.assertEqual(idle[0], actual._layout.initial_layout[2])
def test_unconnected_qubit_subset(self):
"""Fails if qubits in `qubit_subset` are not connected."""
circuit = QuantumCircuit(3)
circuit.cx(0, 1)
coupling = CouplingMap([(0, 1), (1, 3), (3, 2)])
with self.assertRaises(TranspilerError):
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
BIPMapping(coupling, qubit_subset=[0, 1, 2])(circuit)
def test_objective_function(self):
"""Test if ``objective`` functions prioritize metrics correctly."""
# ┌──────┐┌──────┐ ┌──────┐
# q_0: ┤0 ├┤0 ├─────┤0 ├
# │ Dcx ││ │ │ Dcx │
# q_1: ┤1 ├┤ Dcx ├──■──┤1 ├
# └──────┘│ │ │ └──────┘
# q_2: ───■────┤1 ├──┼─────■────
# ┌─┴─┐ └──────┘┌─┴─┐ ┌─┴─┐
# q_3: ─┤ X ├──────────┤ X ├─┤ X ├──
# └───┘ └───┘ └───┘
qc = QuantumCircuit(4)
qc.dcx(0, 1)
qc.cx(2, 3)
qc.dcx(0, 2)
qc.cx(1, 3)
qc.dcx(0, 1)
qc.cx(2, 3)
coupling = CouplingMap(FakeLima().configuration().coupling_map)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
dep_opt = BIPMapping(coupling, objective="depth", qubit_subset=[0, 1, 3, 4])(qc)
with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"):
err_opt = BIPMapping(
coupling,
objective="gate_error",
qubit_subset=[0, 1, 3, 4],
backend_prop=FakeLima().properties(),
)(qc)
# depth = number of su4 layers (mirrored gates have to be consolidated as single su4 gates)
pm_ = PassManager([Collect2qBlocks(), ConsolidateBlocks(basis_gates=["cx", "u"])])
dep_opt = pm_.run(dep_opt)
err_opt = pm_.run(err_opt)
self.assertLessEqual(dep_opt.depth(), err_opt.depth())
# count CNOTs after synthesized
dep_opt = UnitarySynthesis(basis_gates=["cx", "u"])(dep_opt)
err_opt = UnitarySynthesis(basis_gates=["cx", "u"])(err_opt)
self.assertGreater(dep_opt.count_ops()["cx"], err_opt.count_ops()["cx"])