From 3defccfc99379ea155c0ee7f4b9138fcef569dbc Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 20 Oct 2025 17:47:44 +0200 Subject: [PATCH 01/39] feat: add VQA remote execution skeleton, to be modified --- mpqp/core/circuit.py | 13 ++- mpqp/execution/vqa/optimizer.py | 17 ++-- mpqp/execution/vqa/vqa.py | 154 +++++++++++++++++++++++++++++--- 3 files changed, 163 insertions(+), 21 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index ad5d0b01..bd8aa288 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -40,6 +40,8 @@ import numpy as np import numpy.typing as npt +from typeguard import TypeCheckError, typechecked + from mpqp.core.instruction import Instruction from mpqp.core.instruction.barrier import Barrier from mpqp.core.instruction.breakpoint import Breakpoint @@ -55,7 +57,6 @@ from mpqp.tools.errors import NonReversibleWarning, NumberQubitsError from mpqp.tools.generics import OneOrMany from mpqp.tools.maths import matrix_eq -from typeguard import TypeCheckError, typechecked if TYPE_CHECKING: from braket.circuits import Circuit as braket_Circuit @@ -63,12 +64,13 @@ from qat.core.wrappers.circuit import Circuit as myQLM_Circuit from qiskit.circuit import QuantumCircuit from sympy import Basic, Expr + from mpqp.execution.devices import ( ATOSDevice, + AvailableDevice, AWSDevice, GOOGLEDevice, IBMDevice, - AvailableDevice, ) from mpqp.execution.simulated_devices import IBMSimulatedDevice @@ -1830,8 +1832,8 @@ def from_other_language( from mpqp.qasm.open_qasm_2_and_3 import open_qasm_3_to_2 from mpqp.qasm.qasm_to_braket import ( - braket_noise_to_mpqp, braket_custom_gates_to_mpqp, + braket_noise_to_mpqp, ) qasm3_code = qcircuit.to_ir(IRType.OPENQASM) @@ -2028,3 +2030,8 @@ def variables(self) -> set[Basic]: def breakpoints(self) -> list[Breakpoint]: """Returns the breakpoints of the circuit in order.""" return [inst for inst in self.instructions if isinstance(inst, Breakpoint)] + + def bind_parameters(self, param_map, device): + """A placeholder for parameter binding logic to the transpiled circuit.""" + # TODO: implement the logic + raise NotImplementedError("bind_parameters() is not implemented yet. ") diff --git a/mpqp/execution/vqa/optimizer.py b/mpqp/execution/vqa/optimizer.py index 028857c2..88aff2df 100644 --- a/mpqp/execution/vqa/optimizer.py +++ b/mpqp/execution/vqa/optimizer.py @@ -2,14 +2,19 @@ :class:`Optimizer` enum lists all the methods validated with the rest of the library.""" -from enum import Enum, auto +from enum import Enum class Optimizer(Enum): """Enum used to select the optimizer for the VQA.""" - BFGS = auto() - COBYLA = auto() - CMAES = auto() - POWELL = auto() - # NELDER-MEAD = auto() + BFGS = "BFGS" + L_BFGS_B = "L-BFGS-B" + COBYLA = "COBYLA" + POWELL = "POWELL" + NELDER_MEAD = "Nelder-Mead" + CMAES = "CMAES" + SLSQP = "SLSQP" + + +##TODO CMAES and SLSQP implementation diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index 10199433..d31cb4ea 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -1,5 +1,6 @@ from __future__ import annotations +from enum import Enum from functools import partial from typing import TYPE_CHECKING, Any, Callable, Collection, Optional, TypeVar, Union @@ -12,7 +13,7 @@ from mpqp.core.circuit import QCircuit from mpqp.core.instruction import ExpectationMeasure -from mpqp.execution.devices import AvailableDevice +from mpqp.execution.devices import AvailableDevice, AWSDevice, IBMDevice from mpqp.execution.runner import run from mpqp.execution.vqa.optimizer import Optimizer @@ -37,6 +38,16 @@ # TODO: test the minimizer options +class VQAMode(Enum): + JOB = "JOB" + BATCH = "BATCH" + SESSION = "SESSION" + HYBRID_JOB = "HYBRID_JOB" + + def __str__(self): + return self.value + + def _maps(l1: Collection[T1], l2: Collection[T2]) -> dict[T1, T2]: """Does like zip, but with a dictionary instead of a list of tuples""" if len(l1) != len(l2): @@ -55,6 +66,7 @@ def minimize( nb_params: Optional[int] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, + vqa_mode: VQAMode = VQAMode.JOB, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, in order to minimize the measured expectation value of observables associated with the given circuit. @@ -125,16 +137,28 @@ def minimize( if isinstance(optimizable, QCircuit): if device is None: raise ValueError("A device is needed to optimize a circuit") - optimizer = _minimize_remote if device.is_remote() else _minimize_local - return optimizer( - optimizable, - method, - device, - init_params, - nb_params, - optimizer_options, - callback, - ) + # optimizer = _minimize_remote if device.is_remote() else _minimize_local + if device.is_remote(): + return _minimize_remote( + optimizable, + method, + device, + init_params, + nb_params, + optimizer_options, + callback, + vqa_mode=vqa_mode, + ) + else: + return _minimize_local( + optimizable, + method, + device, + init_params, + nb_params, + optimizer_options, + callback, + ) else: # TODO: find a way to know if the job is remote or local from the function return _minimize_local( @@ -157,6 +181,7 @@ def _minimize_remote( nb_params: Optional[int] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, + vqa_mode: VQAMode = VQAMode.JOB, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by it's @@ -189,7 +214,111 @@ def _minimize_remote( TODO to implement on QLM first """ - raise NotImplementedError() + if isinstance(device, IBMDevice): + from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator + + from mpqp.core.languages import Language + from mpqp.execution.connection.ibm_connection import ( + get_backend, + get_QiskitRuntimeService, + ) + + print(f"[VQA] Running on IBM {device.name} in mode {vqa_mode.value}") + + if TYPE_CHECKING: + assert isinstance(optimizable, QCircuit) + + variables: set[Basic] = optimizable.variables() + if not variables: + raise ValueError("No variables found in the circuit to optimize.") + + if len(optimizable.measurements) != 1: + raise ValueError("Expected exactly one ExpectationMeasure in circuit.") + + measurement = optimizable.measurements[0] + if not isinstance(measurement, ExpectationMeasure): + raise ValueError("Expected ExpectationMeasure as measurement.") + + observables = measurement.observables + + service = get_QiskitRuntimeService() + + if TYPE_CHECKING: + assert isinstance(device, IBMDevice) + backend = get_backend(device) + + estimator_options = {"default_shots": measurement.shots} + + def remote_eval(params: OptimizerInput) -> float: + """run expectation evaluation on IBM backend with given parameters.""" + from qiskit.quantum_info import SparsePauliOp + + param_map = _maps(variables, params) + qiskit_circ = optimizable.bind_parameters(param_map, device=device) #TODO: bind_parameters() + + qiskit_observables: list[SparsePauliOp] = [ + obs.to_other_language(Language.QISKIT) for obs in observables + ] # to check here + + if vqa_mode == VQAMode.JOB: + estimator = Runtime_Estimator(mode=backend, options=estimator_options) + ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) + result = ibm_job.result() + + elif vqa_mode == VQAMode.BATCH: + from qiskit_ibm_runtime import Batch + + with Batch(backend=backend) as batch: + estimator = Runtime_Estimator(mode=batch, options=estimator_options) + ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) + result = ibm_job.result() + + elif vqa_mode == VQAMode.SESSION: + from qiskit_ibm_runtime import Session + + with Session(service=service, backend=backend) as session: + estimator = Runtime_Estimator( + mode=session, options=estimator_options + ) + ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) + result = ibm_job.result() + + else: + raise ValueError(f"Unsupported IBM execution mode: {vqa_mode.value}") + + values = result.values + energy = float(np.sum(values)) + print(f" [VQA][IBM] params={params} : value={energy}") + return energy + + if init_params is None: + init_params = [0.0] * len(variables) + + if isinstance(method, Optimizer): + res: OptimizeResult = scipy_minimize( + remote_eval, + x0=np.array(init_params), + method=method.name.lower(), + options=optimizer_options, + callback=callback, + ) + print(f"[VQA][IBM] optimization complete. Best value = {res.fun}") + return float(res.fun), res.x + else: + best_value, best_params = method( + remote_eval, init_params, optimizer_options + ) + print(f"[VQA][IBM] custom optimizer complete. Best value = {best_value}") + return best_value, best_params + + elif isinstance(device, AWSDevice): + # TODO: AWS Braket remote execution to be implemented + print(f"[VQA] Running on IBM {device.name} in mode {vqa_mode.value}") + + raise NotImplementedError("AWS remote execution not implemented yet") + + else: + raise ValueError(f"Unsupported remote device: {type(device).__name__}") @typechecked @@ -361,6 +490,7 @@ def _minimize_local_func( init_params = [0.0] * nb_params if isinstance(method, Optimizer): + # TODO: CMAES integration res: OptimizeResult = scipy_minimize( eval_func, x0=np.array(init_params), From c1d4ef7e288a701f680ea0e30b8591eb73057a30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Oct 2025 15:48:29 +0000 Subject: [PATCH 02/39] chore: Files formated --- mpqp/execution/vqa/vqa.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index d31cb4ea..30e75c09 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -254,11 +254,13 @@ def remote_eval(params: OptimizerInput) -> float: from qiskit.quantum_info import SparsePauliOp param_map = _maps(variables, params) - qiskit_circ = optimizable.bind_parameters(param_map, device=device) #TODO: bind_parameters() + qiskit_circ = optimizable.bind_parameters( + param_map, device=device + ) # TODO: bind_parameters() qiskit_observables: list[SparsePauliOp] = [ obs.to_other_language(Language.QISKIT) for obs in observables - ] # to check here + ] # to check here if vqa_mode == VQAMode.JOB: estimator = Runtime_Estimator(mode=backend, options=estimator_options) From aa840d3d072c2549268314f7760368b9597489ff Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 20 Oct 2025 23:54:45 +0200 Subject: [PATCH 03/39] feat: implement CMA-ES optimizer --- mpqp/execution/vqa/vqa.py | 63 ++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index d31cb4ea..4c08ed23 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -254,11 +254,13 @@ def remote_eval(params: OptimizerInput) -> float: from qiskit.quantum_info import SparsePauliOp param_map = _maps(variables, params) - qiskit_circ = optimizable.bind_parameters(param_map, device=device) #TODO: bind_parameters() + qiskit_circ = optimizable.bind_parameters( + param_map, device=device + ) # TODO: bind_parameters() qiskit_observables: list[SparsePauliOp] = [ obs.to_other_language(Language.QISKIT) for obs in observables - ] # to check here + ] # to check here if vqa_mode == VQAMode.JOB: estimator = Runtime_Estimator(mode=backend, options=estimator_options) @@ -295,20 +297,31 @@ def remote_eval(params: OptimizerInput) -> float: init_params = [0.0] * len(variables) if isinstance(method, Optimizer): - res: OptimizeResult = scipy_minimize( - remote_eval, - x0=np.array(init_params), - method=method.name.lower(), - options=optimizer_options, - callback=callback, - ) - print(f"[VQA][IBM] optimization complete. Best value = {res.fun}") - return float(res.fun), res.x + if method == Optimizer.CMAES: + import cma + + best_params, es = cma.fmin2( + remote_eval, x0=init_params, **(optimizer_options or {}) + ) + best_value = es.result.fbest + return best_value, best_params + else: + res: OptimizeResult = scipy_minimize( + remote_eval, + x0=np.array(init_params), + method=method.name.lower(), + options=optimizer_options, + callback=callback, + ) + print(f"[VQA][IBM] optimization complete. Best value = {res.fun}") + return float(res.fun), res.x else: best_value, best_params = method( remote_eval, init_params, optimizer_options ) - print(f"[VQA][IBM] custom optimizer complete. Best value = {best_value}") + print( + f"[VQA][IBM] custom optimizer complete. Best value = {best_value}" + ) ##add best_params? return best_value, best_params elif isinstance(device, AWSDevice): @@ -490,14 +503,22 @@ def _minimize_local_func( init_params = [0.0] * nb_params if isinstance(method, Optimizer): - # TODO: CMAES integration - res: OptimizeResult = scipy_minimize( - eval_func, - x0=np.array(init_params), - method=method.name.lower(), - options=optimizer_options, - callback=callback, - ) - return float(res.fun), res.x + if method == Optimizer.CMAES: + import cma + + best_params, es = cma.fmin2( + eval_func, x0=init_params, **(optimizer_options or {}) + ) + best_value = es.result.fbest + return best_value, best_params + else: + res: OptimizeResult = scipy_minimize( + eval_func, + x0=np.array(init_params), + method=method.name.lower(), + options=optimizer_options, + callback=callback, + ) + return float(res.fun), res.x else: return method(eval_func, init_params, optimizer_options) From b8fb035a1ae766ac3074ecec0ee9673214597aa8 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Fri, 24 Oct 2025 11:18:53 +0200 Subject: [PATCH 04/39] feat: support qiskit runtime session in ibm_connection --- mpqp/execution/connection/ibm_connection.py | 33 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/mpqp/execution/connection/ibm_connection.py b/mpqp/execution/connection/ibm_connection.py index 543f2c33..1a4aa146 100644 --- a/mpqp/execution/connection/ibm_connection.py +++ b/mpqp/execution/connection/ibm_connection.py @@ -1,5 +1,5 @@ from getpass import getpass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from termcolor import colored from typeguard import typechecked @@ -10,10 +10,11 @@ if TYPE_CHECKING: from qiskit.providers.backend import BackendV2 - from qiskit_ibm_runtime import QiskitRuntimeService + from qiskit_ibm_runtime import QiskitRuntimeService, Session Runtime_Service = None +ibm_session: Optional[Session] = None @typechecked @@ -211,3 +212,31 @@ def get_all_job_ids() -> list[str]: if get_env_variable("IBM_CONFIGURED") == "True": return [job.job_id() for job in get_QiskitRuntimeService().jobs(limit=None)] return [] + + +def get_or_create_ibm_session( + backend: BackendV2, max_time: Optional[int] = None +) -> Session: + """Get an active IBM Runtime session or create a new one.""" + # TODO: to complete docs + global ibm_session + + if ibm_session and ibm_session.status() not in ("Closed", None): + return ibm_session + + ibm_session = Session(backend=backend, max_time=max_time) + return ibm_session + + +def close_ibm_session_final() -> None: + """Close the currently active session, if one exists.""" + # TODO: to complete docs + global ibm_session + + if ibm_session is None: + return + + try: + ibm_session.close() + finally: + ibm_session = None From 5c111b7b77b8b4295d1a152d45ad5bfd05e4d64a Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Fri, 24 Oct 2025 14:08:28 +0200 Subject: [PATCH 05/39] chore: bind parameters for qiskit and braket --- mpqp/core/circuit.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index bd8aa288..6c842bdd 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -34,7 +34,7 @@ from __future__ import annotations from copy import deepcopy -from numbers import Complex +from numbers import Complex, Number from typing import TYPE_CHECKING, Literal, Optional, Sequence, Type, Union, overload from warnings import warn @@ -62,7 +62,7 @@ from braket.circuits import Circuit as braket_Circuit from cirq.circuits.circuit import Circuit as cirq_Circuit from qat.core.wrappers.circuit import Circuit as myQLM_Circuit - from qiskit.circuit import QuantumCircuit + from qiskit.circuit import Parameter, QuantumCircuit from sympy import Basic, Expr from mpqp.execution.devices import ( @@ -2031,7 +2031,20 @@ def breakpoints(self) -> list[Breakpoint]: """Returns the breakpoints of the circuit in order.""" return [inst for inst in self.instructions if isinstance(inst, Breakpoint)] - def bind_parameters(self, param_map, device): - """A placeholder for parameter binding logic to the transpiled circuit.""" - # TODO: implement the logic - raise NotImplementedError("bind_parameters() is not implemented yet. ") + def bind_parameters(self, params: dict[str | Parameter | Basic, Number]) -> None: + """Bind parameter values to the transpiled circuit.""" + # TODO: to enhance docs + transpiled = self.transpiled_circuit + + if isinstance(transpiled, QuantumCircuit): + qiskit_param_map = { + k if isinstance(k, Parameter) else Parameter(str(k)): v + for k, v in params.items() + } + transpiled.assign_parameters(qiskit_param_map, inplace=True) + + elif isinstance(transpiled, braket_Circuit): + braket_param_map = {str(k): v for k, v in params.items()} + self.transpiled_circuit = transpiled.make_bound_circuit(braket_param_map) + else: + raise TypeError(f"Unsupported transpiled circuit yet: {type(transpiled)}") From b2f1f661b07ff2c3010910098cdcf0b8f09158f2 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 12 Nov 2025 14:13:04 +0100 Subject: [PATCH 06/39] feat: transpiled_circuit is device specific --- mpqp/core/circuit.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index 3e346eed..903579c4 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -166,7 +166,10 @@ def __init__( self._user_nb_qubits: Optional[int] = None self._nb_qubits: int - self.transpiled_circuit: "Optional[Union[braket_Circuit, cirq_Circuit, myQLM_Circuit, QuantumCircuit]]" = (None) + self.transpiled_circuit: dict[ + AvailableDevice, + Union[braket_Circuit, cirq_Circuit, myQLM_Circuit, QuantumCircuit], + ] = {} """A pre-transpiled circuit to skip repeated transpilation when running the circuit. Useful when working with a symbolic circuit that needs to be executed with different parameters.""" @@ -2126,6 +2129,13 @@ def __repr__(self) -> str: return f'QCircuit({args_repr})' + def transpiled_for_device(self, device: AvailableDevice): + self.transpiled_circuit[device] = ( + self.to_other_device( # pyright: ignore[reportCallIssue] + device # pyright: ignore[reportArgumentType] + ) + ) + def variables(self) -> set[Basic]: """Returns all the symbolic parameters involved in this circuit. @@ -2151,10 +2161,15 @@ def variables(self) -> set[Basic]: params.update(param.free_symbols) return params - def bind_parameters(self, params: dict[str | Parameter | Basic, Number]) -> None: + def bind_parameters( + self, device: AvailableDevice, params: dict[str | Parameter | Basic, Number] + ) -> None: """Bind parameter values to the transpiled circuit.""" # TODO: to enhance docs - transpiled = self.transpiled_circuit + if device not in self.transpiled_circuit: + self.transpiled_for_device(device) + + transpiled = self.transpiled_circuit[device] if isinstance(transpiled, QuantumCircuit): qiskit_param_map = { @@ -2165,6 +2180,8 @@ def bind_parameters(self, params: dict[str | Parameter | Basic, Number]) -> None elif isinstance(transpiled, braket_Circuit): braket_param_map = {str(k): v for k, v in params.items()} - self.transpiled_circuit = transpiled.make_bound_circuit(braket_param_map) + self.transpiled_circuit[device] = transpiled.make_bound_circuit( + braket_param_map + ) else: raise TypeError(f"Unsupported transpiled circuit yet: {type(transpiled)}") From 286cad9277c105088b3b99123818beb40395849a Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 12 Nov 2025 14:55:18 +0100 Subject: [PATCH 07/39] feat: support device specific pre-transpilation for observables --- .../measurement/expectation_value.py | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index 78f0fda0..925b102b 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -28,6 +28,7 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform from mpqp.tools.maths import is_diagonal, is_hermitian, is_power_of_two if TYPE_CHECKING: + from braket.circuits import Circuit as braket_Circuit from braket.circuits.observables import Hermitian from cirq.circuits.circuit import Circuit as CirqCircuit from cirq.ops.linear_combinations import PauliSum as CirqPauliSum @@ -38,6 +39,7 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform from sympy import Expr from mpqp.core.instruction.gates.custom_controlled_gate import Gate + from mpqp.execution.devices import AvailableDevice class Observable: @@ -81,7 +83,10 @@ def __init__( self._diag_elements: Optional[npt.NDArray[np.float64]] = None self.label = label "See parameter description." - self.pre_transpile = None + self.pre_transpile: dict[ + AvailableDevice, + Union[SparsePauliOp, QLMObservable, CirqPauliSum, CirqPauliString], + ] = {} if isinstance(observable, PauliString): self.nb_qubits = observable.nb_qubits @@ -402,7 +407,11 @@ def __init__( """See parameter description.""" self.optimize_measurement = optimize_measurement """See parameter description.""" - self.pre_transpile = None + self.pre_transpile: dict[ + AvailableDevice, + tuple[list[dict[str, npt.NDArray[np.float64]]], list[braket_Circuit]], + ] = {} + if isinstance(observable, Observable): observable = [observable] else: @@ -508,9 +517,6 @@ def get_pauli_grouping(self) -> list[list[PauliStringMonomial]]: return pauli_grouping_greedy(unique_monos, self.commuting_type) elif self.grouping_method == GroupingMethods.QISKIT: from qiskit.quantum_info import PauliList - from mpqp.core.instruction.measurement.pauli_string import ( - pauli_string_from_str, - ) pauli_labels = [mono.name.replace("@", "") for mono in unique_monos] pauli_list = PauliList(pauli_labels) @@ -522,7 +528,12 @@ def get_pauli_grouping(self) -> list[list[PauliStringMonomial]]: grouped = pauli_list.group_commuting() grouped_monomials = [ - [pauli_string_from_str(mono.to_label()) for mono in pauli] + [ + PauliString.from_str( + mono.to_label() # pyright: ignore[reportAttributeAccessIssue] + ) + for mono in pauli + ] for pauli in grouped ] @@ -580,3 +591,39 @@ def to_dict(self): and not attr_name.startswith("__") and not callable(getattr(self, attr_name)) } + + def pre_transpile_observables(self, device: AvailableDevice): + from mpqp.execution.devices import AWSDevice, IBMDevice + + if isinstance(device, AWSDevice): + from mpqp.core.circuit import QCircuit + from mpqp.tools.pauli_grouping import ( + find_qubitwise_rotations, + pauli_monomial_eigenvalues, + ) + + grouping = self.get_pauli_grouping() + transpiled_pre_measures = [ + QCircuit(find_qubitwise_rotations(group)).to_other_language( + Language.BRAKET + ) + for group in grouping + ] + eigenvalues = [ + {monom.name: pauli_monomial_eigenvalues(monom) for monom in group} + for group in grouping + ] + self.pre_transpile[device] = ( + eigenvalues, + transpiled_pre_measures, + ) + + elif isinstance(device, IBMDevice): + for observable in self.observables: + observable.pre_transpile[device] = observable.to_other_language( + language=Language.QISKIT + ) + else: + raise NotImplementedError( + f"Pre-transpilation for device {type(device).__name__} is not implemented." + ) From db29259a752fd1c1514b04f058b6290cb6539b74 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 12 Nov 2025 13:55:38 +0000 Subject: [PATCH 08/39] chore: Files formated --- mpqp/core/instruction/measurement/expectation_value.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index 925b102b..04f56ff7 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -529,8 +529,8 @@ def get_pauli_grouping(self) -> list[list[PauliStringMonomial]]: grouped_monomials = [ [ - PauliString.from_str( - mono.to_label() # pyright: ignore[reportAttributeAccessIssue] + PauliString.from_str( + mono.to_label() # pyright: ignore[reportAttributeAccessIssue] ) for mono in pauli ] From d2097e5e82bc9f5d460035dbb6647b0db36a3fb5 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 12 Nov 2025 15:32:17 +0100 Subject: [PATCH 09/39] fix: handle single atom case for Braket --- mpqp/core/instruction/measurement/pauli_string.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mpqp/core/instruction/measurement/pauli_string.py b/mpqp/core/instruction/measurement/pauli_string.py index 2913cdab..ddab900a 100644 --- a/mpqp/core/instruction/measurement/pauli_string.py +++ b/mpqp/core/instruction/measurement/pauli_string.py @@ -10,7 +10,7 @@ from enum import Enum, auto from functools import reduce from numbers import Real -from operator import matmul, mul +from operator import mul from typing import TYPE_CHECKING, Any, Literal, Optional, Union import numpy as np @@ -1182,7 +1182,13 @@ def to_other_language( ] from braket.circuits.observables import TensorProduct - return self.coef * TensorProduct(braket_atoms) + if len(braket_atoms) == 1: + return ( + self.coef * braket_atoms[0] + ) # pyright: ignore[reportOperatorIssue] + return self.coef * TensorProduct( + braket_atoms + ) # pyright: ignore[reportOperatorIssue] elif language == Language.CIRQ: from cirq.devices.line_qubit import LineQubit From 604c35677b56638b817ce1bf30efdbbe275bb5ca Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 12 Nov 2025 16:25:43 +0100 Subject: [PATCH 10/39] chore: simplify handling Symbol --- mpqp/core/instruction/gates/native_gates.py | 25 +-------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/mpqp/core/instruction/gates/native_gates.py b/mpqp/core/instruction/gates/native_gates.py index d74489fb..31c54c5d 100644 --- a/mpqp/core/instruction/gates/native_gates.py +++ b/mpqp/core/instruction/gates/native_gates.py @@ -85,33 +85,10 @@ def _qiskit_parameter_adder( return qiskit_param -@conditional_typechecked def _sympy_to_braket_param(val: Expr | float) -> "float | FreeParameter": - from sympy import Expr, Symbol from braket.circuits import FreeParameter - if isinstance(val, Symbol): - return FreeParameter(str(val)) - elif isinstance(val, Expr): - if val.free_symbols: - return FreeParameter(str(val)) # note: Braket won't parse expressions - else: - try: - return float(val.evalf()) # pyright: ignore[reportArgumentType] - except Exception as e: - raise ValueError(f"Failed to evaluate sympy expression '{val}': {e}") - else: - return float(val) - - -@conditional_typechecked -def _sympy_to_braket_param(val: Expr | float) -> "float | FreeParameter": - from sympy import Expr, Symbol - from braket.circuits import FreeParameter - - if isinstance(val, Symbol): - return FreeParameter(str(val)) - elif isinstance(val, Expr): + if isinstance(val, Expr): if val.free_symbols: return FreeParameter(str(val)) # note: Braket won't parse expressions else: From 66eef7431699583dca02be118688fafbd694b0e7 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 12 Nov 2025 16:44:58 +0100 Subject: [PATCH 11/39] fix: type check --- tests/core/instruction/gates/test_custom_gate.py | 1 - tests/core/instruction/measurement/test_basis.py | 2 -- tests/core/test_circuit.py | 2 +- tests/examples/test_demonstrations.py | 3 --- tests/execution/test_validity.py | 5 +---- 5 files changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/core/instruction/gates/test_custom_gate.py b/tests/core/instruction/gates/test_custom_gate.py index cc62cd82..feb89e0b 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -1,4 +1,3 @@ -import contextlib import random from functools import reduce from itertools import product diff --git a/tests/core/instruction/measurement/test_basis.py b/tests/core/instruction/measurement/test_basis.py index c5e08e82..e6daacba 100644 --- a/tests/core/instruction/measurement/test_basis.py +++ b/tests/core/instruction/measurement/test_basis.py @@ -1,4 +1,3 @@ -import contextlib from itertools import product import numpy as np @@ -21,7 +20,6 @@ ) from mpqp.execution import AvailableDevice from mpqp.gates import * -from mpqp.tools.errors import UnsupportedBraketFeaturesWarning from mpqp.tools.maths import matrix_eq diff --git a/tests/core/test_circuit.py b/tests/core/test_circuit.py index 77c06147..ec7495f7 100644 --- a/tests/core/test_circuit.py +++ b/tests/core/test_circuit.py @@ -66,7 +66,7 @@ statevector_from_random_circuit, ) from mpqp.tools.display import one_lined_repr -from mpqp.tools.errors import NonReversibleWarning, UnsupportedBraketFeaturesWarning +from mpqp.tools.errors import NonReversibleWarning from mpqp.tools.generics import Matrix, OneOrMany from mpqp.tools.maths import matrix_eq diff --git a/tests/examples/test_demonstrations.py b/tests/examples/test_demonstrations.py index 86cbfefd..749cefe9 100644 --- a/tests/examples/test_demonstrations.py +++ b/tests/examples/test_demonstrations.py @@ -1,5 +1,3 @@ -from typing import Any, Callable - import numpy as np import pytest from braket.devices import LocalSimulator @@ -15,7 +13,6 @@ QCircuit, run, ) -from mpqp.execution import AvailableDevice from mpqp.gates import * from mpqp.qasm.qasm_to_braket import qasm3_to_braket_Circuit from mpqp.tools.errors import UnsupportedBraketFeaturesWarning diff --git a/tests/execution/test_validity.py b/tests/execution/test_validity.py index 0caaddc3..bb4a51dd 100644 --- a/tests/execution/test_validity.py +++ b/tests/execution/test_validity.py @@ -1,4 +1,3 @@ -import contextlib from copy import deepcopy import numpy as np @@ -40,9 +39,7 @@ from mpqp.noise.noise_model import NOISE_MODELS from mpqp.tools import Matrix, atol, rand_hermitian_matrix, rtol from mpqp.tools.circuit import random_gate, random_noise -from mpqp.tools.errors import ( - DeviceJobIncompatibleError, -) +from mpqp.tools.errors import DeviceJobIncompatibleError from mpqp.tools.maths import matrix_eq pi = np.pi From caf0fa4d97df514b5a038792372d6fe8f0962a84 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 24 Nov 2025 23:17:24 +0100 Subject: [PATCH 12/39] chore: avoid re-transpiling for the same device --- mpqp/core/circuit.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index 903579c4..5d1f5d6e 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -2130,11 +2130,13 @@ def __repr__(self) -> str: return f'QCircuit({args_repr})' def transpiled_for_device(self, device: AvailableDevice): - self.transpiled_circuit[device] = ( - self.to_other_device( # pyright: ignore[reportCallIssue] - device # pyright: ignore[reportArgumentType] + if device not in self.transpiled_circuit: + self.transpiled_circuit[device] = ( + self.to_other_device( # pyright: ignore[reportCallIssue] + device # pyright: ignore[reportArgumentType] + ) ) - ) + return self.transpiled_circuit[device] def variables(self) -> set[Basic]: """Returns all the symbolic parameters involved in this circuit. From a39d309199c6839276133c378205358e25729d7e Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 24 Nov 2025 23:25:36 +0100 Subject: [PATCH 13/39] chore: modify device specific transpilation logic --- mpqp/execution/providers/atos.py | 20 +++++----- mpqp/execution/providers/aws.py | 45 +++++++++++----------- mpqp/execution/providers/azure.py | 7 +--- mpqp/execution/providers/google.py | 25 ++++-------- mpqp/execution/providers/ibm.py | 62 ++++++++++-------------------- 5 files changed, 62 insertions(+), 97 deletions(-) diff --git a/mpqp/execution/providers/atos.py b/mpqp/execution/providers/atos.py index b2377955..80f24f0b 100644 --- a/mpqp/execution/providers/atos.py +++ b/mpqp/execution/providers/atos.py @@ -82,12 +82,10 @@ def job_pre_processing(job: Job) -> "Circuit": "`OBSERVABLE` jobs with shots!=0 are disabled for MPO." ) - if job.circuit.transpiled_circuit is None: - if TYPE_CHECKING: - assert isinstance(job.device, ATOSDevice) - myqlm_circuit = job.circuit.to_other_device(job.device) - else: - myqlm_circuit = job.circuit.transpiled_circuit + if TYPE_CHECKING: + assert isinstance(job.device, ATOSDevice) + + myqlm_circuit = job.circuit.transpiled_for_device(job.device) return myqlm_circuit @@ -250,12 +248,14 @@ def generate_observable_job(myqlm_circuit: "Circuit", job: Job) -> list["JobQLM" # TODO: [multi-obs] update this to take into account the case when we have list of Observables if TYPE_CHECKING: assert job.measure is not None and isinstance(job.measure, ExpectationMeasure) + result = [] for obs in job.measure.observables: - if obs.pre_transpile is None: - qlm_obs = obs.to_other_language(Language.MY_QLM) - else: - qlm_obs = obs.pre_transpile + if job.device not in obs.pre_transpile: + obs.pre_transpile[job.device] = obs.to_other_language(Language.MY_QLM) + + qlm_obs = obs.pre_transpile[job.device] + result.append( myqlm_circuit.to_job( job_type="OBS", diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index f1e46d5b..d5a8de6d 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -3,7 +3,6 @@ import numpy as np -from mpqp.core.languages import Language from mpqp.core.circuit import QCircuit from mpqp.core.instruction.gates import CRk from mpqp.core.instruction.measurement import ( @@ -11,6 +10,7 @@ ExpectationMeasure, Observable, ) +from mpqp.core.languages import Language from mpqp.execution.connection.aws_connection import get_braket_device from mpqp.execution.devices import AWSDevice from mpqp.execution.job import Job, JobStatus, JobType @@ -137,15 +137,13 @@ def run_braket_observable(job: Job): Returns: A result containing the expectation values of the observables. """ - from braket.circuits import Circuit + # from braket.circuits import Circuit from braket.tasks import GateModelQuantumTaskResult assert isinstance(job.device, AWSDevice) - if job.circuit.transpiled_circuit is None: - transpiled_circuit = job.circuit.to_other_device(job.device) - else: - transpiled_circuit = job.circuit.transpiled_circuit - assert isinstance(transpiled_circuit, Circuit) + + transpiled_circuit = job.circuit.transpiled_for_device(job.device) + assert isinstance(transpiled_circuit, Circuit) device = get_braket_device( job.device, @@ -162,7 +160,7 @@ def run_braket_observable(job: Job): pauli_monomial_eigenvalues, ) - if job.measure.pre_transpile is None: + if job.device not in job.measure.pre_transpile: grouping = job.measure.get_pauli_grouping() transpiled_pre_measures = [ QCircuit(find_qubitwise_rotations(group)).to_other_language( @@ -174,9 +172,12 @@ def run_braket_observable(job: Job): {monom.name: pauli_monomial_eigenvalues(monom) for monom in group} for group in grouping ] - + job.measure.pre_transpile[job.device] = ( + eigenvalues, + transpiled_pre_measures, + ) else: - eigenvalues, transpiled_pre_measures = job.measure.pre_transpile + eigenvalues, transpiled_pre_measures = job.measure.pre_transpile[job.device] expectation_values = {} for eigenvalues, pre_measure in zip(eigenvalues, transpiled_pre_measures): @@ -234,9 +235,12 @@ def run_braket_observable(job: Job): for obs in job.measure.observables: from copy import deepcopy + from braket.circuits.observables import Sum copy = deepcopy(transpiled_circuit) + assert isinstance(copy, Circuit) + braket_obs = obs.to_other_language(Language.BRAKET) if isinstance(braket_obs, Sum): targets = [job.measure.targets] * len(braket_obs.summands) @@ -305,13 +309,10 @@ def submit_job_braket(job: Job) -> tuple[str, "QuantumTask"]: device = get_braket_device(job.device, is_noisy=is_noisy) - if job.circuit.transpiled_circuit is None: - braket_circuit = job.circuit.to_other_device(job.device) - else: - braket_circuit = job.circuit.transpiled_circuit - + braket_circuit = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(braket_circuit, Circuit) + if job.job_type == JobType.STATE_VECTOR: # rebind safe_retrieve_samples from braket to Normalize the probability # because the bracket does not do so and this causes a crash. @@ -341,14 +342,12 @@ def safe_retrieve_samples(self): # pyright: ignore[reportMissingParameterType] # TODO : [multi-obs] update this to take into account the case when we have list of Observables if TYPE_CHECKING: assert isinstance(job.measure, ExpectationMeasure) - if job.measure.observables[0].pre_transpile is None: - herm_op = job.measure.observables[0].to_other_language(Language.BRAKET) - else: - herm_op = job.measure.observables[0].pre_transpile - braket_circuit.expectation( # pyright: ignore[reportAttributeAccessIssue] - observable=herm_op, target=job.measure.targets - ) - + job.measure.pre_transpile_observables(job.device) + _, transpiled_pre_measures = job.measure.pre_transpile[job.device] + for herm_op in transpiled_pre_measures: + braket_circuit.expectation( # pyright: ignore[reportAttributeAccessIssue] + observable=herm_op, target=job.measure.targets + ) job.status = JobStatus.RUNNING task = device.run(braket_circuit, shots=job.measure.shots, inputs=None) diff --git a/mpqp/execution/providers/azure.py b/mpqp/execution/providers/azure.py index f3cfa848..60d5361e 100644 --- a/mpqp/execution/providers/azure.py +++ b/mpqp/execution/providers/azure.py @@ -13,7 +13,7 @@ get_jobs_by_id, ) from mpqp.execution.devices import AZUREDevice -from mpqp.execution.job import IBMDevice, Job, JobStatus, JobType +from mpqp.execution.job import Job, JobStatus, JobType from mpqp.execution.result import Result, Sample @@ -54,10 +54,7 @@ def submit_job_azure(job: Job) -> tuple[str, "AzureQuantumJob"]: """ from qiskit import QuantumCircuit - if job.circuit.transpiled_circuit is None: - qiskit_circuit = job.circuit.to_other_device(IBMDevice.AER_SIMULATOR) - else: - qiskit_circuit = job.circuit.transpiled_circuit + qiskit_circuit = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(qiskit_circuit, QuantumCircuit) diff --git a/mpqp/execution/providers/google.py b/mpqp/execution/providers/google.py index 3ee73efe..bfe5bcf6 100644 --- a/mpqp/execution/providers/google.py +++ b/mpqp/execution/providers/google.py @@ -248,13 +248,14 @@ def run_cirq_observable( errors = {} expectation_values = {} for obs in job.measure.observables: - - if obs.pre_transpile is None: + if job.device not in obs.pre_transpile: cirq_obs = obs.to_other_language( language=Language.CIRQ, circuit=circuit ) + obs.pre_transpile[job.device] = cirq_obs else: - cirq_obs = obs.pre_transpile + cirq_obs = obs.pre_transpile[job.device] + if TYPE_CHECKING: assert type(cirq_obs) in (CirqPauliSum, CirqPauliString) job.status = JobStatus.RUNNING @@ -409,11 +410,7 @@ def run_google_remote(job: Job) -> Result: import cirq_ionq as ionq from cirq.circuits.circuit import Circuit as CirqCircuit - if job.circuit.transpiled_circuit is None: - job_CirqCircuit = job.circuit.to_other_device(job.device) - else: - job_CirqCircuit = job.circuit.transpiled_circuit - + job_CirqCircuit = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(job_CirqCircuit, CirqCircuit) @@ -467,11 +464,7 @@ def run_local(job: Job) -> Result: if job.device.is_processor(): return run_local_processor(job) - if job.circuit.transpiled_circuit is None: - cirq_circuit = job.circuit.to_other_device(job.device) - else: - cirq_circuit = job.circuit.transpiled_circuit - + cirq_circuit = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(cirq_circuit, CirqCircuit) @@ -531,11 +524,7 @@ def run_local_processor(job: Job) -> Result: ) simulator = SimulatedLocalEngine([sim_processor]) - if job.circuit.transpiled_circuit is None: - cirq_circuit = job.circuit.to_other_device(job.device) - else: - cirq_circuit = job.circuit.transpiled_circuit - + cirq_circuit = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(cirq_circuit, CirqCircuit) diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 5fe74a16..0e80cbe5 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -99,11 +99,10 @@ def compute_expectation_value( nb_shots = job.measure.shots qiskit_observables: list[SparsePauliOp] = [] + job.measure.pre_transpile_observables(job.device) for obs in job.measure.observables: - if obs.pre_transpile is None: - translated = obs.to_other_language(Language.QISKIT) - else: - translated = obs.pre_transpile + translated = obs.pre_transpile[job.device] + if TYPE_CHECKING: assert isinstance(translated, SparsePauliOp) qiskit_observables.append(translated) @@ -455,29 +454,21 @@ def run_aer(job: Job): # to it directly) backend_sim = job.device.to_noisy_simulator() elif len(job.circuit.noises) != 0: - if job.circuit.transpiled_circuit is not None: - if job.circuit.transpiled_noise_model is None: - raise InstructionParsingError( - "transpiled_noise_model is not initialized" - ) - backend_sim = AerSimulator( - method=job.device.value, noise_model=job.circuit.transpiled_noise_model - ) - else: - noise_model, modified_circuit = generate_qiskit_noise_model(job.circuit) - job_circuit = modified_circuit - backend_sim = AerSimulator(method=job.device.value, noise_model=noise_model) - else: - backend_sim = AerSimulator(method=job.device.value) + job_circuit = job.circuit.transpiled_for_device(job.device) - if job.circuit.transpiled_circuit is None: - qiskit_circuit = job_circuit.to_other_device( - job.device, backend_sim=backend_sim + if job.circuit.transpiled_noise_model is None: + raise InstructionParsingError("transpiled_noise_model is not initialized") + backend_sim = AerSimulator( + method=job.device.value, noise_model=job.circuit.transpiled_noise_model ) + else: - qiskit_circuit = job.circuit.transpiled_circuit - if TYPE_CHECKING: - assert isinstance(qiskit_circuit, QuantumCircuit) + job_circuit = job.circuit.transpiled_for_device(job.device) + backend_sim = AerSimulator(method=job.device.value) + + qiskit_circuit = job_circuit + if TYPE_CHECKING: + assert isinstance(qiskit_circuit, QuantumCircuit) if job.job_type == JobType.STATE_VECTOR: # the save_statevector method is patched on qiskit_aer load, meaning # the type checker can't find it. I hate it but it is what it is. @@ -542,11 +533,7 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: job.device = IBMDevice(backend.name) session = Session(service=service, backend=backend) - if job.circuit.transpiled_circuit is None: - qiskit_circ = job.circuit.to_other_device(job.device) - else: - qiskit_circ = job.circuit.transpiled_circuit - + qiskit_circ = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(qiskit_circ, QuantumCircuit) @@ -554,22 +541,15 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: if TYPE_CHECKING: assert isinstance(meas, ExpectationMeasure) estimator = Runtime_Estimator(mode=session) - qiskit_observables = [ - ( - obs.to_other_language(Language.QISKIT) - if obs.pre_transpile is None - else obs.pre_transpile - ) - for obs in meas.observables - ] + + meas.pre_transpile_observables(job.device) + qiskit_observables = [obs.pre_transpile[job.device] for obs in meas.observables] + if TYPE_CHECKING: assert all(isinstance(obs, SparsePauliOp) for obs in qiskit_observables) qiskit_observables = [ - obs.apply_layout( # pyright: ignore[reportAttributeAccessIssue] - qiskit_circ.layout - ) - for obs in qiskit_observables + obs.apply_layout(qiskit_circ.layout) for obs in qiskit_observables ] # We have to disable all the twirling options and set manually the number of circuits and shots per circuits From 3a86af80e4ce2c1ce63f178f6fb6fe0550ce6942 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 27 Nov 2025 12:09:44 +0100 Subject: [PATCH 14/39] fix: type hinting --- mpqp/execution/connection/ibm_connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mpqp/execution/connection/ibm_connection.py b/mpqp/execution/connection/ibm_connection.py index 88ac7850..04e721ae 100644 --- a/mpqp/execution/connection/ibm_connection.py +++ b/mpqp/execution/connection/ibm_connection.py @@ -13,7 +13,7 @@ Runtime_Service = None -ibm_session: Optional[Session] = None +ibm_session: Optional["Session"] = None def config_ibm_account(token: str): @@ -212,8 +212,8 @@ def get_all_job_ids() -> list[str]: def get_or_create_ibm_session( - backend: BackendV2, max_time: Optional[int] = None -) -> Session: + backend: "BackendV2", max_time: Optional[int] = None +) -> "Session": """Get an active IBM Runtime session or create a new one.""" # TODO: to complete docs global ibm_session From 2b36609dc8ee7ee3499314de5cd4713c419b1ea0 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 1 Dec 2025 15:06:45 +0100 Subject: [PATCH 15/39] chore: update session handling --- mpqp/execution/connection/ibm_connection.py | 31 +++++++++++---------- mpqp/execution/providers/ibm.py | 6 ++-- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/mpqp/execution/connection/ibm_connection.py b/mpqp/execution/connection/ibm_connection.py index 04e721ae..c87c9878 100644 --- a/mpqp/execution/connection/ibm_connection.py +++ b/mpqp/execution/connection/ibm_connection.py @@ -13,7 +13,7 @@ Runtime_Service = None -ibm_session: Optional["Session"] = None +_ibm_sessions: dict[str, "Session"] = {} def config_ibm_account(token: str): @@ -216,24 +216,25 @@ def get_or_create_ibm_session( ) -> "Session": """Get an active IBM Runtime session or create a new one.""" # TODO: to complete docs - global ibm_session - if ibm_session and ibm_session.status() not in ("Closed", None): - return ibm_session + backend_name = backend.name + if backend_name in _ibm_sessions: + session = _ibm_sessions[backend_name] + if session.status() not in ("Closed", None): + return session - ibm_session = Session(backend=backend, max_time=max_time) - return ibm_session + new_session = Session(backend=backend, max_time=max_time) + _ibm_sessions[backend_name] = new_session + return new_session -def close_ibm_session_final() -> None: +def close_ibm_session(backend: "BackendV2") -> None: """Close the currently active session, if one exists.""" # TODO: to complete docs - global ibm_session - if ibm_session is None: - return - - try: - ibm_session.close() - finally: - ibm_session = None + backend_name = backend.name + if backend_name in _ibm_sessions: + try: + _ibm_sessions[backend_name].close() + finally: + del _ibm_sessions[backend_name] diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 0e80cbe5..16fe6158 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -520,18 +520,18 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: from qiskit import QuantumCircuit from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator from qiskit_ibm_runtime import SamplerV2 as Runtime_Sampler - from qiskit_ibm_runtime import Session + + from mpqp.execution.connection.ibm_connection import get_or_create_ibm_session meas = job.measure check_job_compatibility(job) - service = get_QiskitRuntimeService() if TYPE_CHECKING: assert isinstance(job.device, IBMDevice) backend = get_backend(job.device) job.device = IBMDevice(backend.name) - session = Session(service=service, backend=backend) + session = get_or_create_ibm_session(backend) qiskit_circ = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: From cfd31a3456007af3020adfcaa8e82844e5b8953a Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 8 Dec 2025 12:29:17 +0100 Subject: [PATCH 16/39] chore: enhance IBM session use --- mpqp/execution/connection/ibm_connection.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mpqp/execution/connection/ibm_connection.py b/mpqp/execution/connection/ibm_connection.py index c87c9878..660bb641 100644 --- a/mpqp/execution/connection/ibm_connection.py +++ b/mpqp/execution/connection/ibm_connection.py @@ -214,13 +214,20 @@ def get_all_job_ids() -> list[str]: def get_or_create_ibm_session( backend: "BackendV2", max_time: Optional[int] = None ) -> "Session": - """Get an active IBM Runtime session or create a new one.""" - # TODO: to complete docs + """Get an active IBM Runtime session for the given backend. + If a session exists and is valid, reuse it. Otherwise, create a new one. + """ + from qiskit_ibm_runtime import Session backend_name = backend.name if backend_name in _ibm_sessions: session = _ibm_sessions[backend_name] - if session.status() not in ("Closed", None): + try: + status = session.status() + except Exception: + status = "Closed" + + if status not in ("Closed", None): return session new_session = Session(backend=backend, max_time=max_time) @@ -230,11 +237,12 @@ def get_or_create_ibm_session( def close_ibm_session(backend: "BackendV2") -> None: """Close the currently active session, if one exists.""" - # TODO: to complete docs backend_name = backend.name if backend_name in _ibm_sessions: try: _ibm_sessions[backend_name].close() + except Exception: + pass finally: del _ibm_sessions[backend_name] From 9371c45073cafa66230df6f044d10f1f697b70cd Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 8 Dec 2025 12:37:02 +0100 Subject: [PATCH 17/39] feat: add unified execution pipeline, to support job, batch and session --- mpqp/execution/providers/ibm.py | 145 +++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 21 deletions(-) diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 16fe6158..ec13459b 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -3,7 +3,7 @@ import math import warnings from copy import deepcopy -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union import numpy as np @@ -18,8 +18,8 @@ get_QiskitRuntimeService, ) from mpqp.execution.devices import AZUREDevice, IBMDevice -from mpqp.execution.job import Job, JobStatus, JobType -from mpqp.execution.result import Result, Sample, StateVector +from mpqp.execution.job import Job, JobStatus, JobType, VQAMode +from mpqp.execution.result import BatchResult, Result, Sample, StateVector from mpqp.noise import DimensionalNoiseModel from mpqp.tools.errors import ( DeviceJobIncompatibleError, @@ -36,16 +36,18 @@ PubResult, SamplerPubResult, ) + from qiskit.providers.backend import BackendV2 from qiskit.quantum_info import SparsePauliOp from qiskit.result import Result as QiskitResult from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel as Qiskit_NoiseModel - from qiskit_ibm_runtime import RuntimeJobV2 + from qiskit_ibm_runtime import RuntimeJobV2, Session from mpqp.execution.simulated_devices import StaticIBMSimulatedDevice def run_ibm(job: Job) -> Result: + # TODO: update docs """Executes the job on the right IBM Q device precised in the job in parameter. @@ -59,7 +61,13 @@ def run_ibm(job: Job) -> Result: This function is not meant to be used directly, please use :func:`~mpqp.execution.runner.run` instead. """ - return run_aer(job) if not job.device.is_remote() else run_remote_ibm(job) + if not job.device.is_remote(): + return run_aer(job) + + if job.mode == VQAMode.SESSION: + return run_remote_ibm_session(job) + + return run_remote_ibm(job) def compute_expectation_value( @@ -504,7 +512,10 @@ def run_aer(job: Job): return result -def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: +def _submit_remote_ibm( + job: Job, runtime_target: Union[BackendV2, Session] +) -> tuple[str, "RuntimeJobV2"]: + # TODO: rewrite docs if needed """Submits the job on the remote IBM device (quantum computer or simulator). Args: @@ -521,18 +532,10 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator from qiskit_ibm_runtime import SamplerV2 as Runtime_Sampler - from mpqp.execution.connection.ibm_connection import get_or_create_ibm_session - meas = job.measure check_job_compatibility(job) - if TYPE_CHECKING: - assert isinstance(job.device, IBMDevice) - backend = get_backend(job.device) - job.device = IBMDevice(backend.name) - session = get_or_create_ibm_session(backend) - qiskit_circ = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(qiskit_circ, QuantumCircuit) @@ -540,18 +543,17 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: if job.job_type == JobType.OBSERVABLE: if TYPE_CHECKING: assert isinstance(meas, ExpectationMeasure) - estimator = Runtime_Estimator(mode=session) + estimator = Runtime_Estimator(mode=runtime_target) meas.pre_transpile_observables(job.device) - qiskit_observables = [obs.pre_transpile[job.device] for obs in meas.observables] + qiskit_observables = [ + obs.pre_transpile[job.device].apply_layout(qiskit_circ.layout) + for obs in meas.observables + ] if TYPE_CHECKING: assert all(isinstance(obs, SparsePauliOp) for obs in qiskit_observables) - qiskit_observables = [ - obs.apply_layout(qiskit_circ.layout) for obs in qiskit_observables - ] - # We have to disable all the twirling options and set manually the number of circuits and shots per circuits twirling = getattr(estimator.options, "twirling", None) if twirling is not None: @@ -567,7 +569,7 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: elif job.job_type == JobType.SAMPLE: if TYPE_CHECKING: assert isinstance(meas, BasisMeasure) - sampler = Runtime_Sampler(mode=session) + sampler = Runtime_Sampler(mode=runtime_target) ibm_job = sampler.run([qiskit_circ], shots=meas.shots) else: raise NotImplementedError( @@ -579,6 +581,80 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: return job.id, ibm_job +def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: + # TODO: docs + if TYPE_CHECKING: + assert isinstance(job.device, IBMDevice) + backend = get_backend(job.device) + + try: + job.device = IBMDevice(backend.name) + except Exception: + pass + + return _submit_remote_ibm(job, runtime_target=backend) + + +def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"]: + # TODO: docs + if len(jobs) == 0: + raise ValueError( + "Can't submit an IBM batch: job list is empty. " + "Batch execution requires at leat one Job object" + ) + + first_job = jobs[0] + if TYPE_CHECKING: + assert isinstance(first_job.device, IBMDevice) + backend = get_backend(first_job.device) + + from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator + + from mpqp.execution.connection.ibm_connection import get_or_create_ibm_session + + execution_target = ( + get_or_create_ibm_session(backend) + if first_job.mode == VQAMode.SESSION + else backend + ) + estimator = Runtime_Estimator(mode=execution_target) + + per_job_circuits: list[QuantumCircuit] = [] + per_job_observables: list[list[SparsePauliOp]] = [] + + for job in jobs: + meas = job.measure + check_job_compatibility(job) + + qc = job.circuit.transpiled_for_device(job.device) + assert isinstance(qc, QuantumCircuit) + + per_job_circuits.append(qc) + + if TYPE_CHECKING: + assert isinstance(meas, ExpectationMeasure) + meas.pre_transpile_observables(job.device) + obs_list = [ + obs.pre_transpile[job.device].apply_layout(qc.layout) + for obs in meas.observables + ] + per_job_observables.append(obs_list) + + estimator_input = list(zip(per_job_circuits, per_job_observables)) + ibm_job = estimator.run(estimator_input) + + job_ids = [ibm_job.job_id()] * len(jobs) + for job, job_id in zip(jobs, job_ids): + job.id = job_id + + return job_ids, ibm_job + + +def submit_remote_ibm_session(job: Job, session: Session) -> tuple[str, "RuntimeJobV2"]: + # TODO: docs + return _submit_remote_ibm(job, runtime_target=session) + + def run_remote_ibm(job: Job) -> Result: """Submits the job on the right IBM remote device, precised in the job in parameter, and waits until the job is completed. @@ -601,6 +677,33 @@ def run_remote_ibm(job: Job) -> Result: return extract_result(ibm_result, job, job.device) +def run_remote_ibm_session(job: Job) -> Result: + # TODO: docs + from mpqp.execution.connection.ibm_connection import get_or_create_ibm_session + + if TYPE_CHECKING: + assert isinstance(job.device, IBMDevice) + backend = get_backend(job.device) + session = get_or_create_ibm_session(backend) + + _, remote_job = submit_remote_ibm_session(job, session) + ibm_result = remote_job.result() + return extract_result(ibm_result, job, job.device) + + +def run_remote_ibm_batch(jobs: list[Job]) -> BatchResult: + _, remote_job = submit_remote_ibm_batch(jobs) + ibm_batch_results = remote_job.result() + + mpqp_batch_results = [] + for job, res in zip(jobs, ibm_batch_results): + if TYPE_CHECKING: + assert isinstance(job.device, IBMDevice) + mpqp_batch_results.append(extract_result(res, job, job.device)) + + return BatchResult(mpqp_batch_results) + + def extract_result( result: "QiskitResult | EstimatorResult | PrimitiveResult[PubResult | SamplerPubResult]", job: Optional[Job], From ba90fc7e3a13ba90a4ea3232fb69872d8ddcbe8e Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 8 Dec 2025 16:41:35 +0100 Subject: [PATCH 18/39] feat: support batch and session execution modes with IBM runtime --- mpqp/execution/runner.py | 132 +++++++++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 19 deletions(-) diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index 5ff0f2dd..fc7d2716 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -20,7 +20,7 @@ from numbers import Complex from textwrap import indent -from typing import TYPE_CHECKING, Iterable, Optional, Sequence, overload +from typing import TYPE_CHECKING, Optional, Sequence, overload import numpy as np @@ -39,16 +39,15 @@ GOOGLEDevice, IBMDevice, ) -from mpqp.execution.job import Job, JobStatus, JobType +from mpqp.execution.job import Job, JobStatus, JobType, VQAMode from mpqp.execution.providers.atos import run_atos, submit_QLM from mpqp.execution.providers.aws import run_braket, submit_job_braket from mpqp.execution.providers.azure import run_azure, submit_job_azure from mpqp.execution.providers.google import run_google -from mpqp.execution.providers.ibm import run_ibm, submit_remote_ibm from mpqp.execution.result import BatchResult, Result from mpqp.tools.display import state_vector_ket_shape from mpqp.tools.errors import DeviceJobIncompatibleError, RemoteExecutionError -from mpqp.tools.generics import OneOrMany, find_index, flatten +from mpqp.tools.generics import OneOrMany, find_index if TYPE_CHECKING: from sympy import Expr @@ -109,6 +108,7 @@ def generate_job( circuit: QCircuit, device: AvailableDevice, values: "Optional[dict[Expr | str, Complex]]" = None, + mode: Optional[VQAMode] = None, ) -> Job: """Creates the Job of appropriate type and containing the information needed for the execution of the circuit. @@ -128,18 +128,21 @@ def generate_job( if values is not None: circuit = circuit.subs(values, True) + if mode is None: + mode = VQAMode.JOB + m_list = circuit.measurements nb_meas = len(m_list) if nb_meas == 0: - job = Job(JobType.STATE_VECTOR, circuit, device) + job = Job(JobType.STATE_VECTOR, circuit, device, mode) elif nb_meas == 1: measurement = m_list[0] if isinstance(measurement, BasisMeasure): if measurement.shots <= 0: - job = Job(JobType.STATE_VECTOR, circuit, device) + job = Job(JobType.STATE_VECTOR, circuit, device, mode) else: - job = Job(JobType.SAMPLE, circuit, device) + job = Job(JobType.SAMPLE, circuit, device, mode) elif isinstance(measurement, ExpectationMeasure): m = adjust_measure(measurement, circuit) c = circuit.without_measurements() @@ -148,6 +151,7 @@ def generate_job( JobType.OBSERVABLE, c, device, + mode, ) else: raise NotImplementedError( @@ -168,12 +172,13 @@ def _run_diagonal_observables( device: AvailableDevice, observable_job: Job, values: "Optional[dict[Expr | str, Complex]]" = None, + mode: Optional[VQAMode] = None, ) -> Result: adapted_circuit = circuit.without_measurements() adapted_circuit.add(BasisMeasure(exp_measure.targets, shots=exp_measure.shots)) - result = _run_single(adapted_circuit, device, values, False) + result = _run_single(adapted_circuit, device, values, False, mode) probas = result.probabilities error = 0 if exp_measure.shots == 0 else None @@ -206,6 +211,7 @@ def _run_single( device: AvailableDevice, values: "Optional[dict[Expr | str, Complex]]" = None, display_breakpoints: bool = True, + mode: Optional[VQAMode] = None, reservation_arn: Optional[str] = None, ) -> Result: """Runs the circuit on the ``backend``. If the circuit depends on variables, @@ -250,13 +256,15 @@ def _run_single( for k in range(len(circuit.breakpoints)): display_kth_breakpoint(circuit, k, device) - job = generate_job(circuit, device, values) + job = generate_job(circuit, device, values, mode) job.status = JobStatus.INIT if len(circuit.measurements) == 1: measure = circuit.measurements[0] if isinstance(measure, ExpectationMeasure): if measure.optim_diagonal and measure.only_diagonal_observables(): - return _run_diagonal_observables(circuit, measure, device, job, values) + return _run_diagonal_observables( + circuit, measure, device, job, values, mode + ) if len(circuit.noises) != 0: if not device.is_noisy_simulator(): @@ -269,6 +277,11 @@ def _run_single( raise NotImplementedError(f"Noisy simulations not supported on {device}.") if isinstance(device, (IBMDevice, StaticIBMSimulatedDevice)): + from mpqp.execution.providers.ibm import run_ibm, run_remote_ibm_batch + + if job.mode == VQAMode.BATCH: + batch_results = run_remote_ibm_batch([job]) + return batch_results[0] return run_ibm(job) elif isinstance(device, ATOSDevice): return run_atos(job) @@ -289,6 +302,7 @@ def run( values: "Optional[dict[Expr | str, Complex]]" = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, + mode: Optional[VQAMode] = None, ) -> BatchResult: ... @@ -299,6 +313,7 @@ def run( values: "Optional[dict[Expr | str, Complex]]" = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, + mode: Optional[VQAMode] = None, ) -> BatchResult: ... @@ -309,6 +324,7 @@ def run( values: "Optional[dict[Expr | str, Complex]]" = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, + mode: Optional[VQAMode] = None, ) -> Result: ... @@ -318,6 +334,7 @@ def run( values: "Optional[dict[Expr | str, Complex]]" = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, + mode: Optional[VQAMode] = None, ) -> Result | BatchResult: """Runs the circuit on the backend, or list of backend, provided in parameter. @@ -394,28 +411,88 @@ def namer(circ: QCircuit, i: int): circ.label = f"circuit {i}" if circ.label is None else circ.label return circ - if isinstance(circuit, Iterable) or isinstance(device, Iterable): + if isinstance(circuit, QCircuit): + circuits = [circuit] + else: + circuits = list(circuit) + + if isinstance(device, AvailableDevice): + devices = [device] + else: + devices = list(device) + + exec_mode = mode or VQAMode.JOB + + if exec_mode == VQAMode.BATCH: + if len(devices) != 1: + raise ValueError( + "Batch mode is only defined for a single backend, but got " + f"{len(devices)} devices." + ) + + device = devices[0] + jobs = [ + generate_job(namer(circ, i + 1), device, values, exec_mode) + for i, circ in enumerate(circuits) + ] + + if isinstance(device, IBMDevice) and device.is_remote(): + from mpqp.execution.providers.ibm import run_remote_ibm_batch + + for job in jobs: + if job.job_type != JobType.OBSERVABLE: + raise ValueError( + "IBM batch execution supports only observable jobs " + f"(found {job.job_type} in circuit '{job.circuit.label}')." + ) + return run_remote_ibm_batch(jobs) + return BatchResult( [ _run_single( namer(circ, i + 1), - dev, + device, values, display_breakpoints, + mode, reservation_arn, ) - for i, circ in enumerate(flatten(circuit)) - for dev in flatten(device) + for i, circ in enumerate(circuits) ] ) + + if len(circuits) > 1 or len(devices) > 1: + counter = 1 + outputs = [] + + for circ in circuits: + labeled = namer(circ, counter) + counter += 1 + + for device in devices: + outputs.append( + _run_single( + labeled, + device, + values, + display_breakpoints, + exec_mode, + reservation_arn, + ) + ) + return BatchResult(outputs) + else: - return _run_single(circuit, device, values, display_breakpoints) + circ = namer(circuits[0], 1) + device = devices[0] + return _run_single(circ, device, values, display_breakpoints, exec_mode) def submit( circuit: QCircuit, device: AvailableDevice, values: Optional[dict[Expr | str, Complex]] = None, + mode: Optional[VQAMode] = None, reservation_arn: Optional[str] = None, ) -> tuple[str, Job]: """Submit the job related to the circuit on the remote backend provided in @@ -456,11 +533,25 @@ def submit( "submit(...) function is only made for remote device." ) - job = generate_job(circuit, device, values) + job = generate_job(circuit, device, values, mode) job.status = JobStatus.INIT if isinstance(device, IBMDevice): - job_id, _ = submit_remote_ibm(job) + if mode == VQAMode.SESSION: + from mpqp.execution.connection.ibm_connection import ( + get_backend, + get_or_create_ibm_session, + ) + from mpqp.execution.providers.ibm import submit_remote_ibm_session + + backend = get_backend(device) + session = get_or_create_ibm_session(backend) + + job_id, _ = submit_remote_ibm_session(job, session) + else: + from mpqp.execution.providers.ibm import submit_remote_ibm + + job_id, _ = submit_remote_ibm(job) elif isinstance(device, ATOSDevice): job_id, _ = submit_QLM(job) elif isinstance(device, AWSDevice): @@ -474,7 +565,10 @@ def submit( def display_kth_breakpoint( - circuit: QCircuit, k: int, device: AvailableDevice = ATOSDevice.MYQLM_CLINALG + circuit: QCircuit, + k: int, + device: AvailableDevice = ATOSDevice.MYQLM_CLINALG, + mode: Optional[VQAMode] = None, ): """Prints to the standard output the state vector corresponding to the state of the system when it encounters the `k^{th}` breakpoint. @@ -503,7 +597,7 @@ def display_kth_breakpoint( nb_cbits=circuit.nb_cbits, label=circuit.label, ) - res = _run_single(copy, device, None, False) + res = _run_single(copy, device, None, False, mode) if TYPE_CHECKING: assert isinstance(res, Result) print(f"DEBUG: After instruction {bp_instructions_index}{name_part}, state is") From fa30dee07aff50569faa2e714bf889cc9d3b398c Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 8 Dec 2025 17:21:49 +0100 Subject: [PATCH 19/39] feat: add VQAMode support to Job model --- mpqp/execution/__init__.py | 6 +++--- mpqp/execution/job.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/mpqp/execution/__init__.py b/mpqp/execution/__init__.py index 561e0cae..913aec00 100644 --- a/mpqp/execution/__init__.py +++ b/mpqp/execution/__init__.py @@ -3,14 +3,14 @@ ATOSDevice, AvailableDevice, AWSDevice, + AZUREDevice, GOOGLEDevice, IBMDevice, - AZUREDevice, ) -from .simulated_devices import IBMSimulatedDevice -from .job import Job, JobStatus, JobType +from .job import Job, JobStatus, JobType, VQAMode from .result import BatchResult, Result, Sample, StateVector from .runner import adjust_measure, run, submit +from .simulated_devices import IBMSimulatedDevice # This import has to be done after the loading of result to work, `pass` is a # trick to avoid isort to move this line above diff --git a/mpqp/execution/job.py b/mpqp/execution/job.py index 08d23d48..48c5b62e 100644 --- a/mpqp/execution/job.py +++ b/mpqp/execution/job.py @@ -70,6 +70,15 @@ class JobType(Enum): retrieve the expectation value in an optimal manner.""" +class VQAMode(Enum): + JOB = "JOB" + BATCH = "BATCH" + SESSION = "SESSION" + + def __str__(self): + return self.value + + class Job: """Representation of a job, an object regrouping all the information about the submission of a computation/measure of a quantum circuit on a @@ -110,6 +119,7 @@ def __init__( job_type: JobType, circuit: QCircuit, device: AvailableDevice, + mode: VQAMode = VQAMode.JOB, ): self._status = JobStatus.INIT @@ -119,6 +129,7 @@ def __init__( """See parameter description.""" self.device = device """See parameter description.""" + self.mode = mode self.id: Optional[str] = None """Contains the id of the remote job, used to retrieve the result from the remote provider. ``None`` if the job is local. It can take a little @@ -168,7 +179,8 @@ def status(self, job_status: JobStatus): self._status = job_status def __repr__(self) -> str: - return f"{type(self).__name__}({self.job_type}, {repr(self.circuit)}, {self.device})" + #TODO: improve repr + return f"{type(self).__name__}({self.job_type}, {repr(self.circuit)}, {self.device}, mode={self.mode})" def __eq__(self, other): # pyright: ignore[reportMissingParameterType] if not isinstance(other, Job): @@ -185,6 +197,7 @@ def to_dict(self): "job_type": self.job_type, "circuit": self.circuit, "device": self.device, + "mode": self.mode, "measure": self.measure, "id": self.id, "status": self.status, From 555cb07339b04e171309a4f0ce515b73b2fa1787 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Dec 2025 16:22:07 +0000 Subject: [PATCH 20/39] chore: Files formated --- mpqp/execution/job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpqp/execution/job.py b/mpqp/execution/job.py index 48c5b62e..8b4fdd2b 100644 --- a/mpqp/execution/job.py +++ b/mpqp/execution/job.py @@ -179,7 +179,7 @@ def status(self, job_status: JobStatus): self._status = job_status def __repr__(self) -> str: - #TODO: improve repr + # TODO: improve repr return f"{type(self).__name__}({self.job_type}, {repr(self.circuit)}, {self.device}, mode={self.mode})" def __eq__(self, other): # pyright: ignore[reportMissingParameterType] From 463640eced1119ee18c8dc4806fc09ffd8e45a50 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 10 Dec 2025 17:36:58 +0100 Subject: [PATCH 21/39] chore: update bind_parameters in QCircuit --- mpqp/core/circuit.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index b33554be..0f7209c7 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -2164,26 +2164,33 @@ def variables(self) -> set[Basic]: return params def bind_parameters( - self, device: AvailableDevice, params: dict[str | Parameter | Basic, Number] - ) -> None: + self, device: AvailableDevice, values: dict[str | Parameter | Basic, Number] + ) -> QCircuit: """Bind parameter values to the transpiled circuit.""" # TODO: to enhance docs if device not in self.transpiled_circuit: self.transpiled_for_device(device) - transpiled = self.transpiled_circuit[device] + param_values = {str(k): v for k, v in values.items()} + + from qiskit import QuantumCircuit + if isinstance(transpiled, QuantumCircuit): qiskit_param_map = { - k if isinstance(k, Parameter) else Parameter(str(k)): v - for k, v in params.items() + p: param_values[p.name] + for p in transpiled.parameters + if p.name in param_values } - transpiled.assign_parameters(qiskit_param_map, inplace=True) + if qiskit_param_map: + transpiled.assign_parameters(qiskit_param_map, inplace=True) + return self elif isinstance(transpiled, braket_Circuit): - braket_param_map = {str(k): v for k, v in params.items()} self.transpiled_circuit[device] = transpiled.make_bound_circuit( - braket_param_map + param_values ) + return self + else: raise TypeError(f"Unsupported transpiled circuit yet: {type(transpiled)}") From 9f89b8923e1d29b55e73d51bba2928e9eaec99c0 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 11 Dec 2025 17:10:39 +0100 Subject: [PATCH 22/39] chore: update optimizer behavior --- mpqp/execution/vqa/optimizer.py | 55 +++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/mpqp/execution/vqa/optimizer.py b/mpqp/execution/vqa/optimizer.py index 88aff2df..8c945f32 100644 --- a/mpqp/execution/vqa/optimizer.py +++ b/mpqp/execution/vqa/optimizer.py @@ -3,6 +3,18 @@ library.""" from enum import Enum +from functools import partial +from typing import Any, Callable, Optional, Union + +import numpy as np +import numpy.typing as npt +from scipy.optimize import OptimizeResult +from scipy.optimize import minimize as scipy_minimize + +OptimizerInput = Union[list[float], npt.NDArray[np.float_]] +OptimizableFunc = Union[partial[float], Callable[[OptimizerInput], float]] +OptimizerOptions = dict[str, Any] +# OptimizerCallback = Callable[[OptimizerInput, float], None] class Optimizer(Enum): @@ -13,8 +25,47 @@ class Optimizer(Enum): COBYLA = "COBYLA" POWELL = "POWELL" NELDER_MEAD = "Nelder-Mead" - CMAES = "CMAES" SLSQP = "SLSQP" + CMAES = "CMAES" + + +def run_optimizer( + eval_func: OptimizableFunc, + method: Optimizer, + init_params: OptimizerInput, + optimizer_options: Optional[OptimizerOptions] = None, + callback: Optional[Callable[[OptimizerInput], None]] = None, +) -> tuple[float, npt.NDArray[np.float_]]: + + if optimizer_options is None: + optimizer_options = {} + + x0 = np.asarray(init_params, dtype=float) + + if method == Optimizer.CMAES: + import cma + + sigma0 = float(optimizer_options.pop("sigma0", 0.5)) + + best_params, es = cma.fmin2( + eval_func, + x0=x0, + sigma0=sigma0, + options=optimizer_options, + ) + best_value = float(es.result.fbest) + + return best_value, np.asarray(best_params, dtype=float) + + result: OptimizeResult = scipy_minimize( + eval_func, + x0=x0, + method=method.value, + options=optimizer_options, + callback=callback, + ) + best_value = float(result.fun) + best_params = np.asarray(result.x, dtype=float) -##TODO CMAES and SLSQP implementation + return best_value, best_params From efa86c4b00934241960ca688b13827f637612d72 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 11 Dec 2025 18:13:05 +0100 Subject: [PATCH 23/39] chore: update vqa implementation --- mpqp/execution/vqa/vqa.py | 241 ++++++++++++-------------------------- 1 file changed, 78 insertions(+), 163 deletions(-) diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index b3b99c76..38a96d4b 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -1,6 +1,5 @@ from __future__ import annotations -from enum import Enum from functools import partial from typing import TYPE_CHECKING, Any, Callable, Collection, Optional, TypeVar, Union @@ -10,13 +9,14 @@ from scipy.optimize import minimize as scipy_minimize if TYPE_CHECKING: - from sympy import Expr + from sympy import Expr, Basic from mpqp.core.circuit import QCircuit from mpqp.core.instruction import ExpectationMeasure from mpqp.execution.devices import AvailableDevice, AWSDevice, IBMDevice +from mpqp.execution.job import VQAMode from mpqp.execution.runner import run -from mpqp.execution.vqa.optimizer import Optimizer +from mpqp.execution.vqa.optimizer import Optimizer, run_optimizer T1 = TypeVar("T1") T2 = TypeVar("T2") @@ -39,25 +39,19 @@ # TODO: test the minimizer options -class VQAMode(Enum): - JOB = "JOB" - BATCH = "BATCH" - SESSION = "SESSION" - HYBRID_JOB = "HYBRID_JOB" - - def __str__(self): - return self.value - - def _maps(l1: Collection[T1], l2: Collection[T2]) -> dict[T1, T2]: """Does like zip, but with a dictionary instead of a list of tuples""" if len(l1) != len(l2): - ValueError( + raise ValueError( f"Length of the two collections are not equal ({len(l1)} and {len(l2)})." ) return {e1: e2 for e1, e2 in zip(l1, l2)} +def _ordered_variables(circ: QCircuit) -> list[Basic]: + return sorted(circ.variables(), key=lambda s: str(s)) + + def minimize( optimizable: QCircuit | OptimizableFunc, method: Optimizer | OptimizerCallable, @@ -137,28 +131,18 @@ def minimize( if isinstance(optimizable, QCircuit): if device is None: raise ValueError("A device is needed to optimize a circuit") - # optimizer = _minimize_remote if device.is_remote() else _minimize_local - if device.is_remote(): - return _minimize_remote( - optimizable, - method, - device, - init_params, - nb_params, - optimizer_options, - callback, - vqa_mode=vqa_mode, - ) - else: - return _minimize_local( - optimizable, - method, - device, - init_params, - nb_params, - optimizer_options, - callback, - ) + optimizer = _minimize_remote if device.is_remote() else _minimize_local + return optimizer( + optimizable, + method, + device, + init_params, + nb_params, + optimizer_options, + callback, + vqa_mode=vqa_mode, + ) + else: # TODO: find a way to know if the job is remote or local from the function return _minimize_local( @@ -169,6 +153,7 @@ def minimize( nb_params, optimizer_options, callback, + vqa_mode=vqa_mode, ) @@ -213,124 +198,51 @@ def _minimize_remote( TODO to implement on QLM first """ - if isinstance(device, IBMDevice): - from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator - - from mpqp.core.languages import Language - from mpqp.execution.connection.ibm_connection import ( - get_backend, - get_QiskitRuntimeService, - ) + if isinstance(optimizable, QCircuit): + if device is None: + raise ValueError("A device is needed to optimize a circuit") - print(f"[VQA] Running on IBM {device.name} in mode {vqa_mode.value}") + circ = optimizable - if TYPE_CHECKING: - assert isinstance(optimizable, QCircuit) - - variables: set[Basic] = optimizable.variables() - if not variables: + # variables: set["Expr"] = circ.variables() + variables: list[Basic] = _ordered_variables(circ) + if len(variables) == 0: raise ValueError("No variables found in the circuit to optimize.") - if len(optimizable.measurements) != 1: - raise ValueError("Expected exactly one ExpectationMeasure in circuit.") - - measurement = optimizable.measurements[0] - if not isinstance(measurement, ExpectationMeasure): - raise ValueError("Expected ExpectationMeasure as measurement.") - - observables = measurement.observables - - service = get_QiskitRuntimeService() - - if TYPE_CHECKING: - assert isinstance(device, IBMDevice) - backend = get_backend(device) - - estimator_options = {"default_shots": measurement.shots} - - def remote_eval(params: OptimizerInput) -> float: - """run expectation evaluation on IBM backend with given parameters.""" - from qiskit.quantum_info import SparsePauliOp - - param_map = _maps(variables, params) - qiskit_circ = optimizable.bind_parameters( - param_map, device=device - ) # TODO: bind_parameters() - - qiskit_observables: list[SparsePauliOp] = [ - obs.to_other_language(Language.QISKIT) for obs in observables - ] # to check here - - if vqa_mode == VQAMode.JOB: - estimator = Runtime_Estimator(mode=backend, options=estimator_options) - ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) - result = ibm_job.result() - - elif vqa_mode == VQAMode.BATCH: - from qiskit_ibm_runtime import Batch - - with Batch(backend=backend) as batch: - estimator = Runtime_Estimator(mode=batch, options=estimator_options) - ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) - result = ibm_job.result() - - elif vqa_mode == VQAMode.SESSION: - from qiskit_ibm_runtime import Session - - with Session(service=service, backend=backend) as session: - estimator = Runtime_Estimator( - mode=session, options=estimator_options - ) - ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) - result = ibm_job.result() + if len(circ.measurements) != 1: + raise ValueError( + "Cannot optimize a circuit containing several measurements." + ) - else: - raise ValueError(f"Unsupported IBM execution mode: {vqa_mode.value}") + if not isinstance(circ.measurements[0], ExpectationMeasure): + raise ValueError("Expected an ExpectationMeasure to optimize the circuit.") + else: + if len(circ.measurements[0].observables) > 1: + raise ValueError( + "Expected only one observable in the ExpectationMeasure but got" + f" {len(circ.measurements[0].observables)}" + ) - values = result.values - energy = float(np.sum(values)) - print(f" [VQA][IBM] params={params} : value={energy}") - return energy + def eval_circ(params: OptimizerInput) -> float: + from numbers import Complex - if init_params is None: - init_params = [0.0] * len(variables) + params_fixed_type: Collection[Complex] = params - if isinstance(method, Optimizer): - if method == Optimizer.CMAES: - import cma + values: dict["Expr" | str, Complex] = _maps(variables, params_fixed_type) - best_params, es = cma.fmin2( - remote_eval, x0=init_params, **(optimizer_options or {}) - ) - best_value = es.result.fbest - return best_value, best_params + if isinstance(device, (IBMDevice, AWSDevice)): + circ.bind_parameters(device, values) + result = run(circ, device, values=None, mode=vqa_mode) else: - res: OptimizeResult = scipy_minimize( - remote_eval, - x0=np.array(init_params), - method=method.name.lower(), - options=optimizer_options, - callback=callback, - ) - print(f"[VQA][IBM] optimization complete. Best value = {res.fun}") - return float(res.fun), res.x - else: - best_value, best_params = method( - remote_eval, init_params, optimizer_options - ) - print( - f"[VQA][IBM] custom optimizer complete. Best value = {best_value}" - ) ##add best_params? - return best_value, best_params + result = run(circ, device, values, mode=vqa_mode) - elif isinstance(device, AWSDevice): - # TODO: AWS Braket remote execution to be implemented - print(f"[VQA] Running on IBM {device.name} in mode {vqa_mode.value}") + if TYPE_CHECKING: + assert isinstance(result.expectation_values, float) + return result.expectation_values - raise NotImplementedError("AWS remote execution not implemented yet") - - else: - raise ValueError(f"Unsupported remote device: {type(device).__name__}") + return _minimize_local_func( + eval_circ, method, init_params, len(variables), optimizer_options, callback + ) def _minimize_local( @@ -341,6 +253,7 @@ def _minimize_local( nb_params: Optional[int] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, + vqa_mode: VQAMode = VQAMode.JOB, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by it's @@ -375,7 +288,13 @@ def _minimize_local( if device is None: raise ValueError("A device is needed to optimize a circuit") return _minimize_local_circ( - optimizable, device, method, init_params, optimizer_options, callback + optimizable, + device, + method, + init_params, + optimizer_options, + callback, + vqa_mode=vqa_mode, ) else: return _minimize_local_func( @@ -390,6 +309,7 @@ def _minimize_local_circ( init_params: Optional[OptimizerInput] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, + vqa_mode: VQAMode = VQAMode.JOB, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by its @@ -419,7 +339,8 @@ def _minimize_local_circ( # The sympy `free_symbols` method returns in fact sets of Basic, which # are theoretically different from Expr, but in our case the difference # is not relevant. - variables: set["Expr"] = circ.variables() # pyright: ignore[reportAssignmentType] + # variables: set["Expr"] = circ.variables() # pyright: ignore[reportAssignmentType] + variables: list["Expr"] = _ordered_variables(circ) if len(circ.measurements) != 1: raise ValueError("Cannot optimize a circuit containing several measurements.") @@ -443,7 +364,7 @@ def eval_circ(params: OptimizerInput): ) values: dict[Expr | str, Complex] = _maps(variables, params_fixed_type) - result = run(circ, device, values) + result = run(circ, device, values, mode=vqa_mode) if TYPE_CHECKING: assert isinstance(result.expectation_values, float) return result.expectation_values @@ -499,22 +420,16 @@ def _minimize_local_func( init_params = [0.0] * nb_params if isinstance(method, Optimizer): - if method == Optimizer.CMAES: - import cma - best_params, es = cma.fmin2( - eval_func, x0=init_params, **(optimizer_options or {}) - ) - best_value = es.result.fbest - return best_value, best_params - else: - res: OptimizeResult = scipy_minimize( - eval_func, - x0=np.array(init_params), - method=method.name.lower(), - options=optimizer_options, - callback=callback, - ) - return float(res.fun), res.x - else: - return method(eval_func, init_params, optimizer_options) + best_value, best_params = run_optimizer( + eval_func, + method, + init_params, + optimizer_options, + callback, + ) + return best_value, best_params + + best_value, best_params = method(eval_func, init_params, optimizer_options) + + return float(best_value), np.asarray(best_params, dtype=float) From ce9576ad1a5babe791169710e350eb7e115bfbdb Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 16 Dec 2025 10:53:48 +0100 Subject: [PATCH 24/39] feat: handle bind parameters --- mpqp/execution/job.py | 4 ++++ mpqp/execution/providers/ibm.py | 4 ++++ mpqp/execution/runner.py | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mpqp/execution/job.py b/mpqp/execution/job.py index 8b4fdd2b..1c5c34fb 100644 --- a/mpqp/execution/job.py +++ b/mpqp/execution/job.py @@ -13,9 +13,12 @@ from __future__ import annotations +from numbers import Number from typing import TYPE_CHECKING, Optional from aenum import Enum, NoAlias, auto +from qiskit.circuit import Parameter +from sympy import Basic from mpqp.tools.generics import MessageEnum @@ -136,6 +139,7 @@ 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.values: Optional[dict[str | Parameter | Basic, Number]] = None @property def measure(self) -> Optional[Measure]: diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index ec13459b..dd5799e1 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -537,6 +537,10 @@ def _submit_remote_ibm( check_job_compatibility(job) qiskit_circ = job.circuit.transpiled_for_device(job.device) + + if job.values is not None: + job.circuit.bind_parameters(job.device, job.values) + if TYPE_CHECKING: assert isinstance(qiskit_circ, QuantumCircuit) diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index fc7d2716..2119499a 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -125,7 +125,7 @@ def generate_job( Returns: The Job containing information about the execution of the circuit. """ - if values is not None: + if values is not None and not device.is_remote(): circuit = circuit.subs(values, True) if mode is None: From 16f09efc48e95a9a353f30e997bf52de5f175205 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Fri, 23 Jan 2026 14:44:21 +0100 Subject: [PATCH 25/39] fix: improve session handling --- mpqp/execution/connection/ibm_connection.py | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/mpqp/execution/connection/ibm_connection.py b/mpqp/execution/connection/ibm_connection.py index 660bb641..9bd31810 100644 --- a/mpqp/execution/connection/ibm_connection.py +++ b/mpqp/execution/connection/ibm_connection.py @@ -218,13 +218,14 @@ def get_or_create_ibm_session( If a session exists and is valid, reuse it. Otherwise, create a new one. """ from qiskit_ibm_runtime import Session + from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError, IBMRuntimeError backend_name = backend.name if backend_name in _ibm_sessions: session = _ibm_sessions[backend_name] try: status = session.status() - except Exception: + except (IBMNotAuthorizedError, IBMRuntimeError): status = "Closed" if status not in ("Closed", None): @@ -238,11 +239,19 @@ def get_or_create_ibm_session( def close_ibm_session(backend: "BackendV2") -> None: """Close the currently active session, if one exists.""" + from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError, IBMRuntimeError + backend_name = backend.name - if backend_name in _ibm_sessions: - try: - _ibm_sessions[backend_name].close() - except Exception: - pass - finally: - del _ibm_sessions[backend_name] + if backend_name not in _ibm_sessions: + return + + session = _ibm_sessions[backend_name] + try: + session.close() + except (IBMNotAuthorizedError, IBMRuntimeError) as err: + raise IBMRemoteExecutionError( + f"Failed to close IBM session for backend '{backend_name}'. " + "Session was not removed; you can retry.\nTrace: " + str(err) + ) + else: + del _ibm_sessions[backend_name] From 70e06cd37b42526308ae53efbb9f03ea83bba156 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 13:44:41 +0000 Subject: [PATCH 26/39] chore: Files formated --- docs/conf.py | 4 +-- examples/scripts/noisy_simulations.py | 1 - mpqp/execution/connection/qlm_connection.py | 6 ++-- mpqp/local_storage/queries.py | 30 ++++++----------- mpqp/local_storage/setup.py | 24 +++++--------- mpqp/qasm/lexer_utils.py | 1 - tests/core/test_circuit.py | 18 ++++------- tests/qasm/test_open_qasm_2_and_3.py | 36 +++++++-------------- tests/qasm/test_qasm_to_mpqp.py | 6 ++-- 9 files changed, 41 insertions(+), 85 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0098fac1..98fe9bc8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -342,9 +342,7 @@ def __init__(self, **options): # type: ignore PygmentsBridge.latex_formatter = CustomLatexFormatter -latex_elements[ - "preamble" -] += r""" +latex_elements["preamble"] += r""" % One-column index \makeatletter \renewenvironment{theindex}{ diff --git a/examples/scripts/noisy_simulations.py b/examples/scripts/noisy_simulations.py index ece91262..a66e099f 100644 --- a/examples/scripts/noisy_simulations.py +++ b/examples/scripts/noisy_simulations.py @@ -4,7 +4,6 @@ from mpqp.noise import * from mpqp.execution import * - circuit = QCircuit( [Rx(0.3, 2), H(0), CNOT(1, 0), SWAP(2, 1), U(0.9, 0.2, 1, 1), BasisMeasure()] ) diff --git a/mpqp/execution/connection/qlm_connection.py b/mpqp/execution/connection/qlm_connection.py index f9dce628..dffce9cd 100644 --- a/mpqp/execution/connection/qlm_connection.py +++ b/mpqp/execution/connection/qlm_connection.py @@ -51,12 +51,10 @@ def config_qlm_account(username: str, password: str, global_config: bool) -> boo if global_config: print("we are in the global part") with open(netrc_path, "w") as file: - file.write( - f"""\ + file.write(f"""\ machine qlm35e.neasqc.eu login {username} -password {password}""" - ) +password {password}""") # Set the permissions to read and right for user only os.chmod(netrc_path, 0o600) else: diff --git a/mpqp/local_storage/queries.py b/mpqp/local_storage/queries.py index 7b1befbf..a377c354 100644 --- a/mpqp/local_storage/queries.py +++ b/mpqp/local_storage/queries.py @@ -335,12 +335,10 @@ def fetch_jobs_with_result_and_job( json.dumps(repr(res.job.measure)) if res.job.measure else None ) - job_filters.append( - """ + job_filters.append(""" (results.data is ? AND results.error is ? AND results.shots is ? AND jobs.type is ? AND jobs.circuit is ? AND jobs.device is ? AND jobs.measure is ?) - """ - ) + """) params.extend( [ data_json, @@ -413,11 +411,9 @@ def fetch_jobs_with_result(result: Result | BatchResult | list[Result]) -> list[ json.dumps(repr(res.error)) if res.error is not None else None ) - job_filters.append( - """ + job_filters.append(""" (results.data is ? AND results.error is ? AND results.shots is ?) - """ - ) + """) params.extend( [ data_json, @@ -497,12 +493,10 @@ def fetch_results_with_result_and_job( json.dumps(repr(res.job.measure)) if res.job.measure else None ) - result_filters.append( - """ + result_filters.append(""" (results.data is ? AND results.error is ? AND results.shots is ? AND jobs.type is ? AND jobs.circuit is ? AND jobs.device is ? AND jobs.measure is ?) - """ - ) + """) params.extend( [ data_json, @@ -571,11 +565,9 @@ def fetch_results_with_job(jobs: Job | list[Job]) -> list[DictDB]: circuit_json = json.dumps(repr(job.circuit)) measure_json = json.dumps(repr(job.measure)) if job.measure else None - result_filters.append( - """ + result_filters.append(""" (jobs.type is ? AND jobs.circuit is ? AND jobs.device is ? AND jobs.measure is ?) - """ - ) + """) params.extend( [ job.job_type.name, @@ -647,11 +639,9 @@ def fetch_results_with_result( json.dumps(repr(res.error)) if res.error is not None else None ) - result_filters.append( - """ + result_filters.append(""" (results.data is ? AND results.error is ? AND results.shots is ?) - """ - ) + """) params.extend([data_json, error_json, res.shots]) query = f""" diff --git a/mpqp/local_storage/setup.py b/mpqp/local_storage/setup.py index aa20a830..2dd4d502 100644 --- a/mpqp/local_storage/setup.py +++ b/mpqp/local_storage/setup.py @@ -49,12 +49,10 @@ def wrapper(*args: Any, **kwargs: dict[str, Any]) -> T: db_version = get_database_version() if db_version != DATABASE_VERSION: - raise RuntimeError( - f"""\ + raise RuntimeError(f"""\ Database version {db_version} is outdated. Current supported version: {DATABASE_VERSION}. Automated migration is not yet supported, please contact library authors to get\ - help for the migration.""" - ) + help for the migration.""") return func(*args, **kwargs) @@ -90,8 +88,7 @@ def setup_local_storage(path: Optional[str] = None): cursor = connection.cursor() # Create the jobs table - cursor.execute( - ''' + cursor.execute(''' CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, @@ -102,12 +99,10 @@ def setup_local_storage(path: Optional[str] = None): status TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) - ''' - ) + ''') # Create the results table - cursor.execute( - ''' + cursor.execute(''' CREATE TABLE IF NOT EXISTS results ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id INTEGER NOT NULL, @@ -116,17 +111,14 @@ def setup_local_storage(path: Optional[str] = None): shots INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) - ''' - ) + ''') - cursor.execute( - ''' + cursor.execute(''' CREATE TABLE IF NOT EXISTS version ( id INTEGER PRIMARY KEY CHECK (id = 1), -- Ensures only one row exists version VARCHAR ) - ''' - ) + ''') cursor.execute( "INSERT OR IGNORE INTO version (id, version) VALUES (1, ?)", (DATABASE_VERSION,) diff --git a/mpqp/qasm/lexer_utils.py b/mpqp/qasm/lexer_utils.py index b1dd35b1..331a3720 100644 --- a/mpqp/qasm/lexer_utils.py +++ b/mpqp/qasm/lexer_utils.py @@ -111,7 +111,6 @@ def t_error(t): # pyright: ignore[reportMissingParameterType] from mpqp.gates import * - single_qubits_gate_qasm = { "h": H, "x": X, diff --git a/tests/core/test_circuit.py b/tests/core/test_circuit.py index ec7495f7..6d638aa0 100644 --- a/tests/core/test_circuit.py +++ b/tests/core/test_circuit.py @@ -519,8 +519,7 @@ def test_without_measurements(circuit: QCircuit, printed_result_filename: str): QCircuit([CNOT(0, 1), Depolarizing(0.5, [0, 1])]), (Language.BRAKET,), BraketCircuit, - ( - """\ + ("""\ T : │ 0 │ ┌───────────┐ q0 : ───●───┤ DEPO(0.5) ├─ @@ -528,15 +527,13 @@ def test_without_measurements(circuit: QCircuit, printed_result_filename: str): ┌─┴─┐ ┌───────────┐ q1 : ─┤ X ├─┤ DEPO(0.5) ├─ └───┘ └───────────┘ -T : │ 0 │""" - ), +T : │ 0 │"""), ), ( QCircuit([CNOT(0, 1), Depolarizing(0.5, [0, 1], dimension=2)]), (Language.BRAKET,), BraketCircuit, - ( - """\ + ("""\ T : │ 0 │ ┌───────────┐ q0 : ───●───┤ DEPO(0.5) ├─ @@ -544,8 +541,7 @@ def test_without_measurements(circuit: QCircuit, printed_result_filename: str): ┌─┴─┐ ┌─────┴─────┐ q1 : ─┤ X ├─┤ DEPO(0.5) ├─ └───┘ └───────────┘ -T : │ 0 │""" - ), +T : │ 0 │"""), ), ( QCircuit( @@ -553,8 +549,7 @@ def test_without_measurements(circuit: QCircuit, printed_result_filename: str): ), (Language.BRAKET,), BraketCircuit, - ( - """\ + ("""\ T : │ 0 │ ┌───────────┐ q0 : ───●───┤ DEPO(0.5) ├─ @@ -562,8 +557,7 @@ def test_without_measurements(circuit: QCircuit, printed_result_filename: str): ┌─┴─┐ ┌─────┴─────┐ q1 : ─┤ X ├─┤ DEPO(0.5) ├─ └───┘ └───────────┘ -T : │ 0 │""" - ), +T : │ 0 │"""), ), ], ) diff --git a/tests/qasm/test_open_qasm_2_and_3.py b/tests/qasm/test_open_qasm_2_and_3.py index c17eaaa9..47f75e38 100644 --- a/tests/qasm/test_open_qasm_2_and_3.py +++ b/tests/qasm/test_open_qasm_2_and_3.py @@ -85,8 +85,7 @@ def test_circular_dependency_detection_false_positive_3_to_2(): @pytest.mark.parametrize( "qasm_code", [ - ( - """OPENQASM 2.0; + ("""OPENQASM 2.0; include "qelib1.inc"; gate rzz(theta) a,b { @@ -97,10 +96,8 @@ def test_circular_dependency_detection_false_positive_3_to_2(): qreg q[3]; creg c[2]; rzz(0.2) q[1], q[2]; - measure q[2] -> c[0];""" - ), - ( - """OPENQASM 2.0; + measure q[2] -> c[0];"""), + ("""OPENQASM 2.0; include "qelib1.inc"; gate my_gate a,b { h a; @@ -109,25 +106,19 @@ def test_circular_dependency_detection_false_positive_3_to_2(): qreg q[2]; creg c[2]; my_gate q[0], q[1]; - measure q -> c;""" - ), - ( - """OPENQASM 2.0; + measure q -> c;"""), + ("""OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; cx q[0],q[1]; - cx q[1],q[2];""" - ), - ( - """OPENQASM 2.0; + cx q[1],q[2];"""), + ("""OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; creg c[2]; u1(0.2) q[1], q[2]; - measure q[2] -> c[0];""" - ), - ( - """OPENQASM 2.0; + measure q[2] -> c[0];"""), + ("""OPENQASM 2.0; include "qelib1.inc"; gate rzz(theta) a,b { cx a,b; @@ -137,10 +128,8 @@ def test_circular_dependency_detection_false_positive_3_to_2(): qreg q[3]; creg c[2]; rzz(0.2) q[1] , q[2]; - measure q[2] -> c[0];""" - ), - ( - """OPENQASM 2.0; + measure q[2] -> c[0];"""), + ("""OPENQASM 2.0; include "qelib1.inc"; gate MyGate a, b { @@ -158,8 +147,7 @@ def test_circular_dependency_detection_false_positive_3_to_2(): creg c[3]; MyGate q[0], q[1]; - MyGate2 q[0], q[1], q[2];""" - ), + MyGate2 q[0], q[1], q[2];"""), ], ) def test_conversion_2_and_3(qasm_code: str): diff --git a/tests/qasm/test_qasm_to_mpqp.py b/tests/qasm/test_qasm_to_mpqp.py index c27b59f7..dbc0da4d 100644 --- a/tests/qasm/test_qasm_to_mpqp.py +++ b/tests/qasm/test_qasm_to_mpqp.py @@ -176,14 +176,12 @@ def test_qasm2_to_mpqp(qasm_code: str, gate_names: list[str]): @pytest.mark.parametrize( "qasm_code", [ - ( - """OPENQASM 2.0; + ("""OPENQASM 2.0; include "qelib1.inc"; qreg q[1]; h q[0] - cx q[0], """ - ), + cx q[0], """), ], ) def test_invalid_qasm_code(qasm_code: str): From eeb7a1695121d7e767139caf8aea37e1857fb3d0 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Fri, 23 Jan 2026 15:27:23 +0100 Subject: [PATCH 27/39] fix: avoid repeated observable pre_transpile conversion, update types --- .../measurement/expectation_value.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index 94d8a7f5..3c69d832 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -85,7 +85,9 @@ def __init__( "See parameter description." self.pre_transpile: dict[ AvailableDevice, - Union[SparsePauliOp, QLMObservable, CirqPauliSum, CirqPauliString], + Union[ + SparsePauliOp, QLMObservable, CirqPauliSum, CirqPauliString, Hermitian + ], ] = {} if isinstance(observable, PauliString): @@ -406,7 +408,7 @@ def __init__( """See parameter description.""" self.optimize_measurement = optimize_measurement """See parameter description.""" - self.pre_transpile: dict[ + self.translated_pre_measures: dict[ AvailableDevice, tuple[list[dict[str, npt.NDArray[np.float64]]], list[braket_Circuit]], ] = {} @@ -530,8 +532,8 @@ def get_pauli_grouping(self) -> list[list[PauliStringMonomial]]: grouped_monomials = [ [ PauliString.from_str( - mono.to_label() - ) # pyright: ignore[reportAttributeAccessIssue] + mono.to_label() # pyright: ignore[reportAttributeAccessIssue] + ) for mono in pauli ] for pauli in grouped @@ -613,16 +615,17 @@ def pre_transpile_observables(self, device: AvailableDevice): {monom.name: pauli_monomial_eigenvalues(monom) for monom in group} for group in grouping ] - self.pre_transpile[device] = ( + self.translated_pre_measures[device] = ( eigenvalues, transpiled_pre_measures, ) elif isinstance(device, IBMDevice): for observable in self.observables: - observable.pre_transpile[device] = observable.to_other_language( - language=Language.QISKIT - ) + if device not in observable.pre_transpile: + observable.pre_transpile[device] = observable.to_other_language( + language=Language.QISKIT + ) else: raise NotImplementedError( f"Pre-transpilation for device {type(device).__name__} is not implemented." From 275e6057b27dfb1d2d429f828fb3c1ceae9d0c96 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Fri, 23 Jan 2026 16:59:44 +0100 Subject: [PATCH 28/39] chore: update job handling --- mpqp/execution/job.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/mpqp/execution/job.py b/mpqp/execution/job.py index 1c5c34fb..8d5382c5 100644 --- a/mpqp/execution/job.py +++ b/mpqp/execution/job.py @@ -13,7 +13,6 @@ from __future__ import annotations -from numbers import Number from typing import TYPE_CHECKING, Optional from aenum import Enum, NoAlias, auto @@ -73,7 +72,11 @@ class JobType(Enum): retrieve the expectation value in an optimal manner.""" -class VQAMode(Enum): +class ExecutionMode(Enum): + """Execution mode for remote backends. + It controls how jobs are submitted (single job, batch and session). + """ + JOB = "JOB" BATCH = "BATCH" SESSION = "SESSION" @@ -99,6 +102,7 @@ class Job: device: Device (simulator, quantum computer) on which we want to execute the job. measure: Object representing the measure to perform. + mode: Remote execution mode (JOB(single), BATCH(batch/params) and SESSION(IBM Runtime Session)). Examples: >>> circuit = QCircuit(3) @@ -122,7 +126,7 @@ def __init__( job_type: JobType, circuit: QCircuit, device: AvailableDevice, - mode: VQAMode = VQAMode.JOB, + mode: ExecutionMode = ExecutionMode.JOB, ): self._status = JobStatus.INIT @@ -133,13 +137,22 @@ def __init__( self.device = device """See parameter description.""" self.mode = mode + """See parameter description.""" self.id: Optional[str] = None """Contains the id of the remote job, used to retrieve the result from the remote provider. ``None`` if the job is local. It can take a little 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.values: Optional[dict[str | Parameter | Basic, Number]] = None + self.values: Optional[dict[str | Parameter | Basic, float | int | complex]] = ( + None + ) + """Parameter bindings for circuits containing symbolic variables. + + For local execution, parameters are typically substituted directly into the + circuit prior to execution. For remote execution, these bindings are stored in the + ``Job`` so the provider can bind them on the transpiled circuit without + re-transpiling the circuit each time.""" @property def measure(self) -> Optional[Measure]: @@ -235,7 +248,7 @@ def load_by_local_id(job_id: int): Uses :func:`~mpqp.local_storage.load.get_jobs_with_id`. Args: - job_id: Local id of the job you need. + job_id: Local id of the job you need. Example: >>> Job.load_by_local_id(1) # doctest: +ELLIPSIS From 69862b1b50321e8de42fb7bc6cefddda062a4415 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Mon, 26 Jan 2026 11:52:13 +0100 Subject: [PATCH 29/39] fix: bind parameters on transpiled circuit without mutating original --- mpqp/core/circuit.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index 0f7209c7..4f513e72 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -2174,6 +2174,9 @@ def bind_parameters( param_values = {str(k): v for k, v in values.items()} + bound_circuit = deepcopy(self) + bound_circuit.transpiled_circuit = dict(self.transpiled_circuit) + from qiskit import QuantumCircuit if isinstance(transpiled, QuantumCircuit): @@ -2182,15 +2185,18 @@ def bind_parameters( for p in transpiled.parameters if p.name in param_values } - if qiskit_param_map: - transpiled.assign_parameters(qiskit_param_map, inplace=True) - return self + bound_circuit.transpiled_circuit[device] = ( + transpiled.assign_parameters(qiskit_param_map) + if qiskit_param_map + else transpiled + ) + return bound_circuit elif isinstance(transpiled, braket_Circuit): - self.transpiled_circuit[device] = transpiled.make_bound_circuit( + bound_circuit.transpiled_circuit[device] = transpiled.make_bound_circuit( param_values ) - return self + return bound_circuit else: raise TypeError(f"Unsupported transpiled circuit yet: {type(transpiled)}") From ad716f6f157d36ce89095024ee39414076cbf03d Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 27 Jan 2026 13:11:38 +0100 Subject: [PATCH 30/39] chore: update remote execution flow --- mpqp/execution/providers/ibm.py | 40 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index dd5799e1..56e7dc7c 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -18,7 +18,7 @@ get_QiskitRuntimeService, ) from mpqp.execution.devices import AZUREDevice, IBMDevice -from mpqp.execution.job import Job, JobStatus, JobType, VQAMode +from mpqp.execution.job import ExecutionMode, Job, JobStatus, JobType from mpqp.execution.result import BatchResult, Result, Sample, StateVector from mpqp.noise import DimensionalNoiseModel from mpqp.tools.errors import ( @@ -64,7 +64,7 @@ def run_ibm(job: Job) -> Result: if not job.device.is_remote(): return run_aer(job) - if job.mode == VQAMode.SESSION: + if job.mode == ExecutionMode.SESSION: return run_remote_ibm_session(job) return run_remote_ibm(job) @@ -110,7 +110,6 @@ def compute_expectation_value( job.measure.pre_transpile_observables(job.device) for obs in job.measure.observables: translated = obs.pre_transpile[job.device] - if TYPE_CHECKING: assert isinstance(translated, SparsePauliOp) qiskit_observables.append(translated) @@ -451,6 +450,7 @@ def run_aer(job: Job): job_circuit = job.circuit if TYPE_CHECKING: assert isinstance(job.device, (IBMDevice, StaticIBMSimulatedDevice)) + if isinstance(job.device, StaticIBMSimulatedDevice): if len(job.circuit.noises) != 0: warnings.warn( @@ -461,6 +461,8 @@ def run_aer(job: Job): # (grab qiskit NoiseModel from AerSimulator generated below, and add # to it directly) backend_sim = job.device.to_noisy_simulator() + job_circuit = job.circuit.transpiled_for_device(job.device) + elif len(job.circuit.noises) != 0: job_circuit = job.circuit.transpiled_for_device(job.device) @@ -477,6 +479,7 @@ def run_aer(job: Job): qiskit_circuit = job_circuit if TYPE_CHECKING: assert isinstance(qiskit_circuit, QuantumCircuit) + if job.job_type == JobType.STATE_VECTOR: # the save_statevector method is patched on qiskit_aer load, meaning # the type checker can't find it. I hate it but it is what it is. @@ -493,9 +496,7 @@ def run_aer(job: Job): elif job.job_type == JobType.SAMPLE: if TYPE_CHECKING: assert job.measure is not None - job.status = JobStatus.RUNNING - job_sim = backend_sim.run(qiskit_circuit, shots=job.measure.shots) result_sim = job_sim.result() if TYPE_CHECKING: @@ -533,20 +534,20 @@ def _submit_remote_ibm( from qiskit_ibm_runtime import SamplerV2 as Runtime_Sampler meas = job.measure - check_job_compatibility(job) - qiskit_circ = job.circuit.transpiled_for_device(job.device) - + circuit = job.circuit if job.values is not None: - job.circuit.bind_parameters(job.device, job.values) + circuit = circuit.bind_parameters(job.device, job.values) + qiskit_circ = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(qiskit_circ, QuantumCircuit) if job.job_type == JobType.OBSERVABLE: if TYPE_CHECKING: assert isinstance(meas, ExpectationMeasure) + estimator = Runtime_Estimator(mode=runtime_target) meas.pre_transpile_observables(job.device) @@ -567,7 +568,6 @@ def _submit_remote_ibm( twirling.shots_per_randomization = meas.shots setattr(estimator.options, "default_shots", meas.shots) - ibm_job = estimator.run([(qiskit_circ, qiskit_observables)]) elif job.job_type == JobType.SAMPLE: @@ -575,6 +575,7 @@ def _submit_remote_ibm( assert isinstance(meas, BasisMeasure) sampler = Runtime_Sampler(mode=runtime_target) ibm_job = sampler.run([qiskit_circ], shots=meas.shots) + else: raise NotImplementedError( f"{job.job_type} not handled by remote remote IBM devices." @@ -607,10 +608,10 @@ def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"] "Batch execution requires at leat one Job object" ) - first_job = jobs[0] + reference_job = jobs[0] if TYPE_CHECKING: - assert isinstance(first_job.device, IBMDevice) - backend = get_backend(first_job.device) + assert isinstance(reference_job.device, IBMDevice) + backend = get_backend(reference_job.device) from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator @@ -618,7 +619,7 @@ def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"] execution_target = ( get_or_create_ibm_session(backend) - if first_job.mode == VQAMode.SESSION + if reference_job.mode == ExecutionMode.SESSION else backend ) estimator = Runtime_Estimator(mode=execution_target) @@ -630,8 +631,13 @@ def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"] meas = job.measure check_job_compatibility(job) + circuit = job.circuit + if job.values is not None: + circuit = circuit.bind_parameters(job.device, job.values) + qc = job.circuit.transpiled_for_device(job.device) - assert isinstance(qc, QuantumCircuit) + if TYPE_CHECKING: + assert isinstance(qc, QuantumCircuit) per_job_circuits.append(qc) @@ -654,7 +660,9 @@ def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"] return job_ids, ibm_job -def submit_remote_ibm_session(job: Job, session: Session) -> tuple[str, "RuntimeJobV2"]: +def submit_remote_ibm_session( + job: Job, session: "Session" +) -> tuple[str, "RuntimeJobV2"]: # TODO: docs return _submit_remote_ibm(job, runtime_target=session) From 1825b948fb850afcccae7235a65b7935f21dd2be Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Tue, 27 Jan 2026 13:16:30 +0100 Subject: [PATCH 31/39] chore: rename mode, simplify batch handling --- mpqp/execution/runner.py | 182 ++++++++++++++++++++++++++------------- 1 file changed, 121 insertions(+), 61 deletions(-) diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index 2119499a..e463bb94 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -20,7 +20,7 @@ from numbers import Complex from textwrap import indent -from typing import TYPE_CHECKING, Optional, Sequence, overload +from typing import TYPE_CHECKING, Optional, Sequence, Union, overload import numpy as np @@ -39,7 +39,7 @@ GOOGLEDevice, IBMDevice, ) -from mpqp.execution.job import Job, JobStatus, JobType, VQAMode +from mpqp.execution.job import ExecutionMode, Job, JobStatus, JobType from mpqp.execution.providers.atos import run_atos, submit_QLM from mpqp.execution.providers.aws import run_braket, submit_job_braket from mpqp.execution.providers.azure import run_azure, submit_job_azure @@ -53,6 +53,40 @@ from sympy import Expr +ValuesKey = Union["Expr", str] +ValuesDict = dict[ValuesKey, Complex] +BatchValuesInput = Optional[Union[ValuesDict, Sequence[ValuesDict]]] + + +def prepare_run_batch_inputs( + circuits: list[QCircuit], + values: BatchValuesInput, +) -> tuple[list[QCircuit], list[Optional[ValuesDict]]]: + + # TODO: docs + + if values is None: + return circuits, [None] * len(circuits) + + if isinstance(values, dict): + return circuits, [values] * len(circuits) + values_list = list(values) + + if len(circuits) == 1 and len(values_list) > 1: + return [circuits[0] for _ in range(len(values_list))], list(values_list) + + if len(values_list) == 1 and len(circuits) > 1: + return circuits, [values_list[0]] * len(circuits) + + if len(values_list) == len(circuits): + return circuits, list(values_list) + + raise ValueError( + "In BATCH mode, number of circuits must match number of values dicts " + f"Got {len(circuits)} circuits and {len(values_list)} values sets." + ) + + def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): """We allow the measure to not span the entire circuit, but providers usually do not support this behavior. To make this work, we tweak the measure @@ -107,8 +141,8 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): def generate_job( circuit: QCircuit, device: AvailableDevice, - values: "Optional[dict[Expr | str, Complex]]" = None, - mode: Optional[VQAMode] = None, + values: Optional[ValuesDict] = None, + mode: Optional[ExecutionMode] = None, ) -> Job: """Creates the Job of appropriate type and containing the information needed for the execution of the circuit. @@ -128,21 +162,23 @@ def generate_job( if values is not None and not device.is_remote(): circuit = circuit.subs(values, True) - if mode is None: - mode = VQAMode.JOB + exec_mode = mode or ExecutionMode.JOB m_list = circuit.measurements nb_meas = len(m_list) if nb_meas == 0: - job = Job(JobType.STATE_VECTOR, circuit, device, mode) + job = Job(JobType.STATE_VECTOR, circuit, device, exec_mode) + elif nb_meas == 1: measurement = m_list[0] if isinstance(measurement, BasisMeasure): - if measurement.shots <= 0: - job = Job(JobType.STATE_VECTOR, circuit, device, mode) - else: - job = Job(JobType.SAMPLE, circuit, device, mode) + job = ( + Job(JobType.STATE_VECTOR, circuit, device, exec_mode) + if measurement.shots <= 0 + else Job(JobType.SAMPLE, circuit, device, exec_mode) + ) + elif isinstance(measurement, ExpectationMeasure): m = adjust_measure(measurement, circuit) c = circuit.without_measurements() @@ -151,8 +187,9 @@ def generate_job( JobType.OBSERVABLE, c, device, - mode, + exec_mode, ) + else: raise NotImplementedError( f"Measurement type {type(measurement)} not handled" @@ -163,6 +200,9 @@ def generate_job( "circuit." ) + if values is not None and device.is_remote(): + job.values = values + return job @@ -171,8 +211,8 @@ def _run_diagonal_observables( exp_measure: ExpectationMeasure, device: AvailableDevice, observable_job: Job, - values: "Optional[dict[Expr | str, Complex]]" = None, - mode: Optional[VQAMode] = None, + values: Optional[ValuesDict] = None, + mode: Optional[ExecutionMode] = None, ) -> Result: adapted_circuit = circuit.without_measurements() @@ -209,9 +249,9 @@ def _run_diagonal_observables( def _run_single( circuit: QCircuit, device: AvailableDevice, - values: "Optional[dict[Expr | str, Complex]]" = None, + values: Optional[ValuesDict] = None, display_breakpoints: bool = True, - mode: Optional[VQAMode] = None, + mode: Optional[ExecutionMode] = None, reservation_arn: Optional[str] = None, ) -> Result: """Runs the circuit on the ``backend``. If the circuit depends on variables, @@ -258,6 +298,7 @@ def _run_single( job = generate_job(circuit, device, values, mode) job.status = JobStatus.INIT + if len(circuit.measurements) == 1: measure = circuit.measurements[0] if isinstance(measure, ExpectationMeasure): @@ -279,10 +320,12 @@ def _run_single( if isinstance(device, (IBMDevice, StaticIBMSimulatedDevice)): from mpqp.execution.providers.ibm import run_ibm, run_remote_ibm_batch - if job.mode == VQAMode.BATCH: + if job.mode == ExecutionMode.BATCH and device.is_remote(): batch_results = run_remote_ibm_batch([job]) return batch_results[0] + return run_ibm(job) + elif isinstance(device, ATOSDevice): return run_atos(job) elif isinstance(device, AWSDevice): @@ -291,6 +334,7 @@ def _run_single( return run_google(job) elif isinstance(device, AZUREDevice): return run_azure(job) + else: raise NotImplementedError(f"Device {device} not handled") @@ -299,10 +343,11 @@ def _run_single( def run( circuit: OneOrMany[QCircuit], device: Sequence[AvailableDevice], - values: "Optional[dict[Expr | str, Complex]]" = None, + values: BatchValuesInput = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, - mode: Optional[VQAMode] = None, + mode: Optional[ExecutionMode] = None, + values_batch: Optional[list[ValuesDict]] = None, ) -> BatchResult: ... @@ -310,10 +355,11 @@ def run( def run( circuit: Sequence[QCircuit], device: OneOrMany[AvailableDevice], - values: "Optional[dict[Expr | str, Complex]]" = None, + values: Optional[ValuesDict] = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, - mode: Optional[VQAMode] = None, + mode: Optional[ExecutionMode] = None, + values_batch: Optional[list[ValuesDict]] = None, ) -> BatchResult: ... @@ -321,20 +367,22 @@ def run( def run( circuit: QCircuit, device: AvailableDevice, - values: "Optional[dict[Expr | str, Complex]]" = None, + values: Optional[ValuesDict] = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, - mode: Optional[VQAMode] = None, + mode: Optional[ExecutionMode] = None, + values_batch: Optional[list[ValuesDict]] = None, ) -> Result: ... def run( circuit: OneOrMany[QCircuit], device: OneOrMany[AvailableDevice], - values: "Optional[dict[Expr | str, Complex]]" = None, + values: BatchValuesInput = None, display_breakpoints: bool = True, reservation_arn: Optional[str] = None, - mode: Optional[VQAMode] = None, + mode: Optional[ExecutionMode] = None, + values_batch: Optional[list[ValuesDict]] = None, ) -> Result | BatchResult: """Runs the circuit on the backend, or list of backend, provided in parameter. @@ -411,32 +459,36 @@ def namer(circ: QCircuit, i: int): circ.label = f"circuit {i}" if circ.label is None else circ.label return circ - if isinstance(circuit, QCircuit): - circuits = [circuit] - else: - circuits = list(circuit) + circuits = [circuit] if isinstance(circuit, QCircuit) else list(circuit) + devices = [device] if isinstance(device, AvailableDevice) else list(device) - if isinstance(device, AvailableDevice): - devices = [device] - else: - devices = list(device) + exec_mode = mode or ExecutionMode.JOB - exec_mode = mode or VQAMode.JOB + if values_batch is not None and exec_mode != ExecutionMode.BATCH: + raise ValueError("values_batch is only supported when mode == VQAMode.BATCH") - if exec_mode == VQAMode.BATCH: + if exec_mode == ExecutionMode.BATCH: if len(devices) != 1: raise ValueError( "Batch mode is only defined for a single backend, but got " f"{len(devices)} devices." ) - device = devices[0] - jobs = [ - generate_job(namer(circ, i + 1), device, values, exec_mode) - for i, circ in enumerate(circuits) - ] + if values_batch is not None and len(values_batch) != len(circuits): + raise ValueError("values_batch must have the same length as circuits.") + + target_device = devices[0] + per_run_circuits, per_run_values = prepare_run_batch_inputs(circuits, values) - if isinstance(device, IBMDevice) and device.is_remote(): + jobs = [] + for i, circ in enumerate(per_run_circuits): + jobs.append( + generate_job( + namer(circ, i + 1), target_device, per_run_values[i], exec_mode + ) + ) + + if isinstance(target_device, IBMDevice) and target_device.is_remote(): from mpqp.execution.providers.ibm import run_remote_ibm_batch for job in jobs: @@ -447,52 +499,60 @@ def namer(circ: QCircuit, i: int): ) return run_remote_ibm_batch(jobs) - return BatchResult( - [ + results = [] + for i, circ in enumerate(per_run_circuits): + results.append( _run_single( namer(circ, i + 1), - device, - values, + target_device, + per_run_values[i], display_breakpoints, - mode, + exec_mode, reservation_arn, ) - for i, circ in enumerate(circuits) - ] - ) + ) + return BatchResult(results) if len(circuits) > 1 or len(devices) > 1: counter = 1 - outputs = [] + results = [] for circ in circuits: labeled = namer(circ, counter) counter += 1 for device in devices: - outputs.append( + results.append( _run_single( labeled, device, - values, + values if isinstance(values, dict) else None, display_breakpoints, exec_mode, reservation_arn, ) ) - return BatchResult(outputs) + return BatchResult(results) else: - circ = namer(circuits[0], 1) - device = devices[0] - return _run_single(circ, device, values, display_breakpoints, exec_mode) + base_circuit = namer(circuits[0], 1) + target_device = devices[0] + + return _run_single( + base_circuit, + target_device, + values if isinstance(values, dict) else None, + display_breakpoints, + exec_mode, + reservation_arn, + ) def submit( circuit: QCircuit, device: AvailableDevice, - values: Optional[dict[Expr | str, Complex]] = None, - mode: Optional[VQAMode] = None, + values: Optional[ValuesDict] = None, + mode: Optional[ExecutionMode] = None, reservation_arn: Optional[str] = None, ) -> tuple[str, Job]: """Submit the job related to the circuit on the remote backend provided in @@ -537,7 +597,7 @@ def submit( job.status = JobStatus.INIT if isinstance(device, IBMDevice): - if mode == VQAMode.SESSION: + if mode == ExecutionMode.SESSION: from mpqp.execution.connection.ibm_connection import ( get_backend, get_or_create_ibm_session, @@ -546,12 +606,12 @@ def submit( backend = get_backend(device) session = get_or_create_ibm_session(backend) - job_id, _ = submit_remote_ibm_session(job, session) else: from mpqp.execution.providers.ibm import submit_remote_ibm job_id, _ = submit_remote_ibm(job) + elif isinstance(device, ATOSDevice): job_id, _ = submit_QLM(job) elif isinstance(device, AWSDevice): @@ -568,7 +628,7 @@ def display_kth_breakpoint( circuit: QCircuit, k: int, device: AvailableDevice = ATOSDevice.MYQLM_CLINALG, - mode: Optional[VQAMode] = None, + mode: Optional[ExecutionMode] = None, ): """Prints to the standard output the state vector corresponding to the state of the system when it encounters the `k^{th}` breakpoint. From d7f050f0efdb7e278fce0796888b011e6b5faffa Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Thu, 5 Feb 2026 10:54:35 +0100 Subject: [PATCH 32/39] chore: update __init__ with ExecutionMode --- mpqp/execution/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpqp/execution/__init__.py b/mpqp/execution/__init__.py index 913aec00..25771d7a 100644 --- a/mpqp/execution/__init__.py +++ b/mpqp/execution/__init__.py @@ -7,7 +7,7 @@ GOOGLEDevice, IBMDevice, ) -from .job import Job, JobStatus, JobType, VQAMode +from .job import ExecutionMode, Job, JobStatus, JobType from .result import BatchResult, Result, Sample, StateVector from .runner import adjust_measure, run, submit from .simulated_devices import IBMSimulatedDevice From 65fecc4c14122ebadb8296f6d4a2f18c1e4c494f Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 11 Feb 2026 14:00:43 +0100 Subject: [PATCH 33/39] fix: resolve typing, apply_layout handling --- mpqp/execution/providers/ibm.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 56e7dc7c..d2e98218 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -530,6 +530,7 @@ def _submit_remote_ibm( :func:`~mpqp.execution.runner.run` instead. """ from qiskit import QuantumCircuit + from qiskit.quantum_info import SparsePauliOp from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator from qiskit_ibm_runtime import SamplerV2 as Runtime_Sampler @@ -551,10 +552,14 @@ def _submit_remote_ibm( estimator = Runtime_Estimator(mode=runtime_target) meas.pre_transpile_observables(job.device) - qiskit_observables = [ - obs.pre_transpile[job.device].apply_layout(qiskit_circ.layout) - for obs in meas.observables - ] + + qiskit_observables: list[SparsePauliOp] = [] + + for obs in meas.observables: + translated = obs.pre_transpile[job.device] + if TYPE_CHECKING: + assert isinstance(translated, SparsePauliOp) + qiskit_observables.append(translated.apply_layout(qiskit_circ.layout)) if TYPE_CHECKING: assert all(isinstance(obs, SparsePauliOp) for obs in qiskit_observables) @@ -613,6 +618,7 @@ def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"] assert isinstance(reference_job.device, IBMDevice) backend = get_backend(reference_job.device) + from qiskit.quantum_info import SparsePauliOp from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator from mpqp.execution.connection.ibm_connection import get_or_create_ibm_session @@ -644,10 +650,18 @@ def submit_remote_ibm_batch(jobs: list[Job]) -> tuple[list[str], "RuntimeJobV2"] if TYPE_CHECKING: assert isinstance(meas, ExpectationMeasure) meas.pre_transpile_observables(job.device) - obs_list = [ - obs.pre_transpile[job.device].apply_layout(qc.layout) - for obs in meas.observables - ] + + obs_list: list[SparsePauliOp] = [] + + for obs in meas.observables: + translated = obs.pre_transpile[job.device] + if TYPE_CHECKING: + assert isinstance(translated, SparsePauliOp) + obs_list.append(translated.apply_layout(qc.layout)) + + if TYPE_CHECKING: + assert all(isinstance(obs, SparsePauliOp) for obs in obs_list) + per_job_observables.append(obs_list) estimator_input = list(zip(per_job_circuits, per_job_observables)) From 30d1b6000f5dc0041f5d66a3c59dadfe04d39a7b Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 11 Feb 2026 14:13:18 +0100 Subject: [PATCH 34/39] fix: remove invlaid pre_transpile, use translated_pre_measures --- mpqp/execution/providers/aws.py | 51 +++++++++++++-------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index 0528f6b3..ac4fc869 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -138,7 +138,7 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): Returns: A result containing the expectation values of the observables. """ - # from braket.circuits import Circuit + from braket.circuits import Circuit from braket.tasks import GateModelQuantumTaskResult assert isinstance(job.device, AWSDevice) @@ -155,33 +155,18 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): assert isinstance(job.measure, ExpectationMeasure) results, errors = {}, {} + if job.measure.optimize_measurement: - from mpqp.tools.pauli_grouping import ( - find_qubitwise_rotations, - pauli_monomial_eigenvalues, + job.measure.pre_transpile_observables(job.device) + eigenvalues_by_group, transpiled_pre_measures = ( + job.measure.translated_pre_measures[job.device] ) - if job.device not in job.measure.pre_transpile: - grouping = job.measure.get_pauli_grouping() - transpiled_pre_measures = [ - QCircuit(find_qubitwise_rotations(group)).to_other_language( - Language.BRAKET - ) - for group in grouping - ] - eigenvalues = [ - {monom.name: pauli_monomial_eigenvalues(monom) for monom in group} - for group in grouping - ] - job.measure.pre_transpile[job.device] = ( - eigenvalues, - transpiled_pre_measures, - ) - else: - eigenvalues, transpiled_pre_measures = job.measure.pre_transpile[job.device] + expectation_values: dict[str, float] = {} - expectation_values = {} - for eigenvalues, pre_measure in zip(eigenvalues, transpiled_pre_measures): + for group_eigenvalues, pre_measure in zip( + eigenvalues_by_group, transpiled_pre_measures + ): job.status = JobStatus.RUNNING if job.measure.shots == 0: from copy import deepcopy @@ -203,6 +188,7 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): ) result = local_result.result() assert isinstance(result, GateModelQuantumTaskResult) + length = 2**job.circuit.nb_qubits sorted_values: list[float] = [] for i in range(length): @@ -213,7 +199,7 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): ) else: sorted_values.append(0) - for name, eigenvalue in eigenvalues.items(): + for name, eigenvalue in group_eigenvalues.items(): expectation_value: float = np.dot( eigenvalue, np.array(sorted_values, dtype=np.float64), @@ -228,12 +214,12 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): local += expectation_values[monoms.name] * monoms.coef results.update({f"observable_{i}": local}) errors.update({f"observable_{len(errors)}": None}) + if len(results) == 1: return Result(job, results["observable_0"], shots=job.measure.shots) return Result(job, results, errors, shots=job.measure.shots) else: - for obs in job.measure.observables: from copy import deepcopy @@ -247,6 +233,7 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): targets = [job.measure.targets] * len(braket_obs.summands) else: targets = job.measure.targets + copy.expectation( # pyright: ignore[reportAttributeAccessIssue] observable=braket_obs, target=targets ) @@ -260,8 +247,10 @@ def run_braket_observable(job: Job, reservation_arn: Optional[str] = None): assert isinstance(local_result, GateModelQuantumTaskResult) results.update({f"observable_{len(results)}": local_result.values[0].real}) errors.update({f"observable_{len(errors)}": None}) + if len(results) == 1: return Result(job, results["observable_0"], None, job.measure.shots) + return Result(job, results, errors, job.measure.shots) @@ -315,7 +304,6 @@ def submit_job_braket( from braket.circuits import Circuit device = get_braket_device(job.device, is_noisy=is_noisy) - braket_circuit = job.circuit.transpiled_for_device(job.device) if TYPE_CHECKING: assert isinstance(braket_circuit, Circuit) @@ -354,11 +342,12 @@ def safe_retrieve_samples(self): # pyright: ignore[reportMissingParameterType] # TODO : [multi-obs] update this to take into account the case when we have list of Observables if TYPE_CHECKING: assert isinstance(job.measure, ExpectationMeasure) - job.measure.pre_transpile_observables(job.device) - _, transpiled_pre_measures = job.measure.pre_transpile[job.device] - for herm_op in transpiled_pre_measures: + + for obs in job.measure.observables: + braket_obs = obs.to_other_language(Language.BRAKET) braket_circuit.expectation( # pyright: ignore[reportAttributeAccessIssue] - observable=herm_op, target=job.measure.targets + observable=braket_obs, + target=job.measure.targets, ) job.status = JobStatus.RUNNING From 0549399f870cd9e384d793111f095e5035d435cf Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 11 Feb 2026 14:20:19 +0100 Subject: [PATCH 35/39] fix: pre-transpilation caching per device --- mpqp/core/instruction/measurement/expectation_value.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index 3c69d832..06b346b8 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -597,6 +597,9 @@ def to_dict(self): def pre_transpile_observables(self, device: AvailableDevice): from mpqp.execution.devices import AWSDevice, IBMDevice + if device in self.translated_pre_measures: + return + if isinstance(device, AWSDevice): from mpqp.core.circuit import QCircuit from mpqp.tools.pauli_grouping import ( From 9206790d8cf4c2e162b873a72247cb341fa471ff Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 11 Feb 2026 15:04:17 +0100 Subject: [PATCH 36/39] fix: type annotations --- mpqp/execution/job.py | 5 ++--- mpqp/execution/runner.py | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/mpqp/execution/job.py b/mpqp/execution/job.py index 8d5382c5..0e957fb3 100644 --- a/mpqp/execution/job.py +++ b/mpqp/execution/job.py @@ -13,6 +13,7 @@ from __future__ import annotations +from numbers import Number from typing import TYPE_CHECKING, Optional from aenum import Enum, NoAlias, auto @@ -144,9 +145,7 @@ 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.values: Optional[dict[str | Parameter | Basic, float | int | complex]] = ( - None - ) + self.values: Optional[dict[str | Parameter | Basic, Number]] = None """Parameter bindings for circuits containing symbolic variables. For local execution, parameters are typically substituted directly into the diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index e463bb94..9f648940 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -18,7 +18,7 @@ from __future__ import annotations -from numbers import Complex +from numbers import Complex, Number from textwrap import indent from typing import TYPE_CHECKING, Optional, Sequence, Union, overload @@ -50,11 +50,12 @@ from mpqp.tools.generics import OneOrMany, find_index if TYPE_CHECKING: - from sympy import Expr + from qiskit.circuit import Parameter + from sympy import Basic, Expr -ValuesKey = Union["Expr", str] -ValuesDict = dict[ValuesKey, Complex] +ValuesKey = Union["Expr", "Parameter", "Basic", str] +ValuesDict = dict[ValuesKey, Number] BatchValuesInput = Optional[Union[ValuesDict, Sequence[ValuesDict]]] @@ -160,7 +161,18 @@ def generate_job( The Job containing information about the execution of the circuit. """ if values is not None and not device.is_remote(): - circuit = circuit.subs(values, True) + from sympy import Expr + + subs_values: dict[Expr | str, Complex] = {} + for k, v in values.items(): + if isinstance(k, (str, Expr)): + if not isinstance(v, Complex): + raise TypeError( + f"Parameter binding requires numeric values; got {type(v).__name__}." + ) + subs_values[k] = v + + circuit = circuit.subs(subs_values, True) exec_mode = mode or ExecutionMode.JOB From dd06bae30cefacc7b92e9e208c8a68893ac6f6de Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 11 Feb 2026 15:22:00 +0100 Subject: [PATCH 37/39] chore: update CMA-ES usage --- mpqp/execution/vqa/optimizer.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/mpqp/execution/vqa/optimizer.py b/mpqp/execution/vqa/optimizer.py index 8c945f32..e069dbdd 100644 --- a/mpqp/execution/vqa/optimizer.py +++ b/mpqp/execution/vqa/optimizer.py @@ -4,7 +4,7 @@ from enum import Enum from functools import partial -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Optional, Sequence, Union import numpy as np import numpy.typing as npt @@ -36,6 +36,9 @@ def run_optimizer( init_params: OptimizerInput, optimizer_options: Optional[OptimizerOptions] = None, callback: Optional[Callable[[OptimizerInput], None]] = None, + batch_eval: Optional[ + Callable[[Sequence[npt.NDArray[np.float_]]], Sequence[float]] + ] = None, ) -> tuple[float, npt.NDArray[np.float_]]: if optimizer_options is None: @@ -47,16 +50,27 @@ def run_optimizer( import cma sigma0 = float(optimizer_options.pop("sigma0", 0.5)) + es = cma.CMAEvolutionStrategy([float(x) for x in x0], sigma0, optimizer_options) - best_params, es = cma.fmin2( - eval_func, - x0=x0, - sigma0=sigma0, - options=optimizer_options, - ) - best_value = float(es.result.fbest) + best_value = float("inf") + best_params = np.asarray(x0, dtype=float) - return best_value, np.asarray(best_params, dtype=float) + while not es.stop(): + solutions = es.ask() + + if batch_eval is not None: + candidates = [np.asarray(x, dtype=float) for x in solutions] + fit = [float(v) for v in batch_eval(candidates)] + else: + fit = [float(eval_func(np.asarray(x, dtype=float))) for x in solutions] + + es.tell(solutions, fit) + es.disp() + + if callback is not None: + callback(np.asarray(es.best.x, dtype=float)) + + return float(best_value), np.asarray(best_params, dtype=float) result: OptimizeResult = scipy_minimize( eval_func, From 92161e83ebd4511a06cc2370b823514b45f8f649 Mon Sep 17 00:00:00 2001 From: Muhammad Attallah Date: Wed, 11 Feb 2026 17:53:23 +0100 Subject: [PATCH 38/39] chore: update vqa, type issues need to be fixed --- mpqp/execution/vqa/vqa.py | 228 +++++++++++++++++++++----------------- 1 file changed, 126 insertions(+), 102 deletions(-) diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index 38a96d4b..c572a509 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -1,25 +1,23 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Collection, Optional, TypeVar, Union +from typing import Any, Callable, Collection, Optional, Sequence, TypeVar, Union import numpy as np import numpy.typing as npt from scipy.optimize import OptimizeResult -from scipy.optimize import minimize as scipy_minimize - -if TYPE_CHECKING: - from sympy import Expr, Basic from mpqp.core.circuit import QCircuit from mpqp.core.instruction import ExpectationMeasure -from mpqp.execution.devices import AvailableDevice, AWSDevice, IBMDevice -from mpqp.execution.job import VQAMode +from mpqp.execution.devices import AvailableDevice +from mpqp.execution.job import ExecutionMode from mpqp.execution.runner import run from mpqp.execution.vqa.optimizer import Optimizer, run_optimizer T1 = TypeVar("T1") T2 = TypeVar("T2") + + OptimizerInput = Union[list[float], npt.NDArray[np.float_]] OptimizableFunc = Union[partial[float], Callable[[OptimizerInput], float]] OptimizerOptions = dict[str, Any] @@ -48,10 +46,6 @@ def _maps(l1: Collection[T1], l2: Collection[T2]) -> dict[T1, T2]: return {e1: e2 for e1, e2 in zip(l1, l2)} -def _ordered_variables(circ: QCircuit) -> list[Basic]: - return sorted(circ.variables(), key=lambda s: str(s)) - - def minimize( optimizable: QCircuit | OptimizableFunc, method: Optimizer | OptimizerCallable, @@ -60,7 +54,7 @@ def minimize( nb_params: Optional[int] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, - vqa_mode: VQAMode = VQAMode.JOB, + mode: ExecutionMode = ExecutionMode.JOB, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, in order to minimize the measured expectation value of observables associated with the given circuit. @@ -131,20 +125,19 @@ def minimize( if isinstance(optimizable, QCircuit): if device is None: raise ValueError("A device is needed to optimize a circuit") - optimizer = _minimize_remote if device.is_remote() else _minimize_local - return optimizer( - optimizable, - method, - device, - init_params, - nb_params, - optimizer_options, - callback, - vqa_mode=vqa_mode, - ) - else: - # TODO: find a way to know if the job is remote or local from the function + if device.is_remote(): + return _minimize_remote( + optimizable, + method, + device, + init_params, + nb_params, + optimizer_options, + callback, + mode, + ) + return _minimize_local( optimizable, method, @@ -153,9 +146,25 @@ def minimize( nb_params, optimizer_options, callback, - vqa_mode=vqa_mode, + mode, ) + if device is not None and device.is_remote(): + raise ValueError( + "Remote execution is only supported when `optimizable` is a QCircuit." + ) + + return _minimize_local( + optimizable, + method, + device, + init_params, + nb_params, + optimizer_options, + callback, + mode, + ) + def _minimize_remote( optimizable: QCircuit | OptimizableFunc, @@ -165,7 +174,7 @@ def _minimize_remote( nb_params: Optional[int] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, - vqa_mode: VQAMode = VQAMode.JOB, + mode: Optional[ExecutionMode] = None, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by it's @@ -198,51 +207,16 @@ def _minimize_remote( TODO to implement on QLM first """ - if isinstance(optimizable, QCircuit): - if device is None: - raise ValueError("A device is needed to optimize a circuit") - - circ = optimizable - - # variables: set["Expr"] = circ.variables() - variables: list[Basic] = _ordered_variables(circ) - if len(variables) == 0: - raise ValueError("No variables found in the circuit to optimize.") - - if len(circ.measurements) != 1: - raise ValueError( - "Cannot optimize a circuit containing several measurements." - ) - - if not isinstance(circ.measurements[0], ExpectationMeasure): - raise ValueError("Expected an ExpectationMeasure to optimize the circuit.") - else: - if len(circ.measurements[0].observables) > 1: - raise ValueError( - "Expected only one observable in the ExpectationMeasure but got" - f" {len(circ.measurements[0].observables)}" - ) - - def eval_circ(params: OptimizerInput) -> float: - from numbers import Complex - - params_fixed_type: Collection[Complex] = params - - values: dict["Expr" | str, Complex] = _maps(variables, params_fixed_type) - - if isinstance(device, (IBMDevice, AWSDevice)): - circ.bind_parameters(device, values) - result = run(circ, device, values=None, mode=vqa_mode) - else: - result = run(circ, device, values, mode=vqa_mode) - - if TYPE_CHECKING: - assert isinstance(result.expectation_values, float) - return result.expectation_values - - return _minimize_local_func( - eval_circ, method, init_params, len(variables), optimizer_options, callback - ) + return _minimize_local( + optimizable, + method, + device, + init_params, + nb_params, + optimizer_options, + callback, + mode, + ) def _minimize_local( @@ -253,7 +227,7 @@ def _minimize_local( nb_params: Optional[int] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, - vqa_mode: VQAMode = VQAMode.JOB, + mode: Optional[ExecutionMode] = None, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by it's @@ -294,7 +268,7 @@ def _minimize_local( init_params, optimizer_options, callback, - vqa_mode=vqa_mode, + mode, ) else: return _minimize_local_func( @@ -309,7 +283,7 @@ def _minimize_local_circ( init_params: Optional[OptimizerInput] = None, optimizer_options: Optional[dict[str, Any]] = None, callback: Optional[OptimizerCallback] = None, - vqa_mode: VQAMode = VQAMode.JOB, + mode: Optional[ExecutionMode] = None, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by its @@ -336,41 +310,75 @@ def _minimize_local_circ( Returns: The optimal value reached and the parameters used to reach this value. """ - # The sympy `free_symbols` method returns in fact sets of Basic, which - # are theoretically different from Expr, but in our case the difference - # is not relevant. - # variables: set["Expr"] = circ.variables() # pyright: ignore[reportAssignmentType] - variables: list["Expr"] = _ordered_variables(circ) - if len(circ.measurements) != 1: raise ValueError("Cannot optimize a circuit containing several measurements.") if not isinstance(circ.measurements[0], ExpectationMeasure): raise ValueError("Expected an ExpectationMeasure to optimize the circuit.") - else: - if len(circ.measurements[0].observables) > 1: - raise ValueError( - "Expected only one observable in the ExpectationMeasure but got" - f" {len(circ.measurements[0].observables)}" - ) - def eval_circ(params: OptimizerInput): - # pyright is bad with abstract numeric types: - # "float" is incompatible with "Complex" - from numbers import Complex - - params_fixed_type: Collection[Complex] = ( - params # pyright: ignore[reportAssignmentType] + if len(circ.measurements[0].observables) > 1: + raise ValueError( + "Expected only one observable in the ExpectationMeasure but got" + f" {len(circ.measurements[0].observables)}" ) - values: dict[Expr | str, Complex] = _maps(variables, params_fixed_type) - result = run(circ, device, values, mode=vqa_mode) - if TYPE_CHECKING: - assert isinstance(result.expectation_values, float) - return result.expectation_values + variables = sorted(circ.variables(), key=str) + + exec_mode = mode or ExecutionMode.JOB + if exec_mode == ExecutionMode.BATCH and not ( + isinstance(method, Optimizer) and method == Optimizer.CMAES + ): + raise ValueError("Batch mode is supported with CMAES optimizer ") + + single_mode = ( + ExecutionMode.SESSION + if exec_mode == ExecutionMode.SESSION + else ExecutionMode.JOB + ) + + def eval_circ(params: OptimizerInput) -> float: + params_fixed = [complex(x) for x in params] + values = _maps(variables, params_fixed) + res = run(circ, device, values, mode=single_mode) + return float(res.expectation_values) + + batch_eval_fn: Optional[ + Callable[[Sequence[npt.NDArray[np.float_]]], Sequence[float]] + ] = None + + if ( + isinstance(method, Optimizer) + and method == Optimizer.CMAES + and exec_mode == ExecutionMode.BATCH + ): + + def _batch_eval( + candidates: Sequence[npt.NDArray[np.float_]], + ) -> Sequence[float]: + values_list = [ + _maps(variables, [complex(float(x)) for x in cand]) + for cand in candidates + ] + batch_res = run( + circ, + device, + values=values_list, + mode=ExecutionMode.BATCH, + display_breakpoints=False, + ) + + return [float(res.expectation_values) for res in batch_res] + + batch_eval_fn = _batch_eval return _minimize_local_func( - eval_circ, method, init_params, len(variables), optimizer_options, callback + eval_circ, + method, + init_params, + len(variables), + optimizer_options, + callback, + batch_eval=batch_eval_fn, ) @@ -381,6 +389,9 @@ def _minimize_local_func( nb_params: Optional[int] = None, optimizer_options: Optional[OptimizerOptions] = None, callback: Optional[OptimizerCallback] = None, + batch_eval: Optional[ + Callable[[Sequence[npt.NDArray[np.float_]]], Sequence[float]] + ] = None, ) -> tuple[float, OptimizerInput]: """This function runs an optimization on the parameters of the circuit, to minimize the expectation value of the measure of the circuit by it's @@ -419,14 +430,27 @@ def _minimize_local_func( else: init_params = [0.0] * nb_params - if isinstance(method, Optimizer): + def _optimizer_callback(x: OptimizerInput) -> None: + if callback is None: + return + + if isinstance(x, OptimizeResult): + callback(x) + return + try: + callback(OptimizeResult(x=np.array(x, dtype=float))) + except Exception: + callback(x) + + if isinstance(method, Optimizer): best_value, best_params = run_optimizer( eval_func, method, init_params, optimizer_options, - callback, + _optimizer_callback if callback is not None else None, + batch_eval=batch_eval, ) return best_value, best_params From cd83727e292f51f5b70c24463b46c4b86e181e2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Feb 2026 03:44:22 +0000 Subject: [PATCH 39/39] chore: Files formated --- mpqp/execution/runner.py | 2 ++ mpqp/execution/vqa/vqa.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index 4fa6424e..a4d37345 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -362,6 +362,8 @@ def run( mode: Optional[ExecutionMode] = None, values_batch: Optional[list[ValuesDict]] = None, ) -> BatchResult: ... + + # TODO: why using values and values_batch at the same time diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index 4604efa8..592dc346 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -236,7 +236,13 @@ def _minimize_local( ) else: return _minimize_local_func( - optimizable, method, init_params, nb_params, optimizer_options, callback, mode + optimizable, + method, + init_params, + nb_params, + optimizer_options, + callback, + mode, )