Source code for pennylane_rigetti.converter
import warnings
from collections.abc import Sequence
import numpy as np
import pennylane as qml
import pennylane_rigetti as plf
import pyquil
from pyquil import simulation
from pyquil.gates import Gate
pyquil_inv_operation_map = {
"I": qml.Identity,
"X": qml.PauliX,
"Y": qml.PauliY,
"Z": qml.PauliZ,
"H": qml.Hadamard,
"CNOT": qml.CNOT,
"SWAP": qml.SWAP,
"CZ": qml.CZ,
"PHASE": qml.PhaseShift,
"RX": qml.RX,
"RY": qml.RY,
"RZ": qml.RZ,
"CRX": qml.CRX,
"CRY": qml.CRY,
"CRZ": qml.CRZ,
"S": qml.S,
"T": qml.T,
"CCNOT": qml.Toffoli,
"CPHASE": lambda *params, wires: plf.ops.CPHASE(*params, 3, wires=wires),
"CPHASE00": lambda *params, wires: plf.ops.CPHASE(*params, 0, wires=wires),
"CPHASE01": lambda *params, wires: plf.ops.CPHASE(*params, 1, wires=wires),
"CPHASE10": lambda *params, wires: plf.ops.CPHASE(*params, 2, wires=wires),
"CSWAP": qml.CSWAP,
"ISWAP": qml.ISWAP,
"PSWAP": qml.PSWAP,
}
_control_map = {
"X": "CNOT",
"Z": "CZ",
"CNOT": "CCNOT",
"SWAP": "CSWAP",
"RX": "CRX",
"RY": "CRY",
"RZ": "CRZ",
"PHASE": "CPHASE",
}
def _direct_sum(A, B):
"""Return the direct sums of two arrays.
Args:
A (np.array): The first array (upper left array)
B (np.array): The second array (lower right array)
Returns:
np.array: The direct sum of the two input arrays
"""
sum_matrix = np.zeros(np.add(A.shape, B.shape), dtype=A.dtype)
sum_matrix[: A.shape[0], : A.shape[1]] = A
sum_matrix[A.shape[0] :, A.shape[1] :] = B
return sum_matrix
def _controlled_matrix(op):
"""Return the matrix associated with the controlled operation.
Args:
op (np.array): Array representing the operation
that should be controlled
Returns:
np.array: Array representing the controlled operations. If the input
array has shape (N, N) the output shape is (2*N, 2*N).
"""
return _direct_sum(np.eye(op.shape[0], dtype=op.dtype), op)
def _resolve_gate(gate):
"""Resolve the given pyquil Gate as far as possible.
For example, the gate ``CONTROLLED CONTROLLED X`` will be resolved to ``CCNOT``.
The gate ``CONTROLLED CONTROLLED RX(0.3)`` will be resolved to ``CONTROLLED CRX(0.3)``.
Args:
gate (pyquil.quil.Gate): The gate that should be resolved
Returns:
pyquil.quil.Gate: The maximally resolved gate
"""
for i, modifier in enumerate(gate.modifiers):
if modifier == "CONTROLLED":
if gate.name in _control_map:
stripped_gate = Gate(_control_map[gate.name], gate.params, gate.qubits)
stripped_gate.modifiers = gate.modifiers.copy()
del stripped_gate.modifiers[i]
return _resolve_gate(stripped_gate)
else:
break
return gate
def _resolve_params(params, parameter_map):
"""Resolve a parameter list with a given variable map.
Args:
params (List[Union[pyquil.quilatom.MemoryReference, object]]): The parameter list
parameter_map (Dict[str, object]): The map that assigns values to variable names
Returns:
List[object]: The resolved parameters. This list does not contain MemoryReferences anymore.
"""
resolved_params = []
for param in params:
if isinstance(param, pyquil.quilatom.MemoryReference):
resolved_params.append(parameter_map[param.name])
else:
resolved_params.append(param)
return resolved_params
def _normalize_parameter_map(parameter_map):
"""Normalize the given variable map.
Variable maps can have keys that are either strings or
pyquil.quil.MemoryReference instances. This methods replaces
all MemoryReference instances with their name.
Args:
parameter_map (Dict[Union[str, pyquil.quil.MemoryReference], object]): The initial variable map.
Returns:
Dict[str, object]: Variable map with all MemoryReference instances replaced by their name
"""
new_keys = list(parameter_map.keys())
values = list(parameter_map.values())
for i in range(len(new_keys)):
if isinstance(new_keys[i], pyquil.quil.MemoryReference):
new_keys[i] = new_keys[i].name
return dict(zip(new_keys, values))
def _is_controlled(gate):
"""Determine if a gate is controlled.
Args:
gate (pyquil.quil.Gate): The gate that should be checked
Returns:
bool: True if the gate is controlled, False otherwise
"""
return "CONTROLLED" in gate.modifiers
def _is_forked(gate):
"""Determine if a gate is forked.
Args:
gate (pyquil.quil.Gate): The gate that should be checked
Returns:
bool: True if the gate is forked, False otherwise
"""
return "FORKED" in gate.modifiers
def _is_inverted(gate):
"""Determine if a gate is inverted.
Args:
gate (pyquil.quil.Gate): The gate that should be checked
Returns:
bool: True if the gate is inverted, False otherwise
"""
return gate.modifiers.count("DAGGER") % 2 == 1
def _is_gate(instruction):
"""Determine if an instruction is a gate.
Args:
instruction (pyquil.quil.AbstractInstruction): The instruction that should be checked
Returns:
bool: True if the instruction is a gate, False otherwise
"""
return isinstance(instruction, pyquil.quil.Gate)
def _is_declaration(instruction):
"""Determine if an instruction is a declaration.
Args:
instruction (pyquil.quil.AbstractInstruction): The instruction that should be checked
Returns:
bool: True if the instruction is a declaration, False otherwise
"""
return isinstance(instruction, pyquil.quil.Declare)
def _is_measurement(instruction):
"""Determine if an instruction is a measurement.
Args:
instruction (pyquil.quil.AbstractInstruction): The instruction that should be checked
Returns:
bool: True if the instruction is a measurement, False otherwise
"""
return isinstance(instruction, pyquil.quil.Measurement)
def _get_qubit_index(qubit):
"""Return the index of the given qubit.
This function accepts qubit instances and integers and returns
the integer index that is associated with the qubit.
Args:
qubit (Union[pyquil.quilatom.Qubit, int]): The qubit whose index shall be determined
Returns:
int: The qubit index
"""
if isinstance(qubit, int):
return qubit
if isinstance(qubit, pyquil.quilatom.Qubit):
return qubit.index
def _qubits_to_wires(qubits, qubit_to_wire_map):
"""Transform the given qubits with the given map between qubits and wires.
Args:
qubits (Union[Sequence[int], int]): The qubit(s) for which the wires shall be determined
qubit_to_wire_map (Dict[int, int]): The map between qubits and wires
Returns:
Union[Sequence[int], int]: The wire(s) corresponding to the given qubit(s)
"""
if isinstance(qubits, Sequence):
return [qubit_to_wire_map[_get_qubit_index(qubit)] for qubit in qubits]
return qubit_to_wire_map[_get_qubit_index(qubits)]
class ParametrizedGate:
"""Represent a parametrized PennyLane gate.
Args:
pl_gate (type): The PennyLane gate
pyquil_qubits ([type]): The qubits the gate acts on in the corresponding pyquil Program
pyquil_params ([type]): The gate's parameters in the corresponding pyquil Program
is_inverted (bool): Indicates if the gate is inverted
"""
def __init__(self, pl_gate, pyquil_qubits, pyquil_params, is_inverted):
self.pl_gate = pl_gate
self.pyquil_qubits = pyquil_qubits
self.pyquil_params = pyquil_params
self.is_inverted = is_inverted
def instantiate(self, parameter_map, qubit_to_wire_map):
"""Instantiate the parametrized gate.
Args:
parameter_map (Dict[str, object]): The map that assigns values to variable names
qubit_to_wire_map ([type]): The map that assigns wires to qubits
Returns:
qml.Operation: The initiated instance of the parametrized PennyLane gate
"""
resolved_params = _resolve_params(self.pyquil_params, parameter_map)
resolved_wires = _qubits_to_wires(self.pyquil_qubits, qubit_to_wire_map)
pl_gate_instance = self.pl_gate(*resolved_params, wires=resolved_wires)
if self.is_inverted:
return qml.adjoint(pl_gate_instance)
return pl_gate_instance
class ParametrizedQubitUnitary:
"""Represents a QubitUnitary instance already parametrized with a matrix.
Args:
matrix (np.array): The unitary gate matrix
"""
def __init__(self, matrix):
self.matrix = matrix
def __call__(self, wires):
"""Instantiate the QubitUnitary on the given wires.
Args:
wires (List[int]): The wires the QubitUnitary acts on
Returns:
qml.QubitUnitary: The instantiate QubitUnitary instance
"""
return qml.QubitUnitary(self.matrix, wires=wires)
class ProgramLoader:
"""Loads the given pyquil Program as a PennyLane template.
The pyquil Program is parsed once at instantiation and can be
applied as often as desired.
Args:
program (pyquil.quil.Program): The pyquil Program instance that should be loaded
"""
_matrix_dictionary = simulation.matrices.QUANTUM_GATES
def __init__(self, program):
self.program = program
self.qubits = program.get_qubits()
self._load_defined_gate_names()
self._load_declarations()
self._load_measurements()
self._load_template()
def _load_defined_gate_names(self):
"""Extract the names of all defined gates of the pyquil Program."""
self._defined_gate_names = []
for defgate in self.program.defined_gates:
self._defined_gate_names.append(defgate.name)
if isinstance(defgate, pyquil.quil.DefPermutationGate):
matrix = np.eye(defgate.permutation.shape[0])
matrix = matrix[:, defgate.permutation]
elif isinstance(defgate, pyquil.quil.DefGate):
matrix = defgate.matrix
self._matrix_dictionary[defgate.name] = matrix
def _load_declarations(self):
"""Extract the declarations of the pyquil Program."""
self._declarations = [
instruction for instruction in self.program.instructions if _is_declaration(instruction)
]
def _load_measurements(self):
"""Extract the measurements of the pyquil Program."""
measurements = [
instruction for instruction in self.program.instructions if _is_measurement(instruction)
]
self._measurement_variable_names = set(
[measurement.classical_reg.name for measurement in measurements]
)
def _is_defined_gate(self, gate):
"""Determine if the given gate was defined in the pyquil Program.
Args:
gate (pyquil.quil.Gate): The gate that shall be checked
Returns:
bool: True if the gate is defined, False otherwise
"""
return gate.name in self._defined_gate_names
def _load_qubit_to_wire_map(self, wires):
"""Build the map that assigns wires to qubits.
Args:
wires (Sequence[int]): The wires that should be assigned to the qubits
Raises:
qml.DeviceError: When the number of given wires does not match the number of qubits in the pyquil Program
Returns:
Dict[int, int]: The map that assigns wires to qubits
"""
if len(wires) != len(self.qubits):
raise qml.DeviceError(
"The number of given wires does not match the number of qubits in the PyQuil Program. "
+ "{} wires were given, Program has {} qubits".format(len(wires), len(self.qubits))
)
self._qubit_to_wire_map = dict(zip(self.qubits, wires))
return self._qubit_to_wire_map
def _resolve_gate_matrix(self, gate):
"""Resolve the matrix of the given pyquil gate.
Args:
gate (pyquil.quil.Gate): The gate whose matrix should be resolved
Returns:
np.array: The matrix of the given gate
"""
gate_matrix = self._matrix_dictionary[gate.name]
for i, modifier in enumerate(gate.modifiers):
if modifier == "CONTROLLED":
gate_matrix = _controlled_matrix(gate_matrix)
return gate_matrix
def _check_parameter_map(self, parameter_map):
"""Check that all variables of the program are defined.
Only variables used in measurements need not be defined.
Args:
parameter_map (Dict[str, object]): Map that assigns values to variables
Raises:
qml.DeviceError: When not all variables are defined in the variable map
"""
for declaration in self._declarations:
if not declaration.name in parameter_map:
# If the variable is used in measurement we don't complain
if not declaration.name in self._measurement_variable_names:
raise qml.DeviceError(
(
"The PyQuil program defines a variable {} that is not present in the given variable map. "
+ "Instruction: {}"
).format(declaration.name, declaration)
)
@property
def defined_gates(self):
"""The custom gates defined in the pyquil Program.
Returns:
List[Union[pyquil.quil.DefGate, pyquil.quil.DefPermutationGate]]: The gates defined in the pyquil Program
"""
return self.program.defined_gates
@property
def defined_qubits(self):
"""The qubit indices defined in the pyquil Program.
Returns:
List[int]: The qubit indices defined in the pyquil Program
"""
return list(self.qubits)
@property
def defined_gate_names(self):
"""The names of the custom gates defined in the pyquil Program.
Returns:
List[str]: The names of the gates defined in the pyquil Program
"""
return self._defined_gate_names
@property
def declarations(self):
"""The declarations in the pyquil Program.
Returns:
List[pyquil.quil.Declaration]: The declarations in the pyquil Program
"""
return self._declarations
@property
def defined_variable_names(self):
"""The names of the variables defined in the pyquil Program
Returns:
List[str]: The names of the variables defined in the pyquil Program
"""
return [declaration.name for declaration in self._declarations]
def _load_template(self):
"""Load the template corresponding to the pyquil Program.
Raises:
qml.DeviceError: When the import of a forked gate is attempted
"""
self._parametrized_gates = []
for i, instruction in enumerate(self.program.instructions):
# Skip all statements that are not gates (RESET, MEASURE, PRAGMA, ...)
if not _is_gate(instruction):
if not _is_declaration(instruction):
warnings.warn(
"Instruction Nr. {} is not supported by PennyLane and was ignored: {}".format(
i + 1, instruction
)
)
continue
# Rename for better readability
gate = instruction
if _is_forked(gate):
raise qml.DeviceError(
"Forked gates can not be imported into PennyLane, as this functionality is not supported. "
+ "Instruction Nr. {}, {} was a forked gate.".format(i + 1, gate)
)
resolved_gate = _resolve_gate(gate)
# If the gate is a DefGate or not all CONTROLLED statements can be resolved
# we resort to QubitUnitary
if _is_controlled(resolved_gate) or self._is_defined_gate(resolved_gate):
pl_gate = ParametrizedQubitUnitary(self._resolve_gate_matrix(resolved_gate))
else:
pl_gate = pyquil_inv_operation_map[resolved_gate.name]
parametrized_gate = ParametrizedGate(
pl_gate, gate.qubits, gate.params, _is_inverted(gate)
)
self._parametrized_gates.append(parametrized_gate)
def template(self, wires, parameter_map=None):
"""Executes the template extracted from the pyquil Program.
Args:
wires (Sequence[int]): The wires on which the template shall be applied.
parameter_map (Dict[Union[str, pyquil.quilatom.MemoryReference], object], optional): The
map that assigns values to variables. Defaults to an empty dictionary.
Returns:
List[qml.Operation]: A list of all applied gates
"""
if parameter_map is None:
parameter_map = {}
parameter_map = _normalize_parameter_map(parameter_map)
qubit_to_wire_map = self._load_qubit_to_wire_map(wires)
self._check_parameter_map(parameter_map)
applied_gates = []
for parametrized_gate in self._parametrized_gates:
applied_gates.append(parametrized_gate.instantiate(parameter_map, qubit_to_wire_map))
return applied_gates
def __call__(self, wires, parameter_map=None):
"""Executes the template extracted from the pyquil Program.
Args:
wires (Sequence[int]): The wires on which the template shall be applied.
parameter_map (Dict[Union[str, pyquil.quilatom.MemoryReference], object], optional): The
map that assigns values to variables. Defaults to an empty dictionary.
Returns:
List[qml.Operation]: A list of all applied gates
"""
return self.template(wires, parameter_map)
def __str__(self):
"""Give the string representation of the ProgramLoader.
Returns:
str: The string representation of the ProgramLoader
"""
return "PennyLane Program Loader for PyQuil Program:\n" + str(self.program)
[docs]def load_program(program: pyquil.Program):
"""Load a pyquil.Program instance as a PennyLane template.
During loading, gates are converted to PennyLane gates as far as possible. If
the gates are not supported they are replaced with QubitUnitary instances. The
import ignores all statements that are not declarations or gates (e.g. pragmas,
classical control flow and measurements).
Every variable that is present in the Program and that is not used as the target
register of a measurement has to be provided in the ``parameter_map`` of the template.
Args:
program (pyquil.Program): The program that should be loaded
Returns:
ProgramLoader: a ProgramLoader instance that can be called like a template
"""
return ProgramLoader(program)
[docs]def load_quil(quil_str: str):
"""Load a quil string as a PennyLane template.
During loading, gates are converted to PennyLane gates as far as possible. If
the gates are not supported they are replaced with QubitUnitary instances. The
import ignores all statements that are not declarations or gates (e.g. pragmas,
classical control flow and measurements).
Every variable that is present in the Program and that is not used as the target
register of a measurement has to be provided in the ``parameter_map`` of the template.
Args:
quil_str (str): The program that should be loaded
Returns:
ProgramLoader: a ProgramLoader instance that can be called like a template
"""
return load_program(pyquil.Program(quil_str))
[docs]def load_quil_from_file(file_path: str):
"""Load a quil file as a PennyLane template.
During loading, gates are converted to PennyLane gates as far as possible. If
the gates are not supported they are replaced with QubitUnitary instances. The
import ignores all statements that are not declarations or gates (e.g. pragmas,
classical control flow and measurements).
Every variable that is present in the Program and that is not used as the target
register of a measurement has to be provided in the ``parameter_map`` of the template.
Args:
file_path (str): The path to the quil file that should be loaded
Returns:
ProgramLoader: a ProgramLoader instance that can be called like a template
"""
with open(file_path, "r") as file:
quil_str = file.read()
return load_quil(quil_str)
_modules/pennylane_rigetti/converter
Download Python script
Download Notebook
View on GitHub