pyquil
pyquil copied to clipboard
Pyquil v4 Feature Request: Associate a definition with a gate instance
Issue Description
A frequent use-case in simulation and compilation is having a pyquil program, and needing to determine the unitaries associated with each gate. The current approach to do this is to determine the name of the gate, and import it from pyquil.simulation.matrices
.
This issue proposes a cleaner way to approach this use-case by including a gate definition with each instance of the gate.
Proposed Solution
1. Add a definition
property to each gate instance.
The property would contain a DefGate
instance associated with the gate.
class Gate(quil_rs.Gate, AbstractInstruction):
"""
This is the pyQuil object for a quantum gate instruction.
"""
def __new__(
cls,
name: str,
params: Sequence[ParameterDesignator],
qubits: Sequence[Union[Qubit, QubitPlaceholder, FormalArgument, int]],
definition: "DefGate",
modifiers: Sequence[quil_rs.GateModifier] = [],
) -> Self:
return super().__new__(
cls, name, _convert_to_rs_expressions(params), _convert_to_rs_qubits(qubits), __convert_to_rs_definition(definition), list(modifiers)
)
2. Add a unitary
method to the DefGate
class.
For fixed gates, unitary()
would return a simple numpy array. For parametric gates, the return type would depend on the gate parameters provided. The user could provide scalar values for the gate parameters, resulting in a numpy array as the unitary, or they could provide Parameter
s, in which case the expressions would remain in the unitary. A mix is also supported.
def unitary(self, *args) -> np.ndarray:
"""
Get the unitary of the DefGate. For a fixed gate, this is equivalent to the `matrix`.
For a parametric gate, the function accepts the arguments of the gate and returns the unitary.
:example:
>>> import numpy as np
>>> from pyquil.quilatom import Parameter, quil_sin, quil_cos
>>> from pyquil.quilbase import DefGate
>>> theta = Parameter("theta")
>>> rx = DefGate(
"rx",
[[quil_cos(theta/2), -1j*quil_sin(theta/2)], [-1j*quil_sin(theta/2), quil_cos(theta/2)]],
[theta]
)
>>> rx.unitary(np.pi/2)
:return: The unitary of the gate.
"""
if self.parameters:
assert len(args) == len(self.parameters), f"Must provide {len(self.parameters)}"
parameter_map = {p: a for p, a in zip(self.parameters, args)}
return np.asarray(substitute_array(self.matrix, parameter_map)) # type: ignore
else:
np.asarray(self.matrix)
3. Add a unitary()
method to Gate
.
The unitary method would return a numpy array or complex numbers for fixed unitaries. For parametric unitaries, it would pass the gate arguments to it's definition
which would produce either a fixed unitary or one with expressions.
def unitary(self) -> np.ndarray:
"""
Get the unitary associated with this gate.
"""
return self.definition.unitary(self.params)
With a set of fairly simple changes, users would be able to straightforwardly determine the unitaries associated with each gate in a program.
Potential issues
Modifiers. This is a considerable benefit of the proposal. Modifiers can be handled automatically by the unitary
method. For the dagger
modifier, by taking the conjugate and for the controlled
modifier, by adding a block-diagonal identity to the top left. Currently,this is a painful case to handle.
DefGates in the program. If a user overwrites a standard gate definition in their program, the definition associated with the unitary would be incorrect. Two approaches are possible.
- Use beware. Custom gates should always be added with the
constructor
produced by the defgate. This is also currently and issue, unless the user remembers to check the defgates of a program before reaching for the standard gates. - When adding a gate to a program, check if the gate has been redefined and update the definition. Likewise for defgates (update the definitions of all gates)
Option 2 handles the most common use-cases, but could be confusing if gates are removed from the program context.
Additional References
In Cirq, gate ops have a __unitary__
method for this purpose. It raises an error when parameters are not numerical values.
https://github.com/quantumlib/Cirq/blob/f715527bdf0da4763cd196ce2be59832a530dec1/cirq-core/cirq/ops/eigen_gate.py#L338