Files
quantum-bridge-mcp/docs/superpowers/plans/2026-04-29-quantum-tutor/08-explain-result.md
T
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

6.6 KiB

Task 8 — explain_result response (AST-based gate breakdown)

Index: README. Spec: design.

Goal

Build a structured pedagogical breakdown of an executed circuit: gate-by-gate description, key concept (entanglement / superposition / interference / rotation / measurement), dominant and missing outcomes, optional statevector summary. Spec §2.3 mandates AST-based analysis — we use CircuitAnalyzer::list_gates, no string-matching.

Prerequisites

  • Task 4 merged (CircuitAnalyzer).
  • Task 7 merged (tutor_tools.rs populated).

Files

  • Modify: src/tools/tutor_tools.rs

Steps

  • Step 1: Append failing tests
    #[test]
    fn explain_result_bell_circuit_lists_h_then_cx_with_descriptions() {
        let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;";
        let counts = json!({"00": 512, "11": 512});
        let resp = explain_result_response(&loader(), bell, &counts, None);
        let breakdown = resp["gate_breakdown"].as_array().unwrap();
        assert_eq!(breakdown.len(), 2);
        assert_eq!(breakdown[0]["name"].as_str().unwrap(), "h");
        assert_eq!(breakdown[1]["name"].as_str().unwrap(), "cx");
        assert!(breakdown[0]["description"].as_str().unwrap().contains("Hadamard"));
        assert_eq!(resp["key_concept"].as_str().unwrap(), "entanglement");
        assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2);
    }

    #[test]
    fn explain_result_dominant_outcomes_match_counts() {
        let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;";
        let counts = json!({"00": 480, "11": 544});
        let resp = explain_result_response(&loader(), bell, &counts, None);
        let dominant: Vec<&str> = resp["dominant_outcomes"]
            .as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect();
        assert!(dominant.contains(&"00"));
        assert!(dominant.contains(&"11"));
    }

    #[test]
    fn explain_result_for_invalid_circuit_returns_error_payload() {
        let resp = explain_result_response(&loader(), "not valid qasm", &json!({}), None);
        assert!(resp.get("error").is_some());
    }
  • Step 2: Implementation (top of tutor_tools.rs)
use crate::circuit_analyzer::{CircuitAnalyzer, GateCallInfo};
use crate::types::CircuitSource;

pub fn explain_result_response(
    loader: &CurriculumLoader,
    circuit: &str,
    counts: &Value,
    statevector: Option<&Value>,
) -> Value {
    let curriculum = match loader.curriculum() {
        Ok(c) => c,
        Err(e) => return json!({ "error": e.to_string() }),
    };

    let gates = match CircuitAnalyzer::list_gates(&CircuitSource(circuit.to_string())) {
        Ok(g) => g,
        Err(e) => return json!({ "error": e.to_string() }),
    };

    let gate_breakdown: Vec<Value> = gates
        .iter()
        .map(|g| gate_entry(g, &curriculum.gate_descriptions))
        .collect();

    let key_concept = classify_key_concept(&gates);
    let num_qubits = max_qubit_index(&gates).map(|m| m + 1).unwrap_or(0);
    let (dominant, missing) = outcomes_summary(counts, num_qubits);
    let statevector_summary = statevector_summary(statevector);

    json!({
        "gate_breakdown": gate_breakdown,
        "num_qubits": num_qubits,
        "key_concept": key_concept,
        "dominant_outcomes": dominant,
        "missing_outcomes": missing,
        "statevector_summary": statevector_summary,
    })
}

fn gate_entry(
    g: &GateCallInfo,
    descs: &std::collections::HashMap<String, crate::tutor::GateDescription>,
) -> Value {
    let mut entry = json!({
        "name": g.name,
        "qubits": g.qubits,
        "params": g.params,
    });
    if let Some(d) = descs.get(&g.name) {
        entry["description"] = json!(d.short);
        if let Some(eff) = &d.effect_on_zero {
            entry["effect_on_zero"] = json!(eff);
        }
    }
    entry
}

fn classify_key_concept(gates: &[GateCallInfo]) -> &'static str {
    let names: std::collections::HashSet<&str> = gates.iter().map(|g| g.name.as_str()).collect();
    if names.contains("cx") || names.contains("cz") || names.contains("ccx") || names.contains("swap") {
        "entanglement"
    } else if names.contains("h") && (names.contains("rz") || names.contains("z") || names.contains("s") || names.contains("t")) {
        "interference"
    } else if names.contains("h") {
        "superposition"
    } else if names.contains("rx") || names.contains("ry") || names.contains("rz") {
        "rotation"
    } else {
        "measurement"
    }
}

fn max_qubit_index(gates: &[GateCallInfo]) -> Option<usize> {
    gates.iter().flat_map(|g| g.qubits.iter().copied()).max()
}

fn outcomes_summary(counts: &Value, num_qubits: usize) -> (Vec<String>, Vec<String>) {
    let map = match counts.as_object() {
        Some(m) => m,
        None => return (vec![], vec![]),
    };
    let total: u64 = map.values().filter_map(|v| v.as_u64()).sum();
    if total == 0 { return (vec![], vec![]); }
    let threshold = (total as f64 * 0.05) as u64;

    let mut dominant: Vec<String> = map
        .iter()
        .filter(|(_, v)| v.as_u64().unwrap_or(0) > threshold)
        .map(|(k, _)| k.clone())
        .collect();
    dominant.sort();

    let missing: Vec<String> = if num_qubits == 0 || num_qubits > 4 {
        vec![]
    } else {
        let n = num_qubits;
        let mut out: Vec<String> = (0u64..(1u64 << n))
            .map(|i| format!("{i:0>n$b}"))
            .filter(|bs| map.get(bs).and_then(|v| v.as_u64()).unwrap_or(0) == 0)
            .collect();
        out.sort();
        out
    };

    (dominant, missing)
}

fn statevector_summary(sv: Option<&Value>) -> Value {
    let sv = match sv.and_then(|v| v.as_array()) {
        Some(a) => a,
        None => return Value::Null,
    };
    let mut non_zero = Vec::new();
    let mut zero = Vec::new();
    for (i, amp) in sv.iter().enumerate() {
        let r = amp.as_array().and_then(|a| a.first()).and_then(|v| v.as_f64()).unwrap_or(0.0);
        let im = amp.as_array().and_then(|a| a.get(1)).and_then(|v| v.as_f64()).unwrap_or(0.0);
        if r * r + im * im > 1e-10 { non_zero.push(i); } else { zero.push(i); }
    }
    json!({"non_zero_amplitudes": non_zero, "zero_amplitudes": zero})
}
  • Step 3: Run tests
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"

Expected: test result: ok. 11 passed.

  • Step 4: Commit
git add src/tools/tutor_tools.rs
git commit -m "feat: explain_result with AST-based gate breakdown (no string matching)"