Skip to content

Model and execute quantum programs

One Program. Three levels of physical detail.

Write a quantum program once. Use the same Program to explore algorithm behavior, test device constraints, or follow time-dependent physical dynamics.

Python 3.12+ Install from source

Start with one Program, then choose the physical detail at run time.

Under active development

FatQat is under active development, and its interfaces may change between releases. Pin an exact version when reproducibility matters.

  • One quantum program

    Keep gates, measurements, conditions, parameters, qudits, and direct controls in a single Program.

  • Only the detail you need

    Start with algorithm behavior, add device constraints when needed, or follow continuous-time dynamics when the physics matters.

  • One execution workflow

    Every backend accepts a Program and uses the same Result interface, while validating what it can realize.

One workflow, three execution levels

Every path starts from the same Program and returns familiar Job and Result objects. Choose the least physical detail that can answer the question in front of you.

  • General simulation


    Inspect states, counts, and channel noise when algorithm behavior is the question.

    Study simulation

  • Hardware-profile simulation


    Add native gates and connectivity when device constraints matter.

    Model a hardware profile

  • Hamiltonian emulation


    Follow pulse dynamics, leakage, crosstalk, and non-Markovian effects when physical behavior matters.

    Follow physical dynamics

One Grover algorithm, three execution levels

This three-qubit Grover search begins with all eight bit strings equally likely. Two iterations mark 101 and amplify it. The results below show how three execution levels change the outcome as hardware constraints and physical dynamics are introduced.

A compact three-qubit Grover search uses fused RY and RZ rotations with four Toffoli gates to amplify 101.
  • General Simulator

    The ideal Simulator result gives the target outcome 101 a probability of 94.53 percent.

    Circuit-level evolution returns 101 with 94.53% probability.

  • SCQubitSimulator

    The SCQubitSimulator result gives the target outcome 101 a probability of 81.65 percent with 200-microsecond coherence times and additional CZ depolarizing noise of 0.003 on both edges.

    With T1 = T2 = 200 µs, we add CZ depolarizing noise with p = 0.003. Compiled native-gate simulation then returns 101 with 81.65% probability.

  • TransmonEmulator

    The three-level TransmonEmulator result gives the target outcome 101 a probability of about 64.05 percent with the same coherence times.

    Calibrated pulses, three physical levels, and the same coherence times return 101 with about 64.05% probability; physical leakage is 0.0649%. This run realizes the compiled X/SX/RZ/CZ gates with calibrated pulses.

With the fixed routing seed, compilation produces 51 CZ, 172 SX, and 383 RZ gates. These results use the same compiled circuit for both hardware models; the SC noise channels and the Transmon pulse dynamics model different physical effects. The Transmon model also uses 60 ns CZ pulses, while the SC coherence channels use a 50 ns CZ duration. Compilation preserves ideal algorithm behavior, but the gate decomposition and routing affect noisy results.

Shared algorithm source — home_grover_program.py

All three scripts start from the same compact LogicalProgram shown above. General simulation runs it directly; the SC and Transmon scripts use compile_to_sc() to decompose its Toffoli gates and route the circuit onto the three-site line. The routing seed is fixed for reproducible results. The shared axis helper restores logical qubit order when reading a compiled final state.

"""Define the shared Grover source and result ordering for the homepage."""

from math import pi

import fatqat as fq
import fatqat.operations as ops

TARGET = "101"
TARGET_INDEX = int(TARGET, 2)
COUPLINGS = ((0, 1), (1, 2))
COMPILER_SEED = 7


