Skip to content

Commit b1390bb

Browse files
committed
Merge branch '152-basic-implementation-mapper' into 167-initial-register-management-implementation
# Conflicts: # pyproject.toml
2 parents 6b54254 + 55e6b43 commit b1390bb

File tree

13 files changed

+565
-71
lines changed

13 files changed

+565
-71
lines changed

opensquirrel/circuit.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from typing import Callable, Dict
24

35
import numpy as np
@@ -9,6 +11,7 @@
911
from opensquirrel.default_measurements import default_measurement_set
1012
from opensquirrel.exporter import quantify_scheduler_exporter, writer
1113
from opensquirrel.exporter.export_format import ExportFormat
14+
from opensquirrel.mapper import IdentityMapper, Mapper, map_qubits
1215
from opensquirrel.merger import general_merger
1316
from opensquirrel.parser.libqasm.libqasm_ir_creator import LibqasmIRCreator
1417
from opensquirrel.squirrel_ir import Gate, Measure, SquirrelIR
@@ -95,6 +98,17 @@ def decompose(self, decomposer: Decomposer):
9598
"""Generic decomposition pass. It applies the given decomposer function to every gate in the circuit."""
9699
general_decomposer.decompose(self.squirrel_ir, decomposer)
97100

101+
def map_qubits(self, mapper: Mapper | None = None) -> None:
102+
"""Generic qubit mapper pass.
103+
104+
Maps the virtual qubits of the circuit to physical qubits of the target hardware.
105+
106+
Args:
107+
mapper: Mapper class to use. If ``None`` (default) is provided, use the ``IdentityMapper``.
108+
"""
109+
mapper = IdentityMapper() if mapper is None else mapper
110+
map_qubits(self.squirrel_ir, mapper)
111+
98112
def replace(self, gate_generator: Callable[..., Gate], f):
99113
"""Manually replace occurrences of a given gate with a list of gates.
100114
`f` is a callable that takes the arguments of the gate that is to be replaced

opensquirrel/mapper/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from opensquirrel.mapper.general_mapper import Mapper, map_qubits
2+
from opensquirrel.mapper.simple_mappers import HardcodedMapper, IdentityMapper
3+
4+
__all__ = ["map_qubits", "Mapper", "IdentityMapper", "HardcodedMapper"]

opensquirrel/mapper/general_mapper.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""This module contains generic mapping components."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
7+
from opensquirrel.squirrel_ir import Gate, Measure, SquirrelIR
8+
9+
10+
def map_qubits(squirrel_ir: SquirrelIR, mapper: Mapper) -> None:
11+
"""Map the virtual qubits in the `squirrel_ir` to physical qubits using `mapper`.
12+
13+
Args:
14+
squirrel_ir: IR to act on.
15+
mapper: Mapping algorithm to use.
16+
"""
17+
18+
mapping = mapper.map(squirrel_ir)
19+
20+
for statement in squirrel_ir.statements:
21+
if isinstance(statement, (Gate, Measure)):
22+
statement.relabel(mapping)
23+
24+
25+
class Mapper(ABC):
26+
"""Base class for the Mapper pass."""
27+
28+
@abstractmethod
29+
def map(self, squirrel_ir: SquirrelIR) -> dict[int, int]:
30+
"""Produce a mapping between thee virtual qubits in the `squirrel_ir` to physical qubits.
31+
32+
Args:
33+
squirrel_ir: IR to map.
34+
35+
Returns:
36+
Dictionary with as keys the virtual qubits and as values the physical qubits.
37+
"""

opensquirrel/mapper/simple_mappers.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""This module contains the following simple mappers:
2+
3+
* IdentityMapper
4+
* HardcodedMapper
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from collections.abc import Mapping
10+
from typing import SupportsInt
11+
12+
from opensquirrel.mapper.general_mapper import Mapper
13+
from opensquirrel.squirrel_ir import SquirrelIR
14+
15+
16+
class IdentityMapper(Mapper):
17+
18+
def map(self, squirrel_ir: SquirrelIR) -> dict[int, int]:
19+
"""Produce an IdentityMapping.
20+
21+
Args:
22+
squirrel_ir: IR to map.
23+
24+
Returns:
25+
Dictionary with as keys the virtual qubits and as values the physical qubits. The dictionary maps each
26+
virtual qubit to exactly the same physical qubit.
27+
"""
28+
return {i: i for i in range(squirrel_ir.number_of_qubits)}
29+
30+
31+
class HardcodedMapper(Mapper):
32+
33+
def __init__(self, mapping: Mapping[SupportsInt, SupportsInt]) -> None:
34+
"""Init of the ``HardcodedMapper``.
35+
36+
Args:
37+
mapping: Mapping with as keys the virtual qubits and as values the physical qubits.
38+
39+
Raises:
40+
ValueError: If `mapping` is not a valid mapping.
41+
"""
42+
# Check if the provided mapping is valid
43+
self.mapping = {int(virtual_qubit): int(physical_qubit) for virtual_qubit, physical_qubit in mapping.items()}
44+
if set(self.mapping.keys()) != set(self.mapping.values()):
45+
raise ValueError("The set of physical qubits is not equal to the set of virtual qubits.")
46+
47+
def map(self, squirrel_ir: SquirrelIR) -> dict[int, int]:
48+
"""Retrieve tha hardcoded mapping.
49+
50+
Args:
51+
squirrel_ir: IR to map.
52+
53+
Returns:
54+
Dictionary with as keys the virtual qubits and as values the physical qubits.
55+
"""
56+
if set(range(squirrel_ir.number_of_qubits)) != set(self.mapping.keys()):
57+
raise ValueError("Virtual qubits are not labeled correctly.")
58+
59+
return self.mapping

opensquirrel/mapper/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import networkx as nx
2+
3+
from opensquirrel.squirrel_ir import Gate, SquirrelIR
4+
5+
6+
def make_interaction_graph(squirrel_ir: SquirrelIR) -> nx.Graph:
7+
8+
interaction_graph = nx.Graph()
9+
gates = (statement for statement in squirrel_ir.statements if isinstance(statement, Gate))
10+
11+
for gate in gates:
12+
target_qubits = gate.get_qubit_operands()
13+
if len(target_qubits) == 1:
14+
continue
15+
if len(target_qubits) > 2:
16+
raise ValueError(
17+
f"The gate {gate} acts on more than 2 qubits. The gate must be decomposed before an interaction graph "
18+
"can be made."
19+
)
20+
interaction_graph.add_edge(*target_qubits)
21+
22+
return interaction_graph

opensquirrel/squirrel_ir.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from __future__ import annotations
2+
13
import inspect
24
from abc import ABC, abstractmethod
5+
from collections.abc import Mapping
36
from dataclasses import dataclass
47
from functools import wraps
58
from typing import Callable, List, Optional, Tuple
@@ -108,14 +111,23 @@ def name(self) -> Optional[str]:
108111
def __eq__(self, other):
109112
if not isinstance(other, Measure):
110113
return False
111-
return self.qubit == other.qubit and self.axis == other.axis
114+
return self.qubit == other.qubit and np.allclose(self.axis, other.axis, atol=ATOL)
112115

113116
def accept(self, visitor: SquirrelIRVisitor):
114117
visitor.visit_measure(self)
115118

116119
def get_qubit_operands(self) -> List[Qubit]:
117120
return [self.qubit]
118121

122+
def relabel(self, mapping: Mapping[int, int]) -> None:
123+
"""Relabel the qubits using the given mapping.
124+
125+
Args:
126+
mapping: Mapping with as keys the indices of the original qubits and as values the indices of the qubits
127+
after replacement.
128+
"""
129+
self.qubit = Qubit(mapping[self.qubit.index])
130+
119131

120132
class Gate(Statement, ABC):
121133
# Note: two gates are considered equal even when their generators/arguments are different.
@@ -140,12 +152,19 @@ def is_anonymous(self) -> bool:
140152
return self.arguments is None
141153

142154
@abstractmethod
143-
def get_qubit_operands(self) -> List[Qubit]:
144-
raise NotImplementedError
155+
def get_qubit_operands(self) -> List[Qubit]: ...
145156

146157
@abstractmethod
147-
def is_identity(self) -> bool:
148-
raise NotImplementedError
158+
def is_identity(self) -> bool: ...
159+
160+
@abstractmethod
161+
def relabel(self, mapping: Mapping[int, int]) -> None:
162+
"""Relabel the qubits using the given mapping.
163+
164+
Args:
165+
mapping: Mapping with as keys the indices of the original qubits and as values the indices of the qubits
166+
after replacement.
167+
"""
149168

150169

151170
class BlochSphereRotation(Gate):
@@ -200,6 +219,15 @@ def is_identity(self) -> bool:
200219
# Angle and phase are already normalized.
201220
return abs(self.angle) < ATOL and abs(self.phase) < ATOL
202221

222+
def relabel(self, mapping: Mapping[int, int]) -> None:
223+
"""Relabel the qubits using the given mapping.
224+
225+
Args:
226+
mapping: Mapping with as keys the indices of the original qubits and as values the indices of the qubits
227+
after replacement.
228+
"""
229+
self.qubit = Qubit(mapping[self.qubit.index])
230+
203231

204232
class MatrixGate(Gate):
205233
generator: Optional[Callable[..., "MatrixGate"]] = None
@@ -225,6 +253,15 @@ def get_qubit_operands(self) -> List[Qubit]:
225253
def is_identity(self) -> bool:
226254
return np.allclose(self.matrix, np.eye(2 ** len(self.operands)))
227255

256+
def relabel(self, mapping: Mapping[int, int]) -> None:
257+
"""Relabel the qubits using the given mapping.
258+
259+
Args:
260+
mapping: Mapping with as keys the indices of the original qubits and as values the indices of the qubits
261+
after replacement.
262+
"""
263+
self.operands = [Qubit(mapping[qubit.index]) for qubit in self.operands]
264+
228265

229266
class ControlledGate(Gate):
230267
generator: Optional[Callable[..., "ControlledGate"]] = None
@@ -247,6 +284,16 @@ def get_qubit_operands(self) -> List[Qubit]:
247284
def is_identity(self) -> bool:
248285
return self.target_gate.is_identity()
249286

287+
def relabel(self, mapping: Mapping[int, int]) -> None:
288+
"""Relabel the qubits using the given mapping.
289+
290+
Args:
291+
mapping: Mapping with as keys the indices of the original qubits and as
292+
values the indices of the qubits after replacement.
293+
"""
294+
self.control_qubit = Qubit(mapping[self.control_qubit.index])
295+
self.target_gate.relabel(mapping)
296+
250297

251298
def _compare_gate_classes(g1: Gate, g2: Gate) -> bool:
252299
union_mapping = list(set(g1.get_qubit_operands()) | set(g2.get_qubit_operands()))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Ths subpackage contains compatibility checks for the different compilation passes."""
2+
3+
from opensquirrel.utils.check_passes.check_mapper import check_mapper
4+
5+
__all__ = ["check_mapper"]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""This module contains checks for the ``Mapper`` pass."""
2+
3+
from copy import deepcopy
4+
5+
from opensquirrel.mapper.general_mapper import Mapper
6+
from opensquirrel.squirrel_ir import BlochSphereRotation, Comment, ControlledGate, Measure, Qubit, SquirrelIR
7+
8+
9+
def check_mapper(mapper: Mapper) -> None:
10+
"""Check if the `mapper` complies with the OpenSquirrel requirements.
11+
12+
If an implementation of ``Mapper`` passes these checks it should be compatible with the ``Circuit.map_qubits``
13+
method.
14+
15+
Args:
16+
mapper: Mapper to check.
17+
"""
18+
19+
assert isinstance(mapper, Mapper)
20+
21+
squirrel_ir = SquirrelIR(number_of_qubits=10)
22+
_check_scenario(squirrel_ir, deepcopy(mapper))
23+
24+
squirrel_ir = SquirrelIR(number_of_qubits=10)
25+
squirrel_ir.add_comment(Comment("comment"))
26+
squirrel_ir.add_gate(BlochSphereRotation(Qubit(42), (1, 0, 0), 1, 2))
27+
squirrel_ir.add_gate(ControlledGate(Qubit(42), BlochSphereRotation.identity(Qubit(100))))
28+
squirrel_ir.add_measurement(Measure(Qubit(42), (0, 0, 1)))
29+
_check_scenario(squirrel_ir, deepcopy(mapper))
30+
31+
32+
def _check_scenario(squirrel_ir: SquirrelIR, mapper: Mapper) -> None:
33+
"""Check if the given scenario can be mapped.
34+
35+
Args:
36+
squirrel_ir: SquirrelIR containing the scenario to check against.
37+
mapping: Mapping to check.
38+
"""
39+
squirrel_ir_copy = deepcopy(squirrel_ir)
40+
mapping = mapper.map(squirrel_ir)
41+
42+
assert squirrel_ir == squirrel_ir_copy, "A Mapper pass should not change the SquirrelIR"
43+
_check_mapping_format(mapping, squirrel_ir.number_of_qubits)
44+
45+
46+
def _check_mapping_format(mapping: dict[int, int], n_qubits: int) -> None:
47+
"""Check if the mapping has the expected format.
48+
49+
Args:
50+
mapping: Mapping to check.
51+
n_qubits: Number of qubits in the circuit.
52+
"""
53+
assert isinstance(
54+
mapping, dict
55+
), f"Output mapping should be an instance of <class 'dict'>, but was of type {type(mapping)}"
56+
assert set(mapping.keys()) == set(
57+
mapping.values()
58+
), "The set of virtual qubits is not equal to the set of phyical qubits."
59+
assert set(range(n_qubits)) == set(mapping.keys()), "Virtual qubits are not labeled correctly."

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ numpy = [
3636
{ version = "^1.26", python = "^3.9" },
3737
]
3838
libqasm = "0.6.5"
39+
networkx = "^3.0.0"
3940

4041
[tool.poetry.group.dev.dependencies]
4142
black = ">=23.11,<25.0"

test/mapper/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)