Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3defccf
feat: add VQA remote execution skeleton, to be modified
MoHermes Oct 20, 2025
c1d4ef7
chore: Files formated
github-actions[bot] Oct 20, 2025
aa840d3
feat: implement CMA-ES optimizer
MoHermes Oct 20, 2025
4a27341
Merge branch 'feat-vqa-remote' of github.com:ColibrITD-SAS/mpqp into …
MoHermes Oct 20, 2025
b8fb035
feat: support qiskit runtime session in ibm_connection
MoHermes Oct 24, 2025
5c111b7
chore: bind parameters for qiskit and braket
MoHermes Oct 24, 2025
4830dce
Merge remote-tracking branch 'origin/dev' into feat-vqa-remote
MoHermes Oct 24, 2025
36c66a6
Merge branch 'feat-to-other-language-AWS' into feat-vqa-remote
MoHermes Oct 30, 2025
83d709e
Merge branch 'dev' into feat-vqa-remote
MoHermes Nov 6, 2025
b2f1f66
feat: transpiled_circuit is device specific
MoHermes Nov 12, 2025
286cad9
feat: support device specific pre-transpilation for observables
MoHermes Nov 12, 2025
db29259
chore: Files formated
github-actions[bot] Nov 12, 2025
d2097e5
fix: handle single atom case for Braket
MoHermes Nov 12, 2025
91b3662
Merge branch 'feat-vqa-remote' of github.com:ColibrITD-SAS/mpqp into …
MoHermes Nov 12, 2025
604c356
chore: simplify handling Symbol
MoHermes Nov 12, 2025
66eef74
fix: type check
MoHermes Nov 12, 2025
caf0fa4
chore: avoid re-transpiling for the same device
MoHermes Nov 24, 2025
a39d309
chore: modify device specific transpilation logic
MoHermes Nov 24, 2025
926581f
Merge branch 'dev' into feat-vqa-remote
MoHermes Nov 26, 2025
3a86af8
fix: type hinting
MoHermes Nov 27, 2025
81c320e
Merge branch 'feat-to-other-language-AWS' of github.com:ColibrITD-SAS…
MoHermes Nov 27, 2025
2b36609
chore: update session handling
MoHermes Dec 1, 2025
cfd31a3
chore: enhance IBM session use
MoHermes Dec 8, 2025
9371c45
feat: add unified execution pipeline, to support job, batch and session
MoHermes Dec 8, 2025
ba90fc7
feat: support batch and session execution modes with IBM runtime
MoHermes Dec 8, 2025
fa30dee
feat: add VQAMode support to Job model
MoHermes Dec 8, 2025
555cb07
chore: Files formated
github-actions[bot] Dec 8, 2025
463640e
chore: update bind_parameters in QCircuit
MoHermes Dec 10, 2025
9f89b89
chore: update optimizer behavior
MoHermes Dec 11, 2025
efa86c4
chore: update vqa implementation
MoHermes Dec 11, 2025
ce9576a
feat: handle bind parameters
MoHermes Dec 16, 2025
16f09ef
fix: improve session handling
MoHermes Jan 23, 2026
70e06cd
chore: Files formated
github-actions[bot] Jan 23, 2026
eeb7a16
fix: avoid repeated observable pre_transpile conversion, update types
MoHermes Jan 23, 2026
275e605
chore: update job handling
MoHermes Jan 23, 2026
69862b1
fix: bind parameters on transpiled circuit without mutating original
MoHermes Jan 26, 2026
ad716f6
chore: update remote execution flow
MoHermes Jan 27, 2026
1825b94
chore: rename mode, simplify batch handling
MoHermes Jan 27, 2026
d7f050f
chore: update __init__ with ExecutionMode
MoHermes Feb 5, 2026
65fecc4
fix: resolve typing, apply_layout handling
MoHermes Feb 11, 2026
30d1b60
fix: remove invlaid pre_transpile, use translated_pre_measures
MoHermes Feb 11, 2026
0549399
fix: pre-transpilation caching per device
MoHermes Feb 11, 2026
9206790
fix: type annotations
MoHermes Feb 11, 2026
dd06bae
chore: update CMA-ES usage
MoHermes Feb 11, 2026
92161e8
chore: update vqa, type issues need to be fixed
MoHermes Feb 11, 2026
2d7b5d9
Merge branch 'dev' of github.com:ColibrITD-SAS/mpqp into feat-vqa-remote
hJaffaliColibritd Feb 13, 2026
cd83727
chore: Files formated
github-actions[bot] Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions mpqp/core/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -70,7 +70,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 qiskit_aer import AerSimulator
from sympy import Basic, Expr

