Skip to content

Separate three sources of variation in a Hybrid experiment

Prepare an entangled pair, apply an Analog pulse with local detuning, and rotate back for measurement. Compare ideal evolution, dephasing, and dephasing with readout errors. The question is whether a changed count reflects a changed quantum state, an error in recording the measurement, or finite sampling.

Work through shared Hybrid state, physical dephasing, local detuning and experimental design first. Use the installation environment and run from the repository root. The default workload is nine two-atom runs, each with 128 shots and 32 Analog integration steps: 1,152 shots total. All execution is local CPU simulation.

Fix the program before comparing noise

The first Digital block applies H(0) then CX(0, 1) to prepare ( |00⟩ + |11⟩ ) / sqrt(2). The Analog block lasts 0.6 us, with global Ω = 1.1 rad/us and zero phase. A constant local waveform of 0.7 rad/us uses weights q0: 1 and q1: 0.25, so the two local detunings are 0.7 and 0.175 rad/us. No interaction term is declared. The last Digital block applies CX(0, 1) then H(0).

With n = |1⟩⟨1|, the Analog Hamiltonian is H = Ω(X0 + X1)/2 − 0.7 n0 − 0.175 n1. The last block reverses the preparation gates; it does not reverse the intervening Analog evolution. Even the ideal experiment therefore need not end in 00.

Keep this program identical in three cases:

Case State evolution Recorded bit errors
ideal State vector None
dephasing Density matrix, γ = 0.8 /us on both atoms during the Analog block None
dephasing_readout The same density-matrix evolution Each bit independently: 0→1 with probability 0.08, 1→0 with probability 0.12

The dephasing convention is dρ/dt = −i[H,ρ] + (γ/2) Σi (ZiρZi − ρ). With the Hamiltonian disabled, a single-atom off-diagonal entry decays as exp(−γt). These are illustrative model parameters, not fitted device values. Digital gate errors, preparation errors and atom loss are absent.

Run and read the comparison

python3 examples/learning/projects/noisy_hybrid.py

Open artifacts/noisy-hybrid/hybrid-en.html. Blue squares show the simulated probability of 00 before readout errors. Green marks show its expected recorded probability after applying the classical readout channel. Three red dots show the observed frequencies for seeds 4881, 4882 and 4883. Their horizontal offsets only keep points visible.

Open the experiment report

Download raw data (JSON)

{
  "artifacts": {
    "data": "artifacts/noisy-hybrid/comparison.json",
    "report_en": "artifacts/noisy-hybrid/hybrid-en.html",
    "report_zh": "artifacts/noisy-hybrid/hybrid-zh.html"
  },
  "cases": [
    {
      "case": "ideal",
      "expected_recorded_probabilities": {
        "00": 0.5754896316901827,
        "01": 0.36395656142510147,
        "10": 0.05808150545784879,
        "11": 0.002472301426867029
      },
      "method": "state_vector",
      "probabilities_before_readout": {
        "00": 0.5754896316901827,
        "01": 0.36395656142510147,
        "10": 0.05808150545784879,
        "11": 0.002472301426867029
      }
    },
    {
      "case": "dephasing",
      "expected_recorded_probabilities": {
        "00": 0.4327894017200914,
        "01": 0.23601050347734526,
        "10": 0.2900376720088961,
        "11": 0.041162422793667296
      },
      "method": "density_matrix",
      "probabilities_before_readout": {
        "00": 0.4327894017200914,
        "01": 0.23601050347734526,
        "10": 0.2900376720088961,
        "11": 0.041162422793667296
      }
    },
    {
      "case": "dephasing_readout",
      "expected_recorded_probabilities": {
        "00": 0.42498140707779525,
        "01": 0.23005851708015412,
        "10": 0.2732802519053948,
        "11": 0.07167982393665592
      },
      "method": "density_matrix",
      "probabilities_before_readout": {
        "00": 0.4327894017200914,
        "01": 0.23601050347734526,
        "10": 0.2900376720088961,
        "11": 0.041162422793667296
      }
    }
  ],
  "sdk_version": "1.0.8a",
  "shots_per_run": 128,
  "total_shots": 1152
}

The ideal P(00) is about 0.57549. Dephasing changes it to 0.43279. Adding readout errors leaves that state probability unchanged and changes the expected recorded probability to about 0.42498. The last shift is smaller than the sampling standard deviation of roughly 0.044 at 128 shots. Three dots help illustrate sampling; they do not establish a reliable estimate of a small experimental effect.

For a true outcome x and recorded outcome y, the script computes q(y) = Σx P(x) Πi M[yi,xi], where M = [[0.92, 0.12], [0.08, 0.88]]. Each column sums to one. q is calculated from the declared model; it is not another simulation output or a fit to counts. ResultIR.probabilities and the requested exact observables remain properties of the state before readout errors. counts includes the readout channel.

Check state continuity and cost

