Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- The cycle time [seconds] can be set when instantiating the `QuantifySchedulerExporter` through the `cycle_time`
parameter.
- `CircuitBuilder` accepts multiple (qu)bit registers through `add_register` method.
- Add `interaction_graph` as a property of the `Circuit`.

## [ 0.9.0 ] - [ 2025-12-19 ]

Expand Down
20 changes: 19 additions & 1 deletion opensquirrel/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from collections import Counter, defaultdict
from collections.abc import Callable
from itertools import combinations
from typing import TYPE_CHECKING, Any

from opensquirrel.ir import Instruction
from opensquirrel.ir.non_unitary import Measure
from opensquirrel.ir.statement import AsmDeclaration

Expand All @@ -21,6 +23,7 @@

InstructionCount = dict[str, int]
MeasurementToBitMap = defaultdict[str, list[int]]
InteractionGraph = dict[tuple[int, int], int]


class Circuit:
Expand Down Expand Up @@ -117,6 +120,20 @@ def measurement_to_bit_map(self) -> MeasurementToBitMap:
m2b_map[str(qubit_index)].append(bit_index)
return m2b_map

@property
def interaction_graph(self) -> InteractionGraph:
"""Interaction graph of the circuit."""
graph = {}
for statement in self.ir.statements:
if not isinstance(statement, Instruction):
continue
qubit_indices = statement.qubit_indices
if len(qubit_indices) >= 2:
for q_i, q_j in combinations(qubit_indices, 2):
edge = (min(q_i, q_j), max(q_i, q_j))
graph[edge] = graph.get(edge, 0) + 1
return graph