def build_logical_program():
    """Build two compact Grover iterations marking 101."""
    program = fq.LogicalProgram(3)

    def fused_layer(*rotations, rz_targets=()):
        for target, theta in rotations:
            program.add(ops.RY(theta), target)
        for target in rz_targets:
            program.add(ops.RZ(pi), target)

    program.add(ops.H, 0)
    fused_layer((1, pi / 2))
    program.add(ops.CCX, (0, 1, 2))
    fused_layer((0, pi / 2), (1, pi / 2), (2, -pi / 2), rz_targets=(1,))
    program.add(ops.CCX, (0, 1, 2))

    fused_layer((0, -pi / 2), (1, pi / 2), (2, pi / 2), rz_targets=(1,))
    program.add(ops.CCX, (0, 1, 2))
    fused_layer((0, pi / 2), (1, pi / 2), (2, -pi / 2), rz_targets=(1,))
    program.add(ops.CCX, (0, 1, 2))
    fused_layer((0, -pi / 2), (1, -pi / 2))
    program.add(ops.Z, 2)
    return program


def logical_axis_order(compiled, result):
    """Read a routed final state in the source Program's qubit order."""
    refs_by_site = {
        compiled.resource_layout.device_label(ref): ref
        for ref in compiled.resource_layout.refs
    }
    axes_by_ref = {
        axis["register_ref"]: index
        for index, axis in enumerate(result.metadata["state_axes"])
    }
    return tuple(
        axes_by_ref[refs_by_site[site]]
        for _logical, site in compiled.output.final_layout
    )
Run each execution model independently

Each tab is a top-to-bottom script. The general script also verifies that ideal compiled execution matches the source state up to a global phase. The Transmon script binds compiled sites to model subsystems and supplies X/SX pulse definitions using its calibrated RX recipe.

"""Run and compile the shared Grover Program, checking ideal equivalence."""

import matplotlib.pyplot as plt
import numpy as np

import fatqat as fq

from _home_grover_plot import (
    CIRCUIT_FIGURE,
    GENERAL_FIGURE,
    PROGRAM_DRAW_STYLE,
    draw_distribution,
    style_program_figure,
)
from home_grover_program import (
    COMPILER_SEED,
    COUPLINGS,
    TARGET,
    TARGET_INDEX,
    build_logical_program,
    logical_axis_order,
)

program = build_logical_program()
simulator = fq.simulator.Simulator(runtime="numpy")

state = (
    simulator.run(
        program,
        shots=0,
        result_config={"counts": False, "final_state": True},
    )
    .result()
    .get_statevector()
)
probabilities = np.abs(state) ** 2
probabilities /= probabilities.sum()

compiler_backend = fq.simulator.SCQubitSimulator(
    num_qubits=3, couplings=COUPLINGS, runtime="numpy"
)
compiled = fq.compiler.compile_to_sc(program, compiler_backend, seed=COMPILER_SEED)
compiled_result = compiler_backend.run(
    compiled,
    shots=0,
    result_config={"counts": False, "final_state": True},
).result()
compiled_state = (
    compiled_result.get_statevector()
    .reshape((2, 2, 2))
    .transpose(logical_axis_order(compiled, compiled_result))
    .ravel()
)
overlap = np.vdot(state, compiled_state)

assert np.isclose(probabilities.sum(), 1.0)
assert np.argmax(probabilities) == TARGET_INDEX
assert np.isclose(probabilities[TARGET_INDEX], 0.9453125, atol=1e-12, rtol=0)
assert np.isclose(abs(overlap), 1.0, atol=1e-12, rtol=0)
assert np.allclose(compiled_state, overlap * state, atol=1e-12, rtol=0)

print(f"General Simulator P({TARGET}) = {probabilities[TARGET_INDEX]:.8%}")
circuit_figure = plt.figure(CIRCUIT_FIGURE, figsize=(13.0, 3.2), facecolor="white")
circuit_axis = circuit_figure.add_subplot()
program.draw(ax=circuit_axis, **PROGRAM_DRAW_STYLE)
style_program_figure(circuit_figure, circuit_axis)
draw_distribution(GENERAL_FIGURE, probabilities)
if __name__ == "__main__":
    plt.show()
