Source code for pennylane.devices.default_qutrit_mixed
# Copyright 2018-2024 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The default.qutrit.mixed device is PennyLane's standard qutrit simulator for mixed-state
computations."""
import logging
import warnings
from collections.abc import Callable, Sequence
from dataclasses import replace
from functools import partial
from typing import Optional, Union
import numpy as np
import pennylane as qml
from pennylane.logging import debug_logger, debug_logger_init
from pennylane.ops import _qutrit__channel__ops__ as channels
from pennylane.tape import QuantumScript, QuantumScriptOrBatch
from pennylane.transforms.core import TransformProgram
from pennylane.typing import Result, ResultBatch
from . import Device
from .default_qutrit import DefaultQutrit
from .execution_config import DefaultExecutionConfig, ExecutionConfig
from .modifiers import simulator_tracking, single_tape_support
from .preprocess import (
decompose,
no_sampling,
null_postprocessing,
validate_device_wires,
validate_measurements,
validate_observables,
)
from .qutrit_mixed.simulate import simulate
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
observables = {
"THermitian",
"GellMann",
}
[docs]def observable_stopping_condition(obs: qml.operation.Operator) -> bool:
"""Specifies whether an observable is accepted by DefaultQutritMixed."""
if isinstance(obs, qml.operation.Tensor):
return all(observable_stopping_condition(observable) for observable in obs.obs)
if obs.name in {"Prod", "Sum"}:
return all(observable_stopping_condition(observable) for observable in obs.operands)
if obs.name in {"LinearCombination", "Hamiltonian"}:
return all(observable_stopping_condition(observable) for observable in obs.terms()[1])
if obs.name == "SProd":
return observable_stopping_condition(obs.base)
return obs.name in observables
[docs]def stopping_condition(op: qml.operation.Operator) -> bool:
"""Specify whether an Operator object is supported by the device."""
expected_set = DefaultQutrit.operations | {"Snapshot"} | channels
return op.name in expected_set
[docs]def stopping_condition_shots(op: qml.operation.Operator) -> bool:
"""Specify whether an Operator object is supported by the device with shots."""
return stopping_condition(op)
[docs]def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool:
"""Specifies whether a measurement is accepted when sampling."""
return isinstance(m, qml.measurements.SampleMeasurement)
[docs]@qml.transform
def warn_readout_error_state(
tape: qml.tape.QuantumTape,
) -> tuple[Sequence[qml.tape.QuantumTape], Callable]:
"""If a measurement in the QNode is an analytic state or density_matrix, and a readout error
parameter is defined, warn that readout error will not be applied.
Args:
tape (QuantumTape, .QNode, Callable): a quantum circuit.
Returns:
qnode (pennylane.QNode) or quantum function (callable) or tuple[List[.QuantumTape], function]:
The unaltered input circuit.
"""
if not tape.shots:
for m in tape.measurements:
if isinstance(m, qml.measurements.StateMP):
warnings.warn(f"Measurement {m} is not affected by readout error.")
return (tape,), null_postprocessing
[docs]def get_readout_errors(readout_relaxation_probs, readout_misclassification_probs):
r"""Get the list of readout errors that should be applied to each measured wire.
Args:
readout_relaxation_probs (List[float]): Inputs for :class:`~.QutritAmplitudeDamping` channel
of the form :math:`[\gamma_{10}, \gamma_{20}, \gamma_{21}]`. This error models
amplitude damping associated with longer readout and varying relaxation times of
transmon-based qudits.
readout_misclassification_probs (List[float]): Inputs for :class:`~.TritFlip` channel
of the form :math:`[p_{01}, p_{02}, p_{12}]`. This error models misclassification events
in readout.
Returns:
readout_errors (List[Callable]): List of readout error channels that should be
applied to each measured wire.
"""
measure_funcs = []
if readout_relaxation_probs is not None:
try:
with qml.queuing.QueuingManager.stop_recording():
qml.QutritAmplitudeDamping(*readout_relaxation_probs, wires=0)
except Exception as e:
raise qml.DeviceError("Applying damping readout error results in error:\n" + str(e))
measure_funcs.append(partial(qml.QutritAmplitudeDamping, *readout_relaxation_probs))
if readout_misclassification_probs is not None:
try:
with qml.queuing.QueuingManager.stop_recording():
qml.TritFlip(*readout_misclassification_probs, wires=0)
except Exception as e:
raise qml.DeviceError("Applying trit flip readout error results in error:\n" + str(e))
measure_funcs.append(partial(qml.TritFlip, *readout_misclassification_probs))
return None if len(measure_funcs) == 0 else measure_funcs
[docs]@simulator_tracking
@single_tape_support
class DefaultQutritMixed(Device):
r"""A PennyLane Python-based device for mixed-state qutrit simulation.
Args:
wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that
contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings
(``['ancilla', 'q1', 'q2']``). Default ``None`` if not specified.
shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots
to use in executions involving this device.
seed (Union[str, None, int, array_like[int], SeedSequence, BitGenerator, Generator, jax.random.PRNGKey]): A
seed-like parameter matching that of ``seed`` for ``numpy.random.default_rng``, or
a request to seed from numpy's global random number generator.
The default, ``seed="global"`` pulls a seed from NumPy's global generator. ``seed=None``
will pull a seed from the OS entropy.
If a ``jax.random.PRNGKey`` is passed as the seed, a JAX-specific sampling function using
``jax.random.choice`` and the ``PRNGKey`` will be used for sampling rather than
``numpy.random.default_rng``.
readout_relaxation_probs (List[float]): Input probabilities for relaxation errors implemented
with the :class:`~.QutritAmplitudeDamping` channel. The input defines the
channel's parameters :math:`[\gamma_{10}, \gamma_{20}, \gamma_{21}]`.
readout_misclassification_probs (List[float]): Input probabilities for state readout
misclassification events implemented with the :class:`~.TritFlip` channel. The input defines the
channel's parameters :math:`[p_{01}, p_{02}, p_{12}]`.
**Example:**
.. code-block:: python
n_wires = 5
num_qscripts = 5
qscripts = []
for i in range(num_qscripts):
unitary = scipy.stats.unitary_group(dim=3**n_wires, seed=(42 + i)).rvs()
op = qml.QutritUnitary(unitary, wires=range(n_wires))
qs = qml.tape.QuantumScript([op], [qml.expval(qml.GellMann(0, 3))])
qscripts.append(qs)
>>> dev = DefaultQutritMixed()
>>> program, execution_config = dev.preprocess()
>>> new_batch, post_processing_fn = program(qscripts)
>>> results = dev.execute(new_batch, execution_config=execution_config)
>>> post_processing_fn(results)
[0.08015701503959313,
0.04521414211599359,
-0.0215232130089687,
0.062120285032425865,
-0.0635052317625]
This device currently supports backpropagation derivatives:
>>> from pennylane.devices import ExecutionConfig
>>> dev.supports_derivatives(ExecutionConfig(gradient_method="backprop"))
True
For example, we can use jax to jit computing the derivative:
.. code-block:: python
import jax
@jax.jit
def f(x):
qs = qml.tape.QuantumScript([qml.TRX(x, 0)], [qml.expval(qml.GellMann(0, 3))])
program, execution_config = dev.preprocess()
new_batch, post_processing_fn = program([qs])
results = dev.execute(new_batch, execution_config=execution_config)
return post_processing_fn(results)[0]
>>> f(jax.numpy.array(1.2))
DeviceArray(0.36235774, dtype=float32)
>>> jax.grad(f)(jax.numpy.array(1.2))
DeviceArray(-0.93203914, dtype=float32, weak_type=True)
.. details::
:title: Readout Error
``DefaultQutritMixed`` includes readout error support. Two input arguments control
the parameters of error channels applied to each measured wire of the state after
it has been diagonalized for measurement:
* ``readout_relaxation_probs``: Input parameters of a :class:`~.QutritAmplitudeDamping` channel.
This error models state relaxation error that occurs during readout of transmon-based qutrits.
The motivation for this readout error is described in [`1 <https://arxiv.org/abs/2003.03307>`_] (Sec II.A).
* ``readout_misclassification_probs``: Input parameters of a :class:`~.TritFlip` channel.
This error models misclassification events in readout. An example of this readout error
can be seen in [`2 <https://arxiv.org/abs/2309.11303>`_] (Fig 1a).
In the case that both parameters are defined, relaxation error is applied first then
misclassification error is applied.
.. note::
The readout errors will be applied to the state after it has been diagonalized for each
measurement. This may give different results depending on how the observable is defined.
This is because diagonalizing gates for the same observable may return eigenvalues in
different orders. For example, measuring :class:`~.THermitian` with a non-diagonal
GellMann matrix will result in a different measurement result then measuring the
equivalent :class:`~.GellMann` observable, as the THermitian eigenvalues are returned
in increasing order when explicitly diagonalized (i.e., ``[-1, 0, 1]``), while non-diagonal GellManns provided
in PennyLane have their eigenvalues hardcoded (i.e., ``[1, -1, 0]``).
.. details::
:title: Tracking
``DefaultQutritMixed`` tracks:
* ``executions``: the number of unique circuits that would be required on quantum hardware
* ``shots``: the number of shots
* ``resources``: the :class:`~.resource.Resources` for the executed circuit.
* ``simulations``: the number of simulations performed. One simulation can cover multiple QPU executions, such as for non-commuting measurements and batched parameters.
* ``batches``: The number of times :meth:`~.execute` is called.
* ``results``: The results of each call of :meth:`~.execute`
"""
_device_options = ("rng", "prng_key") # tuple of string names for all the device options.
@property
def name(self):
"""The name of the device."""
return "default.qutrit.mixed"
@debug_logger_init
def __init__( # pylint: disable=too-many-arguments
self,
wires=None,
shots=None,
seed="global",
readout_relaxation_probs=None,
readout_misclassification_probs=None,
) -> None:
super().__init__(wires=wires, shots=shots)
seed = np.random.randint(0, high=10000000) if seed == "global" else seed
if qml.math.get_interface(seed) == "jax":
self._prng_key = seed
self._rng = np.random.default_rng(None)
else:
self._prng_key = None
self._rng = np.random.default_rng(seed)
self._debugger = None
self.readout_errors = get_readout_errors(
readout_relaxation_probs, readout_misclassification_probs
)
[docs] @debug_logger
def supports_derivatives(
self,
execution_config: Optional[ExecutionConfig] = None,
circuit: Optional[QuantumScript] = None,
) -> bool:
"""Check whether or not derivatives are available for a given configuration and circuit.
``DefaultQutritMixed`` supports backpropagation derivatives with analytic results.
Args:
execution_config (ExecutionConfig): The configuration of the desired derivative calculation.
circuit (QuantumTape): An optional circuit to check derivatives support for.
Returns:
bool: Whether or not a derivative can be calculated provided the given information.
"""
if execution_config is None or execution_config.gradient_method in {"backprop", "best"}:
return circuit is None or not circuit.shots
return False
def _setup_execution_config(self, execution_config: ExecutionConfig) -> ExecutionConfig:
"""This is a private helper for ``preprocess`` that sets up the execution config.
Args:
execution_config (ExecutionConfig): an unprocessed execution config.
Returns:
ExecutionConfig: a preprocessed execution config.
"""
updated_values = {}
for option in execution_config.device_options:
if option not in self._device_options:
raise qml.DeviceError(f"device option {option} not present on {self}")
if execution_config.gradient_method == "best":
updated_values["gradient_method"] = "backprop"
updated_values["use_device_gradient"] = False
updated_values["grad_on_execution"] = False
updated_values["device_options"] = dict(execution_config.device_options) # copy
for option in self._device_options:
if option not in updated_values["device_options"]:
updated_values["device_options"][option] = getattr(self, f"_{option}")
return replace(execution_config, **updated_values)
[docs] @debug_logger
def preprocess(
self,
execution_config: ExecutionConfig = DefaultExecutionConfig,
) -> tuple[TransformProgram, ExecutionConfig]:
"""This function defines the device transform program to be applied and an updated device
configuration.
Args:
execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure
describing the parameters needed to fully describe the execution.
Returns:
TransformProgram, ExecutionConfig: A transform program that when called returns
``QuantumTape`` objects that the device can natively execute, as well as a postprocessing
function to be called after execution, and a configuration with unset
specifications filled in.
This device:
* Supports any qutrit operations that provide a matrix
* Supports any qutrit channel that provides Kraus matrices
"""
config = self._setup_execution_config(execution_config)
transform_program = TransformProgram()
transform_program.add_transform(validate_device_wires, self.wires, name=self.name)
transform_program.add_transform(
decompose,
stopping_condition=stopping_condition,
stopping_condition_shots=stopping_condition_shots,
name=self.name,
)
transform_program.add_transform(
validate_measurements, sample_measurements=accepted_sample_measurement, name=self.name
)
transform_program.add_transform(
validate_observables, stopping_condition=observable_stopping_condition, name=self.name
)
if config.gradient_method == "backprop":
transform_program.add_transform(no_sampling, name="backprop + default.qutrit")
if self.readout_errors is not None:
transform_program.add_transform(warn_readout_error_state)
return transform_program, config
[docs] @debug_logger
def execute(
self,
circuits: QuantumScriptOrBatch,
execution_config: ExecutionConfig = DefaultExecutionConfig,
) -> Union[Result, ResultBatch]:
interface = (
execution_config.interface
if execution_config.gradient_method in {"best", "backprop", None}
else None
)
return tuple(
simulate(
c,
rng=self._rng,
prng_key=self._prng_key,
debugger=self._debugger,
interface=interface,
readout_errors=self.readout_errors,
)
for c in circuits
)
_modules/pennylane/devices/default_qutrit_mixed
Download Python script
Download Notebook
View on GitHub