Expand Down Expand Up @@ -171,7 +171,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."""
Expand Down Expand Up @@ -2152,6 +2155,15 @@ def __repr__(self) -> str:

return f'QCircuit({args_repr})'

def transpiled_for_device(self, device: AvailableDevice):
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]

Comment on lines +2158 to +2166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some providers, we would call to_other_device for each device , because MPQP translation and transpilation is device specific (like IBM), but for AWS/QLM for instance, we only need to_other_language (which is handled by to_other_device if I remember well), but then would need a different indexing of the dictionnary, otherwise we would export the mpqp circuit many times if we change the AWSDevice for instance (while we could have avoided).

def variables(self) -> set[Basic]:
"""Returns all the symbolic parameters involved in this circuit.

Expand All @@ -2176,3 +2188,41 @@ def variables(self) -> set[Basic]:
if isinstance(param, Expr):
params.update(param.free_symbols)
return params

def bind_parameters(
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()}

bound_circuit = deepcopy(self)
bound_circuit.transpiled_circuit = dict(self.transpiled_circuit)

from qiskit import QuantumCircuit

if isinstance(transpiled, QuantumCircuit):
qiskit_param_map = {
p: param_values[p.name]
for p in transpiled.parameters
if p.name in param_values
}
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):
bound_circuit.transpiled_circuit[device] = transpiled.make_bound_circuit(
param_values
)
return bound_circuit

else:
raise TypeError(f"Unsupported transpiled circuit yet: {type(transpiled)}")
66 changes: 63 additions & 3 deletions mpqp/core/instruction/measurement/expectation_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform

if TYPE_CHECKING:
from braket.circuits.observables import Hermitian, Sum
from braket.circuits import Circuit as braket_Circuit
from cirq.circuits.circuit import Circuit as CirqCircuit
from cirq.ops.linear_combinations import PauliSum as CirqPauliSum
from cirq.ops.pauli_string import PauliString as CirqPauliString
Expand All @@ -39,6 +40,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:
Expand Down Expand Up @@ -81,8 +83,15 @@ def __init__(
self._is_diagonal = None
self._diag_elements: Optional[npt.NDArray[np.float64]] = None
self.label = label
"See parameter description."
self.pre_transpiled = None
"""See parameter description."""
self.pre_transpiled: dict[
AvailableDevice,
Union[
SparsePauliOp, QLMObservable, CirqPauliSum, CirqPauliString, Hermitian
],
] = {} # TODO: do we put None, or empty dict ?
# TODO: docstring
"""TODO: documentation"""

if isinstance(observable, PauliString):
self.nb_qubits = observable.nb_qubits
Expand Down Expand Up @@ -437,7 +446,17 @@ def __init__(
"""See parameter description."""
self.optimize_measurement = optimize_measurement
"""See parameter description."""
self.pre_transpiled = None
# TODO: do we need both pre_transpiled and translated_pre_measures ?
# self.pre_transpiled = None
# TODO : docstring
"""TODO"""
self.translated_pre_measures: dict[
AvailableDevice,
tuple[list[dict[str, npt.NDArray[np.float64]]], list[braket_Circuit]],
] = {}
# TODO : docstring
"""TODO"""

if isinstance(observable, Observable):
observable = [observable]
else:
Expand Down Expand Up @@ -622,3 +641,44 @@ def to_dict(self):
and not attr_name.startswith("__")
and not callable(getattr(self, attr_name))
}

# TODO: double check this method
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 (
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.translated_pre_measures[device] = (
eigenvalues,
transpiled_pre_measures,
)

elif isinstance(device, IBMDevice):
for observable in self.observables:
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."
)
6 changes: 3 additions & 3 deletions mpqp/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ExecutionMode, Job, JobStatus, JobType
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
Expand Down
51 changes: 49 additions & 2 deletions mpqp/execution/connection/ibm_connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from getpass import getpass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional

from termcolor import colored

Expand All @@ -9,10 +9,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_sessions: dict[str, "Session"] = {}


def config_ibm_account(token: str):
Expand Down Expand Up @@ -208,3 +209,49 @@ 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 for the given backend.
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 (IBMNotAuthorizedError, IBMRuntimeError):
status = "Closed"

if status not in ("Closed", None):
return session

new_session = Session(backend=backend, max_time=max_time)
_ibm_sessions[backend_name] = new_session
return new_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 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]
33 changes: 31 additions & 2 deletions mpqp/execution/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -70,6 +73,19 @@ class JobType(Enum):
retrieve the expectation value in an optimal manner."""


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"

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
Expand All @@ -87,6 +103,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)
Expand All @@ -110,6 +127,7 @@ def __init__(
job_type: JobType,
circuit: QCircuit,
device: AvailableDevice,
mode: ExecutionMode = ExecutionMode.JOB,
):
self._status = JobStatus.INIT

Expand All @@ -119,12 +137,21 @@ def __init__(
"""See parameter description."""
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
"""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]:
Expand Down Expand Up @@ -168,7 +195,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):
Expand All @@ -185,6 +213,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,
Expand Down Expand Up @@ -218,7 +247,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
Expand Down
Loading