Skip to content

Commit

Permalink
Merge branch 'main' into ds/bump103
Browse files Browse the repository at this point in the history
  • Loading branch information
nmheim committed Nov 3, 2023
2 parents c2abbd2 + 64e7312 commit e1bb652
Show file tree
Hide file tree
Showing 18 changed files with 410 additions and 151 deletions.
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",
"amazon-braket-sdk",
"graphviz",
# FIXME: will be needed once we support latex labels
Expand Down
17 changes: 14 additions & 3 deletions qadence/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,28 @@
)
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:
_use_gate_params: bool = True
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
Expand All @@ -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

Expand Down
Empty file.
20 changes: 12 additions & 8 deletions qadence/backends/braket/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down
13 changes: 9 additions & 4 deletions qadence/backends/braket/config.py
Original file line number Diff line number Diff line change
@@ -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"""
80 changes: 62 additions & 18 deletions qadence/backends/pulser/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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()
)
Expand Down Expand Up @@ -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
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,
)
Loading

0 comments on commit e1bb652

Please sign in to comment.