Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Add `MIPMapper` documentation
- Add Single Gate Multi Qubit (SGMQ) notation to the `CircuitBuilder`
- The cycle time [seconds] can be set when instantiating the `QuantifySchedulerExporter` through the `cycle_time`
parameter
Expand Down
1 change: 1 addition & 0 deletions docs/compilation-passes/mapping/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ The following mapping passes are available in Opensquirrel:

- [Hardcoded Mapper](hardcoded-mapper.md) (`HardcodedMapper`)
- [Identity Mapper](identity-mapper.md) (`IdentitiyMapper`)
- [MIP Mapper](mip-mapper.md) (`MIPMapper`)
- [Random Mapper](random-mapper.md) (`RandomMapper`)
- [QGym Mapper](qgym-mapper.md) (`QGymMapper`)
175 changes: 175 additions & 0 deletions docs/compilation-passes/mapping/mip-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
The MIP mapper (`MIPMapper`) pass tries to place the circuit on the quantum backend such
that qubits that need to interact end up close to each other.
It is based on the `place_mip` pass of
[OpenQL](https://github.com/QuTech-Delft/OpenQL/blob/develop/src/ql/pass/map/qubits/place_mip/place_mip.cc).

Unlike the [identity](identity-mapper.md), [random](random-mapper.md) and
[hardcoded](hardcoded-mapper.md) mappers, which decide on a placement without looking at
the circuit, this pass takes the two-qubit gates into account.
It solves a _Mixed Integer Programming_ (MIP) problem to find a placement that is
as optimal as possible for the given connectivity.

## How the mapping is computed

For every pair of virtual qubits $i$ and $j$ the pass counts how many two-qubit gates
act on that pair, giving an interaction count $C_{ij}$.
Single-qubit gates and non-unitary instructions are ignored, as they do not constrain
the qubit placement.
It also computes the distance $d_{kl}$ between every pair of physical qubits $k$ and
$l$, _i.e._ the length of the shortest path between them in the connectivity graph,
using Floyd-Warshall.
Neighbouring qubits are at distance 1.

Writing $\pi(i)$ for the physical qubit that virtual qubit $i$ is placed on, the solver
then looks for the placement that minimizes

$$
\sum_{i,j} C_{ij} \, d_{\pi(i)\,\pi(j)},
$$

under the constraints that every virtual qubit gets exactly one physical qubit and that
no physical qubit is used twice.

That objective is quadratic, and as such is handled by
[SciPy's `milp` solver](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html)
in a linearized form.
A small penalty is added for every qubit that does not stay where it was,
which breaks ties in favour of the identity mapping.

!!! note "Mapping is not routing"

Placing interacting qubits close together reduces the number of SWAPs a router has
to insert, but it does not guarantee that none are needed.
Unless every interaction ends up between neighbouring qubits,
a [routing pass](../routing/index.md) is still required to make the circuit
executable.

## Usage

The mapper needs the `connectivity` of the target backend: a dictionary mapping each
physical qubit index to the qubits it is connected to.

```python
from opensquirrel import CircuitBuilder
from opensquirrel.passes.mapper import MIPMapper

connectivity = {"0": [1], "1": [0, 2], "2": [1]}

builder = CircuitBuilder(3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(0, 2)
circuit = builder.to_circuit()

circuit.map(mapper=MIPMapper(connectivity=connectivity))
```

??? example "`print(circuit)`"

```linenums="1"
version 3.0

qubit[3] q

H q[1]
CNOT q[1], q[0]
CNOT q[1], q[2]
```

The connectivity above represents a linear array, so physical qubit `1` is the only one
with two neighbours.
Virtual qubit `0` interacts with both virtual qubits `1` and `2`, so it is placed on the
position of physical qubit `1`, giving the mapping `{0: 1, 1: 0, 2: 2}` and leaving both
CNOTs on neighbouring qubits.

When the circuit can already be executed on the given connectivity there is nothing to
gain, and the tiebreaker leaves every qubit in place:

```python
builder = CircuitBuilder(3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(1, 2)
circuit = builder.to_circuit()

circuit.map(mapper=MIPMapper(connectivity=connectivity))
```

??? example "`print(circuit)`"

```linenums="1"
version 3.0

qubit[3] q

H q[0]
CNOT q[0], q[1]
CNOT q[1], q[2]
```

Non-unitary instructions are remapped along with the gates.
Each measurement follows the qubit it acts on and still writes to the same bit,
so the meaning of the bit register is preserved:

```python
builder = CircuitBuilder(3, 3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(0, 2)
builder.measure(0, 0)
builder.measure(1, 1)
builder.measure(2, 2)
circuit = builder.to_circuit()

circuit.map(mapper=MIPMapper(connectivity=connectivity))
```

??? example "`print(circuit)`"

```linenums="1"
version 3.0

qubit[3] q
bit[3] b

H q[1]
CNOT q[1], q[0]
CNOT q[1], q[2]
b[0] = measure q[1]
b[1] = measure q[0]
b[2] = measure q[2]
```

## Parameters

| Parameter | Description |
|----------------|------------------------------------------------------------------------------|
| `connectivity` | Connectivity of the target backend. |
| `timeout` | Maximum time, in seconds, the solver may spend. No limit by default. |
| `epsilon` | Penalty used to break ties towards the identity mapping. Defaults to `1e-6`. |

## When mapping fails

Finding an optimal mapping is an NP-hard problem, and the solver slows down quickly as
the number of qubits grows.
The `timeout` argument bounds how long it may take, but a solver that hits the limit
raises a `RuntimeError` instead of returning a mapping:

```python
mip_mapper = MIPMapper(connectivity=connectivity, timeout=0.000001)
circuit.map(mapper=mip_mapper)
```

!!! example ""

`RuntimeError: MIP solver failed to find a feasible mapping. Status: 1, Message:
Time limit reached.`

The circuit also has to fit on the backend:

!!! example ""

`RuntimeError: Number of virtual qubits (3) exceeds number of physical qubits (2)`

Fewer virtual qubits than physical ones is fine as they are mapped onto a subset of
them.
1 change: 1 addition & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ nav:
- Mapping: compilation-passes/mapping/index.md
- Hardcoded mapper: compilation-passes/mapping/hardcoded-mapper.md
- Identity mapper: compilation-passes/mapping/identity-mapper.md
- MIP mapper: compilation-passes/mapping/mip-mapper.md
- Random mapper: compilation-passes/mapping/random-mapper.md
- QGym mapper: compilation-passes/mapping/qgym-mapper.md
- Merging:
Expand Down
90 changes: 89 additions & 1 deletion tests/docs/compilation-passes/test_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from opensquirrel import CircuitBuilder
from opensquirrel.passes.mapper import QGymMapper
from opensquirrel.passes.mapper import MIPMapper, QGymMapper

if importlib.util.find_spec("qgym") is None:
pytest.skip("qgym not installed; skipping QGym mapper tests", allow_module_level=True)
Expand Down Expand Up @@ -54,3 +54,91 @@ def test_qgym_mapper(self) -> None:
circuit.map(mapper=qgym_mapper)

assert str(circuit) != initial_circuit_str


class TestMIPMapper:
@pytest.fixture
def connectivity(self) -> dict[str, list[int]]:
return {"0": [1], "1": [0, 2], "2": [1]}

def test_remap_to_middle_qubit(self, connectivity: dict[str, list[int]]) -> None:
builder = CircuitBuilder(3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(0, 2)
circuit = builder.to_circuit()

circuit.map(mapper=MIPMapper(connectivity=connectivity))

assert (
str(circuit)
== """version 3.0

qubit[3] q

H q[1]
CNOT q[1], q[0]
CNOT q[1], q[2]
"""
)

def test_identity_mapping(self, connectivity: dict[str, list[int]]) -> None:
builder = CircuitBuilder(3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(1, 2)
circuit = builder.to_circuit()

circuit.map(mapper=MIPMapper(connectivity=connectivity))

assert (
str(circuit)
== """version 3.0

qubit[3] q

H q[0]
CNOT q[0], q[1]
CNOT q[1], q[2]
"""
)

def test_remap_measurements(self, connectivity: dict[str, list[int]]) -> None:
builder = CircuitBuilder(3, 3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(0, 2)
builder.measure(0, 0)
builder.measure(1, 1)
builder.measure(2, 2)
circuit = builder.to_circuit()

circuit.map(mapper=MIPMapper(connectivity=connectivity))

assert (
str(circuit)
== """version 3.0

qubit[3] q
bit[3] b

H q[1]
CNOT q[1], q[0]
CNOT q[1], q[2]
b[0] = measure q[1]
b[1] = measure q[0]
b[2] = measure q[2]
"""
)

def test_timeout(self, connectivity: dict[str, list[int]]) -> None:
builder = CircuitBuilder(3)
builder.H(0)
builder.CNOT(0, 1)
builder.CNOT(0, 2)
circuit = builder.to_circuit()

mip_mapper = MIPMapper(connectivity=connectivity, timeout=0.000001)

with pytest.raises(RuntimeError, match="MIP solver failed to find a feasible mapping"):
circuit.map(mapper=mip_mapper)
Loading