Initial import
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# Task 8 — `explain_result` response (AST-based gate breakdown)
|
||||
|
||||
> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md).
|
||||
|
||||
## 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**
|
||||
|
||||
```rust
|
||||
#[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`)**
|
||||
|
||||
```rust
|
||||
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**
|
||||
|
||||
```bash
|
||||
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"
|
||||
```
|
||||
|
||||
Expected: `test result: ok. 11 passed`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/tools/tutor_tools.rs
|
||||
git commit -m "feat: explain_result with AST-based gate breakdown (no string matching)"
|
||||
```
|
||||
Reference in New Issue
Block a user