From f4a63f056eb4b1da7fe4a0e99106ca68d40e3a4a Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 17 Feb 2026 17:27:40 +0100 Subject: [PATCH 1/5] fix: handle braket AHS devices incompatibility --- mpqp/execution/connection/aws_connection.py | 30 ++++++++++++++-- mpqp/execution/providers/aws.py | 38 ++++++++++++++++----- mpqp/execution/result.py | 14 ++++++-- mpqp/tools/errors.py | 4 +++ 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/mpqp/execution/connection/aws_connection.py b/mpqp/execution/connection/aws_connection.py index 779a6797..dea1d158 100644 --- a/mpqp/execution/connection/aws_connection.py +++ b/mpqp/execution/connection/aws_connection.py @@ -356,7 +356,11 @@ def get_aws_braket_account_info() -> str: return result -def get_braket_device(device: AWSDevice, is_noisy: bool = False) -> "BraketDevice": +def get_braket_device( + device: AWSDevice, + is_noisy: bool = False, + is_gate_model: bool = True, +) -> "BraketDevice": """Returns the AwsDevice device associate with the AWSDevice in parameter. Args: @@ -378,6 +382,11 @@ def get_braket_device(device: AWSDevice, is_noisy: bool = False) -> "BraketDevic """ from braket.devices import LocalSimulator + from mpqp.tools.errors import ( + AWSBraketRemoteExecutionError, + DeviceJobIncompatibleError, + ) + if not device.is_remote(): if is_noisy: return LocalSimulator("braket_dm") @@ -397,7 +406,8 @@ def get_braket_device(device: AWSDevice, is_noisy: bool = False) -> "BraketDevic aws_session.add_braket_user_agent( user_agent="APN/1.0 ColibriTD/1.0 MPQP/" + mpqp_version ) - return AwsDevice(device.get_arn(), aws_session=aws_session) + braket_device = AwsDevice(device.get_arn(), aws_session=aws_session) + except ValueError as ve: raise AWSBraketRemoteExecutionError( "Failed to retrieve remote AWS device. Please check the arn, or if the " @@ -410,6 +420,22 @@ def get_braket_device(device: AWSDevice, is_noisy: bool = False) -> "BraketDevic "\nTrace: " + str(err) ) + if is_gate_model: + actions = getattr(getattr(braket_device, "properties", None), "action", None) + if actions is not None: + supported = [getattr(k, "value", str(k)) for k in actions.keys()] + supports_gate_model = any( + ("openqasm" in action.lower()) or ("jaqcd" in action.lower()) for action in supported + ) + if not supports_gate_model: + raise DeviceJobIncompatibleError( + f"{device.name} does not support gate-model workloads. " + f"Supported Braket action types: {supported}. " + "This is an AHS device, which cannot run MPQP QCircuit." + ) + + return braket_device + def get_all_task_ids() -> list[str]: """Retrieves all the task ids of this account/group from AWS. diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index b74e35f0..ab6009ef 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -16,7 +16,11 @@ from mpqp.execution.job import Job, JobStatus, JobType from mpqp.execution.result import Result, Sample, StateVector from mpqp.noise.noise_model import NoiseModel -from mpqp.tools.errors import AWSBraketRemoteExecutionError, DeviceJobIncompatibleError +from mpqp.tools.errors import ( + AWSBraketRemoteExecutionError, + DeviceJobIncompatibleError, + DeviceJobIncompatibleWarning, +) if TYPE_CHECKING: from braket.circuits import Circuit @@ -109,16 +113,31 @@ def run_braket(job: Job) -> Result: f"{job.device} instead" ) + import warnings + from braket.tasks import GateModelQuantumTaskResult - if isinstance(job.measure, ExpectationMeasure): - return run_braket_observable(job) - _, task = submit_job_braket(job) - res = task.result() - if TYPE_CHECKING: - assert isinstance(res, GateModelQuantumTaskResult) + try: + if isinstance(job.measure, ExpectationMeasure): + return run_braket_observable(job) + + _, task = submit_job_braket(job) + res = task.result() + if TYPE_CHECKING: + assert isinstance(res, GateModelQuantumTaskResult) - return extract_result(res, job, job.device) + return extract_result(res, job, job.device) + + except DeviceJobIncompatibleError as e: + warnings.warn(str(e), DeviceJobIncompatibleWarning, stacklevel=5) + + job.status = JobStatus.ERROR + return Result( + job, + data=None, + errors="Unsupported Braket backend for QCircuit (see warning).", + shots=0, + ) def run_braket_observable(job: Job): @@ -151,6 +170,7 @@ def run_braket_observable(job: Job): job.device, is_noisy=bool(job.circuit.noises), ) + if job.measure is None: raise NotImplementedError("job.measure is None") assert isinstance(job.measure, ExpectationMeasure) @@ -270,7 +290,7 @@ def run_braket_observable(job: Job): ) if braket_sum is not None: - from braket.program_sets import ProgramSet, CircuitBinding + from braket.program_sets import CircuitBinding, ProgramSet from braket.tasks.program_set_quantum_task_result import ( ProgramSetQuantumTaskResult, ) diff --git a/mpqp/execution/result.py b/mpqp/execution/result.py index 4f679741..38d1e747 100644 --- a/mpqp/execution/result.py +++ b/mpqp/execution/result.py @@ -30,7 +30,7 @@ import numpy.typing as npt from mpqp.core.instruction.measurement.basis_measure import BasisMeasure -from mpqp.execution import Job, JobType +from mpqp.execution import Job, JobStatus, JobType from mpqp.execution.devices import AvailableDevice from mpqp.tools.display import clean_1D_array, clean_number_repr from mpqp.tools.errors import ResultAttributeError @@ -288,8 +288,8 @@ class Result: def __init__( self, job: Job, - data: float | dict["str", float] | StateVector | list[Sample], - errors: Optional[float | dict[Any, Any]] = None, + data: float | dict["str", float] | StateVector | list[Sample] | None, + errors: Optional[float | dict[Any, Any] | str] = None, shots: int = 0, ): self.job = job @@ -305,6 +305,11 @@ def __init__( """See parameter description.""" self._data = data + if data is None: + if job.status != JobStatus.ERROR: + raise TypeError("Result data cannot be None unless job.status == ERROR") + return + # depending on the type of job, fills the result info from the data in parameter if job.job_type == JobType.OBSERVABLE: if not isinstance(data, float) and not isinstance(data, dict): @@ -458,6 +463,9 @@ def __str__(self): label = "" if self.job.circuit.label is None else self.job.circuit.label + ", " header = f"Result: {label}{type(self.device).__name__}, {self.device.name}" + if self.job.status == JobStatus.ERROR: + return f"{header}\n Error: {self.error}" + if self.job.job_type == JobType.SAMPLE: measures = self.job.circuit.measurements if not len(measures) == 1: diff --git a/mpqp/tools/errors.py b/mpqp/tools/errors.py index f44b357e..54a14154 100644 --- a/mpqp/tools/errors.py +++ b/mpqp/tools/errors.py @@ -34,6 +34,10 @@ class DeviceJobIncompatibleError(ValueError): for the selected device (for example SAMPLE job on a statevector simulator).""" +class DeviceJobIncompatibleWarning(UserWarning): + """A warning is issued when a job is not compatible with the selected device.""" + + class RemoteExecutionError(ConnectionError): """Raised when an error occurred during a remote connection, submission or execution.""" From 478b3f09d8810ae2752edfc60444b928d9d9528b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Feb 2026 16:28:23 +0000 Subject: [PATCH 2/5] chore: Files formated --- mpqp/execution/connection/aws_connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mpqp/execution/connection/aws_connection.py b/mpqp/execution/connection/aws_connection.py index dea1d158..05a60e99 100644 --- a/mpqp/execution/connection/aws_connection.py +++ b/mpqp/execution/connection/aws_connection.py @@ -425,7 +425,8 @@ def get_braket_device( if actions is not None: supported = [getattr(k, "value", str(k)) for k in actions.keys()] supports_gate_model = any( - ("openqasm" in action.lower()) or ("jaqcd" in action.lower()) for action in supported + ("openqasm" in action.lower()) or ("jaqcd" in action.lower()) + for action in supported ) if not supports_gate_model: raise DeviceJobIncompatibleError( From 6c6944cf209ff53dfacfcecb5f7ec8827b5d5bae Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 10 Mar 2026 10:52:34 +0100 Subject: [PATCH 3/5] fix: remove pkg_resources for newer python versions --- mpqp/execution/connection/aws_connection.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mpqp/execution/connection/aws_connection.py b/mpqp/execution/connection/aws_connection.py index dea1d158..2bb10f9e 100644 --- a/mpqp/execution/connection/aws_connection.py +++ b/mpqp/execution/connection/aws_connection.py @@ -393,7 +393,8 @@ def get_braket_device( else: return LocalSimulator() - import pkg_resources + from importlib.metadata import PackageNotFoundError, version + from botocore.exceptions import NoRegionError from braket.aws import AwsDevice, AwsSession @@ -402,7 +403,12 @@ def get_braket_device( braket_client = boto3.client("braket", region_name=device.get_region()) aws_session = AwsSession(braket_client=braket_client) - mpqp_version = pkg_resources.get_distribution("mpqp").version[:3] + + try: + mpqp_version = version("mpqp") + except PackageNotFoundError: + mpqp_version = "0.0.0+unknown" + aws_session.add_braket_user_agent( user_agent="APN/1.0 ColibriTD/1.0 MPQP/" + mpqp_version ) @@ -425,7 +431,8 @@ def get_braket_device( if actions is not None: supported = [getattr(k, "value", str(k)) for k in actions.keys()] supports_gate_model = any( - ("openqasm" in action.lower()) or ("jaqcd" in action.lower()) for action in supported + ("openqasm" in action.lower()) or ("jaqcd" in action.lower()) + for action in supported ) if not supports_gate_model: raise DeviceJobIncompatibleError( From dbf109c454de2e9c7381f716843c71d76d91640a Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 17 Mar 2026 16:49:32 +0100 Subject: [PATCH 4/5] chore: update get_braket_device --- mpqp/execution/connection/aws_connection.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mpqp/execution/connection/aws_connection.py b/mpqp/execution/connection/aws_connection.py index 2bb10f9e..61b5c766 100644 --- a/mpqp/execution/connection/aws_connection.py +++ b/mpqp/execution/connection/aws_connection.py @@ -366,6 +366,7 @@ def get_braket_device( Args: device: AWSDevice element describing which remote/local AwsDevice we want. is_noisy: If the expected device is noisy or not. + is_gate_model: If the expected device is gate-model or not. Raises: AWSBraketRemoteExecutionError: If the device or the region could not be @@ -393,21 +394,18 @@ def get_braket_device( else: return LocalSimulator() - from importlib.metadata import PackageNotFoundError, version - from botocore.exceptions import NoRegionError from braket.aws import AwsDevice, AwsSession + import mpqp + try: import boto3 braket_client = boto3.client("braket", region_name=device.get_region()) aws_session = AwsSession(braket_client=braket_client) - try: - mpqp_version = version("mpqp") - except PackageNotFoundError: - mpqp_version = "0.0.0+unknown" + mpqp_version = getattr(mpqp, "__version__", "0.0.0+unknown") aws_session.add_braket_user_agent( user_agent="APN/1.0 ColibriTD/1.0 MPQP/" + mpqp_version From 23d2089cd1d47a07f6a6319a0087c498e6a3e7f8 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 17 Mar 2026 18:01:31 +0100 Subject: [PATCH 5/5] chore: refine execution failure handling, add job status message --- mpqp/execution/job.py | 4 ++++ mpqp/execution/providers/aws.py | 6 ++++-- mpqp/execution/result.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mpqp/execution/job.py b/mpqp/execution/job.py index 08d23d48..a826cbde 100644 --- a/mpqp/execution/job.py +++ b/mpqp/execution/job.py @@ -125,6 +125,9 @@ def __init__( while before it is set to the right value (For instance, a job submission can require handshake protocols to conclude before attributing an id to the job).""" + self.status_message: Optional[str] = None + """Optional message associated with the current job status, especially + for execution errors.""" @property def measure(self) -> Optional[Measure]: @@ -188,6 +191,7 @@ def to_dict(self): "measure": self.measure, "id": self.id, "status": self.status, + "status_message": self.status_message, } @staticmethod diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index ab6009ef..6807a290 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -129,13 +129,15 @@ def run_braket(job: Job) -> Result: return extract_result(res, job, job.device) except DeviceJobIncompatibleError as e: - warnings.warn(str(e), DeviceJobIncompatibleWarning, stacklevel=5) + warnings.warn(str(e), DeviceJobIncompatibleWarning, stacklevel=1) job.status = JobStatus.ERROR + job.status_message = "Job execution failed. See warning for details." + return Result( job, data=None, - errors="Unsupported Braket backend for QCircuit (see warning).", + errors=None, shots=0, ) diff --git a/mpqp/execution/result.py b/mpqp/execution/result.py index 38d1e747..ccefec77 100644 --- a/mpqp/execution/result.py +++ b/mpqp/execution/result.py @@ -289,7 +289,7 @@ def __init__( self, job: Job, data: float | dict["str", float] | StateVector | list[Sample] | None, - errors: Optional[float | dict[Any, Any] | str] = None, + errors: Optional[float | dict[Any, Any]] = None, shots: int = 0, ): self.job = job @@ -464,7 +464,7 @@ def __str__(self): header = f"Result: {label}{type(self.device).__name__}, {self.device.name}" if self.job.status == JobStatus.ERROR: - return f"{header}\n Error: {self.error}" + return f"{header}\n Status: ERROR\n Message: {self.job.status_message}" if self.job.job_type == JobType.SAMPLE: measures = self.job.circuit.measurements