"""MBQC simulator.
Simulates MBQC by executing the pattern.
"""
from __future__ import annotations
import abc
import warnings
from typing import TYPE_CHECKING
import numpy as np
from graphix.clifford import Clifford
from graphix.command import BaseM, CommandKind, M, MeasureUpdate
from graphix.measurements import Measurement
from graphix.sim.base_backend import Backend
from graphix.sim.density_matrix import DensityMatrixBackend
from graphix.sim.statevec import StatevectorBackend
from graphix.sim.tensornet import TensorNetworkBackend
from graphix.states import BasicStates
if TYPE_CHECKING:
from graphix.pattern import Pattern
class MeasureMethod(abc.ABC):
"""Measure method used by the simulator, with default measurement method that implements MBQC.
To be overwritten by custom measurement methods in the case of delegated QC protocols.
Example: class `ClientMeasureMethod` in https://github.com/qat-inria/veriphix
"""
def measure(self, backend: Backend, cmd, noise_model=None) -> None:
"""Perform a measure."""
description = self.get_measurement_description(cmd)
result = backend.measure(cmd.node, description)
if noise_model is not None:
result = noise_model.confuse_result(result)
self.set_measure_result(cmd.node, result)
@abc.abstractmethod
def get_measurement_description(self, cmd: BaseM) -> Measurement:
"""Return the description of the measurement performed by a given measure command (possibly blind)."""
...
@abc.abstractmethod
def get_measure_result(self, node: int) -> bool:
"""Return the result of a previous measurement."""
...
@abc.abstractmethod
def set_measure_result(self, node: int, result: bool) -> None:
"""Store the result of a previous measurement."""
...
class DefaultMeasureMethod(MeasureMethod):
"""Default measurement method implementing standard measurement plane/angle update for MBQC."""
def __init__(self, results=None):
if results is None:
results = {}
self.results = results
def get_measurement_description(self, cmd: BaseM) -> Measurement:
"""Return the description of the measurement performed by a given measure command (cannot be blind in the case of DefaultMeasureMethod)."""
assert isinstance(cmd, M)
angle = cmd.angle * np.pi
# extract signals for adaptive angle
s_signal = sum(self.results[j] for j in cmd.s_domain)
t_signal = sum(self.results[j] for j in cmd.t_domain)
measure_update = MeasureUpdate.compute(cmd.plane, s_signal % 2 == 1, t_signal % 2 == 1, Clifford.I)
angle = angle * measure_update.coeff + measure_update.add_term
return Measurement(angle, measure_update.new_plane)
def get_measure_result(self, node: int) -> bool:
"""Return the result of a previous measurement."""
return self.results[node]
def set_measure_result(self, node: int, result: bool) -> None:
"""Store the result of a previous measurement."""
self.results[node] = result
[docs]
class PatternSimulator:
"""MBQC simulator.
Executes the measurement pattern.
"""
[docs]
def __init__(
self, pattern, backend="statevector", measure_method: MeasureMethod | None = None, noise_model=None, **kwargs
) -> None:
"""
Construct a pattern simulator.
Parameters
----------
pattern: :class:`graphix.pattern.Pattern` object
MBQC pattern to be simulated.
backend: :class:`graphix.sim.backend.Backend` object,
or 'statevector', or 'densitymatrix', or 'tensornetwork'
simulation backend (optional), default is 'statevector'.
noise_model:
kwargs: keyword args for specified backend.
.. seealso:: :class:`graphix.sim.statevec.StatevectorBackend`\
:class:`graphix.sim.tensornet.TensorNetworkBackend`\
:class:`graphix.sim.density_matrix.DensityMatrixBackend`\
"""
if isinstance(backend, Backend):
assert kwargs == {}
self.backend = backend
elif backend == "statevector":
self.backend = StatevectorBackend(**kwargs)
elif backend == "densitymatrix":
if noise_model is None:
self.noise_model = None
self.backend = DensityMatrixBackend(**kwargs)
warnings.warn(
"Simulating using densitymatrix backend with no noise. To add noise to the simulation, give an object of `graphix.noise_models.Noisemodel` to `noise_model` keyword argument.",
stacklevel=1,
)
else:
self.backend = DensityMatrixBackend(pr_calc=True, **kwargs)
self.set_noise_model(noise_model)
elif backend in {"tensornetwork", "mps"} and noise_model is None:
self.noise_model = None
self.backend = TensorNetworkBackend(pattern, **kwargs)
else:
raise ValueError("Unknown backend.")
self.set_noise_model(noise_model)
self.__pattern = pattern
if measure_method is None:
measure_method = DefaultMeasureMethod(pattern.results)
self.__measure_method = measure_method
@property
def pattern(self) -> Pattern:
"""Return the pattern."""
return self.__pattern
@property
def measure_method(self) -> MeasureMethod:
"""Return the measure method."""
return self.__measure_method
def set_noise_model(self, model):
"""Set a noise model."""
if not isinstance(self.backend, DensityMatrixBackend) and model is not None:
self.noise_model = None # if not initialized yet
raise ValueError(f"The backend {self.backend} doesn't support noise but noisemodel was provided.")
self.noise_model = model
[docs]
def run(self, input_state=BasicStates.PLUS) -> None:
"""Perform the simulation.
Returns
-------
state :
the output quantum state,
in the representation depending on the backend used.
"""
if input_state is not None:
self.backend.add_nodes(self.pattern.input_nodes, input_state)
if self.noise_model is None:
for cmd in self.pattern:
if cmd.kind == CommandKind.N:
self.backend.add_nodes(nodes=[cmd.node], data=cmd.state)
elif cmd.kind == CommandKind.E:
self.backend.entangle_nodes(edge=cmd.nodes)
elif cmd.kind == CommandKind.M:
self.__measure_method.measure(self.backend, cmd)
elif cmd.kind in {CommandKind.X, CommandKind.Z}:
self.backend.correct_byproduct(cmd, self.__measure_method)
elif cmd.kind == CommandKind.C:
self.backend.apply_clifford(cmd.node, cmd.clifford)
else:
raise ValueError("invalid commands")
self.backend.finalize(output_nodes=self.pattern.output_nodes)
else:
self.noise_model.assign_simulator(self)
for node in self.pattern.input_nodes:
self.backend.apply_channel(self.noise_model.prepare_qubit(), [node])
for cmd in self.pattern:
if cmd.kind == CommandKind.N:
self.backend.add_nodes([cmd.node])
self.backend.apply_channel(self.noise_model.prepare_qubit(), [cmd.node])
elif cmd.kind == CommandKind.E:
self.backend.entangle_nodes(cmd.nodes)
self.backend.apply_channel(self.noise_model.entangle(), cmd.nodes)
elif cmd.kind == CommandKind.M:
self.backend.apply_channel(self.noise_model.measure(), [cmd.node])
self.__measure_method.measure(self.backend, cmd, noise_model=self.noise_model)
elif cmd.kind == CommandKind.X:
self.backend.correct_byproduct(cmd, self.__measure_method)
if np.mod(sum([self.__measure_method.results[j] for j in cmd.domain]), 2) == 1:
self.backend.apply_channel(self.noise_model.byproduct_x(), [cmd.node])
elif cmd.kind == CommandKind.Z:
self.backend.correct_byproduct(cmd, self.__measure_method)
if np.mod(sum([self.__measure_method.results[j] for j in cmd.domain]), 2) == 1:
self.backend.apply_channel(self.noise_model.byproduct_z(), [cmd.node])
elif cmd.kind == CommandKind.C:
self.backend.apply_clifford(cmd.node, cmd.clifford)
self.backend.apply_channel(self.noise_model.clifford(), [cmd.node])
elif cmd.kind == CommandKind.T:
# T command is a flag for one clock cycle in simulated experiment,
# to be added via hardware-agnostic pattern modifier
self.noise_model.tick_clock()
else:
raise ValueError("Invalid commands.")
self.backend.finalize(self.pattern.output_nodes)