Tessera: Quantum Circuit Transpiler

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, BasisTranslationPass raises a ValueError.
  • Entries must produce only basis gate instructions. BasisTranslationPass is 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 params and 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 is cz, inline each CX as H·CZ·H within the decomposition entry rather than relying on a separate cx entry.

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_GATES

tests/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