Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud interface implementation in pulser backend #117

Merged
merged 7 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/digital_analog_qc/pulser-basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
[[email protected]](mailto:[email protected]).


## Default qubit interaction

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -57,7 +57,7 @@ visualization = [
# "scour",
]
all = [
"pulser>=0.12.0",
"pulser>=0.15.2",
madagra marked this conversation as resolved.
Show resolved Hide resolved
"amazon-braket-sdk",
"graphviz",
# FIXME: will be needed once we support latex labels
Expand Down
72 changes: 55 additions & 17 deletions qadence/backends/pulser/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
madagra marked this conversation as resolved.
Show resolved Hide resolved
else:
return sim_result


@dataclass(frozen=True, eq=True)
Expand Down Expand Up @@ -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()
)
Expand Down Expand Up @@ -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
Expand Down
63 changes: 63 additions & 0 deletions qadence/backends/pulser/cloud.py
Original file line number Diff line number Diff line change
@@ -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,
)
21 changes: 19 additions & 2 deletions qadence/backends/pulser/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
24 changes: 19 additions & 5 deletions qadence/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)