From e876266160e2de0e8c29e5888bd1cf5ef2d96c44 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 15 Sep 2026 09:35:02 +0200 Subject: [PATCH 1/3] Add leakage as bytecode instruction --- crates/ppvm-vihaco/src/bytecode.rs | 3 +++ crates/ppvm-vihaco/src/composite.rs | 2 +- crates/ppvm-vihaco/src/syntax.rs | 1 + crates/vihaco-circuit-isa/src/lib.rs | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 6cf376321..c611f7f77 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -162,6 +162,7 @@ enum BytecodeCircuit { PauliError, Depolarize2, Depolarize, + Leakage, } #[derive(Debug, Clone, vihaco::Instruction)] @@ -257,6 +258,7 @@ fn encode_instruction(inst: &PPVMInstruction) -> eyre::Result BytecodeCircuit::PauliError, vihaco_circuit_isa::CircuitInstruction::Depolarize2 => BytecodeCircuit::Depolarize2, vihaco_circuit_isa::CircuitInstruction::Depolarize => BytecodeCircuit::Depolarize, + vihaco_circuit_isa::CircuitInstruction::Leakage => BytecodeCircuit::Leakage, }), }; Ok(encoded) @@ -344,6 +346,7 @@ fn decode_instruction(inst: BytecodeInstruction) -> PPVMInstruction { BytecodeCircuit::PauliError => vihaco_circuit_isa::CircuitInstruction::PauliError, BytecodeCircuit::Depolarize2 => vihaco_circuit_isa::CircuitInstruction::Depolarize2, BytecodeCircuit::Depolarize => vihaco_circuit_isa::CircuitInstruction::Depolarize, + BytecodeCircuit::Leakage => vihaco_circuit_isa::CircuitInstruction::Leakage, }), } } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index a74fc5c6a..ab16d8a88 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -188,7 +188,7 @@ impl PPVM { let q0 = self.pop_u64()?; Ok(CircuitMessage::TwoQubit(q0, q1)) } - RX | RY | RZ | Depolarize | Loss => { + RX | RY | RZ | Depolarize | Loss | Leakage => { let theta = self.pop_f64()?; let q = self.pop_u64()?; Ok(CircuitMessage::QubitAndFloat(q, theta)) diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 3b4bc35c4..7af45af20 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -225,6 +225,7 @@ impl PPVMResolver { S::PauliError => R::PauliError, S::Depolarize2 => R::Depolarize2, S::Depolarize => R::Depolarize, + S::Leakage => R::Leakage, } } } diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 08f8fd637..aca506b64 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -51,6 +51,7 @@ vihaco::component! { PauliError, Depolarize2, Depolarize, + Leakage, } } @@ -110,6 +111,8 @@ impl std::fmt::Display for runtime::Instruction { PauliError => write!(f, "PauliError"), Depolarize2 => write!(f, "Depolarize2"), Depolarize => write!(f, "Depolarize"), + + Leakage => write!(f, "Leakage"), } } } From 4e0c400df718c68e9ae60eb52e560c4e9a0d2820 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 15 Sep 2026 09:43:06 +0200 Subject: [PATCH 2/3] Fix to two-float args, add test and add missing execution arm --- crates/ppvm-vihaco/src/component.rs | 5 ++- crates/ppvm-vihaco/src/composite.rs | 18 ++++++++- .../tests/paulisum_leakage_error.sst | 13 +++++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 39 +++++++++++++++++++ crates/ppvm-vihaco/tests/tableau_leakage.sst | 20 ++++++++++ crates/vihaco-circuit-isa/src/lib.rs | 2 + 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/paulisum_leakage_error.sst create mode 100644 crates/ppvm-vihaco/tests/tableau_leakage.sst diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 569091e15..dd6e777b7 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -158,6 +158,7 @@ where (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { self.tab.correlated_loss_channel(*addr0, *addr1, *ps) } + (Leakage, &QubitAndTwoFloats(addr, p0, p1)) => self.tab.leakage_channel(addr, p0, p1), /* BATCH OPERATIONS START HERE */ // Batch: dedicated batch methods @@ -389,7 +390,9 @@ macro_rules! dispatch_common_paulisum { // Not supported on either backend (Decision 11 + Gate Support // Matrix). Loss / CorrelatedLoss handling differs by backend // and lives in the caller's impl block, not this macro. - (Measure | Reset, _) => { + // Leakage is GeneralizedTableau-only (pinned |0⟩/|1⟩), not a + // LossyPauliSum qutrit |L⟩ channel. + (Measure | Reset | Leakage, _) => { return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index ab16d8a88..e48ec4880 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -188,11 +188,18 @@ impl PPVM { let q0 = self.pop_u64()?; Ok(CircuitMessage::TwoQubit(q0, q1)) } - RX | RY | RZ | Depolarize | Loss | Leakage => { + RX | RY | RZ | Depolarize | Loss => { let theta = self.pop_f64()?; let q = self.pop_u64()?; Ok(CircuitMessage::QubitAndFloat(q, theta)) } + Leakage => { + // Push order: qubit, p0, p1. Pop reverse. + let p1 = self.pop_f64()?; + let p0 = self.pop_f64()?; + let q = self.pop_u64()?; + Ok(CircuitMessage::QubitAndTwoFloats(q, p0, p1)) + } RXX | RYY | RZZ | Depolarize2 => { let theta = self.pop_f64()?; let q1 = self.pop_u64()?; @@ -939,6 +946,15 @@ mod tests { machine.resolve_circuit(&CircuitInstruction::CorrelatedLoss)?, CircuitMessage::TwoQubitAndFloatArr3(2, 5, [0.1, 0.2, 0.3]) ); + + // Leakage: push q=2, p0, p1 — two floats, unlike Loss. + machine.cpu.stack_push(Value::U64(2)); + machine.cpu.stack_push(Value::F64(0.1)); + machine.cpu.stack_push(Value::F64(0.2)); + assert_eq!( + machine.resolve_circuit(&CircuitInstruction::Leakage)?, + CircuitMessage::QubitAndTwoFloats(2, 0.1, 0.2) + ); Ok(()) } diff --git a/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst b/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst new file mode 100644 index 000000000..1e69b1bae --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst @@ -0,0 +1,13 @@ +device circuit.n_qubits 1; +device circuit.backend paulisum; +device circuit.observable Z; + +// Leakage is GeneralizedTableau-only. PauliSum has no leakage_channel, +// so this program must fail at execute with a backend-rejection error. +fn @main() { + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.0 + cpu::cpu.const f64, 1.0 + circuit::circuit.leakage + cpu::cpu.ret 0 +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index adebf4c2c..fe9d451cb 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -82,6 +82,29 @@ fn dumped_rotxy_runs_and_flips_qubit() { assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); } +#[test] +fn tableau_leakage_pins_to_one_and_skips_x() { + // `tableau_leakage.sst` leaks q0 with (p0, p1) = (0, 1), pinning |1⟩, + // then applies X. A leaked qubit is frozen, so measure still returns 1. + let machine = ppvm_vihaco::run_file("tests/tableau_leakage.sst") + .unwrap_or_else(|e| panic!("run tableau_leakage.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::One], + "leaked q0 must measure the pinned 1, not LOST, and X must be a no-op" + ); +} + +#[test] +fn dumped_tableau_leakage_pins_to_one() { + let machine = dump_load_run("tests/tableau_leakage.sst", "ppvm_dump_tableau_leakage.ssb"); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1); + assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); +} + #[test] fn run_file_via_library_helper() { let machine = @@ -347,6 +370,22 @@ fn paulisum_measure_returns_unsupported_error() { ); } +#[test] +fn paulisum_leakage_returns_unsupported_error() { + // Leakage is tableau-only. PauliSum must reject it the same way it + // rejects Measure — not with a mismatched-argument fallback. + let mut machine = PPVM::default(); + machine + .load_file("tests/paulisum_leakage_error.sst") + .unwrap_or_else(|e| panic!("load paulisum_leakage_error.sst: {e:?}")); + let err = machine.run().unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("not supported on the PauliSum backend"), + "expected PauliSum-rejection error, got: {msg}" + ); +} + // ─── Task 16: Tableau-side Trace, cross-backend agreement ──────────────── #[test] diff --git a/crates/ppvm-vihaco/tests/tableau_leakage.sst b/crates/ppvm-vihaco/tests/tableau_leakage.sst new file mode 100644 index 000000000..cdb75f94e --- /dev/null +++ b/crates/ppvm-vihaco/tests/tableau_leakage.sst @@ -0,0 +1,20 @@ +device circuit.n_qubits 1; + +// Deterministic leakage: p0=0, p1=1 pins q0 to |1⟩ and marks it leaked. +// Stack order for `circuit.leakage`: qubit, then p0, then p1. +// A subsequent X is a no-op on a leaked qubit, so measurement still +// returns 1 (the pinned bit, not LOST). +fn @main() { + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.0 + cpu::cpu.const f64, 1.0 + circuit::circuit.leakage + + cpu::cpu.const u64, 0 + circuit::circuit.x + + cpu::cpu.const u64, 0 + circuit::circuit.measure + + cpu::cpu.ret 0 +} diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index aca506b64..1aefa52fc 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -184,6 +184,8 @@ mod tests { assert_eq!(parse("rxx"), RXX); assert_eq!(parse("depolarize2"), Depolarize2); assert_eq!(parse("depolarize"), Depolarize); + assert_eq!(parse("loss"), Loss); + assert_eq!(parse("leakage"), Leakage); } // ─── Parse: prefix-sensitive disambiguation ─────────────────────────── From 4e2ba2cb580a2b4e8596cf5406470aaba1ba92d1 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 15 Sep 2026 10:09:26 +0200 Subject: [PATCH 3/3] Add leakage to the CLI --- crates/ppvm-tui/src/app.rs | 1 + crates/ppvm-tui/src/command.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index d4a768a76..58f92c44a 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -471,6 +471,7 @@ Gates (q = qubit index; angles / probabilities are floats) u3 rxx ryy rzz depolarize loss

