2026-06-12
·Pytket Integration for Marqov SDK
·Quantum Computing
·8 min read
About Marqov SDK
Marqov Software Development Kit (SDK) is an open source orchestration layer for heterogeneous computing. Marqov SDK allows users to interact with different quantum and classical backends and integrates with:
Quantum
- IBM Quantum
- AWS Braket
- Azure Quantum
- IonQ
- Rigetti
During Unitary Hack 2026, a PR was opened to address issues #3 and #6, which enabled Quantinuum integration with the Marqov SDK. This article describes the work done to complete the integration successfully, as well as the code review process for releasing the changes to production.
About pytket
Pytket is a Python module used to interface with the quantum computing toolkit and optimising compiler TKET. More information about installation can be found here.
Adding pytket integration to Marqov SDK
This contribution adds pytket support to the Marqov SDK through Circuit.to_pytket() and a new QuantinuumExecutor wired in via the factory. Below, we cover the implementation, design decisions, and bug fixes required for production release.
Circuit.to_pytket() Method
The Marqov SDK translates Marqov circuits into native types:
to_qiskit()→ Qiskitto_braket()→ Braketto_pyquil()→ PyQuil
Quantinuum support was not included when Unitary Hack 2026 started, hence issues #3 and #6 were opened. The first step was to add a to_pytket() method.
to_pytket() acts as a bridge between Marqov circuits and pytket (TKET) for compilation, noise analysis, and optimisation. Exporting to pytket is a prerequisite for QuantinuumExecutor.
The method is defined on the Circuit class in marqov/circuit.py. The Circuit class is a backend-agnostic quantum circuit that can be exported to Braket, Qiskit, and PyQuil.
Implementation
Like to_braket(), to_pytket() is an exporter returning a native pytket Circuit object. An ImportError is raised if pytket is not installed, and a NotImplementedError is raised if the circuit contains an unsupported gate after decomposition.
After a lazy import of qiskit_to_tk, self.to_qiskit() is called to produce an interim Qiskit QuantumCircuit. Subsequently, each instruction is validated against a list of supported gates _QISKIT_GATE_MAP. Unlike to_braket() which delegates the circuit directly to QuantumFlow, to_pytket() uses Qiskit as an intermediate representation to support conversion from Qiskit.
# Non-gate instructions to skip silently.
_SKIP_INSTRUCTIONS: set[str] = {"barrier", "measure", "reset", "delay"}
# Qiskit gate name -> Circuit fluent method mapping.
_QISKIT_GATE_MAP: dict[str, str] = {
"h": "h",
"x": "x",
"y": "y",
"z": "z",
"s": "s",
"t": "t",
"rx": "rx",
"ry": "ry",
"rz": "rz",
"cx": "cnot",
"cz": "cz",
"swap": "swap",
}
The loop walks through qiskit_circuit.data and inspects instruction.operation.name:
for instruction in qiskit_circuit.data:
name = instruction.operation.name
if name in self._SKIP_INSTRUCTIONS:
continue
if name not in self._QISKIT_GATE_MAP:
raise NotImplementedError(
f"Unsupported gate '{name}' after decomposition. "
f"Supported gates: {', '.join(sorted(self._QISKIT_GATE_MAP))}"
)
If all checks pass successfully, qiskit_to_tk(qiskit_circuit) is returned. The output of this method is then used by QuantinuumExecutor to submit jobs to Quantinuum via pytket-quantinuum.
Finally, the executor is wired in factory.py.
QuantinuumExecutor Integration
Building on the to_pytket() implementation, the Quantinuum executor was integrated into the SDK to run quantum circuits on Quantinuum devices. To do so, marqov/executors/quantinuum.py was added by following the pattern of existing executors and the instructions in CONTRIBUTING.md. This required defining QuantinuumExecutor and QuantinuumExecutorConfig backed by pytket-quantinuum.
Similarly to marqov/executors/ibm.py and marqov/executors/braket.py, necessary libraries were imported, and QuantinuumExecutor was defined to inherit from BaseExecutor.
TYPE_CHECKING is used to import pytket and Quantinuum types for annotations. This avoids expensive runtime imports when the executor is not being used.
The @dataclass QuantinuumExecutorConfig provides the basic configuration with attributes. These were then defined by using TKET documentation here as references where both required and optional fields with default values were declared.
@dataclass
class QuantinuumExecutorConfig:
"""Configuration for Quantinuum executor.
Attributes:
device_name: Name of the Quantinuum device.
label: Label for the job.
simulator: Simulator type.
group: Group for the job.
provider: Provider for the job.
machine_debug: Whether to enable machine debug.
api_handler: API handler for the job.
compilation_config: Compilation configuration for the job.
options: Options for the job.
poll_interval_seconds: Polling interval for the job.
timeout_seconds: Timeout for the job.
optimisation_level: Optimisation level for the job.
"""
device_name: str
label: str = "job" # not str | None — pytket expects str
simulator: str = "state-vector"
group: str | None = None
provider: str | None = None
machine_debug: bool = False
api_handler: QuantinuumAPI | None = None # optional, use default
compilation_config: QuantinuumBackendCompilationConfig | None = None
options: dict[str, Any] = field(default_factory=dict)
poll_interval_seconds: float = 2.0
timeout_seconds: float | None = 300.0
optimisation_level: int = 2
The class QuantinuumExecutor inheriting from BaseExecutor was defined, and it supports both state-vector and stabilizer.
Example:
>>> config = QuantinuumExecutorConfig(
... device_name="H2-1",
... simulator="state-vector",
... )
>>> executor = QuantinuumExecutor(config)
>>> result = await executor.execute(circuit, shots=1000)
>>> print(result.counts) # {"00": ~500, "11": ~500}
The executor is initialised with QuantinuumExecutorConfig:
def __init__(self, config: QuantinuumExecutorConfig) -> None:
"""Initialize QuantinuumExecutor.
Args:
config: Executor configuration including device settings.
"""
self.config = config
self._api_handler = self.config.api_handler
self._backend: QuantinuumBackend | None = None
self._current_job_id: str | None = None
The executor supports synchronous execution, result normalisation, and device-status checks. The main methods are grouped as follows:
| Group | Method | Description |
|---|---|---|
| Backend setup | _get_backend_sync() | Creates or returns QuantinuumBackend instance synchronously |
_get_backend() | Asynchronous wrapper around _get_backend_sync() | |
| Execution | _run_sync() | Compiles the circuit, submits the job, and retrieves results and job_id |
execute() | Public async entry point for running a circuit with shots and specific backend options. Returns ExecutionResults with measurement counts and metadata | |
| Results | _pytket_counts_to_bitstring() | Normalises pytket count keys (tuples or bitstrings) into Marqov-friendly bitstrings |
| Device status | _get_device_state_sync() | Fetches raw device state from the Quantinuum API |
get_device_status() | Retrieves the current device status (e.g. online or offline) | |
is_device_available() | Returns True if the device is online and available for execution, and False if offline | |
get_status() | Returns live device status from Quantinuum |
Bugs & fixes
Changes were pushed to a forked repository on the feature branch feature/to_pytket. After the initial PR, the code review identified two bugs. Below is how they were resolved.
Count normalisation
Initially, execute() set counts = dict(result.get_counts()). However, pytket’s BackendResult.get_counts() returns keys as tuples of integers, not bitstrings. Other Marqov backends return bitstring keys for counts, and therefore ExecutionResult.counts requires type dict[str, int]. Without conversion, counts would be {(0, 0): 500, (1, 1): 500} instead of {"00": 500, "11": 500}.
The fix adds helper method _pytket_counts_to_bitstring and sets counts = self._pytket_counts_to_bitstring(result.get_counts()). If keys are already strings they are passed through; otherwise keys are joined in order "".join(str(b) for b in key).
@staticmethod
def _pytket_counts_to_bitstring(counts_dict: Mapping[Any, int]) -> dict[str, int]:
"""Convert tuples or bitstrings to a dictionary of bitstrings.
Args:
counts_dict: Dictionary of tuples to convert.
Returns:
Dictionary of bitstrings to counts.
"""
return {
key if isinstance(key, str) else "".join(str(b) for b in key): count
for key, count in counts_dict.items()
}
Using partial in run_in_executor
The initial PR used
result, job_id = await loop.run_in_executor(None, self._run_sync, backend, tket_circuit, shots, **kwargs)
run_in_executor only accepts positional arguments after the callable. Passing **kwargs raises TypeError when backend-specific options are passed. The fix is achieved by wrapping the call with functools.partial before scheduling the call:
result, job_id = await loop.run_in_executor(
None, partial(self._run_sync, backend, tket_circuit, shots, **kwargs)
)
Testing
Unit tests were added in tests/test_circuits.py and tests/test_executor.py and can be run via pytest from the repository root.
Circuit converter tests
To test to_pytket(), a test class TestToPytket was added in tests/test_circuits.py to verify Bell state conversion to a pytket Circuit.
class TestToPytket:
"""Tests for Circuit.to_pytket()."""
def test_to_pytket_bell_state(self) -> None:
"""Bell state converts to pytket Circuit object."""
from pytket import Circuit as PytketCircuit
from pytket.circuit import OpType
expected = PytketCircuit(2)
expected.H(0)
expected.add_gate(OpType.CX, [0, 1])
result = bell_state().to_pytket()
assert isinstance(result, PytketCircuit)
assert result.n_qubits == expected.n_qubits
To run this test one can use:
pytest tests/test_circuits.py::TestToPytket::test_to_pytket_bell_state -v
QuantinuumExecutor tests
Tests in tests/test_executor.py cover configuration, executor construction, device status, and execution:
-
TestQuantinuumExecutor: configuration validation and executor initialisation, includingtest_pytket_counts_tuple_keystest_pytket_counts_string_keys_passthroughtest_execute_converts_tuple_counts
-
TestQuantinuumExecutorGetStatus: checks device status helper methods.
pytest tests/test_executor.py::TestQuantinuumExecutor -v
pytest tests/test_executor.py::TestQuantinuumExecutorGetStatus -v
Conclusions
Issues #3 and #6 were successfully resolved via PRs #31, merging Circuit.to_pytket() and QuantinuumExecutor into the Marqov SDK. Marqov circuits can now be exported to pytket and jobs can be submitted to Quantinuum through the same orchestration layers as other backends.
References
[1] Marqov SDK, https://marqov.ai/
[2] Marqov SDK (GitHub Repository), https://github.com/marqov-dev/marqov-sdk
[3] PyTket API Doumentation, https://docs.quantinuum.com/tket/extensions/pytket-quantinuum/_modules/pytket/extensions/quantinuum/backends/quantinuum.html
Topics
- Quantum Computing
- Marqov SDK
- pytket
Tech Stack
- Python
- pytket
- Marqov SDK