Source code for pennylane.devices.reference_qubit
# 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.
"""
Contains the ReferenceQubit device, a minimal device that can be used for testing
and plugin development purposes.
"""
import numpy as np
from pennylane import math
from pennylane.operation import Operator
from pennylane.tape import QuantumScript
from pennylane.transforms import (
broadcast_expand,
defer_measurements,
diagonalize_measurements,
split_non_commuting,
)
from pennylane.transforms.core import CompilePipeline
from pennylane.typing import Result
from .device_api import Device
from .execution_config import ExecutionConfig
from .modifiers import simulator_tracking, single_tape_support
from .preprocess import (
decompose,
measurements_from_samples,
validate_device_wires,
validate_measurements,
)
[docs]
def sample_state(state: np.ndarray, shots: int, seed=None):
"""Generate samples from the provided state and number of shots."""
probs = np.imag(state) ** 2 + np.real(state) ** 2
basis_states = np.arange(len(probs))
num_wires = int(np.log2(len(probs)))
rng = np.random.default_rng(seed)
probs /= np.sum(probs) # Fix: Normalize to prevent sum ≠ 1 errors in NumPy 2.0+
basis_samples = rng.choice(basis_states, shots, p=probs)
# convert basis state integers to array of booleans
bin_strings = (format(s, f"0{num_wires}b") for s in basis_samples)
return np.array([[int(val) for val in s] for s in bin_strings])
[docs]
def simulate(tape: QuantumScript, seed=None) -> Result:
"""Simulate a tape and turn it into results.
Args:
tape (.QuantumTape): a representation of a circuit
seed (Any): A seed to use to control the generation of samples.
"""
# 1) create the initial state
state = np.zeros(2 ** len(tape.wires))
state[0] = 1.0
# 2) apply all the operations
for op in tape.operations:
op_mat = op.matrix(wire_order=tape.wires)
if math.get_interface(op_mat) != "numpy":
raise ValueError("Reference qubit can only work with numpy data.")
state = math.matmul(op_mat, state)
# 3) perform measurements
# note that shots are pulled from the tape, not from the device
if tape.shots:
samples = sample_state(state, shots=tape.shots.total_shots, seed=seed)
# Shot vector support
results = []
for lower, upper in tape.shots.bins():
sub_samples = samples[lower:upper]
results.append(
tuple(mp.process_samples(sub_samples, tape.wires) for mp in tape.measurements)
)
if len(tape.measurements) == 1:
results = [res[0] for res in results]
if not tape.shots.has_partitioned_shots:
results = results[0]
else:
results = tuple(results)
else:
results = tuple(mp.process_state(state, tape.wires) for mp in tape.measurements)
if len(tape.measurements) == 1:
results = results[0]
return results
operations = frozenset(
{"PauliX", "PauliY", "PauliZ", "Hadamard", "CNOT", "CZ", "RX", "RY", "RZ", "GlobalPhase"}
)
[docs]
def supports_operation(op: Operator) -> bool:
"""This function used by preprocessing determines what operations
are natively supported by the device.
While in theory ``simulate`` can support any operation with a matrix, we limit the target
gate set for improved testing and reference purposes.
"""
return getattr(op, "name", None) in operations
[docs]
@simulator_tracking # update device.tracker with some relevant information
@single_tape_support # add support for device.execute(tape) in addition to device.execute((tape,))
class ReferenceQubit(Device):
"""A slimmed down numpy-based simulator for reference and testing purposes.
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
(``['aux', 'q1', 'q2']``). Default ``None`` if not specified. While this device allows
for ``wires`` to be unspecified at construction time, other devices may make this argument
mandatory. Devices can also implement additional restrictions on the possible wires.
shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots
to use in executions involving this device. Note that during execution, shots
are pulled from the circuit, not from the 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``. This is an optional
keyword argument added to follow recommend NumPy best practices. Other devices do not need
this parameter if it does not make sense for them.
"""
name = "reference.qubit"
def __init__(self, wires=None, shots=None, seed=None):
super().__init__(wires=wires, shots=shots)
# seed and rng not necessary for a device, but part of recommended
# numpy practices to use a local random number generator
self._rng = np.random.default_rng(seed)
[docs]
def preprocess(self, execution_config: ExecutionConfig | None = None):
if execution_config is None:
execution_config = ExecutionConfig()
# Here we convert an arbitrary tape into one natively supported by the device
program = CompilePipeline()
program.add_transform(validate_device_wires, wires=self.wires, name="reference.qubit")
program.add_transform(defer_measurements, allow_postselect=False)
program.add_transform(split_non_commuting)
program.add_transform(diagonalize_measurements)
program.add_transform(measurements_from_samples)
program.add_transform(
decompose,
stopping_condition=supports_operation,
skip_initial_state_prep=False,
name="reference.qubit",
)
program.add_transform(validate_measurements, name="reference.qubit")
program.add_transform(broadcast_expand)
# no need to preprocess the config as the device does not support derivatives
return program, execution_config
[docs]
def execute(self, circuits, execution_config: ExecutionConfig | None = None):
for tape in circuits:
assert all(supports_operation(op) for op in tape.operations)
return tuple(simulate(tape, seed=self._rng) for tape in circuits)
_modules/pennylane/devices/reference_qubit
Download Python script
Download Notebook
View on GitHub