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)