From 54b97478cc20a7cc63216ab09996a21e5d5f5c35 Mon Sep 17 00:00:00 2001 From: Mario Dagrada <29142759+madagra@users.noreply.github.com> Date: Fri, 3 Nov 2023 09:49:12 +0100 Subject: [PATCH 1/2] [Feat] Add transpilation passes to the backend configuration (#115) * add custom transpilation passes to the backend configuration. The default is None which applies the default transpilation, otherwise the supplied functions are used. Notice that custom transpilation passes are not JSON serializable. * move the flatten() transpilation routine to a separate module and allow it for the QuantumCircuit instances as well * fixed an issue in the original_circuit assignation for the Braket backend --- qadence/backend.py | 17 +++- qadence/backends/backends/__init__.py | 0 qadence/backends/braket/backend.py | 20 +++-- qadence/backends/braket/config.py | 13 +++- qadence/backends/pulser/backend.py | 8 +- qadence/backends/pulser/config.py | 35 +++++---- qadence/backends/pyqtorch/backend.py | 25 +++--- qadence/backends/pyqtorch/config.py | 26 ++++++- qadence/extensions.py | 8 +- qadence/transpile/__init__.py | 2 +- qadence/transpile/block.py | 71 +---------------- qadence/transpile/flatten.py | 108 ++++++++++++++++++++++++++ tests/backends/test_backends.py | 24 +++++- tests/qadence/test_transpile.py | 2 +- 14 files changed, 233 insertions(+), 126 deletions(-) delete mode 100644 qadence/backends/backends/__init__.py create mode 100644 qadence/transpile/flatten.py diff --git a/qadence/backend.py b/qadence/backend.py index 29348840a..5f280d32e 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -20,10 +20,13 @@ ) from qadence.blocks.analog import ConstantAnalogRotation, WaitBlock from qadence.circuit import QuantumCircuit +from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.parameters import stringify from qadence.types import BackendName, DiffMode, Endianness +logger = get_logger(__name__) + @dataclass class BackendConfiguration: @@ -31,6 +34,14 @@ class BackendConfiguration: use_sparse_observable: bool = False use_gradient_checkpointing: bool = False use_single_qubit_composition: bool = False + transpilation_passes: list[Callable] | None = None + + def __post_init__(self) -> None: + if self.transpilation_passes is not None: + assert all( + [callable(f) for f in self.transpilation_passes] + ), "Wrong transpilation passes supplied" + logger.warning("Custom transpilation passes cannot be serialized in JSON format!") def available_options(self) -> str: """Return as a string the available fields with types of the configuration @@ -39,10 +50,10 @@ def available_options(self) -> str: str: a string with all the available fields, one per line """ conf_msg = "" - for field in fields(self): - if not field.name.startswith("_"): + for _field in fields(self): + if not _field.name.startswith("_"): conf_msg += ( - f"Name: {field.name} - Type: {field.type} - Default value: {field.default}\n" + f"Name: {_field.name} - Type: {_field.type} - Default value: {_field.default}\n" ) return conf_msg diff --git a/qadence/backends/backends/__init__.py b/qadence/backends/backends/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 5c7aa8687..9046a1d48 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -17,9 +17,10 @@ from qadence.circuit import QuantumCircuit from qadence.measurements import Measurements from qadence.overlap import overlap_exact +from qadence.transpile import transpile from qadence.utils import Endianness -from .config import Configuration +from .config import Configuration, default_passes from .convert_ops import convert_block @@ -54,14 +55,17 @@ def __post_init__(self) -> None: if self.is_remote: raise NotImplementedError("Braket backend does not support cloud execution yet") - def circuit(self, circ: QuantumCircuit) -> ConvertedCircuit: - from qadence.transpile import digitalize, fill_identities, transpile + def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: + passes = self.config.transpilation_passes + if passes is None: + passes = default_passes - # make sure that we don't have empty wires. braket does not like it. - transpilations = [fill_identities, digitalize] - abstract_circ = transpile(*transpilations)(circ) # type: ignore[call-overload] - native = BraketCircuit(convert_block(abstract_circ.block)) - return ConvertedCircuit(native=native, abstract=abstract_circ, original=circ) + original_circ = circuit + if len(passes) > 0: + circuit = transpile(*passes)(circuit) + + native = BraketCircuit(convert_block(circuit.block)) + return ConvertedCircuit(native=native, abstract=circuit, original=original_circ) def observable(self, obs: AbstractBlock, n_qubits: int = None) -> Any: if n_qubits is None: diff --git a/qadence/backends/braket/config.py b/qadence/backends/braket/config.py index 9f83f8cd1..94d1856fd 100644 --- a/qadence/backends/braket/config.py +++ b/qadence/backends/braket/config.py @@ -1,15 +1,20 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import Callable from qadence.backend import BackendConfiguration +from qadence.logger import get_logger +from qadence.transpile import digitalize, fill_identities + +logger = get_logger(__name__) + +default_passes: list[Callable] = [fill_identities, digitalize] @dataclass class Configuration(BackendConfiguration): # FIXME: currently not used - # credentials for connecting to the cloud - # and executing on real QPUs cloud_credentials: dict = field(default_factory=dict) - # Braket requires gate-level parameters - use_gate_params = True + """Credentials for connecting to the cloud + and executing on the QPUs available on Amazon Braket""" diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 6566494fd..9e86a7ca0 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -22,6 +22,7 @@ from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.register import Register +from qadence.transpile import transpile from qadence.utils import Endianness from .channels import GLOBAL_CHANNEL, LOCAL_CHANNEL @@ -130,9 +131,14 @@ class Backend(BackendInterface): config: Configuration = Configuration() def circuit(self, circ: QuantumCircuit) -> Sequence: + passes = self.config.transpilation_passes + original_circ = circ + if passes is not None and len(passes) > 0: + circ = transpile(*passes)(circ) + native = make_sequence(circ, self.config) - return ConvertedCircuit(native=native, abstract=circ, original=circ) + return ConvertedCircuit(native=native, abstract=circ, original=original_circ) def observable(self, observable: AbstractBlock, n_qubits: int = None) -> Tensor: from qadence.transpile import flatten, scale_primitive_blocks_only, transpile diff --git a/qadence/backends/pulser/config.py b/qadence/backends/pulser/config.py index d247f9125..5915b17e9 100644 --- a/qadence/backends/pulser/config.py +++ b/qadence/backends/pulser/config.py @@ -7,48 +7,55 @@ from qadence.backend import BackendConfiguration from qadence.blocks.analog import Interaction +from qadence.logger import get_logger from .devices import Device +logger = get_logger(__name__) + @dataclass class Configuration(BackendConfiguration): - # device type device_type: Device = Device.IDEALIZED + """The type of quantum Device to use in the simulations. Choose + between IDEALIZED and REALISTIC. This influences pulse duration, max + amplitude, minimum atom spacing and other properties of the system""" - # atomic spacing spacing: Optional[float] = None + """Atomic spacing for Pulser register""" - # sampling rate to be used for local simulations sampling_rate: float = 1.0 + """Sampling rate to be used for local simulations. Set to 1.0 + to avoid any interpolation in the solving procedure""" - # solver method to pass to the Qutip solver method_solv: str = "adams" + """Solver method to pass to the Qutip solver""" - # number of solver steps to pass to the Qutip solver n_steps_solv: float = 1e8 + """Number of solver steps to pass to the Qutip solver""" - # simulation configuration with optional noise options sim_config: Optional[SimConfig] = None + """Simulation configuration with optional noise options""" - # add modulation to the local execution with_modulation: bool = False + """Add laser modulation to the local execution. This will take + into account realistic laser pulse modulation when simulating + a pulse sequence""" - # Use gate-level parameters - use_gate_params = True - - # pulse amplitude on local channel amplitude_local: Optional[float] = None + """Default pulse amplitude on local channel""" - # pulse amplitude on global channel amplitude_global: Optional[float] = None + """Default pulse amplitude on global channel""" - # detuning value detuning: Optional[float] = None + """Default value for the detuning pulses""" - # interaction type interaction: Interaction = Interaction.NN + """Type of interaction introduced in the Hamiltonian. Currently, only + NN interaction is support. XY interaction is possible but not implemented""" def __post_init__(self) -> None: + super().__post_init__() if self.sim_config is not None and not isinstance(self.sim_config, SimConfig): raise TypeError("Wrong 'sim_config' attribute type, pass a valid SimConfig object!") diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index e12c5632a..66ead5a7b 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -18,8 +18,6 @@ from qadence.overlap import overlap_exact from qadence.states import zero_state from qadence.transpile import ( - add_interaction, - blockfn_to_circfn, chain_single_qubit_ops, flatten, scale_primitive_blocks_only, @@ -27,7 +25,7 @@ ) from qadence.utils import Endianness, int_to_basis -from .config import Configuration +from .config import Configuration, default_passes from .convert_ops import convert_block, convert_observable @@ -46,18 +44,17 @@ class Backend(BackendInterface): config: Configuration = Configuration() def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: - transpilations = [ - lambda circ: add_interaction(circ, interaction=self.config.interaction), - lambda circ: blockfn_to_circfn(chain_single_qubit_ops)(circ) - if self.config.use_single_qubit_composition - else blockfn_to_circfn(flatten)(circ), - blockfn_to_circfn(scale_primitive_blocks_only), - ] + passes = self.config.transpilation_passes + if passes is None: + passes = default_passes(self.config) + + original_circ = circuit + if len(passes) > 0: + circuit = transpile(*passes)(circuit) - abstract = transpile(*transpilations)(circuit) # type: ignore[call-overload] - ops = convert_block(abstract.block, n_qubits=circuit.n_qubits, config=self.config) - native = pyq.QuantumCircuit(abstract.n_qubits, ops) - return ConvertedCircuit(native=native, abstract=abstract, original=circuit) + ops = convert_block(circuit.block, n_qubits=circuit.n_qubits, config=self.config) + native = pyq.QuantumCircuit(circuit.n_qubits, ops) + return ConvertedCircuit(native=native, abstract=circuit, original=original_circ) def observable(self, observable: AbstractBlock, n_qubits: int) -> ConvertedObservable: # make sure only leaves, i.e. primitive blocks are scaled diff --git a/qadence/backends/pyqtorch/config.py b/qadence/backends/pyqtorch/config.py index df9e95a7d..f1f5c4963 100644 --- a/qadence/backends/pyqtorch/config.py +++ b/qadence/backends/pyqtorch/config.py @@ -4,18 +4,36 @@ from typing import Callable from qadence.backend import BackendConfiguration +from qadence.logger import get_logger +from qadence.transpile import ( + add_interaction, + blockfn_to_circfn, + chain_single_qubit_ops, + flatten, + scale_primitive_blocks_only, +) from qadence.types import AlgoHEvo, Interaction +logger = get_logger(__name__) + + +def default_passes(config: Configuration) -> list[Callable]: + return [ + lambda circ: add_interaction(circ, interaction=config.interaction), + lambda circ: blockfn_to_circfn(chain_single_qubit_ops)(circ) + if config.use_single_qubit_composition + else blockfn_to_circfn(flatten)(circ), + blockfn_to_circfn(scale_primitive_blocks_only), + ] + @dataclass class Configuration(BackendConfiguration): - # FIXME: currently not used - # determine which kind of Hamiltonian evolution - # algorithm to use algo_hevo: AlgoHEvo = AlgoHEvo.EXP + """Determine which kind of Hamiltonian evolution algorithm to use""" - # number of steps for the Hamiltonian evolution n_steps_hevo: int = 100 + """Default number of steps for the Hamiltonian evolution""" use_gradient_checkpointing: bool = False """Use gradient checkpointing. Recommended for higher-order optimization tasks.""" diff --git a/qadence/extensions.py b/qadence/extensions.py index dd564c443..f3b7428e7 100644 --- a/qadence/extensions.py +++ b/qadence/extensions.py @@ -68,12 +68,10 @@ def _set_backend_config(backend: Backend, diff_mode: DiffMode) -> None: _validate_diff_mode(backend, diff_mode) + # (1) When using PSR with any backend or (2) we use the backends Pulser or Braket, + # we have to use gate-level parameters if not backend.supports_ad or diff_mode != DiffMode.AD: backend.config._use_gate_params = True - - # (1) When using PSR with any backend or (2) we use the backends Pulser or Braket, - # we have to use gate-level parameters - else: assert diff_mode == DiffMode.AD backend.config._use_gate_params = False @@ -81,8 +79,6 @@ def _set_backend_config(backend: Backend, diff_mode: DiffMode) -> None: if backend.name == BackendName.PYQTORCH: backend.config.use_single_qubit_composition = True - # For pyqtorch, we enable some specific transpilation passes. - # if proprietary qadence_plus is available import the # right function since more backends are supported diff --git a/qadence/transpile/__init__.py b/qadence/transpile/__init__.py index a50aa6bb2..465323d69 100644 --- a/qadence/transpile/__init__.py +++ b/qadence/transpile/__init__.py @@ -2,7 +2,6 @@ from .block import ( chain_single_qubit_ops, - flatten, repeat, scale_primitive_blocks_only, set_trainable, @@ -11,6 +10,7 @@ from .circuit import fill_identities from .digitalize import digitalize from .emulate import add_interaction +from .flatten import flatten from .invert import invert_endianness, reassign from .transpile import blockfn_to_circfn, transpile diff --git a/qadence/transpile/block.py b/qadence/transpile/block.py index 0478ad044..459511af3 100644 --- a/qadence/transpile/block.py +++ b/qadence/transpile/block.py @@ -1,8 +1,8 @@ from __future__ import annotations from copy import deepcopy -from functools import reduce, singledispatch -from typing import Callable, Generator, Iterable, Type +from functools import singledispatch +from typing import Callable, Iterable, Type import sympy @@ -33,73 +33,6 @@ logger = get_logger(__name__) -def _flat_blocks(block: AbstractBlock, T: Type) -> Generator: - """Constructs a generator that flattens nested `CompositeBlock`s of type `T`. - - Example: - ```python exec="on" source="material-block" result="json" - from qadence.transpile.block import _flat_blocks - from qadence.blocks import ChainBlock - from qadence import chain, X - - x = chain(chain(chain(X(0)), X(0))) - assert tuple(_flat_blocks(x, ChainBlock)) == (X(0), X(0)) - ``` - """ - if isinstance(block, T): - # here we do the flattening - for b in block.blocks: - if isinstance(b, T): - yield from _flat_blocks(b, T) - else: - yield flatten(b, [T]) - elif isinstance(block, CompositeBlock): - # here we make sure that we don't get stuck at e.g. `KronBlock`s if we - # want to flatten `ChainBlock`s - yield from (flatten(b, [T]) for b in block.blocks) - elif isinstance(block, ScaleBlock): - blk = deepcopy(block) - blk.block = flatten(block.block, [T]) - yield blk - else: - yield block - - -def flatten(block: AbstractBlock, types: list = [ChainBlock, KronBlock, AddBlock]) -> AbstractBlock: - """Flattens the given types of `CompositeBlock`s if possible. - - Example: - ```python exec="on" source="material-block" result="json" - from qadence import chain, kron, X - from qadence.transpile import flatten - from qadence.blocks import ChainBlock, KronBlock, AddBlock - - x = chain(chain(chain(X(0))), kron(kron(X(0)))) - - # flatten only `ChainBlock`s - assert flatten(x, [ChainBlock]) == chain(X(0), kron(kron(X(0)))) - - # flatten `ChainBlock`s and `KronBlock`s - assert flatten(x, [ChainBlock, KronBlock]) == chain(X(0), kron(X(0))) - - # flatten `AddBlock`s (does nothing in this case) - assert flatten(x, [AddBlock]) == x - ``` - """ - if isinstance(block, CompositeBlock): - - def fn(b: AbstractBlock, T: Type) -> AbstractBlock: - return _construct(type(block), tuple(_flat_blocks(b, T))) - - return reduce(fn, types, block) # type: ignore[arg-type] - elif isinstance(block, ScaleBlock): - blk = deepcopy(block) - blk.block = flatten(block.block, types=types) - return blk - else: - return block - - def repeat( Block: Type[TPrimitiveBlock], support: Iterable[int], parameter: str | Parameter | None = None ) -> KronBlock: diff --git a/qadence/transpile/flatten.py b/qadence/transpile/flatten.py new file mode 100644 index 000000000..577c79c99 --- /dev/null +++ b/qadence/transpile/flatten.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from copy import deepcopy +from functools import reduce, singledispatch +from typing import Generator, Type, overload + +from qadence import QuantumCircuit +from qadence.blocks import ( + AbstractBlock, + AddBlock, + ChainBlock, + CompositeBlock, + KronBlock, + ScaleBlock, +) +from qadence.blocks.utils import _construct + + +def _flat_blocks(block: AbstractBlock, T: Type) -> Generator: + """Constructs a generator that flattens nested `CompositeBlock`s of type `T`. + + Example: + ```python exec="on" source="material-block" result="json" + from qadence.transpile.block import _flat_blocks + from qadence.blocks import ChainBlock + from qadence import chain, X + + x = chain(chain(chain(X(0)), X(0))) + assert tuple(_flat_blocks(x, ChainBlock)) == (X(0), X(0)) + ``` + """ + if isinstance(block, T): + # here we do the flattening + for b in block.blocks: + if isinstance(b, T): + yield from _flat_blocks(b, T) + else: + yield flatten(b, [T]) + elif isinstance(block, CompositeBlock): + # here we make sure that we don't get stuck at e.g. `KronBlock`s if we + # want to flatten `ChainBlock`s + yield from (flatten(b, [T]) for b in block.blocks) + elif isinstance(block, ScaleBlock): + blk = deepcopy(block) + blk.block = flatten(block.block, [T]) + yield blk + else: + yield block + + +@overload +def flatten( + circuit: QuantumCircuit, types: list = [ChainBlock, KronBlock, AddBlock] +) -> QuantumCircuit: + ... + + +@overload +def flatten(block: AbstractBlock, types: list = [ChainBlock, KronBlock, AddBlock]) -> AbstractBlock: + ... + + +@singledispatch +def flatten( + circ_or_block: AbstractBlock | QuantumCircuit, types: list = [ChainBlock, KronBlock, AddBlock] +) -> AbstractBlock | QuantumCircuit: + raise NotImplementedError(f"digitalize is not implemented for {type(circ_or_block)}") + + +@flatten.register # type: ignore[attr-defined] +def _(block: AbstractBlock, types: list = [ChainBlock, KronBlock, AddBlock]) -> AbstractBlock: + """Flattens the given types of `CompositeBlock`s if possible. + + Example: + ```python exec="on" source="material-block" result="json" + from qadence import chain, kron, X + from qadence.transpile import flatten + from qadence.blocks import ChainBlock, KronBlock, AddBlock + + x = chain(chain(chain(X(0))), kron(kron(X(0)))) + + # flatten only `ChainBlock`s + assert flatten(x, [ChainBlock]) == chain(X(0), kron(kron(X(0)))) + + # flatten `ChainBlock`s and `KronBlock`s + assert flatten(x, [ChainBlock, KronBlock]) == chain(X(0), kron(X(0))) + + # flatten `AddBlock`s (does nothing in this case) + assert flatten(x, [AddBlock]) == x + ``` + """ + if isinstance(block, CompositeBlock): + + def fn(b: AbstractBlock, T: Type) -> AbstractBlock: + return _construct(type(block), tuple(_flat_blocks(b, T))) + + return reduce(fn, types, block) # type: ignore[arg-type] + elif isinstance(block, ScaleBlock): + blk = deepcopy(block) + blk.block = flatten(block.block, types=types) + return blk + else: + return block + + +@flatten.register # type: ignore[attr-defined] +def _(circuit: QuantumCircuit, types: list = [ChainBlock, KronBlock, AddBlock]) -> QuantumCircuit: + return QuantumCircuit(circuit.n_qubits, flatten(circuit.block, types=types)) diff --git a/tests/backends/test_backends.py b/tests/backends/test_backends.py index 3b038340a..76a90e01d 100644 --- a/tests/backends/test_backends.py +++ b/tests/backends/test_backends.py @@ -13,7 +13,7 @@ from qadence import BackendName, DiffMode from qadence.backend import BackendConfiguration -from qadence.backends.api import backend_factory +from qadence.backends.api import backend_factory, config_factory from qadence.blocks import AbstractBlock, chain, kron from qadence.circuit import QuantumCircuit from qadence.constructors import total_magnetization @@ -29,6 +29,7 @@ random_state, zero_state, ) +from qadence.transpile import flatten from qadence.utils import nqubits_to_basis BACKENDS = BackendName.list() @@ -318,3 +319,24 @@ def test_output_cphase_batching(bsize: int) -> None: assert torch.allclose(exp_list[0], exp_list[1]) assert equivalent_state(wf_list[0], wf_list[1]) + + +def test_custom_transpilation_passes() -> None: + backend_list = [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER] + + block = chain(chain(chain(RX(0, np.pi / 2))), kron(kron(RX(0, np.pi / 2)))) + circuit = QuantumCircuit(1, block) + + for name in backend_list: + config = config_factory(name, {}) + config.transpilation_passes = [flatten] + backend = backend_factory(name, configuration=config) + conv = backend.convert(circuit) + + config = config_factory(name, {}) + config.transpilation_passes = [] + backend_no_transp = backend_factory(name, configuration=config) + conv_no_transp = backend_no_transp.convert(circuit) + + assert conv.circuit.original == conv_no_transp.circuit.original + assert conv.circuit.abstract != conv_no_transp.circuit.abstract diff --git a/tests/qadence/test_transpile.py b/tests/qadence/test_transpile.py index 9862f6f70..085bbc14d 100644 --- a/tests/qadence/test_transpile.py +++ b/tests/qadence/test_transpile.py @@ -8,7 +8,7 @@ def test_flatten() -> None: - from qadence.transpile.block import _flat_blocks + from qadence.transpile.flatten import _flat_blocks x: AbstractBlock From 64e7312a87ae568c57ccd32525eae5ff1dcfa65a Mon Sep 17 00:00:00 2001 From: Vytautas Abramavicius <145791635+vytautas-a@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:53:04 +0200 Subject: [PATCH 2/2] Cloud interface implementation in pulser backend (#117) Added also a better exception message in the execution.py module when a block with global support is passed Co-authored-by: Mario Dagrada <mariodagrada24@gmail.com> --- docs/digital_analog_qc/pulser-basic.md | 22 +++++++- pyproject.toml | 4 +- qadence/backends/pulser/backend.py | 72 ++++++++++++++++++++------ qadence/backends/pulser/cloud.py | 63 ++++++++++++++++++++++ qadence/backends/pulser/config.py | 21 +++++++- qadence/execution.py | 24 +++++++-- 6 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 qadence/backends/pulser/cloud.py diff --git a/docs/digital_analog_qc/pulser-basic.md b/docs/digital_analog_qc/pulser-basic.md index 12a551709..6d192da31 100644 --- a/docs/digital_analog_qc/pulser-basic.md +++ b/docs/digital_analog_qc/pulser-basic.md @@ -6,7 +6,27 @@ to directly manipulate them if required by, for instance, optimal pulse shaping. !!! note The Pulser backend is still experimental and the interface might change in the future. - Please note that it does not support `DiffMode.AD`. + Please note that it does not support `DiffMode.AD`. + +!!! note + With the Pulser backend, `qadence` simulations can be executed on the cloud emulators available on the PASQAL + cloud platform. In order to do so, make to have valid credentials for the PASQAL cloud platform and use + the following configuration for the Pulser backend: + + ```python exec="off" source="material-block" html="1" session="pulser-basic" + config = { + "cloud_configuration": { + "username": "<changeme>", + "password": "<changeme>", + "project_id": "<changeme>", # the project should have access to emulators + "platform": "EMU_FREE" # choose between `EMU_TN` and `EMU_FREE` + } + } + ``` + +For inquiries and more details on the cloud credentials, please contact +[info@pasqal.com](mailto:info@pasqal.com). + ## Default qubit interaction diff --git a/pyproject.toml b/pyproject.toml index 29ffb298b..e6b09f887 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ allow-direct-references = true allow-ambiguous-features = true [project.optional-dependencies] -pulser = ["pulser>=v0.12.0"] +pulser = ["pulser>=v0.15.2", "pasqal-cloud>=0.3.5"] braket = ["amazon-braket-sdk"] visualization = [ "graphviz", @@ -57,7 +57,7 @@ visualization = [ # "scour", ] all = [ - "pulser>=0.12.0", + "pulser>=0.15.2", "amazon-braket-sdk", "graphviz", # FIXME: will be needed once we support latex labels diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 9e86a7ca0..9dbd21d32 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -23,14 +23,17 @@ from qadence.overlap import overlap_exact from qadence.register import Register from qadence.transpile import transpile -from qadence.utils import Endianness +from qadence.utils import Endianness, get_logger from .channels import GLOBAL_CHANNEL, LOCAL_CHANNEL +from .cloud import get_client from .config import Configuration from .convert_ops import convert_observable from .devices import Device, IdealDevice, RealisticDevice from .pulses import add_pulses +logger = get_logger(__file__) + WEAK_COUPLING_CONST = 1.2 DEFAULT_SPACING = 8.0 # µm (standard value) @@ -101,20 +104,46 @@ def make_sequence(circ: QuantumCircuit, config: Configuration) -> Sequence: # TODO: make it parallelized -# TODO: add execution on the cloud platform def simulate_sequence( - sequence: Sequence, config: Configuration, state: np.ndarray | None -) -> SimulationResults: - simulation = QutipEmulator.from_sequence( - sequence, - sampling_rate=config.sampling_rate, - config=config.sim_config, - with_modulation=config.with_modulation, - ) - if state is not None: - simulation.set_initial_state(qutip.Qobj(state)) + sequence: Sequence, config: Configuration, state: Tensor, n_shots: int | None = None +) -> SimulationResults | Counter: + if config.cloud_configuration is not None: + client = get_client(config.cloud_configuration) + + serialized_sequence = sequence.to_abstract_repr() + params: list[dict] = [{"runs": n_shots, "variables": {}}] + + batch = client.create_batch( + serialized_sequence, + jobs=params, + emulator=str(config.cloud_configuration.platform), + wait=True, + ) + + job = list(batch.jobs.values())[0] + if job.errors is not None: + logger.error( + f"The cloud job with ID {job.id} has " + f"failed for the following reason: {job.errors}" + ) + + return Counter(job.result) - return simulation.run(nsteps=config.n_steps_solv, method=config.method_solv) + else: + simulation = QutipEmulator.from_sequence( + sequence, + sampling_rate=config.sampling_rate, + config=config.sim_config, + with_modulation=config.with_modulation, + ) + if state is not None: + simulation.set_initial_state(qutip.Qobj(state)) + + sim_result = simulation.run(nsteps=config.n_steps_solv, method=config.method_solv) + if n_shots is not None: + return sim_result.sample_final_state(n_shots) + else: + return sim_result @dataclass(frozen=True, eq=True) @@ -177,14 +206,24 @@ def run( endianness: Endianness = Endianness.BIG, ) -> Tensor: vals = to_list_of_dicts(param_values) + + # TODO: relax this constraint + if self.config.cloud_configuration is not None: + raise ValueError( + "Cannot retrieve the wavefunction from cloud simulations. Do not" + "specify any cloud credentials to use the .run() method" + ) + state = state if state is None else _convert_init_state(state) batched_wf = np.zeros((len(vals), 2**circuit.abstract.n_qubits), dtype=np.complex128) for i, param_values_el in enumerate(vals): sequence = self.assign_parameters(circuit, param_values_el) - sim_result = simulate_sequence(sequence, self.config, state) + sim_result = simulate_sequence(sequence, self.config, state, n_shots=None) wf = ( - sim_result.get_final_state(ignore_global_phase=False, normalize=True) + sim_result.get_final_state( # type:ignore [union-attr] + ignore_global_phase=False, normalize=True + ) .full() .flatten() ) @@ -219,8 +258,7 @@ def sample( samples = [] for param_values_el in vals: sequence = self.assign_parameters(circuit, param_values_el) - sim_result = simulate_sequence(sequence, self.config, state) - sample = sim_result.sample_final_state(n_shots) + sample = simulate_sequence(sequence, self.config, state, n_shots=n_shots) samples.append(sample) if endianness != self.native_endianness: from qadence.transpile import invert_endianness diff --git a/qadence/backends/pulser/cloud.py b/qadence/backends/pulser/cloud.py new file mode 100644 index 000000000..8f7f06705 --- /dev/null +++ b/qadence/backends/pulser/cloud.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import os +from functools import lru_cache +from typing import Optional + +from pasqal_cloud import AUTH0_CONFIG, PASQAL_ENDPOINTS, SDK, Auth0Conf, Endpoints, TokenProvider + +from .config import DEFAULT_CLOUD_ENV, CloudConfiguration + + +@lru_cache(maxsize=5) +def _get_client( + username: Optional[str] = None, + password: Optional[str] = None, + project_id: Optional[str] = None, + environment: Optional[str] = None, + token_provider: Optional[TokenProvider] = None, +) -> SDK: + auth0conf: Optional[Auth0Conf] = None + endpoints: Optional[Endpoints] = None + + username = os.environ.get("PASQAL_CLOUD_USERNAME", "") if username is None else username + password = os.environ.get("PASQAL_CLOUD_PASSWORD", "") if password is None else password + project_id = os.environ.get("PASQAL_CLOUD_PROJECT_ID", "") if project_id is None else project_id + + environment = ( + os.environ.get("PASQAL_CLOUD_ENV", DEFAULT_CLOUD_ENV) + if environment is None + else environment + ) + + # setup configuration for environments different than production + if environment == "preprod": + auth0conf = AUTH0_CONFIG["preprod"] + endpoints = PASQAL_ENDPOINTS["preprod"] + elif environment == "dev": + auth0conf = AUTH0_CONFIG["dev"] + endpoints = PASQAL_ENDPOINTS["dev"] + + if all([username, password, project_id]) or all([token_provider, project_id]): + pass + else: + raise Exception("You must either provide project_id and log-in details or a token provider") + + return SDK( + username=username, + password=password, + project_id=project_id, + auth0=auth0conf, + endpoints=endpoints, + token_provider=token_provider, + ) + + +def get_client(credentials: CloudConfiguration) -> SDK: + return _get_client( + username=credentials.username, + password=credentials.password, + project_id=credentials.project_id, + environment=credentials.environment, + token_provider=credentials.token_provider, + ) diff --git a/qadence/backends/pulser/config.py b/qadence/backends/pulser/config.py index 5915b17e9..c1e97b625 100644 --- a/qadence/backends/pulser/config.py +++ b/qadence/backends/pulser/config.py @@ -3,15 +3,26 @@ from dataclasses import dataclass from typing import Optional +from pasqal_cloud import TokenProvider +from pasqal_cloud.device import EmulatorType from pulser_simulation.simconfig import SimConfig from qadence.backend import BackendConfiguration from qadence.blocks.analog import Interaction -from qadence.logger import get_logger from .devices import Device -logger = get_logger(__name__) +DEFAULT_CLOUD_ENV = "prod" + + +@dataclass +class CloudConfiguration: + platform: EmulatorType = EmulatorType.EMU_FREE + username: str | None = None + password: str | None = None + project_id: str | None = None + environment: str = "prod" + token_provider: TokenProvider | None = None @dataclass @@ -55,7 +66,13 @@ class Configuration(BackendConfiguration): """Type of interaction introduced in the Hamiltonian. Currently, only NN interaction is support. XY interaction is possible but not implemented""" + # configuration for cloud simulations + cloud_configuration: Optional[CloudConfiguration] = None + def __post_init__(self) -> None: super().__post_init__() if self.sim_config is not None and not isinstance(self.sim_config, SimConfig): raise TypeError("Wrong 'sim_config' attribute type, pass a valid SimConfig object!") + + if isinstance(self.cloud_configuration, dict): + self.cloud_configuration = CloudConfiguration(**self.cloud_configuration) diff --git a/qadence/execution.py b/qadence/execution.py index ecbeb2ff3..90ff1a641 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -10,6 +10,7 @@ from qadence.backend import BackendConfiguration, BackendName from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit +from qadence.qubit_support import QubitSupport from qadence.register import Register from qadence.types import DiffMode from qadence.utils import Endianness @@ -18,6 +19,18 @@ __all__ = ["run", "sample", "expectation"] +def _n_qubits_block(block: AbstractBlock) -> int: + if isinstance(block.qubit_support, QubitSupport) and block.qubit_support.is_global: + raise ValueError( + "You cannot determine the number of qubits for" + "a block with global qubit support. Use a QuantumCircuit" + "instead and explicitly supply the number of qubits as follows: " + "\nn_qubits = 4\nQuantumCircuit(n_qubits, block)" + ) + else: + return block.n_qubits + + @singledispatch def run( x: Union[QuantumCircuit, AbstractBlock, Register, int], @@ -79,7 +92,8 @@ def _(n_qubits: int, block: AbstractBlock, **kwargs: Any) -> Tensor: @run.register def _(block: AbstractBlock, **kwargs: Any) -> Tensor: - return run(Register(block.n_qubits), block, **kwargs) + n_qubits = _n_qubits_block(block) + return run(Register(n_qubits), block, **kwargs) @singledispatch @@ -146,8 +160,8 @@ def _(n_qubits: int, block: AbstractBlock, **kwargs: Any) -> Tensor: @sample.register def _(block: AbstractBlock, **kwargs: Any) -> Tensor: - reg = Register(block.n_qubits) - return sample(reg, block, **kwargs) + n_qubits = _n_qubits_block(block) + return sample(Register(n_qubits), block, **kwargs) @singledispatch @@ -260,5 +274,5 @@ def _( def _( block: AbstractBlock, observable: Union[list[AbstractBlock], AbstractBlock], **kwargs: Any ) -> Tensor: - reg = Register(block.n_qubits) - return expectation(QuantumCircuit(reg, block), observable, **kwargs) + n_qubits = _n_qubits_block(block) + return expectation(QuantumCircuit(Register(n_qubits), block), observable, **kwargs)