"""Compile the shared Grover Program and run it on SCQubitSimulator."""

import matplotlib.pyplot as plt
import numpy as np

import fatqat as fq
import fatqat.operations as ops
from fatqat.compiler import compile_to_sc

from _home_grover_plot import draw_distribution
from home_grover_program import (
    COMPILER_SEED,
    COUPLINGS,
    TARGET,
    TARGET_INDEX,
    build_logical_program,
    logical_axis_order,
)

PROFILE_FIGURE = "grover-sc-profile.png"
T1_SECONDS = 200e-6
T2_SECONDS = 200e-6
SX_DURATION_SECONDS = 20e-9
CZ_DURATION_SECONDS = 50e-9
EDGE_CZ_DEPOLARIZING_P = {
    (0, 1): 0.003,
    (1, 2): 0.003,
}


compiler_backend = fq.simulator.SCQubitSimulator(
    num_qubits=3,
    couplings=COUPLINGS,
    runtime="numpy",
)
compiled = compile_to_sc(build_logical_program(), compiler_backend, seed=COMPILER_SEED)
noise = fq.NoiseModel()


def coherence_channels(duration):
    """Return finite simulator channels for the configured T1 and T2."""
    amplitude_p = -np.expm1(-duration / T1_SECONDS)
    pure_dephasing_rate = 1 / T2_SECONDS - 1 / (2 * T1_SECONDS)
    phase_p = -np.expm1(-pure_dephasing_rate * duration)
    return (
        fq.noise.AmplitudeDamping(p=amplitude_p),
        fq.noise.PhaseDamping(p=phase_p),
    )


for operation in (ops.X, ops.SX):
    damping, dephasing = coherence_channels(SX_DURATION_SECONDS)
    noise.add(damping, operation=operation)
    noise.add(dephasing, operation=operation)

damping, dephasing = coherence_channels(CZ_DURATION_SECONDS)
for target_position in (0, 1):
    noise.add(damping, operation=ops.CZ, target_positions=(target_position,))
    noise.add(dephasing, operation=ops.CZ, target_positions=(target_position,))

# Noise target tuples are ordered; routing can use either CZ orientation.
for edge, depolarizing_p in EDGE_CZ_DEPOLARIZING_P.items():
    for ordered_edge in (edge, edge[::-1]):
        noise.add(
            fq.noise.Depolarizing(p=depolarizing_p),
            operation=ops.CZ,
            targets=ordered_edge,
        )

result = (
    fq.simulator.SCQubitSimulator(
        num_qubits=3,
        couplings=COUPLINGS,
        method="density_matrix",
        runtime="numpy",
        noise=noise,
    )
    .run(
        compiled,
        shots=0,
        result_config={"counts": False, "final_state": True},
    )
    .result()
)
density_matrix = result.get_density_matrix()
probabilities = np.clip(np.real(np.diag(density_matrix)), 0.0, None)
probabilities = (
    probabilities.reshape((2, 2, 2))
    .transpose(logical_axis_order(compiled, result))
    .ravel()
)
probabilities /= probabilities.sum()

assert np.isclose(probabilities.sum(), 1.0)
assert np.argmax(probabilities) == TARGET_INDEX
assert np.isclose(probabilities[TARGET_INDEX], 0.8164862609, atol=5e-7)

print(f"SCQubitSimulator P({TARGET}) = {probabilities[TARGET_INDEX]:.8%}")
draw_distribution(PROFILE_FIGURE, probabilities)
if __name__ == "__main__":
    plt.show()
"""Run the compiled Grover Program on a three-level TransmonEmulator."""

from itertools import product

import matplotlib.pyplot as plt
import numpy as np

import fatqat as fq
import fatqat.operations as ops

from _home_grover_plot import draw_distribution
from home_grover_program import (
    TARGET,
    TARGET_INDEX,
    COMPILER_SEED,
    COUPLINGS,
    build_logical_program,
    logical_axis_order,
)

