6.6 KiB
6.6 KiB
Task 8 — explain_result response (AST-based gate breakdown)
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.rspopulated).
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)"