"""Gate-to-MBQC transpiler.
accepts desired gate operations and transpile into MBQC measurement patterns.
"""
from __future__ import annotations
import dataclasses
from typing import TYPE_CHECKING, SupportsFloat
from typing_extensions import assert_never
from graphix import command, instruction, parameter
from graphix.branch_selector import BranchSelector, RandomBranchSelector
from graphix.command import E, M, N, X, Z
from graphix.fundamentals import ANGLE_PI, Axis, Plane
from graphix.instruction import Instruction, InstructionKind
from graphix.measurements import Measurement
from graphix.ops import Ops
from graphix.pattern import Pattern
from graphix.sim.statevec import Statevec, StatevectorBackend
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from numpy.random import Generator
from graphix.command import Command
from graphix.fundamentals import ParameterizedAngle
from graphix.parameter import ExpressionOrFloat, Parameter
from graphix.sim import Data
from graphix.sim.base_backend import Matrix
[docs]
@dataclasses.dataclass
class TranspileResult:
"""
The result of a transpilation.
pattern : :class:`graphix.pattern.Pattern` object
classical_outputs : tuple[int,...], index of nodes measured with *M* gates
"""
pattern: Pattern
classical_outputs: tuple[int, ...]
[docs]
@dataclasses.dataclass
class SimulateResult:
"""
The result of a simulation.
statevec : :class:`graphix.sim.statevec.Statevec` object
classical_measures : tuple[int,...], classical measures
"""
statevec: Statevec
classical_measures: tuple[int, ...]
def _check_target(out: Sequence[int | None], index: int) -> int:
target = out[index]
if target is None:
msg = f"Qubit {index} has already been measured."
raise ValueError(msg)
return target
[docs]
class Circuit:
"""Gate-to-MBQC transpiler.
Holds gate operations and translates into MBQC measurement patterns.
Attributes
----------
width : int
Number of logical qubits (for gate network)
instruction : list
List containing the gate sequence applied.
"""
instruction: list[Instruction]
[docs]
def __init__(self, width: int, instr: Iterable[Instruction] | None = None) -> None:
"""
Construct a circuit.
Parameters
----------
width : int
number of logical qubits for the gate network
instr : list[instruction.Instruction] | None
Optional. List of initial instructions.
"""
self.width = width
self.instruction = []
self.active_qubits = set(range(width))
if instr is not None:
self.extend(instr)
def add(self, instr: Instruction) -> None:
"""Add an instruction to the circuit."""
if instr.kind == InstructionKind.CCX:
self.ccx(instr.controls[0], instr.controls[1], instr.target)
elif instr.kind == InstructionKind.RZZ:
self.rzz(instr.control, instr.target, instr.angle)
elif instr.kind == InstructionKind.CNOT:
self.cnot(instr.control, instr.target)
elif instr.kind == InstructionKind.SWAP:
self.swap(instr.targets[0], instr.targets[1])
elif instr.kind == InstructionKind.CZ:
self.cz(instr.targets[0], instr.targets[1])
elif instr.kind == InstructionKind.H:
self.h(instr.target)
elif instr.kind == InstructionKind.S:
self.s(instr.target)
elif instr.kind == InstructionKind.X:
self.x(instr.target)
elif instr.kind == InstructionKind.Y:
self.y(instr.target)
elif instr.kind == InstructionKind.Z:
self.z(instr.target)
elif instr.kind == InstructionKind.I:
self.i(instr.target)
elif instr.kind == InstructionKind.M:
self.m(instr.target, instr.axis)
elif instr.kind == InstructionKind.RX:
self.rx(instr.target, instr.angle)
elif instr.kind == InstructionKind.RY:
self.ry(instr.target, instr.angle)
elif instr.kind == InstructionKind.RZ:
self.rz(instr.target, instr.angle)
else:
assert_never(instr.kind)
def extend(self, instrs: Iterable[Instruction]) -> None:
"""Add instructions to the circuit."""
for instr in instrs:
self.add(instr)
def __repr__(self) -> str:
"""Return a representation of the Circuit."""
return f"Circuit(width={self.width}, instr={self.instruction})"
[docs]
def cnot(self, control: int, target: int) -> None:
"""Apply a CNOT gate.
Parameters
----------
control : int
control qubit
target : int
target qubit
"""
assert control in self.active_qubits
assert target in self.active_qubits
assert control != target
self.instruction.append(instruction.CNOT(control=control, target=target))
def swap(self, qubit1: int, qubit2: int) -> None:
"""Apply a SWAP gate.
Parameters
----------
qubit1 : int
first qubit to be swapped
qubit2 : int
second qubit to be swapped
"""
assert qubit1 in self.active_qubits
assert qubit2 in self.active_qubits
assert qubit1 != qubit2
self.instruction.append(instruction.SWAP(targets=(qubit1, qubit2)))
def cz(self, qubit1: int, qubit2: int) -> None:
"""Apply a CNOT gate.
Parameters
----------
qubit1 : int
control qubit
qubit2 : int
target qubit
"""
assert qubit1 in self.active_qubits
assert qubit2 in self.active_qubits
assert qubit1 != qubit2
self.instruction.append(instruction.CZ(targets=(qubit1, qubit2)))
[docs]
def h(self, qubit: int) -> None:
"""Apply a Hadamard gate.
Parameters
----------
qubit : int
target qubit
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.H(target=qubit))
[docs]
def s(self, qubit: int) -> None:
"""Apply an S gate.
Parameters
----------
qubit : int
target qubit
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.S(target=qubit))
[docs]
def x(self, qubit: int) -> None:
"""Apply a Pauli X gate.
Parameters
----------
qubit : int
target qubit
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.X(target=qubit))
[docs]
def y(self, qubit: int) -> None:
"""Apply a Pauli Y gate.
Parameters
----------
qubit : int
target qubit
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.Y(target=qubit))
[docs]
def z(self, qubit: int) -> None:
"""Apply a Pauli Z gate.
Parameters
----------
qubit : int
target qubit
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.Z(target=qubit))
[docs]
def rx(self, qubit: int, angle: ParameterizedAngle) -> None:
"""Apply an X rotation gate.
Parameters
----------
qubit : int
target qubit
angle : ParameterizedAngle
rotation angle in units of π
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.RX(target=qubit, angle=angle))
[docs]
def ry(self, qubit: int, angle: ParameterizedAngle) -> None:
"""Apply a Y rotation gate.
Parameters
----------
qubit : int
target qubit
angle : ParameterizedAngle
angle in units of π
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.RY(target=qubit, angle=angle))
[docs]
def rz(self, qubit: int, angle: ParameterizedAngle) -> None:
"""Apply a Z rotation gate.
Parameters
----------
qubit : int
target qubit
angle : ParameterizedAngle
rotation angle in units of π
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.RZ(target=qubit, angle=angle))
def rzz(self, control: int, target: int, angle: ParameterizedAngle) -> None:
r"""Apply a ZZ-rotation gate.
Equivalent to the sequence
CNOT(control, target),
Rz(target, angle),
CNOT(control, target)
and realizes rotation expressed by
:math:`e^{-i \frac{\theta}{2} Z_c Z_t}`.
Parameters
----------
control : int
control qubit
target : int
target qubit
angle : ParameterizedAngle
rotation angle in units of π
"""
assert control in self.active_qubits
assert target in self.active_qubits
self.instruction.append(instruction.RZZ(control=control, target=target, angle=angle))
[docs]
def ccx(self, control1: int, control2: int, target: int) -> None:
r"""Apply a CCX (Toffoli) gate.
Prameters
---------
control1 : int
first control qubit
control2 : int
second control qubit
target : int
target qubit
"""
assert control1 in self.active_qubits
assert control2 in self.active_qubits
assert target in self.active_qubits
assert control1 != control2
assert control1 != target
assert control2 != target
self.instruction.append(instruction.CCX(controls=(control1, control2), target=target))
def i(self, qubit: int) -> None:
"""Apply an identity (teleportation) gate.
Parameters
----------
qubit : int
target qubit
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.I(target=qubit))
[docs]
def m(self, qubit: int, axis: Axis) -> None:
"""Measure a quantum qubit.
The measured qubit cannot be used afterwards.
Parameters
----------
qubit : int
target qubit
axis : Axis
measurement basis
"""
assert qubit in self.active_qubits
self.instruction.append(instruction.M(target=qubit, axis=axis))
self.active_qubits.remove(qubit)
[docs]
def transpile(self) -> TranspileResult:
"""Transpile the circuit to a pattern.
Returns
-------
result : :class:`TranspileResult` object
"""
n_node = self.width
out: list[int | None] = list(range(self.width))
pattern = Pattern(input_nodes=list(range(self.width)))
classical_outputs = []
for instr in _transpile_rzz(self.instruction):
if instr.kind == instruction.InstructionKind.CZ:
target0 = _check_target(out, instr.targets[0])
target1 = _check_target(out, instr.targets[1])
seq = self._cz_command(target0, target1)
pattern.extend(seq)
elif instr.kind == instruction.InstructionKind.CNOT:
ancilla = [n_node, n_node + 1]
control = _check_target(out, instr.control)
target = _check_target(out, instr.target)
out[instr.control], out[instr.target], seq = self._cnot_command(control, target, ancilla)
pattern.extend(seq)
n_node += 2
elif instr.kind == instruction.InstructionKind.SWAP:
target0 = _check_target(out, instr.targets[0])
target1 = _check_target(out, instr.targets[1])
out[instr.targets[0]], out[instr.targets[1]] = (
target1,
target0,
)
elif instr.kind == instruction.InstructionKind.I:
pass
elif instr.kind == instruction.InstructionKind.H:
single_ancilla = n_node
target = _check_target(out, instr.target)
out[instr.target], seq = self._h_command(target, single_ancilla)
pattern.extend(seq)
n_node += 1
elif instr.kind == instruction.InstructionKind.S:
ancilla = [n_node, n_node + 1]
target = _check_target(out, instr.target)
out[instr.target], seq = self._s_command(target, ancilla)
pattern.extend(seq)
n_node += 2
elif instr.kind == instruction.InstructionKind.X:
ancilla = [n_node, n_node + 1]
target = _check_target(out, instr.target)
out[instr.target], seq = self._x_command(target, ancilla)
pattern.extend(seq)
n_node += 2
elif instr.kind == instruction.InstructionKind.Y:
ancilla = [n_node, n_node + 1, n_node + 2, n_node + 3]
target = _check_target(out, instr.target)
out[instr.target], seq = self._y_command(target, ancilla)
pattern.extend(seq)
n_node += 4
elif instr.kind == instruction.InstructionKind.Z:
ancilla = [n_node, n_node + 1]
target = _check_target(out, instr.target)
out[instr.target], seq = self._z_command(target, ancilla)
pattern.extend(seq)
n_node += 2
elif instr.kind == instruction.InstructionKind.RX:
ancilla = [n_node, n_node + 1]
target = _check_target(out, instr.target)
out[instr.target], seq = self._rx_command(target, ancilla, instr.angle)
pattern.extend(seq)
n_node += 2
elif instr.kind == instruction.InstructionKind.RY:
ancilla = [n_node, n_node + 1, n_node + 2, n_node + 3]
target = _check_target(out, instr.target)
out[instr.target], seq = self._ry_command(target, ancilla, instr.angle)
pattern.extend(seq)
n_node += 4
elif instr.kind == instruction.InstructionKind.RZ:
ancilla = [n_node, n_node + 1]
target = _check_target(out, instr.target)
out[instr.target], seq = self._rz_command(target, ancilla, instr.angle)
pattern.extend(seq)
n_node += 2
elif instr.kind == instruction.InstructionKind.CCX:
ancilla = [n_node + i for i in range(18)]
control0 = _check_target(out, instr.controls[0])
control1 = _check_target(out, instr.controls[1])
target = _check_target(out, instr.target)
(
out[instr.controls[0]],
out[instr.controls[1]],
out[instr.target],
seq,
) = self._ccx_command(
control0,
control1,
target,
ancilla,
)
pattern.extend(seq)
n_node += 18
elif instr.kind == instruction.InstructionKind.M:
target = _check_target(out, instr.target)
seq = self._m_command(target, instr.axis)
pattern.extend(seq)
classical_outputs.append(target)
out[instr.target] = None
else:
raise ValueError("Unknown instruction, commands not added")
output_nodes = [node for node in out if node is not None]
pattern.reorder_output_nodes(output_nodes)
return TranspileResult(pattern, tuple(classical_outputs))
@classmethod
def _cnot_command(
cls, control_node: int, target_node: int, ancilla: Sequence[int]
) -> tuple[int, int, list[command.Command]]:
"""MBQC commands for CNOT gate.
Parameters
----------
control_node : int
control node on graph
target : int
target node on graph
ancilla : list of two ints
ancilla node indices to be added to graph
Returns
-------
control_out : int
control node on graph after the gate
target_out : int
target node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 2
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])]
seq.extend(
(
E(nodes=(target_node, ancilla[0])),
E(nodes=(control_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
M(node=target_node),
M(node=ancilla[0], s_domain={target_node}),
X(node=ancilla[1], domain={ancilla[0]}),
Z(node=ancilla[1], domain={target_node}),
Z(node=control_node, domain={target_node}),
)
)
return control_node, ancilla[1], seq
@classmethod
def _cz_command(cls, target_1: int, target_2: int) -> list[Command]:
"""MBQC commands for CZ gate.
Parameters
----------
target_1 : int
target node on graph
target_2 : int
other target node on graph
Returns
-------
commands : list
list of MBQC commands
"""
return [E(nodes=(target_1, target_2))]
@classmethod
def _m_command(cls, input_node: int, axis: Axis) -> list[Command]:
"""MBQC commands for measuring qubit.
Parameters
----------
input_node : int
target node on graph
axis : Axis
measurement basis
Returns
-------
commands : list
list of MBQC commands
"""
measurement = _measurement_of_axis(axis)
# `measurement.angle` and `M.angle` are both expressed in units of π.
return [M(node=input_node, plane=measurement.plane, angle=measurement.angle)]
@classmethod
def _h_command(cls, input_node: int, ancilla: int) -> tuple[int, list[Command]]:
"""MBQC commands for Hadamard gate.
Parameters
----------
input_node : int
target node on graph
ancilla : int
ancilla node index to be added
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
seq: list[Command] = [N(node=ancilla)]
seq.extend((E(nodes=(input_node, ancilla)), M(node=input_node), X(node=ancilla, domain={input_node})))
return ancilla, seq
@classmethod
def _s_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]:
"""MBQC commands for S gate.
Parameters
----------
input_node : int
input node index
ancilla : list of two ints
ancilla node indices to be added to graph
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 2
seq: list[Command] = [N(node=ancilla[0]), command.N(node=ancilla[1])]
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
M(node=input_node, angle=-ANGLE_PI / 2),
M(node=ancilla[0], s_domain={input_node}),
X(node=ancilla[1], domain={ancilla[0]}),
Z(node=ancilla[1], domain={input_node}),
)
)
return ancilla[1], seq
@classmethod
def _x_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]:
"""MBQC commands for Pauli X gate.
Parameters
----------
input_node : int
input node index
ancilla : list of two ints
ancilla node indices to be added to graph
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 2
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])]
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
M(node=input_node),
M(node=ancilla[0], angle=-ANGLE_PI, s_domain={input_node}),
X(node=ancilla[1], domain={ancilla[0]}),
Z(node=ancilla[1], domain={input_node}),
)
)
return ancilla[1], seq
@classmethod
def _y_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]:
"""MBQC commands for Pauli Y gate.
Parameters
----------
input_node : int
input node index
ancilla : list of four ints
ancilla node indices to be added to graph
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 4
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])]
seq.extend([N(node=ancilla[2]), N(node=ancilla[3])])
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
E(nodes=(ancilla[1], ancilla[2])),
E(nodes=(ancilla[2], ancilla[3])),
M(node=input_node, angle=ANGLE_PI / 2),
M(node=ancilla[0], angle=ANGLE_PI, s_domain={input_node}),
M(node=ancilla[1], angle=-ANGLE_PI / 2, s_domain={ancilla[0]}, t_domain={input_node}),
M(node=ancilla[2], s_domain={ancilla[1]}, t_domain={ancilla[0]}),
X(node=ancilla[3], domain={ancilla[2]}),
Z(node=ancilla[3], domain={ancilla[1]}),
)
)
return ancilla[3], seq
@classmethod
def _z_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]:
"""MBQC commands for Pauli Z gate.
Parameters
----------
input_node : int
input node index
ancilla : list of two ints
ancilla node indices to be added to graph
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 2
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])]
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
M(node=input_node, angle=-1),
M(node=ancilla[0], s_domain={input_node}),
X(node=ancilla[1], domain={ancilla[0]}),
Z(node=ancilla[1], domain={input_node}),
)
)
return ancilla[1], seq
@classmethod
def _rx_command(
cls, input_node: int, ancilla: Sequence[int], angle: ParameterizedAngle
) -> tuple[int, list[command.Command]]:
"""MBQC commands for X rotation gate.
Parameters
----------
input_node : int
input node index
ancilla : list of two ints
ancilla node indices to be added to graph
angle : ParameterizedAngle
measurement angle in units of π
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 2
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])]
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
M(node=input_node),
M(node=ancilla[0], angle=-angle, s_domain={input_node}),
X(node=ancilla[1], domain={ancilla[0]}),
Z(node=ancilla[1], domain={input_node}),
)
)
return ancilla[1], seq
@classmethod
def _ry_command(
cls, input_node: int, ancilla: Sequence[int], angle: ParameterizedAngle
) -> tuple[int, list[command.Command]]:
"""MBQC commands for Y rotation gate.
Parameters
----------
input_node : int
input node index
ancilla : list of four ints
ancilla node indices to be added to graph
angle : ParameterizedAngle
rotation angle in units of π
Returns
-------
out_node : int
control node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 4
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])]
seq.extend([N(node=ancilla[2]), N(node=ancilla[3])])
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
E(nodes=(ancilla[1], ancilla[2])),
E(nodes=(ancilla[2], ancilla[3])),
M(node=input_node, angle=ANGLE_PI / 2),
M(node=ancilla[0], angle=-angle, s_domain={input_node}),
M(node=ancilla[1], angle=-ANGLE_PI / 2, s_domain={ancilla[0]}, t_domain={input_node}),
M(node=ancilla[2], s_domain={ancilla[1]}, t_domain={ancilla[0]}),
X(node=ancilla[3], domain={ancilla[2]}),
Z(node=ancilla[3], domain={ancilla[1]}),
)
)
return ancilla[3], seq
@classmethod
def _rz_command(
cls, input_node: int, ancilla: Sequence[int], angle: ParameterizedAngle
) -> tuple[int, list[command.Command]]:
"""MBQC commands for Z rotation gate.
Parameters
----------
input_node : int
input node index
ancilla : list of two ints
ancilla node indices to be added to graph
angle : ParameterizedAngle
measurement angle in units of π
Returns
-------
out_node : int
node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 2
seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] # assign new qubit labels
seq.extend(
(
E(nodes=(input_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
M(node=input_node, angle=-angle),
M(node=ancilla[0], s_domain={input_node}),
X(node=ancilla[1], domain={ancilla[0]}),
Z(node=ancilla[1], domain={input_node}),
)
)
return ancilla[1], seq
@classmethod
def _ccx_command(
cls,
control_node1: int,
control_node2: int,
target_node: int,
ancilla: Sequence[int],
) -> tuple[int, int, int, list[command.Command]]:
"""MBQC commands for CCX gate.
Parameters
----------
control_node1 : int
first control node on graph
control_node2 : int
second control node on graph
target_node : int
target node on graph
ancilla : list of int
ancilla node indices to be added to graph
Returns
-------
control_out1 : int
first control node on graph after the gate
control_out2 : int
second control node on graph after the gate
target_out : int
target node on graph after the gate
commands : list
list of MBQC commands
"""
assert len(ancilla) == 18
seq: list[Command] = [N(node=ancilla[i]) for i in range(18)] # assign new qubit labels
seq.extend(
(
E(nodes=(target_node, ancilla[0])),
E(nodes=(ancilla[0], ancilla[1])),
E(nodes=(ancilla[1], ancilla[2])),
E(nodes=(ancilla[1], control_node2)),
E(nodes=(control_node1, ancilla[14])),
E(nodes=(ancilla[2], ancilla[3])),
E(nodes=(ancilla[14], ancilla[4])),
E(nodes=(ancilla[3], ancilla[5])),
E(nodes=(ancilla[3], ancilla[4])),
E(nodes=(ancilla[5], ancilla[6])),
E(nodes=(control_node2, ancilla[6])),
E(nodes=(control_node2, ancilla[9])),
E(nodes=(ancilla[6], ancilla[7])),
E(nodes=(ancilla[9], ancilla[4])),
E(nodes=(ancilla[9], ancilla[10])),
E(nodes=(ancilla[7], ancilla[8])),
E(nodes=(ancilla[10], ancilla[11])),
E(nodes=(ancilla[4], ancilla[8])),
E(nodes=(ancilla[4], ancilla[11])),
E(nodes=(ancilla[4], ancilla[16])),
E(nodes=(ancilla[8], ancilla[12])),
E(nodes=(ancilla[11], ancilla[15])),
E(nodes=(ancilla[12], ancilla[13])),
E(nodes=(ancilla[16], ancilla[17])),
M(node=target_node),
M(node=ancilla[0], s_domain={target_node}),
M(node=ancilla[1], s_domain={ancilla[0]}, t_domain={target_node}),
M(node=control_node1),
M(node=ancilla[2], angle=-7 * ANGLE_PI / 4, s_domain={ancilla[1]}, t_domain={ancilla[0]}),
M(node=ancilla[14], s_domain={control_node1}),
M(node=ancilla[3], s_domain={ancilla[2]}, t_domain={ancilla[1], ancilla[14]}),
M(node=ancilla[5], angle=-ANGLE_PI / 4, s_domain={ancilla[3]}, t_domain={ancilla[2]}),
M(node=control_node2, angle=-ANGLE_PI / 4, t_domain={ancilla[5], ancilla[0]}),
M(node=ancilla[6], s_domain={ancilla[5]}, t_domain={ancilla[3]}),
M(node=ancilla[9], s_domain={control_node2}, t_domain={ancilla[14]}),
M(node=ancilla[7], angle=-7 * ANGLE_PI / 4, s_domain={ancilla[6]}, t_domain={ancilla[5]}),
M(node=ancilla[10], angle=-7 * ANGLE_PI / 4, s_domain={ancilla[9]}, t_domain={control_node2}),
M(
node=ancilla[4],
angle=-ANGLE_PI / 4,
s_domain={ancilla[14]},
t_domain={control_node1, control_node2, ancilla[2], ancilla[7], ancilla[10]},
),
M(node=ancilla[8], s_domain={ancilla[7]}, t_domain={ancilla[14], ancilla[6]}),
M(node=ancilla[11], s_domain={ancilla[10]}, t_domain={ancilla[9], ancilla[14]}),
M(node=ancilla[12], angle=-ANGLE_PI / 4, s_domain={ancilla[8]}, t_domain={ancilla[7]}),
M(
node=ancilla[16],
s_domain={ancilla[4]},
t_domain={ancilla[14]},
),
X(node=ancilla[17], domain={ancilla[16]}),
X(node=ancilla[15], domain={ancilla[11]}),
X(node=ancilla[13], domain={ancilla[12]}),
Z(node=ancilla[17], domain={ancilla[4]}),
Z(node=ancilla[15], domain={ancilla[10]}),
Z(node=ancilla[13], domain={ancilla[8]}),
)
)
return ancilla[17], ancilla[15], ancilla[13], seq
[docs]
def simulate_statevector(
self,
input_state: Data | None = None,
branch_selector: BranchSelector | None = None,
rng: Generator | None = None,
) -> SimulateResult:
"""Run statevector simulation of the gate sequence.
Parameters
----------
input_state : Data
branch_selector: :class:`graphix.branch_selector.BranchSelector`
branch selector for measures (default: :class:`RandomBranchSelector`).
rng: Generator, optional
Random-number generator for measurements.
This generator is used only in case of random branch selection
(see :class:`RandomBranchSelector`).
Returns
-------
result : :class:`SimulateResult`
output state of the statevector simulation and results of classical measures.
"""
if branch_selector is None:
branch_selector = RandomBranchSelector()
backend = StatevectorBackend(branch_selector=branch_selector)
if input_state is None:
backend.add_nodes(range(self.width))
else:
backend.add_nodes(range(self.width), input_state)
classical_measures = []
for i in range(len(self.instruction)):
instr = self.instruction[i]
def evolve_single(op: Matrix, target: int) -> None:
backend.state.evolve_single(op, backend.node_index.index(target))
def evolve(op: Matrix, qargs: Iterable[int]) -> None:
backend.state.evolve(op, [backend.node_index.index(qarg) for qarg in qargs])
if instr.kind == instruction.InstructionKind.CNOT:
backend.state.cnot((backend.node_index.index(instr.control), backend.node_index.index(instr.target)))
elif instr.kind == instruction.InstructionKind.SWAP:
u, v = instr.targets
backend.state.swap((backend.node_index.index(u), backend.node_index.index(v)))
elif instr.kind == instruction.InstructionKind.CZ:
u, v = instr.targets
backend.state.entangle((backend.node_index.index(u), backend.node_index.index(v)))
elif instr.kind == instruction.InstructionKind.I:
pass
elif instr.kind == instruction.InstructionKind.S:
evolve_single(Ops.S, instr.target)
elif instr.kind == instruction.InstructionKind.H:
evolve_single(Ops.H, instr.target)
elif instr.kind == instruction.InstructionKind.X:
evolve_single(Ops.X, instr.target)
elif instr.kind == instruction.InstructionKind.Y:
evolve_single(Ops.Y, instr.target)
elif instr.kind == instruction.InstructionKind.Z:
evolve_single(Ops.Z, instr.target)
elif instr.kind == instruction.InstructionKind.RX:
evolve_single(Ops.rx(instr.angle), instr.target)
elif instr.kind == instruction.InstructionKind.RY:
evolve_single(Ops.ry(instr.angle), instr.target)
elif instr.kind == instruction.InstructionKind.RZ:
evolve_single(Ops.rz(instr.angle), instr.target)
elif instr.kind == instruction.InstructionKind.RZZ:
evolve(Ops.rzz(instr.angle), [instr.control, instr.target])
elif instr.kind == instruction.InstructionKind.CCX:
evolve(Ops.CCX, [instr.controls[0], instr.controls[1], instr.target])
elif instr.kind == instruction.InstructionKind.M:
result = backend.measure(
instr.target,
_measurement_of_axis(instr.axis),
rng=rng,
)
classical_measures.append(result)
else:
raise ValueError(f"Unknown instruction: {instr}")
return SimulateResult(backend.state, tuple(classical_measures))
def map_angle(self, f: Callable[[ParameterizedAngle], ParameterizedAngle]) -> Circuit:
"""Apply `f` to all angles that occur in the circuit."""
result = Circuit(self.width)
for instr in self.instruction:
# Use == for mypy
if (
instr.kind == InstructionKind.RZZ # noqa: PLR1714
or instr.kind == InstructionKind.RX
or instr.kind == InstructionKind.RY
or instr.kind == InstructionKind.RZ
):
new_instr = dataclasses.replace(instr, angle=f(instr.angle))
result.instruction.append(new_instr)
else:
result.instruction.append(instr)
return result
def is_parameterized(self) -> bool:
"""
Return `True` if there is at least one measurement angle that is not just an instance of `SupportsFloat`.
A parameterized circuit is a circuit where at least one
measurement angle is an expression that is not a number,
typically an instance of `sympy.Expr` (but we don't force to
choose `sympy` here).
"""
# Use of `==` here for mypy
return any(
not isinstance(instr.angle, SupportsFloat)
for instr in self.instruction
if instr.kind == InstructionKind.RZZ # noqa: PLR1714
or instr.kind == InstructionKind.RX
or instr.kind == InstructionKind.RY
or instr.kind == InstructionKind.RZ
)
def subs(self, variable: Parameter, substitute: ExpressionOrFloat) -> Circuit:
"""Return a copy of the circuit where all occurrences of the given variable in measurement angles are substituted by the given value."""
return self.map_angle(lambda angle: parameter.subs(angle, variable, substitute))
def xreplace(self, assignment: Mapping[Parameter, ExpressionOrFloat]) -> Circuit:
"""Return a copy of the circuit where all occurrences of the given keys in measurement angles are substituted by the given values in parallel."""
return self.map_angle(lambda angle: parameter.xreplace(angle, assignment))
def transpile_measurements_to_z_axis(self) -> Circuit:
"""Return an equivalent circuit where all measurements are on Z axis."""
circuit = Circuit(width=self.width)
for instr in self.instruction:
if instr.kind == InstructionKind.M:
if instr.axis == Axis.X:
circuit.h(instr.target)
circuit.m(instr.target, Axis.Z)
elif instr.axis == Axis.Y:
circuit.rx(instr.target, ANGLE_PI / 2)
circuit.m(instr.target, Axis.Z)
else:
circuit.add(instr)
else:
circuit.add(instr)
return circuit
def _extend_domain(measure: M, domain: set[int]) -> None:
"""Extend the correction domain of ``measure`` by ``domain``.
Parameters
----------
measure : M
Measurement command to modify.
domain : set[int]
Set of nodes to XOR into the appropriate domain of ``measure``.
"""
if measure.plane == Plane.XY:
measure.s_domain ^= domain
else:
measure.t_domain ^= domain
def _transpile_rzz(instructions: Iterable[Instruction]) -> Iterator[Instruction]:
for instr in instructions:
if instr.kind == InstructionKind.RZZ:
yield instruction.CNOT(control=instr.control, target=instr.target)
yield instruction.RZ(target=instr.target, angle=instr.angle)
yield instruction.CNOT(control=instr.control, target=instr.target)
else:
yield instr
def _measurement_of_axis(axis: Axis) -> Measurement:
"""Return the MBQC measurement (plane + angle) corresponding to a measurement on the given axis.
The returned measurement `m` satisfies `m.plane.polar(m.angle) = (x, y, z)`,
where `x` (resp. `y` or `z`) is 1 if `axis` is `Axis.X` (resp. `Axis.Y` or
`Axis.Z`), and 0 otherwise.
"""
# The definition of `polar` for each plane is given in the comments below.
if axis == Axis.X:
return Measurement(plane=Plane.XY, angle=0) # (cos, sin, 0)
if axis == Axis.Y:
# `Measurement.angle` is expressed in units of π.
return Measurement(plane=Plane.YZ, angle=ANGLE_PI / 2) # (0, sin, cos)
return Measurement(plane=Plane.XZ, angle=0) # (sin, 0, cos)