diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f40251f..e92469f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Empty outputs layer removed from flow's partial order. - Flow well-formedness check does not trigger false negative for flows on open graphs without outputs. +- #569: Keyword arguments for `simulate_pattern` are now type-checked. + ### Changed - #452: Use `uv` for dependency management diff --git a/docs/source/simulator.rst b/docs/source/simulator.rst index 821a1be48..93353c438 100644 --- a/docs/source/simulator.rst +++ b/docs/source/simulator.rst @@ -6,11 +6,27 @@ Pattern Simulation .. currentmodule:: graphix.simulator -.. autoclass:: PatternSimulator +.. autoclass:: PrepareMethod + :members: + +.. autoclass:: DefaultPrepareMethod + :members: + +.. autoclass:: MeasureMethod + :members: + +.. autoclass:: DefaultMeasureMethod + :members: + +.. autoclass:: SimulatorKwargs + :members: - .. automethod:: __init__ +.. autoclass:: SimulatorOptions + :members: + +.. autoclass:: PatternSimulator + :members: - .. automethod:: run Simulator backends ++++++++++++++++++ diff --git a/graphix/pattern.py b/graphix/pattern.py index ddfebcf3d..1d8c9c558 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -36,7 +36,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Collection, Container, Iterator - from typing import Any, TypeVar + from typing import TypeVar from numpy.random import Generator @@ -51,7 +51,7 @@ from graphix.sim import Backend, Data, DensityMatrixBackend, StatevectorBackend from graphix.sim.base_backend import _StateT_co from graphix.sim.tensornet import TensorNetworkBackend - from graphix.simulator import _BackendLiteral + from graphix.simulator import SimulatorKwargs, _BackendLiteral from graphix.space_minimization import SpaceMinimizationHeuristic from graphix.states import State from graphix.visualization import DrawKwargs @@ -1408,7 +1408,7 @@ def simulate_pattern( | Iterable[Iterable[ExpressionOrSupportsComplex]] | None = ..., rng: Generator | None = ..., - **kwargs: Any, + **kwargs: Unpack[SimulatorKwargs], ) -> Statevec: ... @overload @@ -1422,7 +1422,7 @@ def simulate_pattern( | Iterable[Iterable[ExpressionOrSupportsComplex]] | None = ..., rng: Generator | None = ..., - **kwargs: Any, + **kwargs: Unpack[SimulatorKwargs], ) -> DensityMatrix: ... @overload @@ -1435,7 +1435,7 @@ def simulate_pattern( | Iterable[Iterable[ExpressionOrSupportsComplex]] | None = ..., rng: Generator | None = ..., - **kwargs: Any, + **kwargs: Unpack[SimulatorKwargs], ) -> MBQCTensorNet: ... @overload @@ -1444,7 +1444,7 @@ def simulate_pattern( backend: Backend[_StateT_co], input_state: Data | None = ..., rng: Generator | None = ..., - **kwargs: Any, + **kwargs: Unpack[SimulatorKwargs], ) -> _StateT_co: ... def simulate_pattern( @@ -1454,7 +1454,7 @@ def simulate_pattern( rng: Generator | None = None, *, stacklevel: int = 1, - **kwargs: Any, + **kwargs: Unpack[SimulatorKwargs], ) -> _StateT_co | _BuiltinBackendState: """Simulate the execution of the pattern by using :class:`graphix.simulator.PatternSimulator`. @@ -1474,7 +1474,8 @@ def simulate_pattern( stacklevel : int, optional Stack level to use for warnings. Defaults to 1, meaning that warnings are reported at this function's call site. - kwargs: keyword args for specified backend. + kwargs: Unpack[SimulatorKwargs] + Options controlling simulator. See :class:`SimulatorOptions`. Returns ------- diff --git a/graphix/simulator.py b/graphix/simulator.py index 50410d1e2..70b64a530 100644 --- a/graphix/simulator.py +++ b/graphix/simulator.py @@ -9,7 +9,8 @@ import abc import logging import warnings -from typing import TYPE_CHECKING, Generic, Literal, TypeVar, overload +from dataclasses import dataclass +from typing import TYPE_CHECKING, Generic, Literal, TypedDict, TypeVar, overload # assert_never introduced in Python 3.11 # override introduced in Python 3.12 @@ -32,6 +33,9 @@ from numpy.random import Generator + # Unpack introduced in Python 3.12 + from typing_extensions import Unpack + from graphix.command import BaseN from graphix.measurements import Measurement, Outcome from graphix.noise_models.noise_model import CommandOrNoise, NoiseModel @@ -56,11 +60,11 @@ class PrepareMethod(abc.ABC): """Prepare method used by the simulator. - See `DefaultPrepareMethod` for the default prepare method that implements MBQC. + See :class:`DefaultPrepareMethod` for the default prepare method that implements MBQC. To be overwritten by custom preparation methods in the case of delegated QC protocols. - Example: class `ClientPrepareMethod` in https://github.com/qat-inria/veriphix + Example: class ``ClientPrepareMethod`` in https://github.com/qat-inria/veriphix """ @abc.abstractmethod @@ -84,7 +88,7 @@ class MeasureMethod(abc.ABC): To be overwritten by custom measurement methods in the case of delegated QC protocols. - Example: class `ClientMeasureMethod` in https://github.com/qat-inria/veriphix + Example: class ``ClientMeasureMethod`` in https://github.com/qat-inria/veriphix """ def measure( @@ -192,8 +196,8 @@ def __init__(self, results: Mapping[int, Outcome] | None = None): Notes ----- If a mapping is provided, it is treated as read-only. Measurements - performed during simulation are stored in `self.results`, which is a copy - of the given mapping. The original `results` mapping is not modified. + performed during simulation are stored in ``self.results``, which is a copy + of the given mapping. The original ``results`` mapping is not modified. """ # results is coerced into dict, since `store_measurement_outcome` mutates it. self.results = {} if results is None else dict(results) @@ -253,6 +257,48 @@ def store_measurement_outcome(self, node: int, result: Outcome) -> None: self.results[node] = result +class SimulatorKwargs(TypedDict, total=False): + """Common keyword arguments for simulator. + + The keys correspond to the fields of :class:`SimulatorOptions`. + """ + + prepare_method: PrepareMethod | None + measure_method: MeasureMethod | None + noise_model: NoiseModel | None + branch_selector: BranchSelector | None + graph_prep: str | None + symbolic: bool + + +@dataclass(frozen=True) +class SimulatorOptions: + """Options controlling simulator. + + Parameters + ---------- + prepare_method: :class:`PrepareMethod`, optional + Prepare method used by the simulator. Default is :class:`DefaultPrepareMethod`. + measure_method: :class:`MeasureMethod`, optional + Measure method used by the simulator. Default is :class:`DefaultMeasureMethod`. + noise_model: :class:`NoiseModel`, optional + [Density matrix backend only] Noise model used by the simulator. + branch_selector: :class:`BranchSelector`, optional + Branch selector used for measurements. Can only be specified if ``backend`` is not an already instantiated :class:`Backend` object. Default is :class:`RandomBranchSelector`. + graph_prep: str, optional + [Tensor network backend only] Strategy for preparing the graph state. See :class:`TensorNetworkBackend`. + symbolic : bool, optional + [State vector and density matrix backends only] If True, support arbitrary objects (typically, symbolic expressions) in measurement angles. + """ + + prepare_method: PrepareMethod | None = None + measure_method: MeasureMethod | None = None + noise_model: NoiseModel | None = None + branch_selector: BranchSelector | None = None + graph_prep: str | None = None + symbolic: bool = False + + class PatternSimulator(Generic[_StateT_co]): """MBQC simulator. @@ -267,14 +313,9 @@ def __init__( self: PatternSimulator[Statevec], pattern: Pattern, backend: Literal["statevector"] = ..., - prepare_method: PrepareMethod | None = None, - measure_method: MeasureMethod | None = None, - noise_model: NoiseModel | None = None, - branch_selector: BranchSelector | None = None, - graph_prep: str | None = None, - symbolic: bool = False, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> None: ... @overload @@ -282,14 +323,9 @@ def __init__( self: PatternSimulator[DensityMatrix], pattern: Pattern, backend: Literal["densitymatrix"] = ..., - prepare_method: PrepareMethod | None = None, - measure_method: MeasureMethod | None = None, - noise_model: NoiseModel | None = None, - branch_selector: BranchSelector | None = None, - graph_prep: str | None = None, - symbolic: bool = False, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> None: ... @overload @@ -297,14 +333,9 @@ def __init__( self: PatternSimulator[MBQCTensorNet], pattern: Pattern, backend: Literal["tensornetwork", "mps"] = ..., - prepare_method: PrepareMethod | None = None, - measure_method: MeasureMethod | None = None, - noise_model: NoiseModel | None = None, - branch_selector: BranchSelector | None = None, - graph_prep: str | None = None, - symbolic: bool = False, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> None: ... @overload @@ -312,28 +343,18 @@ def __init__( self: PatternSimulator[_StateT], pattern: Pattern, backend: Backend[_StateT] = ..., - prepare_method: PrepareMethod | None = None, - measure_method: MeasureMethod | None = None, - noise_model: NoiseModel | None = None, - branch_selector: BranchSelector | None = None, - graph_prep: str | None = None, - symbolic: bool = False, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> None: ... def __init__( self: PatternSimulator[_StateT | _BuiltinBackendState], pattern: Pattern, backend: Backend[_StateT] | _BackendLiteral = "statevector", - prepare_method: PrepareMethod | None = None, - measure_method: MeasureMethod | None = None, - noise_model: NoiseModel | None = None, - branch_selector: BranchSelector | None = None, - graph_prep: str | None = None, - symbolic: bool = False, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> None: """ Construct a pattern simulator. @@ -345,37 +366,22 @@ def __init__( backend : :class:`Backend` or {'statevector', 'densitymatrix', 'tensornetwork'}, optional The simulator backend to use: either an instantiated backend or the name of a built-in backend. Default: ``'statevector'``. - prepare_method: :class:`PrepareMethod`, optional - Prepare method used by the simulator. Default is :class:`DefaultPrepareMethod`. - measure_method: :class:`MeasureMethod`, optional - Measure method used by the simulator. Default is :class:`DefaultMeasureMethod`. - noise_model: :class:`NoiseModel`, optional - [Density matrix backend only] Noise model used by the simulator. - branch_selector: :class:`BranchSelector`, optional - Branch selector used for measurements. Can only be specified if ``backend`` is not an already instantiated :class:`Backend` object. Default is :class:`RandomBranchSelector`. - graph_prep: str, optional - [Tensor network backend only] Strategy for preparing the graph state. See :class:`TensorNetworkBackend`. - symbolic : bool, optional - [State vector and density matrix backends only] If True, support arbitrary objects (typically, symbolic expressions) in measurement angles. stacklevel : int, optional Stack level to use for warnings. Defaults to 1, meaning that warnings are reported at this function's call site. + kwargs: Unpack[SimulatorKwargs] + Options controlling simulator. See :class:`SimulatorOptions`. .. seealso:: :class:`graphix.sim.statevec.StatevectorBackend`\ :class:`graphix.sim.tensornet.TensorNetworkBackend`\ :class:`graphix.sim.density_matrix.DensityMatrixBackend`\ """ - self.backend = _initialize_backend( - pattern, backend, noise_model, branch_selector, graph_prep, symbolic, stacklevel=stacklevel + 1 - ) - self.noise_model = noise_model + options = SimulatorOptions(**kwargs) + self.backend = _initialize_backend(pattern, backend, stacklevel=stacklevel + 1, **kwargs) + self.noise_model = options.noise_model self.__pattern = pattern - if prepare_method is None: - prepare_method = DefaultPrepareMethod() - self.__prepare_method = prepare_method - if measure_method is None: - measure_method = DefaultMeasureMethod() - self.__measure_method = measure_method + self.__prepare_method = options.prepare_method or DefaultPrepareMethod() + self.__measure_method = options.measure_method or DefaultMeasureMethod() @property def pattern(self) -> Pattern: @@ -477,12 +483,9 @@ def run( def _initialize_backend( pattern: Pattern, backend: StatevectorBackend | Literal["statevector"], - noise_model: NoiseModel | None, - branch_selector: BranchSelector | None, - graph_prep: str | None, - symbolic: bool, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> StatevectorBackend: ... @@ -490,12 +493,9 @@ def _initialize_backend( def _initialize_backend( pattern: Pattern, backend: DensityMatrixBackend | Literal["densitymatrix"], - noise_model: NoiseModel | None, - branch_selector: BranchSelector | None, - graph_prep: str | None, - symbolic: bool, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> DensityMatrixBackend: ... @@ -503,12 +503,9 @@ def _initialize_backend( def _initialize_backend( pattern: Pattern, backend: TensorNetworkBackend | Literal["tensornetwork", "mps"], - noise_model: NoiseModel | None, - branch_selector: BranchSelector | None, - graph_prep: str | None, - symbolic: bool, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> TensorNetworkBackend: ... @@ -516,24 +513,18 @@ def _initialize_backend( def _initialize_backend( pattern: Pattern, backend: Backend[_StateT_co], - noise_model: NoiseModel | None, - branch_selector: BranchSelector | None, - graph_prep: str | None, - symbolic: bool, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> Backend[_StateT_co]: ... def _initialize_backend( pattern: Pattern, backend: Backend[_StateT_co] | _BackendLiteral, - noise_model: NoiseModel | None, - branch_selector: BranchSelector | None, - graph_prep: str | None, - symbolic: bool, *, stacklevel: int = 1, + **kwargs: Unpack[SimulatorKwargs], ) -> _BuiltinBackend | Backend[_StateT_co]: """ Initialize the backend. @@ -543,51 +534,47 @@ def _initialize_backend( backend: :class:`Backend` object, 'statevector', or 'densitymatrix', or 'tensornetwork' simulation backend (optional), default is 'statevector'. - noise_model: :class:`NoiseModel`, optional - [Density matrix backend only] Noise model used by the simulator. - branch_selector: :class:`BranchSelector`, optional - Branch selector used for measurements. Can only be specified if ``backend`` is not an already instantiated :class:`Backend` object. Default is :class:`RandomBranchSelector`. - graph_prep: str, optional - [Tensor network backend only] Strategy for preparing the graph state. See :class:`TensorNetworkBackend`. - symbolic : bool, optional - [State vector and density matrix backends only] If True, support arbitrary objects (typically, symbolic expressions) in measurement angles. + stacklevel : int, optional + Stack level to use for warnings. Defaults to 1, meaning that warnings + are reported at this function's call site. + kwargs: Unpack[SimulatorKwargs] + Options controlling simulator. See :class:`SimulatorOptions`. Returns ------- :class:`Backend` matching the appropriate backend """ + options = SimulatorOptions(**kwargs) if isinstance(backend, Backend): - if branch_selector is not None: + if options.branch_selector is not None: raise ValueError("`branch_selector` cannot be specified if `backend` is already instantiated.") - if graph_prep is not None: + if options.graph_prep is not None: raise ValueError("`graph_prep` cannot be specified if `backend` is already instantiated.") - if symbolic: + if options.symbolic: raise ValueError("`symbolic` cannot be specified if `backend` is already instantiated.") return backend - if branch_selector is None: - branch_selector = RandomBranchSelector() + branch_selector = RandomBranchSelector() if options.branch_selector is None else options.branch_selector if backend in {"tensornetwork", "mps"}: - if noise_model is not None: + if options.noise_model is not None: raise ValueError("`noise_model` cannot be specified for tensor network backend.") - if symbolic: + if options.symbolic: raise ValueError("`symbolic` cannot be specified for tensor network backend.") - if graph_prep is None: - graph_prep = "auto" + graph_prep = "auto" if options.graph_prep is None else options.graph_prep return TensorNetworkBackend(pattern, branch_selector=branch_selector, graph_prep=graph_prep) - if graph_prep is not None: + if options.graph_prep is not None: raise ValueError("`graph_prep` can only be specified for tensor network backend.") match backend: case "statevector": - if noise_model is not None: + if options.noise_model is not None: raise ValueError("`noise_model` cannot be specified for state vector backend.") - return StatevectorBackend(branch_selector=branch_selector, symbolic=symbolic) + return StatevectorBackend(branch_selector=branch_selector, symbolic=options.symbolic) case "densitymatrix": - if noise_model is None: + if options.noise_model is None: 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=stacklevel + 1, ) - return DensityMatrixBackend(branch_selector=branch_selector, symbolic=symbolic) + return DensityMatrixBackend(branch_selector=branch_selector, symbolic=options.symbolic) case _: raise ValueError(f"Unknown backend {backend}.") diff --git a/tests/test_simulator.py b/tests/test_simulator.py index 6c9ecbb81..54e42e195 100644 --- a/tests/test_simulator.py +++ b/tests/test_simulator.py @@ -1,14 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from graphix import BasicStates, Pattern, Statevec, StatevectorBackend +from graphix.command import BaseN if TYPE_CHECKING: from numpy.random import Generator + from graphix.command import CommandType + def test_no_explicit_input_state(hadamardpattern: Pattern, fx_rng: Generator) -> None: # No explicit input state: the default initial state is |+⟩. @@ -58,3 +61,15 @@ def test_node_index_after_finalize() -> None: backend = StatevectorBackend() pattern.simulate_pattern(backend=backend) assert list(backend.node_index) == [1, 0] + + +def test_default_prepare_method_requires_n() -> None: + # The type annotations require the pattern to contain only + # elements of type `CommandType`, which excludes `BaseN`. We hope + # pattern types will become more precise in the near future. + # See https://github.com/TeamGraphix/graphix/issues/266 + pattern = Pattern(cmds=[cast("CommandType", BaseN(0))]) + with pytest.raises( + TypeError, match=r"The default prepare method requires all preparation commands to be of type `N`." + ): + pattern.simulate_pattern()