def asm_filter(self, backend_name: str) -> None:
self.ir.statements = [
statement
Expand Down Expand Up @@ -146,7 +163,8 @@ def map(self, mapper: Mapper) -> None:
"""
from opensquirrel.passes.mapper.qubit_remapper import remap_ir

mapping = mapper.map(self.ir, self.qubit_register_size)
mapping = mapper.map(self, self.qubit_register_size)

remap_ir(self, mapping)

def merge(self, merger: Merger) -> None:
Expand Down
5 changes: 2 additions & 3 deletions opensquirrel/passes/mapper/general_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from opensquirrel.ir import IR
from opensquirrel import Circuit
from opensquirrel.passes.mapper.mapping import Mapping


Expand All @@ -16,5 +16,4 @@ class Mapper(ABC):
def __init__(self, **kwargs: Any) -> None: ...

@abstractmethod
def map(self, ir: IR, qubit_register_size: int) -> Mapping:
raise NotImplementedError
def map(self, circuit: Circuit, qubit_register_size: int) -> Mapping: ...
12 changes: 8 additions & 4 deletions opensquirrel/passes/mapper/mip_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from opensquirrel.passes.mapper.mapping import Mapping

if TYPE_CHECKING:
from opensquirrel import Connectivity
from opensquirrel import Circuit, Connectivity
from opensquirrel.ir import IR

DISTANCE_UL = 999999
Expand Down Expand Up @@ -67,7 +67,11 @@ def __init__(
self.num_w_vars = 0
self.num_vars = 0

def map(self, ir: IR, qubit_register_size: int) -> Mapping:
def map(
self,
circuit: Circuit,
qubit_register_size: int,
) -> Mapping:
Comment thread
elenbaasc marked this conversation as resolved.
"""
Find an initial mapping of virtual qubits to physical qubits that minimizes
the sum of distances between mapped operands of all two-qubit gates, using
Expand All @@ -78,7 +82,7 @@ def map(self, ir: IR, qubit_register_size: int) -> Mapping:
gates, given the connectivity.

Args:
ir (IR): The intermediate representation of the quantum circuit to be mapped.
circuit (Circuit): The quantum circuit to be mapped.
qubit_register_size (int): The number of virtual qubits in the circuit.

Returns:
Expand All @@ -102,7 +106,7 @@ def map(self, ir: IR, qubit_register_size: int) -> Mapping:
raise RuntimeError(error_message)

distance = self._get_distance()
reference_counter = self._get_reference_counter(ir, self.num_virtual_qubits)
reference_counter = self._get_reference_counter(circuit.ir, self.num_virtual_qubits)

cost, constraints, integrality, bounds = self._get_linearized_formulation(reference_counter, distance)

Expand Down
41 changes: 36 additions & 5 deletions opensquirrel/passes/mapper/qgym_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
pass

if TYPE_CHECKING:
from opensquirrel import Connectivity
from opensquirrel import Circuit, Connectivity

try:
from stable_baselines3.common.base_class import BaseAlgorithm
Expand All @@ -40,13 +40,17 @@ def __init__(
self.env = InitialMapping(connection_graph=self.hardware_connectivity, **(env_kwargs or {}))
self.agent = self._load_agent(agent_class, agent_path)

def map(self, ir: IR, qubit_register_size: int) -> Mapping:
def map(
self,
circuit: Circuit,
qubit_register_size: int,
) -> Mapping:
"""
Compute an initial logical-to-physical qubit mapping using a trained
Stable-Baselines3 agent acting in the QGym InitialMapping environment.

Args:
ir (IR): Intermediate representation of the quantum circuit to be mapped.
circuit (Circuit): The quantum circuit to be mapped.
qubit_register_size (int): Number of logical (virtual) qubits in the circuit.

Returns:
Expand All @@ -65,7 +69,11 @@ def map(self, ir: IR, qubit_register_size: int) -> Mapping:
)
raise ValueError(msg)

circuit_graph = self._ir_to_interaction_graph(ir)
circuit_graph = (
self._ir_to_graph(circuit.ir)
if not circuit.interaction_graph
else self._convert_interaction_graph(circuit.interaction_graph)
)

obs, _ = self.env.reset(options={"interaction_graph": circuit_graph})

Expand Down Expand Up @@ -110,7 +118,7 @@ def _load_agent(agent_class: str, agent_path: str) -> BaseAlgorithm:
return cast("BaseAlgorithm", agent_cls.load(agent_path))

@staticmethod
def _ir_to_interaction_graph(ir: IR) -> nx.Graph:
def _ir_to_graph(ir: IR) -> nx.Graph:
"""Build an undirected interaction graph representation of the IR.

Args:
Expand All @@ -135,6 +143,29 @@ def _ir_to_interaction_graph(ir: IR) -> nx.Graph:
interaction_graph.add_edge(q_i, q_j, weight=1)
return interaction_graph

@staticmethod
def _convert_interaction_graph(edges: dict[tuple[int, int], int]) -> nx.Graph:
"""Convert Circuit's simple interaction graph to NetworkX graph.

Args:
edges: Dictionary mapping (qubit_i, qubit_j) tuples to interaction weights.

Returns:
NetworkX graph representation of the quantum circuit, compatible with QGym.
"""
graph = nx.Graph()

all_nodes = set()
for q_i, q_j in edges:
all_nodes.add(q_i)
all_nodes.add(q_j)
graph.add_nodes_from(all_nodes)

for (q_i, q_j), weight in edges.items():
graph.add_edge(q_i, q_j, weight=weight)

return graph

@staticmethod
def _get_mapping(last_obs: Any, qubit_register_size: int) -> Mapping:
"""Extract and convert QGym's physical-to-logical mapping to OpenSquirrel's logical-to-physical mapping.
Expand Down
20 changes: 16 additions & 4 deletions opensquirrel/passes/mapper/simple_mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@
from opensquirrel.passes.mapper.mapping import Mapping

if TYPE_CHECKING:
from opensquirrel.ir import IR
from opensquirrel import Circuit


class IdentityMapper(Mapper):
def __init__(self, **kwargs: Any) -> None:
"""An ``IdentityMapper`` maps each virtual qubit to exactly the same physical qubit."""
super().__init__(**kwargs)

def map(self, ir: IR, qubit_register_size: int) -> Mapping:
def map(
self,
circuit: Circuit,
qubit_register_size: int,
) -> Mapping:
"""Create identity mapping."""
return Mapping(list(range(qubit_register_size)))

Expand All @@ -38,7 +42,11 @@ def __init__(self, mapping: Mapping, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._mapping = mapping

def map(self, ir: IR, qubit_register_size: int) -> Mapping:
def map(
self,
circuit: Circuit,
qubit_register_size: int,
) -> Mapping:
"""Return the hardcoded mapping."""
if qubit_register_size != self._mapping.size():
msg = (
Expand All @@ -60,7 +68,11 @@ def __init__(self, seed: int | None = None, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.seed = seed

def map(self, ir: IR, qubit_register_size: int) -> Mapping:
def map(
self,
circuit: Circuit,
qubit_register_size: int,
) -> Mapping:
"""Create a random mapping."""
if self.seed:
random.seed(self.seed)
Expand Down
10 changes: 5 additions & 5 deletions opensquirrel/passes/router/astar_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def _astar_pathfinder(self, graph: nx.Graph, source: int, target: int) -> Any:
graph,
source=source,
target=target,
heuristic=lambda q0_index, q1_index: calculate_distance(
q0_index, q1_index, num_columns, self._distance_metric
)
if self._distance_metric
else None,
heuristic=lambda q0_index, q1_index: (
calculate_distance(q0_index, q1_index, num_columns, self._distance_metric)
if self._distance_metric
else None
),
)
3 changes: 2 additions & 1 deletion tests/ir/test_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from numpy.typing import NDArray

from opensquirrel.common import ATOL
from opensquirrel.ir import Axis, AxisLike, Bit, Float, Int, Qubit
from opensquirrel.ir.expression import Expression

Expand All @@ -15,7 +16,7 @@ def test_type_error(self) -> None:
Float("f") # type: ignore

def test_init(self) -> None:
assert Float(1).value == 1.0
assert Float(1).value == pytest.approx(1.0, abs=ATOL)


class TestInt:
Expand Down
2 changes: 1 addition & 1 deletion tests/passes/exporter/test_quantify_scheduler_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def _check_waiting_cycles(exported_schedule: Schedule, expected_waiting_cycles:
exported_schedule.schedulables.values(), expected_waiting_cycles, strict=False
):
waiting_time = schedulable_data.data["timing_constraints"][0].rel_time
assert waiting_time == -1.0 * expected_waiting_cycle * CYCLE_TIME
assert waiting_time == pytest.approx(-1.0 * expected_waiting_cycle * CYCLE_TIME, abs=ATOL)


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/passes/mapper/test_general_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from opensquirrel.passes.mapper.mapping import Mapping

if TYPE_CHECKING:
from opensquirrel.ir import IR
from opensquirrel import Circuit


class TestMapper:
Expand All @@ -20,7 +20,7 @@ def __init__(self, qubit_register_size: int, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._qubit_register = qubit_register_size

def map(self, ir: IR, qubit_register_size: int) -> Mapping:
def map(self, circuit: Circuit, qubit_register_size: int) -> Mapping:
return Mapping(list(range(self._qubit_register)))

Mapper2(qubit_register_size=1)
Expand Down
23 changes: 12 additions & 11 deletions tests/passes/mapper/test_mip_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,32 +101,33 @@ def test_identity_mapping(mapper: str, circuit: str, expected_mapping: Mapping,
mapper_fixture = request.getfixturevalue(mapper)
circuit_fixture = request.getfixturevalue(circuit)

computed_mapping = mapper_fixture.map(circuit_fixture.ir, circuit_fixture.qubit_register_size)
computed_mapping = mapper_fixture.map(circuit_fixture, circuit_fixture.qubit_register_size)

assert computed_mapping == expected_mapping


def test_mip_mapper_remaps_when_needed(mapper2: MIPMapper, circuit2: Circuit) -> None:
if sys.platform.startswith("linux") or sys.platform == "win32":
expected_mapping = Mapping([2, 1, 3, 0, 4, 5, 6])
elif sys.platform == "darwin": # pragma: no cover
expected_mapping = Mapping([3, 4, 2, 0, 1, 5, 6])
else: # pragma: no cover
pytest.skip(f"Unknown platform: {sys.platform}")
mapping = mapper2.map(circuit2.ir, circuit2.qubit_register_size)
if sys.version_info < (3, 11):
if sys.platform.startswith("linux") or sys.platform == "win32":
expected_mapping = Mapping([2, 1, 3, 0, 4, 5, 6])
else:
expected_mapping = Mapping([3, 4, 2, 0, 1, 5, 6])
else:
expected_mapping = Mapping([5, 1, 0, 3, 4, 2, 6])
mapping = mapper2.map(circuit2, circuit2.qubit_register_size)

assert mapping == expected_mapping


def test_more_logical_qubits_than_physical(mapper1: MIPMapper, circuit3: Circuit) -> None:
with pytest.raises(RuntimeError, match=r"Number of virtual qubits (.*) exceeds number of physical qubits (.*)"):
mapper1.map(circuit3.ir, circuit3.qubit_register_size)
mapper1.map(circuit3, circuit3.qubit_register_size)


def test_timeout(mapper3: MIPMapper, circuit2: Circuit) -> None:
with pytest.raises(RuntimeError, match="MIP solver failed"):
# timeout used: 0.000001
mapper3.map(circuit2.ir, circuit2.qubit_register_size)
mapper3.map(circuit2, circuit2.qubit_register_size)


def test_fewer_virtual_than_physical_qubits(mapper1: MIPMapper) -> None:
Expand All @@ -136,7 +137,7 @@ def test_fewer_virtual_than_physical_qubits(mapper1: MIPMapper) -> None:
builder.CNOT(1, 2)
circuit = builder.to_circuit()

mapping = mapper1.map(circuit.ir, circuit.qubit_register_size)
mapping = mapper1.map(circuit, circuit.qubit_register_size)

assert len(mapping) == 3

Expand Down
Loading