Source code for qbraid.runtime.pasqal.provider

# Copyright 2026 qBraid
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Module defining Pasqal provider class.

"""

from __future__ import annotations

import os
from typing import TYPE_CHECKING

from qbraid._caching import cached_method
from qbraid._logging import logger
from qbraid.programs import QPROGRAM_REGISTRY, ExperimentType, ProgramSpec
from qbraid.runtime.exceptions import ResourceNotFoundError
from qbraid.runtime.profile import TargetProfile
from qbraid.runtime.provider import QuantumProvider

from .device import PasqalDevice

if TYPE_CHECKING:
    from pasqal_cloud import SDK as PasqalSDK
    from pasqal_cloud.authentication import TokenProvider


# Default device IDs exposed by Pasqal Cloud. Mirrors
# ``pasqal_cloud.device.DeviceTypeName``; kept here as a literal so we don't
# need to import ``pasqal_cloud`` to enumerate devices ahead of authentication.
_DEFAULT_DEVICE_IDS: tuple[str, ...] = (
    "FRESNEL",
    "FRESNEL_CAN1",
    "EMU_FREE",
    "EMU_TN",
    "EMU_FRESNEL",
    "EMU_MPS",
    "EMU_SV",
)


def _is_simulator(device_id: str) -> bool:
    """Return ``True`` for emulator-style device IDs (``EMU_*``)."""
    return device_id.upper().startswith("EMU_")


def _build_profile(device_id: str, num_qubits: int | None = None) -> TargetProfile:
    """Build a :class:`TargetProfile` for a Pasqal device.

    Args:
        device_id: Pasqal device identifier (e.g. ``"FRESNEL"``, ``"EMU_FREE"``).
        num_qubits: Optional max-qubit count, when known. Pasqal exposes device
            constraints through ``SDK.get_device_specs_dict``; the value is
            stored on the profile as additional metadata under
            ``device_specs`` when provided by the caller.

    Returns:
        A :class:`TargetProfile` describing the device for the qBraid runtime.
    """
    sequence_type = QPROGRAM_REGISTRY.get("pulser")
    if sequence_type is None:
        # pylint: disable-next=import-outside-toplevel
        from pulser import Sequence as sequence_type  # type: ignore[assignment]

    program_spec = ProgramSpec(
        sequence_type,
        alias="pulser",
        serialize=lambda sequence: sequence.to_abstract_repr(),
    )
    return TargetProfile(
        device_id=device_id,
        simulator=_is_simulator(device_id),
        experiment_type=ExperimentType.ANALOG,
        num_qubits=num_qubits,
        program_spec=program_spec,
        provider_name="pasqal",
    )


[docs] class PasqalProvider(QuantumProvider): """Implements qBraid's :class:`QuantumProvider` interface for Pasqal Cloud Services. Authentication mirrors the ``pasqal_cloud.SDK`` constructor: provide a ``username`` / ``password`` pair, a ``token_provider``, or set the ``PASQAL_USERNAME``, ``PASQAL_PASSWORD``, and ``PASQAL_PROJECT_ID`` environment variables. Example: >>> from qbraid.runtime.pasqal import PasqalProvider >>> provider = PasqalProvider( ... username="me@example.com", ... password="...", ... project_id="...", ... ) >>> device = provider.get_device("EMU_FREE") """
[docs] def __init__( # pylint: disable=too-many-arguments self, username: str | None = None, password: str | None = None, project_id: str | None = None, token_provider: TokenProvider | None = None, sdk: PasqalSDK | None = None, ): """Initialize the Pasqal provider. Args: username: Pasqal Cloud Services account username. Falls back to the ``PASQAL_USERNAME`` environment variable. password: Pasqal Cloud Services password. Falls back to the ``PASQAL_PASSWORD`` environment variable. If left unset and a ``username`` is provided, ``pasqal-cloud`` will prompt interactively. project_id: Pasqal project ID under which batches are submitted. Falls back to the ``PASQAL_PROJECT_ID`` environment variable. token_provider: Custom :class:`pasqal_cloud.TokenProvider` for machine-to-machine or pre-issued-token authentication. Takes precedence over username/password when supplied. sdk: Pre-built ``pasqal_cloud.SDK`` instance. When provided, all other authentication arguments are ignored. Useful for testing and advanced setups. Raises: ValueError: If no authentication material can be resolved. """ if sdk is not None: self._sdk = sdk return # pylint: disable-next=import-outside-toplevel from pasqal_cloud import SDK resolved_project_id = project_id or os.getenv("PASQAL_PROJECT_ID") resolved_username = username or os.getenv("PASQAL_USERNAME") resolved_password = password or os.getenv("PASQAL_PASSWORD") if token_provider is None and not resolved_username: raise ValueError( "Pasqal authentication is required. Provide a `username` (and" " optionally a `password`), a `token_provider`, or set the" " PASQAL_USERNAME / PASQAL_PASSWORD environment variables." ) self._sdk = SDK( username=resolved_username, password=resolved_password, token_provider=token_provider, project_id=resolved_project_id, )
@property def sdk(self) -> PasqalSDK: """Return the underlying :class:`pasqal_cloud.SDK` instance.""" return self._sdk def _device_specs(self) -> dict[str, str]: """Return Pasqal device specs, swallowing transport errors. ``get_device_specs_dict`` may require authentication, so we degrade gracefully and return an empty dict on failure -- callers can still instantiate emulator devices without it. """ try: return self._sdk.get_device_specs_dict() except Exception as exc: # pylint: disable=broad-except logger.warning("Could not fetch Pasqal device specs: %s", exc) return {} @cached_method def get_devices(self) -> list[PasqalDevice]: """Return the list of Pasqal devices currently available to the caller. Falls back to a static enumeration of well-known device IDs when the cloud spec endpoint is unreachable, so users can still build emulator runs offline. """ specs = self._device_specs() device_ids = set(specs.keys()) | set(_DEFAULT_DEVICE_IDS) return [ PasqalDevice(profile=_build_profile(device_id), sdk=self._sdk) for device_id in device_ids ] @cached_method def get_device(self, device_id: str) -> PasqalDevice: """Return a :class:`PasqalDevice` for the given device identifier. Args: device_id: Pasqal device identifier (case-insensitive). Common values include ``"FRESNEL"`` (QPU), ``"EMU_FREE"``, ``"EMU_TN"``, ``"EMU_FRESNEL"``, ``"EMU_MPS"``, and ``"EMU_SV"``. Raises: ResourceNotFoundError: If the device identifier is not recognised by Pasqal Cloud and is not one of the well-known defaults. """ normalized = device_id.strip().upper() specs = self._device_specs() known = set(specs.keys()) | set(_DEFAULT_DEVICE_IDS) if normalized not in known: raise ResourceNotFoundError( f"Device '{device_id}' not found in Pasqal Cloud. " f"Available devices: {sorted(known)}" ) return PasqalDevice(profile=_build_profile(normalized), sdk=self._sdk) def __hash__(self) -> int: # The SDK holds credentials and a session; pasqal_cloud objects are not # hashable, so we key on the project_id plus instance identity. return hash(("pasqal", getattr(self._sdk, "project_id", None), id(self._sdk)))