comparison.json contains the program, units, options, noise definitions, nine raw results, seeds, counts and observables. For each run, the three entries in state_chain follow Digital → Analog → Digital. Each block's output state hash must match the next block's input state hash. Logical time advances from 0 to 0.6 us in the Analog block; the ideal Digital gates have no duration here. These records help check that the simulation passes the state between blocks.

The requested observables are Z(q0), Z(q1) and Z(q0)*Z(q1) after the final rotation. exact_state or exact_density identifies an expectation calculated from the simulated state. A zero reported standard error means no measurement sampling was used for that estimate. It does not bound integration error or uncertainty in the physical model.

Both engines use complex128. Two qubits need four complex state amplitudes (64 bytes) or sixteen density-matrix entries (256 bytes). These are state-storage figures. estimated_peak_bytes also includes workspaces and sampling buffers, and is a planning estimate rather than measured process memory. At this small size, fixed overhead can make the total estimate for the density path slightly smaller; the underlying state sizes still scale as 2^n and 4^n.

"""Separate dephasing, readout errors and finite sampling in a two-atom experiment."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

from bokeh.embed import file_html
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.resources import INLINE

import cascaqit
from cascaqit import (
    AHSProgram,
    AtomRegister,
    Circuit,
    HybridProgram,
    LocalBackend,
    ObservableSet,
    PauliZ,
    PauliZZ,
    SimulationOptions,
    SitePattern,
    Waveform,
)
from cascaqit.simulators import NoiseChannel, NoiseModel


def readout_distribution(
    probabilities: dict[str, float], p01: float, p10: float
) -> dict[str, float]:
    """Apply independent classical bit errors to an exact state distribution."""
    distribution = {bits: 0.0 for bits in ("00", "01", "10", "11")}
    for source, probability in probabilities.items():
        for target in distribution:
            weight = probability
            for before, after in zip(source, target):
                error = p01 if before == "0" else p10
                weight *= error if before != after else 1 - error
            distribution[target] += weight
    return distribution


def experiment(
    output_dir: Path, *, shots: int = 128, time_steps: int = 32
) -> dict[str, Any]:
    """Save every execution before drawing the two language reports."""
    if shots < 1 or time_steps < 1:
        raise ValueError("Shots and time steps must be positive.")
    output_dir.mkdir(parents=True, exist_ok=True)
    duration, omega, detuning, rate = 0.6, 1.1, 0.7, 0.8
    p01, p10 = 0.08, 0.12
    analog = (
        AHSProgram(AtomRegister.line(count=2, spacing=5.0))
        .drive(
            rabi=Waveform.constant(omega, duration=duration),
            detuning=Waveform.constant(0.0, duration=duration),
            phase=0.0,
        )
        .local_detuning(
            waveform=Waveform.constant(detuning, duration=duration),
            pattern=SitePattern.from_mapping({"q0": 1.0, "q1": 0.25}),
        )
    )
    program = (
        HybridProgram("project.noisy.hybrid")
        .digital("prepare", Circuit(2).h(0).cx(0, 1))
        .analog("evolve", analog)
        .digital("readout_rotation", Circuit(2).cx(0, 1).h(0))
        .measure_all()
    )
    cases = (
        ("ideal", None),
        ("dephasing", NoiseModel("phase", (NoiseChannel.dephasing(rate),))),
        (
            "dephasing_readout",
            NoiseModel(
                "phase.readout",
                (NoiseChannel.dephasing(rate), NoiseChannel.readout(p01, p10=p10)),
            ),
        ),
    )
    rows: list[dict[str, Any]] = []
    raw: list[dict[str, Any]] = []
    for label, noise in cases:
        options = SimulationOptions(
            method="state_vector" if noise is None else "density_matrix",
            dtype="complex128",
            integrator="fixed_step_krylov",
            max_steps=time_steps,
        )
        for seed in (4881, 4882, 4883):
            result = (
                LocalBackend(analog_time_steps=time_steps)
                .run(
                    program,
                    shots=shots,
                    seed=seed,
                    noise=noise,
                    options=options,
                    observables=ObservableSet(
                        (PauliZ("q0"), PauliZ("q1"), PauliZZ("q0", "q1"))
                    ),
                )
                .result()
            )
            if result.probabilities is None or result.observable_batch is None:
                raise RuntimeError(
                    "The experiment requires probabilities and observables."
                )
            expected = readout_distribution(
                result.probabilities,
                p01 if label == "dephasing_readout" else 0.0,
                p10 if label == "dephasing_readout" else 0.0,
            )
            rows.append(
                {
                    "case": label,
                    "seed": seed,
                    "counts": result.counts,
                    "probabilities_before_readout": result.probabilities,
                    "expected_recorded_probabilities": expected,
                    "frequency_00": result.counts.get("00", 0) / shots,
                    "observables": result.observable_batch.to_dict(),
                    "state_chain": [
                        item.to_dict() for item in result.state_transitions()
                    ],
                    "method": result.metadata["simulation_plan"]["method_selected"],
                    "resource_estimate": result.metadata[
                        "simulation_resource_estimate"
                    ],
                    "options": options.to_dict(),
                    "noise_model": None if noise is None else noise.to_dict(),
                }
            )
            raw.append(result.to_dict())
    artifacts = {"data": str(output_dir / "comparison.json")}
    for language in ("en", "zh"):
        title = (
            "Probability and recorded frequency of 00"
            if language == "en"
            else "00 的概率与记录频率"
        )
        names = (
            ("Ideal", "Dephasing", "Dephasing + readout")
            if language == "en"
            else ("理想", "退相干", "退相干与读出误差")
        )
        plot = figure(
            title=title, x_range=list(names), y_range=(0, 1), width=880, height=430
        )
        first = rows[::3]
        plot.scatter(
            list(names),
            [r["probabilities_before_readout"]["00"] for r in first],
            size=14,
            marker="square",
            color="#2563eb",
            legend_label="Before readout" if language == "en" else "读出前概率",
        )
        plot.scatter(
            list(names),
            [r["expected_recorded_probabilities"]["00"] for r in first],
            size=16,
            marker="dash",
            color="#15803d",
            legend_label="Expected recorded" if language == "en" else "预期记录概率",
        )
        for index in range(3):
            plot.scatter(
                [(name, (index - 1) * 0.12) for name in names],
                [rows[case * 3 + index]["frequency_00"] for case in range(3)],
                size=8,
                color="#dc2626",
                legend_label="Sampled (3 seeds)"
                if language == "en"
                else "采样频率(三个种子)",
            )
        plot.legend.location = "bottom_left"
        path = output_dir / f"hybrid-{language}.html"
        path.write_text(file_html(column(plot), INLINE, title), encoding="utf-8")
        artifacts[f"report_{language}"] = str(path)
    payload = {
        "sdk_version": cascaqit.__version__,
        "program": program.to_dict(),
        "program_hash": program.stable_hash(),
        "duration_us": duration,
        "omega_rad_per_us": omega,
        "local_detuning_rad_per_us": [detuning, detuning * 0.25],
        "dephasing_rate_per_us": rate,
        "readout_p01": p01,
        "readout_p10": p10,
        "shots_per_run": shots,
        "total_shots": shots * len(rows),
        "time_steps": time_steps,
        "rows": rows,
        "raw_results": raw,
        "artifacts": artifacts,
    }
    Path(artifacts["data"]).write_text(
        json.dumps(payload, indent=2) + "\n", encoding="utf-8"
    )
    return {
        "sdk_version": cascaqit.__version__,
        "shots_per_run": shots,
        "total_shots": shots * len(rows),
        "cases": [
            {
                key: r[key]
                for key in (
                    "case",
                    "method",
                    "probabilities_before_readout",
                    "expected_recorded_probabilities",
                )
            }
            for r in rows[::3]
        ],
        "artifacts": artifacts,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--output-dir", type=Path, default=Path("artifacts/noisy-hybrid")
    )
    parser.add_argument("--shots", type=int, default=128)
    parser.add_argument("--time-steps", type=int, default=32)
    args = parser.parse_args()
    print(
        json.dumps(
            experiment(args.output_dir, shots=args.shots, time_steps=args.time_steps),
            sort_keys=True,
        )
    )


if __name__ == "__main__":
    main()

Download the full script

Submit an experiment record

Preserve the script revision, dependency versions, JSON and report. Explain which comparison changes the state, then answer these questions with a small table of your results:

  1. Repeat with --shots 2048 --output-dir artifacts/hybrid-more-shots. Which probabilities and observables stay fixed? How should the sampling standard deviation change?
  2. Repeat with --time-steps 64 --output-dir artifacts/hybrid-refined. Compare each probability with its 32-step counterpart. Why is this still needed for exact_density?
  3. In a copy, set both readout error probabilities to zero. Which two cases should now agree, including their counts when they share a seed?
  4. Derive ⟨Z0⟩ = P00 + P01 − P10 − P11 and compare it with the saved observable. Would substituting sampled counts produce the same kind of estimate?
Check your reasoning

More shots leave state probabilities and exact observables fixed. Increasing 128 to 2,048 shots reduces the binomial standard deviation by a factor of four. The density evolution uses a finite-step splitting method; increasing the step count checks its numerical error even though the observable is not sampled. Zero readout errors leave the dephasing state and sampled bitstrings unchanged. The observable formula uses the declared q0,q1 order. Substituting count frequencies gives a sampled estimate with statistical uncertainty, and with nonzero readout errors it estimates the recorded distribution.

For a research extension, preselect a range of dephasing rates, increase repetitions and report every run. Check integration convergence at the strongest rate before interpreting trends. Reusing a seed across two different execution paths does not by itself create a controlled paired statistical estimator. This small model supports conclusions about the specified simulated channels; comparison with laboratory data needs measured control and noise parameters.

中文版

SDK 1.0.8a · `8b227bff`