diff --git a/docs/digital_analog_qc/pulser-basic.md b/docs/digital_analog_qc/pulser-basic.md index 12a55170..6d192da3 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": "", + "password": "", + "project_id": "", # 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 29ffb298..e6b09f88 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/backend.py b/qadence/backend.py index 29348840..5f280d32 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 e69de29b..00000000 diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 5c7aa868..9046a1d4 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 9f83f8cd..94d1856f 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 6566494f..9dbd21d3 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -22,14 +22,18 @@ from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.register import Register -from qadence.utils import Endianness +from qadence.transpile import transpile +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) @@ -100,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 simulation.run(nsteps=config.n_steps_solv, method=config.method_solv) + return Counter(job.result) + + 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) @@ -130,9 +160,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 @@ -171,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() ) @@ -213,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 00000000..8f7f0670 --- /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 d247f912..c1e97b62 100644 --- a/qadence/backends/pulser/config.py +++ b/qadence/backends/pulser/config.py @@ -3,6 +3,8 @@ 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 @@ -10,45 +12,67 @@ from .devices import Device +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 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""" + + # 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/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index e12c5632..66ead5a7 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 df9e95a7..f1f5c496 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/execution.py b/qadence/execution.py index ecbeb2ff..90ff1a64 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) diff --git a/qadence/extensions.py b/qadence/extensions.py index dd564c44..f3b7428e 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 a50aa6bb..465323d6 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 0478ad04..459511af 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 00000000..577c79c9 --- /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 3b038340..76a90e01 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 9862f6f7..085bbc14 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