depolarize2

+ leakage pauli_error correlated_loss Line editing: ←/→ move · Home/End · Backspace/Del · ↑/↓ history"; diff --git a/crates/ppvm-tui/src/command.rs b/crates/ppvm-tui/src/command.rs index 75fc30000..d3b5732a7 100644 --- a/crates/ppvm-tui/src/command.rs +++ b/crates/ppvm-tui/src/command.rs @@ -50,6 +50,9 @@ pub fn gate_spec(name: &str) -> Option { "loss" => (Loss, 1, 1), "pauli_error" => (PauliError, 1, 3), "correlated_loss" => (CorrelatedLoss, 2, 3), + // Same shape as `r`: one qubit, two floats. Push order is q, p0, p1 + // (probabilities of leaking into pinned |0⟩ / |1⟩). + "leakage" => (Leakage, 1, 2), _ => return None, }; Some(GateSpec { @@ -208,6 +211,20 @@ mod tests { ); } + #[test] + fn leakage_parses_qubit_and_two_probs() { + assert_eq!( + parse_command("leakage 0 0.0 1.0").unwrap(), + Command::Gate { + inst: CircuitInstruction::Leakage, + qubits: vec![0], + params: vec![0.0, 1.0], + } + ); + assert!(parse_command("leakage 0 1.0").is_err()); + assert!(parse_command("leakage 0").is_err()); + } + #[test] fn meta_commands() { assert_eq!(parse_command(":q").unwrap(), Command::Quit);