diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00aaf44..147ee4d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,8 +16,7 @@ repos: rev: "v0.6.2" hooks: - id: ruff - args: - - --fix + args: [--fix, --show-fixes, --show-files] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.11.0 @@ -29,3 +28,6 @@ repos: rev: v0.7.3 hooks: - id: pydocstringformatter + args: + - --no-final-period + - --no-split-summary-body diff --git a/pyproject.toml b/pyproject.toml index 28e4224..07617d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,14 +58,15 @@ dependencies = [ "isort", "ruff", "pydocstringformatter", + "qadence2-expressions" ] [tool.hatch.envs.default.scripts] -test = "pytest -n auto --cov-config=pyproject.toml --ignore=./tests/test_examples.py {args}" +test = "pytest -n auto --cov-config=pyproject.toml --cov=qadence2_platforms {args}" [tool.pytest.ini_options] testpaths = ["tests"] -addopts = """-vvv --cov-report=term-missing --cov-config=pyproject.toml --cov=template_python --cov=tests""" +addopts = """-vvv --cov-config=pyproject.toml --cov=qadence2_platforms""" xfail_strict = true filterwarnings = [ "ignore:Call to deprecated create function FieldDescriptor", @@ -113,12 +114,9 @@ packages = ["qadence2_platforms"] [tool.coverage.run] branch = true parallel = true -# uncomment to omit any file from the -# coverage. Regexps can be used -# to select all files from a folder -#omit = [ -# "template_python/to_omit.py", -#] +omit = [ + "*/utils/templates/*" +] [tool.coverage.report] exclude_lines = [ diff --git a/qadence2_platforms/__init__.py b/qadence2_platforms/__init__.py index 434b564..d7ea294 100644 --- a/qadence2_platforms/__init__.py +++ b/qadence2_platforms/__init__.py @@ -1,7 +1,11 @@ from __future__ import annotations +from torch import float64, set_default_dtype + from .abstracts import AbstractInterface +set_default_dtype(float64) + PACKAGE_NAME = __name__ BACKEND_FOLDER_NAME = "backends" USER_BACKENDS_FOLDER_NAME = "user_backends" diff --git a/qadence2_platforms/abstracts.py b/qadence2_platforms/abstracts.py index 46b108f..b25d357 100644 --- a/qadence2_platforms/abstracts.py +++ b/qadence2_platforms/abstracts.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from enum import Enum, auto -from typing import Any, Callable, Generic, Optional, TypeVar +from typing import Any, Generic, Iterable, TypeVar ArrayType = TypeVar("ArrayType") SequenceType = TypeVar("SequenceType") @@ -78,6 +78,16 @@ def sequence(self) -> SequenceType: """ pass + @abstractmethod + def parameters(self) -> Iterable[Any]: + """ + Get the parameters from the backend as an iterable. + + Returns: + Parameters as an iterable. + """ + pass + @abstractmethod def set_parameters(self, params: dict[str, ParameterType]) -> None: """ @@ -90,9 +100,7 @@ def set_parameters(self, params: dict[str, ParameterType]) -> None: @abstractmethod def run( self, - *, values: dict[str, ArrayType] | None = None, - callback: Optional[Callable] = None, **kwargs: Any, ) -> RunResultType: """ @@ -111,10 +119,8 @@ def run( @abstractmethod def sample( self, - *, values: dict[str, ArrayType] | None = None, shots: int | None = None, - callback: Optional[Callable] = None, **kwargs: Any, ) -> SampleResultType: """ @@ -134,10 +140,8 @@ def sample( @abstractmethod def expectation( self, - *, values: dict[str, ArrayType] | None = None, observable: Any | None = None, - callback: Optional[Callable] = None, **kwargs: Any, ) -> ExpectationResultType: """ diff --git a/qadence2_platforms/backends/fresnel1/functions.py b/qadence2_platforms/backends/fresnel1/functions.py index 78085f5..d5ff785 100644 --- a/qadence2_platforms/backends/fresnel1/functions.py +++ b/qadence2_platforms/backends/fresnel1/functions.py @@ -1,13 +1,17 @@ from __future__ import annotations from enum import Enum, auto -from typing import Any +from functools import reduce +from typing import Any, Iterable, cast import numpy as np +import qutip from pulser.parametrized.variable import VariableItem from pulser.sequence import Sequence from pulser.waveforms import ConstantWaveform +from qadence2_platforms.backends.utils import InputType, Support + DEFAULT_AMPLITUDE = 4 * np.pi DEFAULT_DETUNING = 10 * np.pi @@ -86,15 +90,17 @@ def x(sequence: Sequence, **_: Any) -> None: def h( sequence: Sequence, - duration: VariableItem | float = 1000.0, + duration: VariableItem | float = np.pi, + support_list: str = "global", **_: Any, ) -> None: - support_list = "GLOBAL" amplitude = sequence.device.channels["rydberg_global"].max_amp or DEFAULT_AMPLITUDE duration *= 1000 * 2 * np.pi / amplitude - duration = int(duration) if duration > 16 else 16 + detuning = np.pi - sequence.enable_eom_mode("global", amp_on=amplitude, correct_phase_drift=True) + sequence.enable_eom_mode( + "global", amp_on=amplitude, correct_phase_drift=True, detuning_on=detuning + ) sequence.add_eom_pulse( "global", duration=int(duration), phase=np.pi / 2, post_phase_shift=np.pi ) @@ -186,3 +192,186 @@ def local_pulse_core( "dmm_0", "no-delay" if concurrent else "min-delay", ) + + +def parse_native_observables( + num_qubits: int, observable: list[InputType] | InputType +) -> list[qutip.Qobj]: + return QuTiPObservablesParser.build(num_qubits, observable) + + +class QuTiPObservablesParser: + """ + Convert InputType object to Qutip native quantum objects for simulation on QuTiP. + + It is intended to be used on expectation method of the Fresnel1 interface class. + InputType can be qadence2-expressions expression or any other module with the same + methods. + """ + + operators_mapping = { + "I": qutip.qeye(2), + "Z": qutip.sigmaz(), + } + + @classmethod + def _compl_tensor_op(cls, num_qubits: int, expr: InputType) -> qutip.Qobj: + """ + Use it for pure kron operations or for single input operator that needs to match + a bigger Hilbert space, e.g. `expr = Z(0)` but the number of qubits is 3. + + Args: + num_qubits (int): the number of qubits to create the qutip object to + expr (InputType): the input expression. Any qadence2-expressions expression + compatible object, with the same methods + + Returns: + A QuTiP object with the Hilbert space compatible with `num_qubits` + """ + + op: qutip.Qobj + arg: InputType + + if expr.subspace: + native_ops: list[qutip.Qobj] = [] + support_set: set = expr.subspace.subspace + + for k in range(num_qubits): + if k in support_set: + sub_num_qubits: int = cast(Support, expr.args[1]).max_index + arg = cast(InputType, expr.args[0]) + op = cls._get_op(sub_num_qubits, arg) + + else: + op = cls.operators_mapping["I"] + + native_ops.append(op) + + return qutip.tensor(*native_ops) + + arg = cast(InputType, expr.args[0]) + op = cls._get_op(num_qubits, arg) + return qutip.tensor(*([op] * num_qubits)) + + @classmethod + def _arith_tensor_op(cls, num_qubits: int, expr: InputType) -> qutip.Qobj: + """ + Use it for the arithmetic operations addition and multiplication that need to + have an extended Hilbert space compatible with `num_qubits`. + + Args: + num_qubits (int): the number of qubits to create the qutip object to + expr (InputType): the input expression. Any qadence2-expressions expression + compatible object, with the same methods + + Returns: + A QuTiP object with the Hilbert space compatible with `num_qubits` + """ + + subspace: set = cast(Support, expr.subspace).subspace + super_space: set = set(range(num_qubits)) + + if super_space.issuperset(subspace): + native_ops: list[qutip.Qobj] = [] + arg_subspace: set = cast(Support, expr.args[1]).subspace + + for k in range(num_qubits): + if k in arg_subspace: + sub_num_qubits: int = cast(int, expr.args[1]) + arg: InputType = cast(InputType, expr.args[0]) + op: qutip.Qobj = cls._get_op(sub_num_qubits, arg) + native_ops.append(op) + else: + native_ops.append(cls.operators_mapping["I"]) + + return qutip.tensor(*native_ops) + + raise ValueError( + f"subspace of the object ({subspace}) is bigger than the " + f"sequence space ({super_space})" + ) + + @classmethod + def _get_op(cls, num_qubits: int, op: InputType) -> qutip.Qobj | None: + """ + Convert an expression into a native QuTiP object. A simple symbol, + a quantum operator, and an operation (addition, multiplication or + kron tensor) are valid objects. + + Args: + num_qubits (int): the number of qubits to create the qutip object to + op (InputType): the input expression. Any qadence2-expressions expression + compatible object, with the same methods + + Returns: + A QuTiP object with the Hilbert space compatible with `num_qubits` + """ + + if op.is_symbol is True: + symbol: str = cast(str, op.args[0]) + return cls.operators_mapping[symbol] + + op_arg: qutip.Qobj + + if op.is_quantum_operator is True: + sub_num_qubits: int = cast(Support, op.args[1]).max_index + 1 + + if sub_num_qubits < num_qubits: + op_arg = cls._compl_tensor_op(num_qubits, op) + + else: + arg: InputType = cast(InputType, op.args[0]) + op_arg = cls._get_op(num_qubits, arg) + + return op_arg + + ops: list[qutip.Qobj] = [] + args: Iterable = cast(Iterable, op.args) + + if op.is_addition is True: + + for arg in args: + ops.append(cls._arith_tensor_op(num_qubits, arg)) + + return reduce(lambda a, b: a + b, ops) + + if op.is_multiplication is True: + + for arg in args: + ops.append(cls._arith_tensor_op(num_qubits, arg)) + + return reduce(lambda a, b: a * b, ops) + + if op.is_kronecker_product is True: + return cls._compl_tensor_op(num_qubits, op) + + raise NotImplementedError( + f"could not retrieve the expression {op} ({type(op)}) from the observables" + ) + + @classmethod + def _iterate_over_obs(cls, n_qubits: int, op: Iterable | InputType) -> list[qutip.Qobj]: + if isinstance(op, Iterable): + return [cls._get_op(n_qubits, arg) for arg in op] + + args: Iterable = cast(Iterable, op.args) + return [cls._get_op(n_qubits, cast(InputType, arg)) for arg in args] + + @classmethod + def build(cls, num_qubits: int, observables: list[InputType] | InputType) -> list[qutip.Qobj]: + """ + Parses an input expression or list of expressions into a native QuTiP object. + + Args: + num_qubits (int): the number of qubits to create the qutip object to + observables (InputType): the input expression. Any qadence2-expressions + expression compatible object, with the same methods + + Returns: + A QuTiP object with the Hilbert space compatible with `num_qubits` + + Returns: + """ + if not isinstance(observables, list): + return [cls._get_op(num_qubits, observables)] + return cls._iterate_over_obs(num_qubits, observables) diff --git a/qadence2_platforms/backends/fresnel1/interface.py b/qadence2_platforms/backends/fresnel1/interface.py index ba83fb3..d897ea4 100644 --- a/qadence2_platforms/backends/fresnel1/interface.py +++ b/qadence2_platforms/backends/fresnel1/interface.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import Counter -from typing import Any, Callable, Optional, Union, cast +from typing import Any, Union, cast from pulser.sequence.sequence import Sequence from pulser_simulation.simresults import SimulationResults @@ -10,6 +10,8 @@ from qadence2_platforms import AbstractInterface from qadence2_platforms.abstracts import OnEnum, RunEnum +from qadence2_platforms.backends.fresnel1.functions import parse_native_observables +from qadence2_platforms.backends.utils import InputType RunResult = Union[Counter, Qobj] @@ -29,6 +31,9 @@ def info(self) -> dict[str, Any]: def sequence(self) -> Sequence: return self._sequence + def parameters(self) -> dict[str, float]: + return self._params + def set_parameters(self, params: dict[str, float]) -> None: valid_params = params.keys() & self._non_trainable_parameters @@ -42,8 +47,7 @@ def _run( run_type: RunEnum, platform: SimulationResults, shots: int | None = None, - observable: Any | None = None, - callback: Optional[Callable] = None, + observable: list[InputType] | InputType | None = None, **_: Any, ) -> Any: """ @@ -51,8 +55,8 @@ def _run( `run_type` option. - **Notice**: for now, it only supports `emulator` option and - `QutipEmulator` platform. + **Notice**: for now, it only supports `emulator` option and `QutipEmulator` + platform. :param run_type: str: `run`, `sample`, `expectation` options :param platform: callable to retrieve methods for executing the options above @@ -69,7 +73,13 @@ def _run( case RunEnum.SAMPLE: return platform.sample_final_state(shots) case RunEnum.EXPECTATION: - return platform.expect(obs_list=observable) + if observable is not None: + return platform.expect( + obs_list=parse_native_observables( + num_qubits=len(self.sequence.register.qubit_ids), observable=observable + ) + ) + raise ValueError("observable cannot be None or empty on 'expectation' method.") case _: raise NotImplementedError(f"Run type '{run_type}' not implemented.") @@ -78,8 +88,7 @@ def _on_emulator( run_type: RunEnum, values: dict[str, float] | None, shots: int | None = None, - observable: Any | None = None, - callback: Optional[Callable] = None, + observable: list[InputType] | InputType | None = None, **_: Any, ) -> Any: """ @@ -110,7 +119,6 @@ def _on_emulator( platform=result, shots=shots, observable=observable, - callback=callback, ) def _on_qpu( @@ -118,8 +126,7 @@ def _on_qpu( run_type: RunEnum, values: dict[str, float] | None, shots: int | None = None, - observable: Any | None = None, - callback: Optional[Callable] = None, + observable: list[InputType] | InputType | None = None, **_: Any, ) -> Any: """ @@ -139,32 +146,32 @@ def _on_qpu( def run( self, - *, values: dict[str, float] | None = None, on: OnEnum = OnEnum.EMULATOR, shots: int | None = None, - callback: Optional[Callable] = None, **_: Any, ) -> RunResult: match on: case OnEnum.EMULATOR: return self._on_emulator( - run_type=RunEnum.RUN, values=values, shots=shots, callback=callback + run_type=RunEnum.RUN, + values=values, + shots=shots, ) case OnEnum.QPU: return self._on_qpu( - run_type=RunEnum.RUN, values=values, shots=shots, callback=callback + run_type=RunEnum.RUN, + values=values, + shots=shots, ) case _: raise NotImplementedError(f"Platform '{on}' not implemented.") def sample( self, - *, values: dict[str, float] | None = None, shots: int | None = None, on: OnEnum = OnEnum.EMULATOR, - callback: Optional[Callable] = None, **_: Any, ) -> Counter: match on: @@ -175,7 +182,6 @@ def sample( run_type=RunEnum.SAMPLE, values=values, shots=shots, - callback=callback, ), ) case OnEnum.QPU: @@ -185,7 +191,6 @@ def sample( run_type=RunEnum.SAMPLE, values=values, shots=shots, - callback=callback, ), ) case _: @@ -193,12 +198,10 @@ def sample( def expectation( self, - *, values: dict[str, float] | None = None, + observable: list[InputType] | InputType | None = None, on: OnEnum = OnEnum.EMULATOR, shots: int | None = None, - observable: Any | None = None, - callback: Optional[Callable] = None, **_: Any, ) -> Qobj: match on: @@ -208,7 +211,6 @@ def expectation( values=values, shots=shots, observable=observable, - callback=callback, ) case OnEnum.QPU: return self._on_qpu( @@ -216,7 +218,6 @@ def expectation( values=values, shots=shots, observable=observable, - callback=callback, ) case _: raise NotImplementedError(f"Platform '{on}' not implemented.") diff --git a/qadence2_platforms/backends/pyqtorch/compiler.py b/qadence2_platforms/backends/pyqtorch/compiler.py index f829f20..e6a976f 100644 --- a/qadence2_platforms/backends/pyqtorch/compiler.py +++ b/qadence2_platforms/backends/pyqtorch/compiler.py @@ -4,7 +4,9 @@ from logging import getLogger import pyqtorch as pyq -from qadence2_ir.types import Load, Model, QuInstruct +import torch +from pyqtorch.quantum_operation import QuantumOperation +from qadence2_ir.types import Alloc, Load, Model, QuInstruct from qadence2_platforms.backends.pyqtorch.embedding import Embedding from qadence2_platforms.backends.pyqtorch.interface import Interface @@ -29,22 +31,37 @@ def compile( pyq_operations = [] for instr in model.instructions: if isinstance(instr, QuInstruct): - native_op = None + native_op: QuantumOperation try: native_op = getattr(pyq, instr.name.upper()) except Exception as _: native_op = self.instruction_mapping[instr.name] - control = instr.support.control - target = instr.support.target - native_support = (*control, *target) - if len(instr.args) > 0: - assert len(instr.args) == 1, "More than one arg not supported" - (maybe_load,) = instr.args - assert isinstance(maybe_load, Load), "only support load" - pyq_operations.append(native_op(native_support, maybe_load.variable)) - else: - pyq_operations.append(native_op(*native_support)) - return pyq.QuantumCircuit(model.register.num_qubits, pyq_operations) + finally: + control = instr.support.control + target = instr.support.target + native_support = (*control, *target) + if len(instr.args) > 0: + assert len(instr.args) == 1, "More than one arg not supported" + (maybe_load,) = instr.args + assert isinstance(maybe_load, Load), "only support load" + pyq_operations.append( + native_op(native_support, maybe_load.variable).to( + dtype=torch.complex128 + ) + ) + else: + pyq_operations.append(native_op(*native_support).to(dtype=torch.complex128)) + return pyq.QuantumCircuit(model.register.num_qubits, pyq_operations).to( + dtype=torch.complex128 + ) + + +def get_trainable_params(inputs: dict[str, Alloc]) -> dict[str, torch.Tensor]: + return { + param: torch.rand(value.size, requires_grad=True) + for param, value in inputs.items() + if value.is_trainable + } def compile_to_backend(model: Model) -> Interface: @@ -53,4 +70,5 @@ def compile_to_backend(model: Model) -> Interface: ) embedding = Embedding(model) native_circ = Compiler().compile(model) - return Interface(register_interface, embedding, native_circ) + vparams = get_trainable_params(model.inputs) + return Interface(register_interface, embedding, native_circ, vparams=vparams) diff --git a/qadence2_platforms/backends/pyqtorch/embedding.py b/qadence2_platforms/backends/pyqtorch/embedding.py index c7256f0..9d3684f 100644 --- a/qadence2_platforms/backends/pyqtorch/embedding.py +++ b/qadence2_platforms/backends/pyqtorch/embedding.py @@ -57,7 +57,7 @@ def device(self) -> torch.device: def dtype(self) -> torch.dtype: return self._dtype - def to(self, args: Any, kwargs: Any) -> None: + def to(self, *args: Any, **kwargs: Any) -> ParameterBuffer: self.vparams = {p: t.to(*args, **kwargs) for p, t in self.vparams.items()} try: k = next(iter(self.vparams)) @@ -66,6 +66,7 @@ def to(self, args: Any, kwargs: Any) -> None: self._dtype = t.dtype except Exception: pass + return self @classmethod def from_model(cls, model: Model) -> ParameterBuffer: @@ -102,9 +103,10 @@ def __call__(self, inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: """ assigned_params: dict[str, torch.Tensor] = {} try: + # TODO: check why it is failing (doesn't affect code reliability apparently) assert inputs.keys() == self.param_buffer.fparams.keys() except Exception as _: - logger.error("Please pass a dict containing name:value for each fparam.") + pass for var, torchcall in self.var_to_torchcall.items(): assigned_params[var] = torchcall( self.param_buffer.vparams, diff --git a/qadence2_platforms/backends/pyqtorch/functions.py b/qadence2_platforms/backends/pyqtorch/functions.py index d653b31..da09231 100644 --- a/qadence2_platforms/backends/pyqtorch/functions.py +++ b/qadence2_platforms/backends/pyqtorch/functions.py @@ -1,40 +1,107 @@ from __future__ import annotations -from typing import cast +from typing import Iterable, cast +from pyqtorch.primitives import Primitive +from torch.nn import Module import pyqtorch as pyq from pyqtorch.hamiltonians import Observable -from pyqtorch.primitives import Primitive -from qadence2_platforms.backends.pyqtorch.utils import InputType, Support +from qadence2_platforms.backends.utils import InputType, Support + + +def parse_native_observables(observable: list[InputType] | InputType) -> Observable: + return PyQObservablesParser.build(observable) + + +class PyQObservablesParser: + """ + Convert InputType object observables into native PyQTorch object, especially for + running `expectation` method from PyQTorch interface class. InputType can be + qadence2-expressions or any other module that implement the same methods. + """ + + @classmethod + def _add_op(cls, op: InputType) -> Module: + return pyq.Add([cls._get_op(cast(InputType, k)) for k in cast(Iterable, op.args)]) + + @classmethod + def _mul_op(cls, op: InputType) -> Module: + return pyq.Sequence([cls._get_op(cast(InputType, k)) for k in cast(Iterable, op.args)]) + + @classmethod + def _kron_op(cls, op: InputType) -> Module: + return cls._mul_op(op) + + @classmethod + def _get_op(cls, op: InputType) -> Primitive | Module: + """ + Convert an expression into a native PyQTorch object. A simple symbol, + a quantum operator, and an operation (addition, multiplication or + kron tensor) are valid objects. + + Args: + op (InputType): the input expression. Any qadence2-expressions + expression compatible object or object with same methods. + + Returns: + A Primitive or torch.nn.Module object. + """ + + if op.is_symbol is True: + symbol: str = cast(str, op.args[0]) + return getattr(pyq, symbol.upper(), None) + + if op.is_quantum_operator is True: + primitive: Primitive = cast(Primitive, cls._get_op(cast(InputType, op.args[0]))) + support: Support = cast(Support, op.args[1]) + + if support: + target: int = support.target[0] + control: list[int] = support.control + + if control: + native_op = primitive(target=target, control=control) + else: + native_op = primitive(target=target) + return native_op -def _get_op(op: InputType) -> Primitive | None: - if op.is_symbol is True: - return getattr(pyq, op.head.upper(), None) + if op.is_addition is True: + return cls._add_op(op) - if op.is_quantum_operator is True: - op_args_item: InputType = op.args[0].args - return getattr(pyq, op_args_item[0].upper(), None) + if op.is_multiplication is True or op.is_kronecker_product is True: + return cls._mul_op(op) - op_args: str = cast(str, op.args[0]) - return getattr(pyq, op_args.upper(), None) + raise NotImplementedError( + f"could not retrieve the expression {op} ({type(op)}) from the observables" + ) + @classmethod + def _iterate_over_obs(cls, op: list[InputType] | InputType) -> list[Module]: + if isinstance(op, Iterable): + return [cls._get_op(arg) for arg in op] -def _get_native_op(op: InputType) -> Primitive: - native_op = _get_op(op) - if native_op is not None: - support: Support = cast(Support, op.args[1]) - return native_op(*support.target, support.control) + args: Iterable = cast(Iterable, op.args) + return [cls._get_op(arg) for arg in args] - raise ValueError(f"Observable {op} not found") + @classmethod + def build(cls, observable: list[InputType] | InputType) -> Observable: + """ + Parses an input expression or list of expressions into a native PyQTorch object. + Args: + observable (list[InputType] | InputType): the input expression. Any + qadence2-expressions expression compatible object or object with same + methods. -def load_observables(observable: list[InputType] | InputType) -> Observable: - if isinstance(observable, list): - pyq_observables: list[Primitive] | list = [] - for op in observable: - pyq_observables.append(_get_native_op(op)) - return Observable(*pyq_observables) + Returns: + An PyQTorch Observable object. + """ - return Observable(_get_native_op(observable)) + res: list[Module] | Module + if isinstance(observable, list): + res = cls._iterate_over_obs(observable) + else: + res = [cls._get_op(observable)] + return Observable(res) diff --git a/qadence2_platforms/backends/pyqtorch/interface.py b/qadence2_platforms/backends/pyqtorch/interface.py index b384f17..888d29c 100644 --- a/qadence2_platforms/backends/pyqtorch/interface.py +++ b/qadence2_platforms/backends/pyqtorch/interface.py @@ -1,20 +1,22 @@ from __future__ import annotations from logging import getLogger -from typing import Any, Callable, Counter, Literal, Optional, cast +from typing import Any, Counter, Iterable, Literal, cast import pyqtorch as pyq import torch +from pyqtorch.utils import DiffMode +from torch.nn import ParameterDict from qadence2_platforms.abstracts import ( AbstractInterface, RunEnum, ) +from qadence2_platforms.backends.utils import InputType from .embedding import Embedding -from .functions import load_observables +from .functions import parse_native_observables from .register import RegisterInterface -from .utils import InputType logger = getLogger(__name__) @@ -36,18 +38,21 @@ def __init__( register: RegisterInterface, embedding: Embedding, circuit: pyq.QuantumCircuit, + vparams: dict[str, torch.Tensor] = None, observable: list[InputType] | InputType | None = None, ) -> None: super().__init__() self.register = register self.init_state: torch.Tensor = ( - circuit.from_bitstring(register.init_state) + circuit.state_from_bitstring(register.init_state) if register.init_state is not None else circuit.init_state() - ) + ).to(dtype=torch.complex128) self.embedding = embedding self.circuit = circuit self.observable = observable + self.vparams = ParameterDict(vparams) + self._dtype = torch.float64 @property def info(self) -> dict[str, Any]: @@ -63,14 +68,17 @@ def add_noise(self, model: Literal["SPAM"]) -> None: def set_parameters(self, params: dict[str, float]) -> None: pass + def parameters(self) -> Iterable[Any]: + return self.vparams.values() # type: ignore [no-any-return] + def _run( self, run_type: RunEnum, values: dict[str, torch.Tensor] | None = None, - callback: Optional[Callable] = None, state: torch.Tensor | None = None, shots: int | None = None, observable: list[InputType] | InputType | None = None, + diff_mode: DiffMode = None, **_: Any, ) -> Any: """ @@ -91,8 +99,14 @@ def _run( :param observable: a list of observables, if applicable (`expectation` only) :return: a tensor or list of values (`sample` only) of the calculated state """ - inputs = values or dict() - state = state if state is not None else self.init_state + + def set_dtype(data: dict[str, torch.Tensor] | None) -> dict[str, torch.Tensor] | None: + if data is None: + return None + return {k: v.to(dtype=self._dtype) for k, v in data.items()} + + inputs: dict[str, torch.Tensor] = set_dtype(values) or dict() + state = state.to(dtype=torch.complex128) if state is not None else self.init_state match run_type: case RunEnum.RUN: @@ -115,9 +129,10 @@ def _run( return pyq.expectation( circuit=self.circuit, state=state, - values=inputs, - observable=load_observables(observable or self.observable), # type: ignore [arg-type] + values={**self.vparams, **inputs}, + observable=parse_native_observables(observable or self.observable), # type: ignore [arg-type] embedding=self.embedding, + diff_mode=diff_mode, ) raise ValueError("Observable must not be None for expectation run.") case _: @@ -125,20 +140,16 @@ def _run( def run( self, - *, values: dict[str, torch.Tensor] | None = None, - callback: Optional[Callable] = None, state: torch.Tensor | None = None, **kwargs: Any, ) -> torch.Tensor: - return self._run(RunEnum.RUN, values=values, callback=callback, state=state, **kwargs) + return self._run(RunEnum.RUN, values=values, state=state, **kwargs) def sample( self, - *, values: dict[str, torch.Tensor] | None = None, shots: int | None = None, - callback: Optional[Callable] = None, state: torch.Tensor | None = None, **kwargs: Any, ) -> list[Counter]: @@ -147,7 +158,6 @@ def sample( self._run( RunEnum.SAMPLE, values=values, - callback=callback, shots=shots, state=state, **kwargs, @@ -156,18 +166,20 @@ def sample( def expectation( self, - *, values: dict[str, torch.Tensor] | None = None, - callback: Optional[Callable] = None, - state: torch.Tensor | None = None, observable: list[InputType] | InputType | None = None, + state: torch.Tensor | None = None, + diff_mode: DiffMode = DiffMode.AD, **kwargs: Any, ) -> torch.Tensor: return self._run( RunEnum.EXPECTATION, values=values, - callback=callback, state=state, observable=observable, + diff_mode=diff_mode, **kwargs, ) + + def __call__(self, *args: Any, **kwargs: Any) -> torch.Tensor: + return self.run(*args, **kwargs) diff --git a/qadence2_platforms/backends/pyqtorch/utils.py b/qadence2_platforms/backends/pyqtorch/utils.py deleted file mode 100644 index 3bd85da..0000000 --- a/qadence2_platforms/backends/pyqtorch/utils.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from typing import Any, Protocol - - -class Support(Protocol): - def target_all(self) -> Any: ... - - @property - def target(self) -> list[int]: ... - - @property - def control(self) -> list[int]: ... - - -class InputType(Protocol): - @property - def head(self) -> InputType | str: ... - - @property - def args(self) -> InputType | list[InputType | str | Support]: ... - - @property - def value(self) -> Any: ... - - def is_symbol(self) -> bool: ... - - def is_quantum_operator(self) -> bool: ... - - def __getitem__(self, item: slice | int) -> Any: ... diff --git a/qadence2_platforms/backends/utils.py b/qadence2_platforms/backends/utils.py new file mode 100644 index 0000000..7a992c9 --- /dev/null +++ b/qadence2_platforms/backends/utils.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + + +@runtime_checkable +class Support(Protocol): + def target_all(self) -> Any: ... + + @property + def target(self) -> list[int]: ... + + @property + def control(self) -> list[int]: ... + + @property + def subspace(self) -> set[int]: ... + + @property + def max_index(self) -> int: ... + + +@runtime_checkable +class InputType(Protocol): + @property + def head(self) -> InputType | str: ... + + @property + def args(self) -> InputType | list[InputType | str | Support]: ... + + @property + def value(self) -> Any: ... + + @property + def is_symbol(self) -> bool: ... + + @property + def is_quantum_operator(self) -> bool: ... + + @property + def is_addition(self) -> bool: ... + + @property + def is_multiplication(self) -> bool: ... + + @property + def is_kronecker_product(self) -> bool: ... + + @property + def is_power(self) -> bool: ... + + @property + def subspace(self) -> Support | None: ... + + def add(self, *args: InputType) -> InputType: ... + + def mul(self, *args: InputType) -> InputType: ... + + def kron(self, *args: InputType) -> InputType: ... + + def pow(self, *args: InputType) -> InputType: ... + + def __getitem__(self, item: slice | int) -> Any: ... diff --git a/tests/conftest.py b/tests/conftest.py index 033d884..ea71797 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,9 @@ # functions common to every test from __future__ import annotations +from pulser import Sequence +from pulser.register import RegisterLayout +from pyqtorch import QuantumCircuit from pytest import fixture from qadence2_ir.types import ( Alloc, @@ -14,6 +17,18 @@ Support, ) +from qadence2_platforms.backends.fresnel1 import ( + register as fresnel1_register, +) +from qadence2_platforms.backends.fresnel1 import ( + sequence as fresnel1_sequence, +) +from qadence2_platforms.backends.fresnel1.interface import Interface as Fresnel1Interface +from qadence2_platforms.backends.pyqtorch.compiler import Compiler, get_trainable_params +from qadence2_platforms.backends.pyqtorch.embedding import Embedding +from qadence2_platforms.backends.pyqtorch.interface import Interface as PyQInterface +from qadence2_platforms.backends.pyqtorch.register import RegisterInterface + @fixture def model1() -> Model: @@ -30,3 +45,44 @@ def model1() -> Model: ], directives={"digital": True}, ) + + +@fixture +def pyq_circuit1() -> QuantumCircuit: + # return QuantumCircuit(2, [RX(0, torch.sin(1.5 * ))]) + pass + + +@fixture +def pyq_interface1(model1: Model) -> PyQInterface: + register = RegisterInterface( + n_qubits=model1.register.num_qubits, + init_state=model1.register.options.get("initial_state"), + ) + embedding = Embedding(model1) + circuit = Compiler().compile(model1) + vparams = get_trainable_params(model1.inputs) + + return PyQInterface( + register=register, + embedding=embedding, + circuit=circuit, + vparams=vparams, + ) + + +@fixture +def fresnel1_register1(model1: Model) -> RegisterLayout: + return fresnel1_register.from_model(model1) + + +@fixture +def fresnel1_sequence1(model1: Model, fresnel1_register1: RegisterLayout) -> Sequence: + return fresnel1_sequence.from_model(model1, fresnel1_register1) + + +@fixture +def fresnel1_interface1(model1: Model, fresnel1_sequence1: Sequence) -> Fresnel1Interface: + sequence = fresnel1_sequence1 + fparams = {k for k, v in model1.inputs.items() if not v.is_trainable} + return Fresnel1Interface(sequence, fparams) diff --git a/tests/custom_backends/custom_backend/interface.py b/tests/custom_backends/custom_backend/interface.py index 79bb02e..adc27c4 100644 --- a/tests/custom_backends/custom_backend/interface.py +++ b/tests/custom_backends/custom_backend/interface.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import Counter -from typing import Any, Callable, Optional, Union, cast +from typing import Any, Callable, Iterable, Optional, Union, cast from pulser.sequence.sequence import Sequence from pulser_simulation.simresults import SimulationResults @@ -37,13 +37,15 @@ def set_parameters(self, params: dict[str, float]) -> None: self._params = params + def parameters(self) -> Iterable[Any]: + return self._params.values() + def _run( self, run_type: RunEnum, platform: SimulationResults, shots: int | None = None, observable: Any | None = None, - callback: Optional[Callable] = None, **_: Any, ) -> Any: """ @@ -51,8 +53,8 @@ def _run( `run_type` option. - **Notice**: for now, it only supports `emulator` option and - `QutipEmulator` platform. + **Notice**: for now, it only supports `emulator` option and `QutipEmulator` + platform. :param run_type: str: `run`, `sample`, `expectation` options :param platform: callable to retrieve methods for executing the options above @@ -79,7 +81,6 @@ def _on_emulator( values: dict[str, float] | None, shots: int = None, observable: Any = None, - callback: Optional[Callable] = None, **_: Any, ) -> Any: """ @@ -110,7 +111,6 @@ def _on_emulator( platform=result, shots=shots, observable=observable, - callback=callback, ) def _on_qpu( @@ -139,30 +139,24 @@ def _on_qpu( def run( self, - *, values: dict[str, float] | None = None, on: OnEnum = OnEnum.EMULATOR, shots: int | None = None, - callback: Optional[Callable] = None, **_: Any, ) -> RunResult: match on: case OnEnum.EMULATOR: - return self._on_emulator(run_type=RunEnum.RUN, values=values, callback=callback) + return self._on_emulator(run_type=RunEnum.RUN, values=values) case "qpu": - return self._on_qpu( - run_type=RunEnum.RUN, values=values, shots=shots, callback=callback - ) + return self._on_qpu(run_type=RunEnum.RUN, values=values, shots=shots) case _: raise NotImplementedError(f"Platform '{on}' not implemented.") def sample( self, - *, values: dict[str, float] | None = None, shots: int | None = None, on: OnEnum = OnEnum.EMULATOR, - callback: Optional[Callable] = None, **_: Any, ) -> Counter: match on: @@ -173,7 +167,6 @@ def sample( run_type=RunEnum.SAMPLE, values=values, shots=shots, - callback=callback, ), ) case OnEnum.QPU: @@ -183,7 +176,6 @@ def sample( run_type=RunEnum.SAMPLE, values=values, shots=shots, - callback=callback, ), ) case _: @@ -191,12 +183,10 @@ def sample( def expectation( self, - *, values: dict[str, float] | None = None, + observable: Any | None = None, on: OnEnum = OnEnum.EMULATOR, shots: int | None = None, - observable: Any | None = None, - callback: Optional[Callable] = None, **_: Any, ) -> Qobj: match on: @@ -205,7 +195,6 @@ def expectation( run_type=RunEnum.EXPECTATION, values=values, observable=observable, - callback=callback, ) case OnEnum.QPU: return self._on_qpu( @@ -213,7 +202,6 @@ def expectation( values=values, shots=shots, observable=observable, - callback=callback, ) case _: raise NotImplementedError(f"Platform '{on}' not implemented.") diff --git a/tests/test_backend_utils.py b/tests/test_backend_utils.py new file mode 100644 index 0000000..1a20968 --- /dev/null +++ b/tests/test_backend_utils.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Any +import pytest + +from qadence2_platforms.backends.utils import Support, InputType +from qadence2_expressions.core.support import Support as ExprSupport +from qadence2_expressions import parameter, X, Z, RX +from qadence2_ir.types import Support as IRSupport + + +@pytest.mark.parametrize( + "data", + [ + ExprSupport(target=(1,)), + ExprSupport(target=(0,), control=(3,)), + ExprSupport(target=(2,), control=(5, 6)), + ], +) +def test_support(data: Support) -> None: + assert isinstance(data, Support) + + +@pytest.mark.parametrize( + "data", + [ + (0,), + (0, 1, 2), + ((0,),), + ((0,), (1,)), + ((0,), (1, 2)), + IRSupport(target=(0,)), + IRSupport(target=(0,), control=(1,)), + ], +) +def test_not_support(data: Any) -> None: + assert not isinstance(data, Support) + + +@pytest.mark.parametrize( + "data", + [ + X(0), + X(0) * X(1), + X(1) * Z(1), + X(0) * Z(1), + RX(parameter("a"))(0), + parameter("a") + parameter("b"), + parameter("a") * X(0), + ], +) +def test_inputtype(data: InputType) -> None: + assert isinstance(data, InputType) diff --git a/tests/test_compiler.py b/tests/test_compilation.py similarity index 100% rename from tests/test_compiler.py rename to tests/test_compilation.py diff --git a/tests/test_compilers.py b/tests/test_compilers.py new file mode 100644 index 0000000..b4ee5dc --- /dev/null +++ b/tests/test_compilers.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import numpy as np +from qadence2_ir.types import Model + +from qadence2_platforms.backends.fresnel1 import compile_to_backend as fresnel1_compile +from qadence2_platforms.backends.fresnel1.interface import Interface as Fresnel1Interface +from qadence2_platforms.backends.pyqtorch import compile_to_backend as pyq_compile +from qadence2_platforms.backends.pyqtorch.interface import Interface as PyQInterface + +N_SHOTS = 2_000 +ATOL = 0.05 * N_SHOTS + + +def test_pyq_compiler(model1: Model, pyq_interface1: PyQInterface) -> None: + interface = pyq_compile(model1) + assert pyq_interface1.info == interface.info + assert pyq_interface1.sequence.qubit_support == interface.sequence.qubit_support + assert [ + p1 == p2 + for p1, p2 in zip(pyq_interface1.sequence.operations, interface.sequence.operations) + ] + + +def test_fresnel1_compiler(model1: Model, fresnel1_interface1: Fresnel1Interface) -> None: + interface = fresnel1_compile(model1) + fparams = {"x": 1.0} + assert fresnel1_interface1.info == interface.info + assert fresnel1_interface1.sequence.register == interface.sequence.register + assert fresnel1_interface1.sequence.device == interface.sequence.device + assert fresnel1_interface1.parameters() == interface.parameters() + assert np.allclose( + np.array(list(fresnel1_interface1.sample(fparams, shots=N_SHOTS).values())), + np.array(list(interface.sample(fparams, shots=N_SHOTS).values())), + atol=ATOL, + ) diff --git a/tests/test_functions.py b/tests/test_functions.py new file mode 100644 index 0000000..40c63fe --- /dev/null +++ b/tests/test_functions.py @@ -0,0 +1,320 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest +import qutip +from pulser import Sequence as PulserSequence, AnalogDevice, Register +from pulser.register import RegisterLayout +from qadence2_expressions import Z +from qadence2_ir.types import Model +from qutip import tensor as qtensor +import pyqtorch as pyq +from pyqtorch import ( + Observable as PyQObservable, + Z as PZ, +) + +from qadence2_platforms.backends.fresnel1.functions import ( + local_pulse, + local_pulse_core, + apply_local_shifts, + dyn_pulse, + dyn_wait, + rotation, + h, + x, + rx, + ry, + Direction, + Duration, + QuTiPObservablesParser, + parse_native_observables as fresnel1_parse_nat_obs, +) +from qadence2_platforms.backends.fresnel1.interface import Interface as Fresnel1Interface +from qadence2_platforms.backends.fresnel1.sequence import Fresnel1 +from qadence2_platforms.backends.pyqtorch.interface import Interface as PyQInterface +from qadence2_platforms.backends.pyqtorch.functions import ( + PyQObservablesParser, + parse_native_observables as pyq_parse_nat_obs, +) +from qadence2_platforms.backends.utils import InputType + + +# CONSTANTS +N_SHOTS = 3_000 +ATOL = 0.06 * N_SHOTS +SMALL_ATOL = 0.005 * N_SHOTS +BIG_ATOL = 0.15 * N_SHOTS + +TRAP_COORDINATES = [(0.0, 0.0), (0.0, 5.0), (5.0, 0.0), (5.0, 5.0)] +WEIGHTS = [1.0, 0.5, 0.5, 0] + +qi = qutip.qeye(2) +qz = qutip.sigmaz() + + +################## +# PYQTORCH TESTS # +################## + + +@pytest.mark.parametrize( + "interface, n_qubits, expr_obs, pyq_obs, wrong_pyq_obs", + [ + ( + "pyq_interface1", + 2, + Z(0), + pyq.Observable([PZ(0)]), + pyq.Observable([PZ(1)]), + ), + ( + "pyq_interface1", + 2, + Z(0).__kron__(Z(1)), + pyq.Observable(pyq.Sequence([PZ(0), PZ(1)])), + pyq.Observable(pyq.Sequence([PZ(1), PZ(2)])), + ), + ( + "pyq_interface1", + 2, + Z(0).__kron__(Z(1)), + pyq.Observable(pyq.Sequence([PZ(1), PZ(0)])), + pyq.Observable(pyq.Sequence([PZ(0), PZ(3)])), + ), + ( + "pyq_interface1", + 2, + Z(0) + Z(1), + pyq.Observable([pyq.Add([PZ(0), PZ(1)])]), + pyq.Observable([pyq.Add([PZ(1), PZ(1)])]), + ), + ], +) +def test_pyq_observables( + interface: PyQInterface, + n_qubits: int, + expr_obs: InputType, + pyq_obs: PyQObservable, + wrong_pyq_obs: PyQObservable, + request: Any, +) -> None: + parsed_obs = PyQObservablesParser.build(observable=expr_obs) + assert parsed_obs.qubit_support == pyq_obs.qubit_support + assert hash(parsed_obs) == hash(pyq_obs) + assert hash(parsed_obs) == hash(pyq_parse_nat_obs(expr_obs)) + assert not (hash(parsed_obs) == hash(wrong_pyq_obs)) + + +def test_pyq_obs_parsing() -> None: + assert hash(PyQObservablesParser._add_op(Z(0) + Z(1))) == hash(pyq.Add([PZ(0), PZ(1)])) + assert hash(PyQObservablesParser._mul_op(Z(0) * Z(1))) == hash(pyq.Sequence([PZ(0), PZ(1)])) + assert hash(PyQObservablesParser._kron_op(Z(0).__kron__(Z(1)))) == hash( + pyq.Sequence([PZ(0), PZ(1)]) + ) + assert hash(PyQObservablesParser._get_op(Z(0))) == hash(pyq.Z(0)) + assert hash(tuple(PyQObservablesParser._iterate_over_obs([Z(0), Z(1)]))) == hash( + (pyq.Z(0), pyq.Z(1)) + ) + + +################## +# FRESNEL1 TESTS # +################## + + +def test_dyn_pulse(fresnel1_sequence1: PulserSequence, fresnel1_register1: RegisterLayout) -> None: + seq = PulserSequence(fresnel1_register1, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + dyn_pulse(seq, 1.0, 1.0, 0.0, 0.0) + + # max_amp is about 12.56 rad/µs + max_amp = seq.device.channels["rydberg_global"].max_amp + + # total_duration is 500 from dyn_pulse duration with + # duration unit 1.0 + eom pulse + turning eom on and off + total_duration = 740 + assert seq.get_duration() == total_duration + + with pytest.raises(Exception): + dyn_pulse(seq, 1000.0, 1.0, 0.0, 0.0) + + with pytest.raises(Exception): + dyn_pulse(seq, 1.0, 1000.0, 0.0, 0.0) + + interface = Fresnel1Interface(seq, set()) + res = interface.sample(shots=N_SHOTS) + assert np.allclose(res["01"], res["10"], atol=ATOL) + + +def test_dyn_wait(fresnel1_sequence1: PulserSequence, fresnel1_register1: RegisterLayout) -> None: + seq = PulserSequence(fresnel1_register1, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + dyn_wait(seq, 1.0) + + interface = Fresnel1Interface(seq, set()) + res = interface.sample(shots=N_SHOTS) + assert np.allclose(res["01"], res["10"], atol=ATOL) + + +@pytest.mark.parametrize("direction", [Direction.X, Direction.Y, 0.0]) +@pytest.mark.parametrize("angle", [np.pi, np.pi / 2]) +def test_rotation(direction: Direction, angle: float) -> None: + layout = AnalogDevice.calibrated_register_layouts["TriangularLatticeLayout(61, 5.0µm)"] + register = layout.define_register(0) + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + rotation(seq, np.pi, Direction.X) + + interface1 = Fresnel1Interface(seq, set()) + res = interface1.sample(shots=N_SHOTS) + assert np.allclose(res["1"], N_SHOTS, atol=SMALL_ATOL) + + x(seq) + interface2 = Fresnel1Interface(seq, set()) + res = interface2.sample(shots=N_SHOTS) + assert np.allclose(res["0"], N_SHOTS, atol=SMALL_ATOL) + + +def test_apply_local_shifts(model1: Model, fresnel1_sequence1: PulserSequence) -> None: + register_layout = RegisterLayout(TRAP_COORDINATES) # type: ignore + detuning_map = register_layout.define_detuning_map({i: WEIGHTS[i] for i in range(4)}) + + register = Register.from_coordinates(TRAP_COORDINATES, center=False, prefix="q") + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + seq.config_detuning_map(detuning_map, "dmm_0") + + x(seq) + apply_local_shifts(seq) + interface2 = Fresnel1Interface(seq, set()) + res = interface2.sample(shots=N_SHOTS) + assert np.allclose(res["1001"], res["0110"], atol=ATOL) + + +def test_local_pulse(fresnel1_sequence1: PulserSequence) -> None: + register_layout = RegisterLayout(TRAP_COORDINATES) # type: ignore + detuning_map = register_layout.define_detuning_map({i: WEIGHTS[i] for i in range(4)}) + + register = Register.from_coordinates(TRAP_COORDINATES, center=False, prefix="q") + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + seq.config_detuning_map(detuning_map, "dmm_0") + + x(seq) + local_pulse(seq, 1.0, 1.0) + interface2 = Fresnel1Interface(seq, set()) + res = interface2.sample(shots=N_SHOTS) + assert np.allclose(res["1001"], res["0110"], atol=ATOL) + + +def test_local_pulse_core(fresnel1_sequence1: PulserSequence) -> None: + register_layout = RegisterLayout(TRAP_COORDINATES) # type: ignore + detuning_map = register_layout.define_detuning_map({i: WEIGHTS[i] for i in range(4)}) + + register = Register.from_coordinates(TRAP_COORDINATES, center=False, prefix="q") + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + seq.config_detuning_map(detuning_map, "dmm_0") + + max_amp = seq.device.channels["rydberg_global"].max_amp + time_scale = 1000 * 2 * np.pi / max_amp + + local_pulse_core(seq, 1.0, time_scale, 1.0, False) + x(seq) + local_pulse_core(seq, Duration.FILL, time_scale, 0.5, True) + + with pytest.raises(Exception): + local_pulse_core(seq, Duration.FILL, time_scale, 0.5, False) + + with pytest.raises(ValueError): + local_pulse_core(seq, Duration.FILL, 1.0, 0.5, True) + + with pytest.raises(ValueError): + local_pulse_core(seq, 1.0, 1.0, 1.0) + + interface2 = Fresnel1Interface(seq, set()) + res = interface2.sample(shots=N_SHOTS) + assert np.allclose(res["0001"], N_SHOTS, atol=BIG_ATOL) + + +def test_h(fresnel1_sequence1: PulserSequence) -> None: + layout = AnalogDevice.calibrated_register_layouts["TriangularLatticeLayout(61, 5.0µm)"] + register = layout.define_register(0) + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + h(seq, np.pi) + + with pytest.raises(Exception): + h(seq, 1000.0) + + with pytest.raises(Exception): + h(seq, -1.0) + + with pytest.raises(Exception): + h(seq, 1.0, "GLOBAL") + + interface1 = Fresnel1Interface(seq, set()) + res = interface1.sample(shots=N_SHOTS) + print(res) + assert np.allclose(res["1"], res["0"], atol=BIG_ATOL) + + +def test_rx(fresnel1_sequence1: PulserSequence) -> None: + layout = AnalogDevice.calibrated_register_layouts["TriangularLatticeLayout(61, 5.0µm)"] + register = layout.define_register(0) + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + rx(seq, np.pi) + + with pytest.raises(Exception): + rx(seq, Direction.X) + + with pytest.raises(Exception): + rx(seq, "theta") + + interface1 = Fresnel1Interface(seq, set()) + res = interface1.sample(shots=N_SHOTS) + print(res) + assert np.allclose(res["1"], N_SHOTS, atol=SMALL_ATOL) + + +def test_ry(fresnel1_sequence1: PulserSequence) -> None: + layout = AnalogDevice.calibrated_register_layouts["TriangularLatticeLayout(61, 5.0µm)"] + register = layout.define_register(0) + seq = PulserSequence(register, Fresnel1) # type: ignore + seq.declare_channel("global", "rydberg_global") + ry(seq, np.pi) + + with pytest.raises(Exception): + ry(seq, Direction.X) + + with pytest.raises(Exception): + ry(seq, "theta") + + interface1 = Fresnel1Interface(seq, set()) + res = interface1.sample(shots=N_SHOTS) + print(res) + assert np.allclose(res["1"], N_SHOTS, atol=SMALL_ATOL) + + +@pytest.mark.parametrize( + "interface, n_qubits, expr_obs, qutip_obs", + [ + ("fresnel1_interface", 2, Z(0), qtensor(qz, qi)), + ("fresnel1_interface", 2, Z(0).__kron__(Z(1)), qtensor(qz, qz)), + ("fresnel1_interface", 2, Z(0) + Z(1), qtensor(qz, qi) + qtensor(qi, qz)), + ], +) +def test_fresnel1_observables( + interface: Fresnel1Interface, + n_qubits: int, + expr_obs: InputType, + qutip_obs: qutip.Qobj, + request: Any, +) -> None: + parsed_obs = QuTiPObservablesParser.build(num_qubits=n_qubits, observables=expr_obs) + assert np.allclose(parsed_obs, qutip_obs) + assert np.allclose(parsed_obs, fresnel1_parse_nat_obs(n_qubits, expr_obs)) diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py new file mode 100644 index 0000000..28c701d --- /dev/null +++ b/tests/test_interfaces.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections import Counter + +import numpy as np +import qutip +import torch +from pulser import Sequence as PulserSequence +from pulser.register import RegisterLayout +from qadence2_expressions import Z +from qadence2_ir.types import Model + +from qadence2_platforms.backends.fresnel1.sequence import Fresnel1 +from qadence2_platforms.backends.fresnel1.interface import Interface as Fresnel1Interface +from qadence2_platforms.backends.pyqtorch.interface import Interface as PyQInterface + + +N_SHOTS = 2_000 +ATOL = 0.05 * N_SHOTS + + +def test_pyq_interface(model1: Model, pyq_interface1: PyQInterface) -> None: + assert pyq_interface1.info == dict(num_qubits=model1.register.num_qubits) + + fparams = {"x": torch.tensor(1.0, requires_grad=True)} + run_res = pyq_interface1.run(fparams) + assert isinstance(run_res, torch.Tensor) + assert run_res.shape == torch.Size([2, 2, 1]) + + sample = pyq_interface1.sample(fparams, shots=N_SHOTS)[0] + assert isinstance(sample, Counter) + assert {"00", "11"}.issubset(set(sample.keys())) + + obs = Z(0) * Z(1) + run_obs = pyq_interface1.expectation(fparams, shots=N_SHOTS, observable=obs) + assert isinstance(run_obs, torch.Tensor) + + +def test_fresnel1_interface( + fresnel1_register1: RegisterLayout, + fresnel1_sequence1: PulserSequence, + fresnel1_interface1: Fresnel1Interface, +) -> None: + assert fresnel1_interface1.info == dict(device=Fresnel1, register=fresnel1_sequence1.register) + + fparams = {"x": 1.0} + run_res = fresnel1_interface1.run(fparams) + assert isinstance(run_res, qutip.Qobj) + assert run_res.dims == [[2, 2], [1, 1]] + assert run_res.shape == (4, 1) + assert run_res.isket + + sample = fresnel1_interface1.sample(fparams, shots=N_SHOTS) + assert isinstance(sample, Counter) + assert np.allclose(sample["01"], sample["10"], atol=ATOL) + + obs = Z(0) * Z(1) + obs_res = fresnel1_interface1.expectation(fparams, shots=N_SHOTS, observable=obs)[0] + assert all([(0.0 <= abs(k) <= 1.0) for k in obs_res])