TRANSMON_FIGURE = "grover-transmon.png"
T1_NANOSECONDS = 200_000.0
T2_NANOSECONDS = 200_000.0

model_document = {
    "format": {"id": "sc.transmon_exchange", "version": 1},
    "model": {"id": "grover-three-transmon-line", "revision": "2026-08-30"},
    "system": {
        "subsystem_type": "transmon",
        "subsystems": ["q0", "q1", "q2"],
        "control_edges": [
            {"id": "e01", "subsystems": ["q0", "q1"]},
            {"id": "e12", "subsystems": ["q1", "q2"]},
        ],
    },
    "units": {"frequency": "GHz", "anharmonicity": "GHz"},
    "parameters": {
        "subsystems": {
            "q0": {"frequency": 5.10, "anharmonicity": -0.22},
            "q1": {"frequency": 5.22, "anharmonicity": -0.24},
            "q2": {"frequency": 5.34, "anharmonicity": -0.22},
        }
    },
}
model = fq.emulator.TransmonModel.from_document(model_document)

calibration_document = {
    "format": {"id": "sc.transmon_exchange_fixed_pulse", "version": 1},
    "calibration": {
        "id": "grover-three-transmon-line",
        "revision": "2026-08-30",
    },
    "units": {"time": "ns", "frequency": "GHz", "dimensionless": "1"},
    "recipes": {
        "rx_ry": {"duration": 20.0, "drag_coefficient": 1.0},
        "iswap": {"duration": 40.0},
        "cz": {
            "edges": [
                {
                    "canonical_edge": ["q0", "q1"],
                    "recipe": {
                        "detuned_subsystem": "q0",
                        "duration": 60.0,
                        "ramp_duration": 3.0,
                        "park_detuning_ghz": 0.22,
                        "branch_tolerance_ghz": 1e-12,
                    },
                },
                {
                    "canonical_edge": ["q1", "q2"],
                    "recipe": {
                        "detuned_subsystem": "q1",
                        "duration": 60.0,
                        "ramp_duration": 3.0,
                        "park_detuning_ghz": 0.24,
                        "branch_tolerance_ghz": 1e-12,
                    },
                },
            ],
        },
    },
}
calibration = fq.emulator.TransmonCalibration(calibration_document)

noise = fq.NoiseModel()
for subsystem in model.subsystem_ids:
    noise.add(
        fq.noise.TransitionRelaxation(
            rate=1 / T1_NANOSECONDS,
            coefficients={(1, 0): 1.0, (2, 1): np.sqrt(2.0)},
        ),
        targets=subsystem,
    )
    noise.add(
        fq.noise.PhaseDamping(rate=1 / T2_NANOSECONDS - 1 / (2 * T1_NANOSECONDS)),
        targets=subsystem,
    )

compiler_backend = fq.simulator.SCQubitSimulator(
    num_qubits=3, couplings=COUPLINGS, runtime="numpy"
)
compiled = fq.compiler.compile_to_sc(
    build_logical_program(), compiler_backend, seed=COMPILER_SEED
)
# Bind the compiler's integer sites to this model's named transmons.
resource_layout = fq.ResourceLayout(
    {
        ref: model.subsystem_ids[compiled.resource_layout.device_label(ref)]
        for ref in compiled.resource_layout.refs
    }
)
implementations = fq.emulator.default_transmon_gate_implementation_map(
    model=model,
    calibration=calibration,
)
# Realize the canonical X/SX gates with the calibrated RX pulse recipe.
rx_pulse = implementations.implementation_for(ops.RX)
for subsystem in model.subsystem_ids:
    for gate, angle in ((ops.X, np.pi), (ops.SX, np.pi / 2)):
        implementations.add(
            gate,
            rx_pulse(ops.RX(angle), device_operands=(subsystem,)),
            device_operands=(subsystem,),
        )
