Adding a Backend
Adding a new backend touches four files and requires no changes to any pass, the pipeline, or the transpiler core. The backend registry pattern is designed so that the rest of the codebase automatically picks up anything registered correctly.
Step 1: Add the Basis Gate Set
Open tessera/backends/basis_gate_sets.py and add a set of gate name strings representing the gates natively supported by the new backend:
# tessera/backends/basis_gate_sets.py
MY_BACKEND_BASIS_GATES = {"gate_a", "gate_b", "gate_c"}Gates in this set will pass through BasisTranslationPass unchanged. Everything not in this set must have a decomposition entry in the next step.
Step 2: Add the Decomposition Map
Open tessera/backends/decomposition_maps.py and add a decomposition map, a dict mapping non-basis gate names to their equivalent sequences of basis gate instructions.
# tessera/backends/decomposition_maps.py
MY_BACKEND_DECOMP_MAP = {
"h": [
TesseraInstruction("gate_a", [0], [], [pi / 2]),
TesseraInstruction("gate_b", [0], [], [pi / 2]),
],
"rx": lambda params: [
TesseraInstruction("gate_b", [0], [], [params[0]])
],
# add entries for every gate not in your basis set
}Important rules:
- Every gate a user might reasonably pass in that isn't in your basis set must have an entry. If a gate is encountered with no decomposition,
BasisTranslationPassraises aValueError. - Entries must produce only basis gate instructions.
BasisTranslationPassis single-pass with no recursion. If a decomposition produces a non-basis gate, it will not be caught. - For static decompositions (no parameters) use a plain list. For parameterized gates use a lambda that takes
paramsand returns a list. - If your basis does not include
cx, every multi-qubit decomposition must inline the CX expansion directly. For example if your native two-qubit gate iscz, inline each CX as H·CZ·H within the decomposition entry rather than relying on a separatecxentry.
Step 3: Add the Coupling Maps
Open tessera/backends/coupling_maps.py. Build at least one TesseraCouplingMap for your backend and add it to COUPLING_MAP_REGISTRY with a string key.
# tessera/backends/coupling_maps.py
def _build_my_backend_coupling_map():
edges = [
(0, 1), (1, 0),
(1, 2), (2, 1),
# add all qubit connections here
]
return TesseraCouplingMap(num_qubits, edges)
MY_BACKEND_COUPLING_MAP = _build_my_backend_coupling_map()
COUPLING_MAP_REGISTRY = {
# existing entries...
"MY_BACKEND_DEFAULT": MY_BACKEND_COUPLING_MAP
}If you have multiple devices for the same backend, add one map per device and pick the most commonly used one as the default. See the IonQ or Rigetti entries for examples of how to structure multiple maps for the same backend.
Step 4: Register the Backend
Open tessera/backends/backend_registry.py. Import your new basis gate set and decomposition map, then add an entry to BACKEND_REGISTRY:
# tessera/backends/backend_registry.py
from tessera.backends.basis_gate_sets import ..., MY_BACKEND_BASIS_GATES
from tessera.backends.decomposition_maps import ..., MY_BACKEND_DECOMP_MAP
BACKEND_REGISTRY = {
# existing entries...
"MY_BACKEND": {
"basis_gates": MY_BACKEND_BASIS_GATES,
"decomp_map": MY_BACKEND_DECOMP_MAP,
"coupling_map": "MY_BACKEND_DEFAULT" # must match a key in COUPLING_MAP_REGISTRY
}
}The coupling_map value must exactly match a key in COUPLING_MAP_REGISTRY. This is the coupling map used when no override is passed to transpile().
After this step your backend is immediately usable:
transpile(qc, backend="MY_BACKEND")Step 5: Write the Tests
Add tests to the relevant existing test files rather than creating new ones:
tests/test_basis_gate_sets.py
Verify your basis set contains the expected gates and excludes non-basis gates:
def test_my_backend_basis_gates_contains_expected():
assert "gate_a" in MY_BACKEND_BASIS_GATES
def test_my_backend_basis_gates_excludes_non_basis():
assert "h" not in MY_BACKEND_BASIS_GATEStests/test_decomposition_maps.py
Verify all expected gates are present in the decomp map and spot-check that parameterized entries use their params correctly:
def test_my_backend_all_expected_gates_present():
expected = {"h", "x", "cx", ...}
assert expected.issubset(MY_BACKEND_DECOMP_MAP.keys())tests/test_backend_registry.py
Verify the backend is registered with all required fields and the correct default coupling map key:
def test_my_backend_in_registry():
assert "MY_BACKEND" in BACKEND_REGISTRY
def test_my_backend_has_required_fields():
entry = BACKEND_REGISTRY["MY_BACKEND"]
assert "basis_gates" in entry
assert "decomp_map" in entry
assert "coupling_map" in entry
def test_my_backend_default_coupling_map_key():
assert BACKEND_REGISTRY["MY_BACKEND"]["coupling_map"] == "MY_BACKEND_DEFAULT"tests/test_basis_translation_pass.py
Verify that a circuit with known non-basis gates translates correctly using your backend:
def test_my_backend_h_decomposes_correctly():
circuit = TesseraCircuit(1, 0, [TesseraInstruction("h", [0], [], [])])
result = BasisTranslationPass("MY_BACKEND").run(circuit)
names = [i.name for i in result.instructions]
assert "h" not in names
assert all(g in MY_BACKEND_BASIS_GATES for g in names)tests/test_transpile_api.py
Verify end-to-end that a circuit transpiles without error and produces output only containing basis gates:
def test_transpile_with_my_backend():
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
result = transpile(qc, backend="MY_BACKEND")
gate_names = {i.operation.name for i in result.data if i.operation.name != "measure"}
assert gate_names.issubset(MY_BACKEND_BASIS_GATES)Run the full suite and check for code coverage when done. Coverage can be checked against the table in TEST.md:
pytest tests/ --cov=. --cov-report=term-missing