Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pam verify bug fixes #192

Merged
merged 12 commits into from
Oct 17, 2023
1 change: 1 addition & 0 deletions bqskit/compiler/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,7 @@ def build_seqpam_mapping_optimization_workflow(
PAMLayoutPass(num_layout_passes),
PAMRoutingPass(0.1),
post_pam_seq,
ApplyPlacement(),
UnfoldPass(),
],
),
Expand Down
14 changes: 12 additions & 2 deletions bqskit/compiler/passdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ def placement(self, _val: Sequence[int]) -> None:

@property
def initial_mapping(self) -> list[int]:
"""Return the initial mapping of logical to physical qudits."""
"""
Return the initial mapping of logical to physical qudits.

This always maps how the logical qudits from the original circuit start
on the physical qudits of the current circuit.
"""
return self._initial_mapping

@initial_mapping.setter
Expand All @@ -166,7 +171,12 @@ def initial_mapping(self, _val: Sequence[int]) -> None:

@property
def final_mapping(self) -> list[int]:
"""Return the final mapping of logical to physical qudits."""
"""
Return the final mapping of logical to physical qudits.

This always maps how the logical qudits from the original circuit end on
the physical qudits of the current circuit.
"""
return self._final_mapping

@final_mapping.setter
Expand Down
9 changes: 4 additions & 5 deletions bqskit/ir/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,8 @@ def append_circuit(
(Default: False)

Returns:
int: The starting cycle index of the appended circuit.
int: The starting cycle index of the appended circuit. If the
appended circuit is empty, then this will be -1.

Raises:
ValueError: If `circuit` is not the same size as `location`.
Expand All @@ -1178,16 +1179,14 @@ def append_circuit(
op = Operation(CircuitGate(circuit, move), location, circuit.params)
return self.append(op)

cycle_index: int | None = None
cycle_index = -1

for op in circuit:
mapped_location = [location[q] for q in op.location]
ci = self.append(Operation(op.gate, mapped_location, op.params))
if cycle_index is None:
cycle_index = ci

if cycle_index is None:
raise RuntimeError('Cannot append empty circuit.')

return cycle_index

def extend(self, ops: Iterable[Operation]) -> None:
Expand Down
12 changes: 6 additions & 6 deletions bqskit/passes/mapping/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
"""Perform the pass's operation, see :class:`BasePass` for more."""
model = data.model
placement = data.placement

# Place circuit on the model according to the placement
physical_circuit = Circuit(model.num_qudits, model.radixes)
physical_circuit.append_circuit(circuit, placement)
circuit.become(physical_circuit)
if 'final_mapping' in data:
pi = data['final_mapping']
data['final_mapping'] = [placement[p] for p in pi]
if 'initial_mapping' in data:
pi = data['initial_mapping']
data['initial_mapping'] = [placement[p] for p in pi]

# Update the relevant data variables
data.initial_mapping = [placement[p] for p in data.initial_mapping]
data.final_mapping = [placement[p] for p in data.final_mapping]
data.placement = list(i for i in range(model.num_qudits))
5 changes: 4 additions & 1 deletion bqskit/passes/mapping/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ async def run(self, circuit: Circuit, data: PassData) -> None:

datas = []
for graph in graphs:
model = MachineModel(circuit.num_qudits, graph, data.gate_set, data.model.radixes)
model = MachineModel(
circuit.num_qudits, graph,
data.gate_set, data.model.radixes,
)
target_data = copy.deepcopy(data)
target_data.model = model
datas.append(target_data)
Expand Down
7 changes: 4 additions & 3 deletions bqskit/passes/mapping/layout/pam.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
if not subgraph.is_fully_connected():
raise RuntimeError('Cannot route circuit on disconnected qudits.')

pi = data.initial_mapping
pi = [i for i in range(circuit.num_qudits)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pi is the permutation of the initial mapping we are solving for? So it gets modified by the forward and backwards passes? Might be helpful to comment this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout is also called after ApplyPlacement correct? So what good does changing the placement do here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, ok, in the test_sabre I see that ApplyPlacement is called last, do we need to change this in other workflows?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good questions. A few points:

  • The initial and final mappings are always with respect to the original circuit (mapping from original to current), so until the circuit gets modified (ApplyPlacement, Routing), they shouldn't be changed.
  • The current placement tracks the current placement of the current circuit qudits to model qudits, so layout and routing need to start with this information
  • pi always starts as [0, 1, 2, 3, ...], because the placement info is encoded into the data.connectivity call, the actual coupling graph is permuted there to account for it

Yes, ApplyPlacement should be called after SABRE layout and routing. What other workflows are you referring to?

Also, Rather than running GreedyPlacement, another valid placement strategy would be to ApplyPlacement before mapping to "extend" the circuit to the model size, then running layout and hoping SABRE finds a good placement. This is sort of the strategy we take with PAM. We are definitely lacking in the placement department tho

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, so even before we were calling ApplyPlacement before and after PAMLayout and PAMRouting.


for _ in range(self.total_passes):
self.forward_pass(circuit, pi, subgraph, perm_data)
self.backward_pass(circuit, pi, subgraph)

data.initial_mapping = pi
_logger.info(f'Found layout: {str(pi)}')
self._apply_perm(pi, data.placement)
_logger.info(f'Found layout: {pi}, new placement: {data.placement}')
10 changes: 5 additions & 5 deletions bqskit/passes/mapping/layout/sabre.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ def __init__(

async def run(self, circuit: Circuit, data: PassData) -> None:
"""Perform the pass's operation, see :class:`BasePass` for more."""
subgraph = self.get_connectivity(circuit, data)
subgraph = data.connectivity
if not subgraph.is_fully_connected():
raise RuntimeError('Cannot layout circuit on disconnected qudits.')

pi = data.initial_mapping
pi = [i for i in range(circuit.num_qudits)]

for _ in range(self.total_passes):
self.forward_pass(circuit, pi, subgraph)
self.backward_pass(circuit, pi, subgraph)

# select qubits
data.initial_mapping = pi
_logger.info(f'Found layout: {str(pi)}')
self._apply_perm(pi, data.placement)
_logger.info(f'Found layout: {pi}, new placement: {data.placement}')
7 changes: 0 additions & 7 deletions bqskit/passes/mapping/pam.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,10 +403,3 @@ def _score_perm(

pi[:] = pi_bkp[:]
return front + extend

def _apply_perm(self, perm: Sequence[int], pi: list[int]) -> None:
"""Apply the `perm` permutation to the current mapping `pi`."""
_logger.debug('applying permutation %s' % str(perm))
pi_c = {q: pi[perm[i]] for i, q in enumerate(sorted(perm))}
for q in perm:
pi[q] = pi_c[q]
21 changes: 6 additions & 15 deletions bqskit/passes/mapping/routing/pam.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""This module implements the PAMRoutingPass."""
from __future__ import annotations

import copy
import logging

from bqskit.compiler.basepass import BasePass
Expand All @@ -21,26 +20,18 @@ class PAMRoutingPass(PermutationAwareMappingAlgorithm, BasePass):

async def run(self, circuit: Circuit, data: PassData) -> None:
"""Perform the pass's operation, see :class:`BasePass` for more."""
model = self.get_model(circuit, data)
placement = self.get_placement(circuit, data)
subgraph = model.coupling_graph.get_subgraph(placement)
subgraph = data.connectivity
if not subgraph.is_fully_connected():
raise RuntimeError('Cannot route circuit on disconnected qudits.')

perm_data: dict[CircuitPoint, PAMBlockTAPermData] = {}
block_datas = data[ForEachBlockPass.key][-1]
for block_data in block_datas:
perm_data[block_data['point']] = block_data['permutation_data']

if not subgraph.is_fully_connected():
raise RuntimeError('Cannot route circuit on disconnected qudits.')

if 'initial_mapping' in data:
pi = copy.deepcopy(data['initial_mapping'])
else:
pi = [i for i in range(circuit.num_qudits)]

pi = [i for i in range(circuit.num_qudits)]
out_data = self.forward_pass(circuit, pi, subgraph, perm_data, True)
if 'final_mapping' in data:
self._apply_perm(data['final_mapping'], pi)
data['final_mapping'] = pi
data.final_mapping = [pi[x] for x in data.final_mapping]

_logger.info(f'Finished routing with layout: {str(pi)}')
data[self.out_data_key] = out_data
13 changes: 4 additions & 9 deletions bqskit/passes/mapping/routing/sabre.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""This module implements the GeneralizedSabreRoutingPass class."""
from __future__ import annotations

import copy
import logging

from bqskit.compiler.basepass import BasePass
Expand All @@ -22,16 +21,12 @@ class GeneralizedSabreRoutingPass(BasePass, GeneralizedSabreAlgorithm):

async def run(self, circuit: Circuit, data: PassData) -> None:
"""Perform the pass's operation, see :class:`BasePass` for more."""
subgraph = self.get_connectivity(circuit, data)
subgraph = data.connectivity
if not subgraph.is_fully_connected():
raise RuntimeError('Cannot route circuit on disconnected qudits.')

if 'initial_mapping' in data:
pi = copy.deepcopy(data['initial_mapping'])
else:
pi = [i for i in range(circuit.num_qudits)]

pi = [i for i in range(circuit.num_qudits)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is the initial mapping always going to 0,1,2,3,... and all of the actual mapping information is in placement? Do we even need initial mapping anymore?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the initial mapping gets updated when we actually apply the placement. Until then, the circuit's input qubit ordering actually doesn't change

self.forward_pass(circuit, pi, subgraph, modify_circuit=True)
# TODO: if final_mapping is already in data, apply it first
data['final_mapping'] = pi
data.final_mapping = [pi[x] for x in data.final_mapping]

_logger.info(f'Finished routing with layout: {str(pi)}')
14 changes: 13 additions & 1 deletion bqskit/passes/mapping/sabre.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import numpy as np

from bqskit.ir.circuit import Circuit
from bqskit.ir.gates.circuitgate import CircuitGate
from bqskit.ir.gates.constant.swap import SwapGate
from bqskit.ir.operation import Operation
from bqskit.ir.point import CircuitPoint
Expand Down Expand Up @@ -329,9 +330,13 @@ def backward_pass(

def _can_exe(self, op: Operation, pi: list[int], cg: CouplingGraph) -> bool:
"""Return true if `op` is executable given the current mapping `pi`."""
# TODO: check if circuitgate of only 1-qubit gates
if isinstance(op.gate, CircuitGate):
if all(g.num_qudits == 1 for g in op.gate._circuit.gate_set):
return True

if op.num_qudits == 1:
return True

physical_qudits = [pi[i] for i in op.location]
return cg.get_subgraph(physical_qudits).is_fully_connected()

Expand Down Expand Up @@ -510,3 +515,10 @@ def _uphill_swaps(
if pi[center_qudit] == p1 or pi[center_qudit] == p2:
continue
yield (p1, p2)

def _apply_perm(self, perm: Sequence[int], pi: list[int]) -> None:
"""Apply the `perm` permutation to the current mapping `pi`."""
_logger.debug('applying permutation %s' % str(perm))
pi_c = {q: pi[perm[i]] for i, q in enumerate(sorted(perm))}
for q in perm:
pi[q] = pi_c[q]
42 changes: 21 additions & 21 deletions bqskit/passes/mapping/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,20 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
# calculate exact panel unitary
exact_circuit = Circuit(circuit.num_qudits, circuit.radixes)
for op in circuit:
if not isinstance(op.gate, TaggedGate):
raise RuntimeError('Expected tagged gate.')

pi = op.gate.tag['pre_perm']
pf = op.gate.tag['post_perm']
in_utry = op.gate.tag['original_utry']
PI = PermutationMatrix.from_qubit_location(in_utry.num_qudits, pi)
PF = PermutationMatrix.from_qubit_location(in_utry.num_qudits, pf)
exact_circuit.append_gate(
ConstantUnitaryGate(PF @ in_utry @ PI.T),
op.location,
)
if isinstance(op.gate, TaggedGate):
pi = op.gate.tag['pre_perm']
pf = op.gate.tag['post_perm']
in_utry = op.gate.tag['original_utry']
n = in_utry.num_qudits
PI = PermutationMatrix.from_qubit_location(n, pi)
PF = PermutationMatrix.from_qubit_location(n, pf)
exact_circuit.append_gate(
ConstantUnitaryGate(PF.T @ in_utry @ PI),
op.location,
)

else:
exact_circuit.append_gate(op.gate, op.location, op.params)

exact_unitary = exact_circuit.get_unitary()

Expand All @@ -74,15 +76,13 @@ class UnTagPAMBlockDataPass(BasePass):
async def run(self, circuit: Circuit, data: PassData) -> None:
"""Perform the pass's operation, see :class:`BasePass` for more."""
for cycle, op in circuit.operations_with_cycles():
if not isinstance(op.gate, TaggedGate):
raise RuntimeError('Expected tagged gate.')

circuit.replace_gate(
(cycle, op.location[0]),
op.gate.gate,
op.location,
op.params,
)
if isinstance(op.gate, TaggedGate):
circuit.replace_gate(
(cycle, op.location[0]),
op.gate.gate,
op.location,
op.params,
)


class PAMVerificationSequence(PassAlias):
Expand Down
34 changes: 34 additions & 0 deletions tests/compiler/compile/test_compile.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
from __future__ import annotations

from typing import Callable

import pytest

from bqskit.compiler.compile import compile
from bqskit.compiler.compiler import Compiler
from bqskit.compiler.machine import MachineModel
from bqskit.ir.circuit import Circuit
from bqskit.qis.graph import CouplingGraph
from bqskit.qis.permutation import PermutationMatrix


def default_model_gen(circuit: Circuit) -> MachineModel:
"""Generate a default model for the given circuit."""
return MachineModel(circuit.num_qudits)


def linear_model_gen(circuit: Circuit) -> MachineModel:
"""Generate a linear model for the given circuit."""
return MachineModel(
circuit.num_qudits,
CouplingGraph.linear(circuit.num_qudits),
)


@pytest.mark.parametrize(
'gen_model',
[
default_model_gen,
linear_model_gen,
],
ids=[
'default',
'linear',
],
)
def test_medium_circuit_compile(
compiler: Compiler,
optimization_level: int,
medium_qasm_file: str,
gen_model: Callable[[Circuit], MachineModel],
) -> None:
circuit = Circuit.from_file(medium_qasm_file)
model = gen_model(circuit)
out_circuit, pi, pf = compile(
circuit,
optimization_level=optimization_level,
with_mapping=True,
compiler=compiler,
model=model,
)
in_utry = circuit.get_unitary()
out_utry = out_circuit.get_unitary()
PI = PermutationMatrix.from_qubit_location(out_circuit.num_qudits, pi)
PF = PermutationMatrix.from_qubit_location(out_circuit.num_qudits, pf)
error = out_utry.get_distance_from(PF.T @ in_utry @ PI, 1)
assert error <= 1e-8
assert model.is_compatible(out_circuit)
6 changes: 6 additions & 0 deletions tests/passes/mapping/test_sabre.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from bqskit.passes import GeneralizedSabreRoutingPass
from bqskit.passes import GreedyPlacementPass
from bqskit.passes import SetModelPass
from bqskit.passes.mapping.apply import ApplyPlacement
from bqskit.qis import PermutationMatrix
from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix

Expand Down Expand Up @@ -38,12 +39,17 @@ def test_simple(compiler: Compiler) -> None:
GreedyPlacementPass(),
GeneralizedSabreLayoutPass(),
GeneralizedSabreRoutingPass(),
ApplyPlacement(),
]

cc, data = compiler.compile(circuit, workflow, True)
pi = data['initial_mapping']
pf = data['final_mapping']
PI = PermutationMatrix.from_qubit_location(5, pi)
PF = PermutationMatrix.from_qubit_location(5, pf)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did some qudits become inactive? Not sure I understand

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you apply the placement, the logical, input circuit size is extended to fit the physical, output machine model. So in this workflow, this means a good portion of them won't be used.

inactive = [i for i in range(cc.num_qudits) if i not in cc.active_qudits]
inactive.sort(reverse=True)
for i in inactive:
cc.pop_qudit(i)
assert cc.get_unitary().get_distance_from(PF.T @ in_utry @ PI) < 1e-7
assert all(e in cg for e in cc.coupling_graph)