emulator = fq.emulator.TransmonEmulator(
    model,
    method="density_matrix",
    noise=noise,
    gate_implementation_map=implementations,
)
result = emulator.run(
    compiled.program,
    resource_layout=resource_layout,
    shots=0,
    result_config={"counts": False, "final_state": True},
).result()

density_matrix = result.get_density_matrix()
physical = np.clip(np.real(np.diag(density_matrix)), 0.0, None)
physical /= physical.sum()
physical = physical.reshape((3, 3, 3)).transpose(logical_axis_order(compiled, result))
binary = np.zeros(8)
leakage = 0.0
for levels in product(range(3), repeat=3):
    probability = physical[levels]
    outcome = sum(
        (level > 0) << (len(levels) - 1 - factor) for factor, level in enumerate(levels)
    )
    binary[outcome] += probability
    if 2 in levels:
        leakage += probability

probabilities = binary / binary.sum()
assert np.isclose(probabilities.sum(), 1.0)
assert np.argmax(probabilities) == TARGET_INDEX
assert np.isclose(probabilities[TARGET_INDEX], 0.64046294, atol=1e-3)
assert np.isclose(leakage, 0.0006491483, atol=5e-7)

print(
    f"TransmonEmulator P({TARGET}) = {probabilities[TARGET_INDEX]:.8%}; "
    f"leakage = {leakage:.8%}"
)
draw_distribution(TRANSMON_FIGURE, probabilities)
if __name__ == "__main__":
    plt.show()

Encode hardware behavior when needed

Algorithm developers can stay with general simulation. When their work requires more detail, compiler and hardware developers can add device-specific operations to a Program. The backend validates when those operations are allowed and tracks their effects.

AtomArraySimulator provides one example. Sites begin empty, so Put loads the atoms; Pair makes native CZ available, and Unpair removes that connection. Noise attached to these operations can perturb the state or remove an atom from the array.

Two occupied atoms begin separated, pair so that CZ becomes legal, and unpair while optional depolarizing noise follows the pairing operations.
Occupancy stays explicit while pairing changes which native interactions are legal.
Run the atom-array Program
import numpy as np
import fatqat as fq
import fatqat.operations as ops

atoms = fq.Program(2, 2)
atoms.add(ops.Put, (0, 1))
atoms.add(ops.Pair, (0, 1))
atoms.add(ops.RX(np.pi), 0)
atoms.add(ops.CZ, (0, 1))
atoms.add(ops.Unpair, (0, 1))
atoms.measure_all()

counts = fq.simulator.AtomArraySimulator().run(
    atoms,
    shots=8,
    simulation_config={"seed": 7},
).result().get_counts()
print(counts)  # {'10': 8}

Loss models the second outcome. It removes a present atom after a matched operation. Occupancy is tracked independently for every shot, and measuring an empty site returns the erasure digit 2.

An occupied atom undergoes a matched operation, after which Loss either leaves it present with probability one minus p or removes it with probability p; measurement of the empty site returns 2.
Loss is sampled after the matched operation and changes per-shot occupancy.
Simulate atom loss

Loss(p=0.1) is sampled after RX. Surviving atoms return 1; lost atoms return 2.

import numpy as np
import fatqat as fq
import fatqat.operations as ops

loss_model = fq.NoiseModel()
loss_model.add(fq.noise.Loss(p=0.1), operation=ops.RX)

lossy_atoms = fq.Program(1, 1)
lossy_atoms.add(ops.Put, 0)
lossy_atoms.add(ops.RX(np.pi), 0)
lossy_atoms.measure_all()

lossy_counts = fq.simulator.AtomArraySimulator(noise=loss_model).run(
    lossy_atoms,
    shots=100,
    simulation_config={"seed": 7},
).result().get_counts()
print(lossy_counts)  # {'1': 86, '2': 14}

Track occupancy, pairing, and loss

Explore the documentation