Skip to main content

quantrs2_core/batch/
operations.rs

1//! Batch operations for quantum gates using SciRS2 parallel algorithms
2
3use super::{BatchGateOp, BatchStateVector};
4use crate::{
5    error::{QuantRS2Error, QuantRS2Result},
6    gate::{single::*, GateOp},
7    qubit::QubitId,
8};
9use scirs2_core::ndarray::{s, Array1, Array2, Array3, Axis};
10use scirs2_core::Complex64;
11// use scirs2_core::parallel_ops::*;
12use crate::parallel_ops_stubs::*;
13// use scirs2_core::simd_ops::SimdUnifiedOps;
14use crate::simd_ops_stubs::{SimdComplex64, SimdF64};
15
16/// Apply a single-qubit gate to all states in a batch
17pub fn apply_single_qubit_gate_batch(
18    batch: &mut BatchStateVector,
19    gate_matrix: &[Complex64; 4],
20    target: QubitId,
21) -> QuantRS2Result<()> {
22    let n_qubits = batch.n_qubits;
23    let target_idx = target.0 as usize;
24
25    if target_idx >= n_qubits {
26        return Err(QuantRS2Error::InvalidQubitId(target.0));
27    }
28
29    let batch_size = batch.batch_size();
30    // let _state_size = 1 << n_qubits;
31
32    // Use optimized SIMD batch processing for large batches
33    if batch_size > 32 {
34        apply_single_qubit_batch_simd(batch, gate_matrix, target_idx, n_qubits)?;
35    } else if batch_size > 16 {
36        // Use parallel processing for medium batches
37        batch
38            .states
39            .axis_iter_mut(Axis(0))
40            .into_par_iter()
41            .try_for_each(|mut state_row| -> QuantRS2Result<()> {
42                let mut state = state_row.to_owned();
43                apply_single_qubit_to_state_optimized(
44                    &mut state,
45                    gate_matrix,
46                    target_idx,
47                    n_qubits,
48                )?;
49                state_row.assign(&state);
50                Ok(())
51            })?;
52    } else {
53        // Sequential for small batches
54        for i in 0..batch_size {
55            let mut state = batch.states.row(i).to_owned();
56            apply_single_qubit_to_state_optimized(&mut state, gate_matrix, target_idx, n_qubits)?;
57            batch.states.row_mut(i).assign(&state);
58        }
59    }
60
61    Ok(())
62}
63
64/// Apply a two-qubit gate to all states in a batch
65pub fn apply_two_qubit_gate_batch(
66    batch: &mut BatchStateVector,
67    gate_matrix: &[Complex64; 16],
68    control: QubitId,
69    target: QubitId,
70) -> QuantRS2Result<()> {
71    let n_qubits = batch.n_qubits;
72    let control_idx = control.0 as usize;
73    let target_idx = target.0 as usize;
74
75    if control_idx >= n_qubits || target_idx >= n_qubits {
76        return Err(QuantRS2Error::InvalidQubitId(if control_idx >= n_qubits {
77            control.0
78        } else {
79            target.0
80        }));
81    }
82
83    if control_idx == target_idx {
84        return Err(QuantRS2Error::InvalidInput(
85            "Control and target qubits must be different".to_string(),
86        ));
87    }
88
89    let batch_size = batch.batch_size();
90
91    // Use parallel processing for large batches
92    if batch_size > 16 {
93        batch
94            .states
95            .axis_iter_mut(Axis(0))
96            .into_par_iter()
97            .try_for_each(|mut state_row| -> QuantRS2Result<()> {
98                let mut state = state_row.to_owned();
99                apply_two_qubit_to_state(
100                    &mut state,
101                    gate_matrix,
102                    control_idx,
103                    target_idx,
104                    n_qubits,
105                )?;
106                state_row.assign(&state);
107                Ok(())
108            })?;
109    } else {
110        // Sequential for small batches
111        for i in 0..batch_size {
112            let mut state = batch.states.row(i).to_owned();
113            apply_two_qubit_to_state(&mut state, gate_matrix, control_idx, target_idx, n_qubits)?;
114            batch.states.row_mut(i).assign(&state);
115        }
116    }
117
118    Ok(())
119}
120
121/// Apply a single-qubit gate to a state vector (optimized version)
122fn apply_single_qubit_to_state_optimized(
123    state: &mut Array1<Complex64>,
124    gate_matrix: &[Complex64; 4],
125    target_idx: usize,
126    n_qubits: usize,
127) -> QuantRS2Result<()> {
128    let state_size = 1 << n_qubits;
129    let target_mask = 1 << target_idx;
130
131    for i in 0..state_size {
132        if i & target_mask == 0 {
133            let j = i | target_mask;
134
135            let a = state[i];
136            let b = state[j];
137
138            state[i] = gate_matrix[0] * a + gate_matrix[1] * b;
139            state[j] = gate_matrix[2] * a + gate_matrix[3] * b;
140        }
141    }
142
143    Ok(())
144}
145
146/// SIMD-optimized batch single-qubit gate application
147fn apply_single_qubit_batch_simd(
148    batch: &mut BatchStateVector,
149    gate_matrix: &[Complex64; 4],
150    target_idx: usize,
151    n_qubits: usize,
152) -> QuantRS2Result<()> {
153    // use scirs2_core::simd_ops::SimdUnifiedOps;
154    use scirs2_core::ndarray::ArrayView1;
155
156    let batch_size = batch.batch_size();
157    let state_size = 1 << n_qubits;
158    let target_mask = 1 << target_idx;
159
160    // Extract gate matrix components
161    let g00 = gate_matrix[0];
162    let g01 = gate_matrix[1];
163    let g10 = gate_matrix[2];
164    let g11 = gate_matrix[3];
165
166    // Process using scirs2_core SIMD operations
167    // We'll process multiple batch items simultaneously using SIMD
168    // Collect all pairs of amplitudes that need to be transformed
169    let _pairs_per_batch = state_size / 2;
170    let total_pairs = batch_size * _pairs_per_batch;
171
172    // For simpler implementation, process each batch item individually
173    // but use SIMD within each batch item
174    for batch_idx in 0..batch_size {
175        // Collect indices and values for SIMD processing
176        let mut idx_pairs = Vec::new();
177        let mut a_values = Vec::new();
178        let mut b_values = Vec::new();
179
180        for i in 0..state_size {
181            if i & target_mask == 0 {
182                let j = i | target_mask;
183                idx_pairs.push((i, j));
184                a_values.push(batch.states[[batch_idx, i]]);
185                b_values.push(batch.states[[batch_idx, j]]);
186            }
187        }
188
189        if idx_pairs.is_empty() {
190            continue;
191        }
192
193        // Apply gate transformation using SIMD
194        // new_a = g00 * a + g01 * b
195        // new_b = g10 * a + g11 * b
196
197        // Extract real and imaginary parts
198        let _len = a_values.len();
199        let a_real: Vec<f64> = a_values.iter().map(|c| c.re).collect();
200        let a_imag: Vec<f64> = a_values.iter().map(|c| c.im).collect();
201        let b_real: Vec<f64> = b_values.iter().map(|c| c.re).collect();
202        let b_imag: Vec<f64> = b_values.iter().map(|c| c.im).collect();
203
204        // Compute new_a using SIMD
205        let a_real_view = ArrayView1::from(&a_real);
206        let a_imag_view = ArrayView1::from(&a_imag);
207        let b_real_view = ArrayView1::from(&b_real);
208        let b_imag_view = ArrayView1::from(&b_imag);
209
210        // new_a_real = g00.re * a.re - g00.im * a.im + g01.re * b.re - g01.im * b.im
211        let term1 = <f64 as SimdF64>::simd_scalar_mul(&a_real_view, g00.re);
212        let term2 = <f64 as SimdF64>::simd_scalar_mul(&a_imag_view, g00.im);
213        let term3 = <f64 as SimdF64>::simd_scalar_mul(&b_real_view, g01.re);
214        let term4 = <f64 as SimdF64>::simd_scalar_mul(&b_imag_view, g01.im);
215
216        let temp1 = <f64 as SimdF64>::simd_sub_arrays(&term1.view(), &term2.view());
217        let temp2 = <f64 as SimdF64>::simd_sub_arrays(&term3.view(), &term4.view());
218        let new_a_real = <f64 as SimdF64>::simd_add_arrays(&temp1.view(), &temp2.view());
219
220        // new_a_imag = g00.re * a.im + g00.im * a.re + g01.re * b.im + g01.im * b.re
221        let term5 = <f64 as SimdF64>::simd_scalar_mul(&a_imag_view, g00.re);
222        let term6 = <f64 as SimdF64>::simd_scalar_mul(&a_real_view, g00.im);
223        let term7 = <f64 as SimdF64>::simd_scalar_mul(&b_imag_view, g01.re);
224        let term8 = <f64 as SimdF64>::simd_scalar_mul(&b_real_view, g01.im);
225
226        let temp3 = <f64 as SimdF64>::simd_add_arrays(&term5.view(), &term6.view());
227        let temp4 = <f64 as SimdF64>::simd_add_arrays(&term7.view(), &term8.view());
228        let new_a_imag = <f64 as SimdF64>::simd_add_arrays(&temp3.view(), &temp4.view());
229
230        // Compute new_b using SIMD (similar process)
231        let term9 = <f64 as SimdF64>::simd_scalar_mul(&a_real_view, g10.re);
232        let term10 = <f64 as SimdF64>::simd_scalar_mul(&a_imag_view, g10.im);
233        let term11 = <f64 as SimdF64>::simd_scalar_mul(&b_real_view, g11.re);
234        let term12 = <f64 as SimdF64>::simd_scalar_mul(&b_imag_view, g11.im);
235
236        let temp5 = <f64 as SimdF64>::simd_sub_arrays(&term9.view(), &term10.view());
237        let temp6 = <f64 as SimdF64>::simd_sub_arrays(&term11.view(), &term12.view());
238        let new_b_real = <f64 as SimdF64>::simd_add_arrays(&temp5.view(), &temp6.view());
239
240        let term13 = <f64 as SimdF64>::simd_scalar_mul(&a_imag_view, g10.re);
241        let term14 = <f64 as SimdF64>::simd_scalar_mul(&a_real_view, g10.im);
242        let term15 = <f64 as SimdF64>::simd_scalar_mul(&b_imag_view, g11.re);
243        let term16 = <f64 as SimdF64>::simd_scalar_mul(&b_real_view, g11.im);
244
245        let temp7 = <f64 as SimdF64>::simd_add_arrays(&term13.view(), &term14.view());
246        let temp8 = <f64 as SimdF64>::simd_add_arrays(&term15.view(), &term16.view());
247        let new_b_imag = <f64 as SimdF64>::simd_add_arrays(&temp7.view(), &temp8.view());
248
249        // Write back results
250        for (idx, &(i, j)) in idx_pairs.iter().enumerate() {
251            batch.states[[batch_idx, i]] = Complex64::new(new_a_real[idx], new_a_imag[idx]);
252            batch.states[[batch_idx, j]] = Complex64::new(new_b_real[idx], new_b_imag[idx]);
253        }
254    }
255
256    Ok(())
257}
258
259/// Apply a two-qubit gate to a state vector
260fn apply_two_qubit_to_state(
261    state: &mut Array1<Complex64>,
262    gate_matrix: &[Complex64; 16],
263    control_idx: usize,
264    target_idx: usize,
265    n_qubits: usize,
266) -> QuantRS2Result<()> {
267    let state_size = 1 << n_qubits;
268    let control_mask = 1 << control_idx;
269    let target_mask = 1 << target_idx;
270
271    for i in 0..state_size {
272        if (i & control_mask == 0) && (i & target_mask == 0) {
273            let i00 = i;
274            let i01 = i | target_mask;
275            let i10 = i | control_mask;
276            let i11 = i | control_mask | target_mask;
277
278            let a00 = state[i00];
279            let a01 = state[i01];
280            let a10 = state[i10];
281            let a11 = state[i11];
282
283            state[i00] = gate_matrix[0] * a00
284                + gate_matrix[1] * a01
285                + gate_matrix[2] * a10
286                + gate_matrix[3] * a11;
287            state[i01] = gate_matrix[4] * a00
288                + gate_matrix[5] * a01
289                + gate_matrix[6] * a10
290                + gate_matrix[7] * a11;
291            state[i10] = gate_matrix[8] * a00
292                + gate_matrix[9] * a01
293                + gate_matrix[10] * a10
294                + gate_matrix[11] * a11;
295            state[i11] = gate_matrix[12] * a00
296                + gate_matrix[13] * a01
297                + gate_matrix[14] * a10
298                + gate_matrix[15] * a11;
299        }
300    }
301
302    Ok(())
303}
304
305/// Batch-optimized Hadamard gate using SciRS2
306pub struct BatchHadamard;
307
308impl BatchGateOp for Hadamard {
309    fn apply_batch(
310        &self,
311        batch: &mut BatchStateVector,
312        target_qubits: &[QubitId],
313    ) -> QuantRS2Result<()> {
314        if target_qubits.len() != 1 {
315            return Err(QuantRS2Error::InvalidInput(
316                "Hadamard gate requires exactly one target qubit".to_string(),
317            ));
318        }
319
320        let gate_matrix = [
321            Complex64::new(1.0 / std::f64::consts::SQRT_2, 0.0),
322            Complex64::new(1.0 / std::f64::consts::SQRT_2, 0.0),
323            Complex64::new(1.0 / std::f64::consts::SQRT_2, 0.0),
324            Complex64::new(-1.0 / std::f64::consts::SQRT_2, 0.0),
325        ];
326
327        apply_single_qubit_gate_batch(batch, &gate_matrix, target_qubits[0])
328    }
329}
330
331/// Batch-optimized Pauli-X gate
332impl BatchGateOp for PauliX {
333    fn apply_batch(
334        &self,
335        batch: &mut BatchStateVector,
336        target_qubits: &[QubitId],
337    ) -> QuantRS2Result<()> {
338        if target_qubits.len() != 1 {
339            return Err(QuantRS2Error::InvalidInput(
340                "Pauli-X gate requires exactly one target qubit".to_string(),
341            ));
342        }
343
344        let gate_matrix = [
345            Complex64::new(0.0, 0.0),
346            Complex64::new(1.0, 0.0),
347            Complex64::new(1.0, 0.0),
348            Complex64::new(0.0, 0.0),
349        ];
350
351        apply_single_qubit_gate_batch(batch, &gate_matrix, target_qubits[0])
352    }
353}
354
355/// Returns `true` for single-qubit gates whose matrix is *constant*
356/// (non-parameterized). For these, [`GateOp::name`] uniquely determines the
357/// matrix, so it is safe to compile the matrix once and reuse it across every
358/// occurrence in a batch sequence.
359fn is_fixed_single_qubit_gate(name: &str) -> bool {
360    matches!(
361        name,
362        "H" | "X" | "Y" | "Z" | "S" | "T" | "S†" | "T†" | "I" | "√X" | "√X†"
363    )
364}
365
366/// Returns `true` for two-qubit gates whose matrix is constant
367/// (non-parameterized) — CNOT chains, CZ/SWAP layers, etc.
368fn is_fixed_two_qubit_gate(name: &str) -> bool {
369    matches!(
370        name,
371        "CNOT" | "CZ" | "CY" | "CH" | "CS" | "CSX" | "SWAP" | "iSWAP" | "DCX" | "ECR"
372    )
373}
374
375/// Compile a single-qubit gate's 2×2 matrix into a flat `[Complex64; 4]`.
376fn compile_single_qubit_matrix(gate: &dyn GateOp) -> QuantRS2Result<[Complex64; 4]> {
377    let matrix = gate.matrix()?;
378    if matrix.len() < 4 {
379        return Err(QuantRS2Error::InvalidInput(format!(
380            "gate '{}' produced a {}-element matrix; expected >= 4 for a single-qubit gate",
381            gate.name(),
382            matrix.len()
383        )));
384    }
385    let mut gate_array = [Complex64::new(0.0, 0.0); 4];
386    gate_array.copy_from_slice(&matrix[..4]);
387    Ok(gate_array)
388}
389
390/// Compile a two-qubit gate's 4×4 matrix into a flat `[Complex64; 16]`.
391fn compile_two_qubit_matrix(gate: &dyn GateOp) -> QuantRS2Result<[Complex64; 16]> {
392    let matrix = gate.matrix()?;
393    if matrix.len() < 16 {
394        return Err(QuantRS2Error::InvalidInput(format!(
395            "gate '{}' produced a {}-element matrix; expected >= 16 for a two-qubit gate",
396            gate.name(),
397            matrix.len()
398        )));
399    }
400    let mut gate_array = [Complex64::new(0.0, 0.0); 16];
401    gate_array.copy_from_slice(&matrix[..16]);
402    Ok(gate_array)
403}
404
405/// Apply multiple gates to a batch using SciRS2 batch operations.
406///
407/// Common fixed (non-parameterized) gates — Hadamard, Paulis, S/T, CNOT, CZ,
408/// SWAP, … — are *detected by name* (see [`is_fixed_single_qubit_gate`] /
409/// [`is_fixed_two_qubit_gate`]) and their compiled matrices are cached for the
410/// duration of the sequence. Repeated-gate patterns such as CNOT chains or
411/// Hadamard layers therefore compile their matrix exactly once instead of on
412/// every occurrence. Parameterized gates (RX/RY/RZ/U/…) are recompiled per call,
413/// because two instances sharing a name may carry different angles.
414pub fn apply_gate_sequence_batch(
415    batch: &mut BatchStateVector,
416    gates: &[(Box<dyn GateOp>, Vec<QubitId>)],
417) -> QuantRS2Result<()> {
418    use std::collections::HashMap;
419
420    // Per-sequence caches of compiled matrices, keyed by the (constant) gate name.
421    let mut single_cache: HashMap<&'static str, [Complex64; 4]> = HashMap::new();
422    let mut two_cache: HashMap<&'static str, [Complex64; 16]> = HashMap::new();
423
424    for (gate, qubits) in gates {
425        match qubits.len() {
426            1 => {
427                let name = gate.name();
428                let gate_array = if is_fixed_single_qubit_gate(name) {
429                    match single_cache.get(name) {
430                        Some(arr) => *arr,
431                        None => {
432                            let arr = compile_single_qubit_matrix(gate.as_ref())?;
433                            single_cache.insert(name, arr);
434                            arr
435                        }
436                    }
437                } else {
438                    compile_single_qubit_matrix(gate.as_ref())?
439                };
440                apply_single_qubit_gate_batch(batch, &gate_array, qubits[0])?;
441            }
442            2 => {
443                let name = gate.name();
444                let gate_array = if is_fixed_two_qubit_gate(name) {
445                    match two_cache.get(name) {
446                        Some(arr) => *arr,
447                        None => {
448                            let arr = compile_two_qubit_matrix(gate.as_ref())?;
449                            two_cache.insert(name, arr);
450                            arr
451                        }
452                    }
453                } else {
454                    compile_two_qubit_matrix(gate.as_ref())?
455                };
456                apply_two_qubit_gate_batch(batch, &gate_array, qubits[0], qubits[1])?;
457            }
458            _ => {
459                return Err(QuantRS2Error::InvalidInput(
460                    "Batch operations for gates with more than 2 qubits not yet supported"
461                        .to_string(),
462                ));
463            }
464        }
465    }
466
467    Ok(())
468}
469
470/// Batch matrix multiplication
471/// Note: SciRS2 batch_matmul doesn't support Complex numbers, so we implement our own
472pub fn batch_state_matrix_multiply(
473    batch: &BatchStateVector,
474    matrices: &Array3<Complex64>,
475) -> QuantRS2Result<BatchStateVector> {
476    let batch_size = batch.batch_size();
477    let (num_matrices, rows, cols) = matrices.dim();
478
479    if num_matrices != batch_size {
480        return Err(QuantRS2Error::InvalidInput(format!(
481            "Number of matrices {num_matrices} doesn't match batch size {batch_size}"
482        )));
483    }
484
485    if cols != batch.states.ncols() {
486        return Err(QuantRS2Error::InvalidInput(format!(
487            "Matrix columns {} don't match state size {}",
488            cols,
489            batch.states.ncols()
490        )));
491    }
492
493    // Perform batch matrix multiplication manually
494    let mut result_states = Array2::zeros((batch_size, rows));
495
496    // Use parallel processing for large batches
497    if batch_size > 16 {
498        // use scirs2_core::parallel_ops::*;
499        use crate::parallel_ops_stubs::*;
500
501        let results: Vec<_> = (0..batch_size)
502            .into_par_iter()
503            .map(|i| {
504                let matrix = matrices.slice(s![i, .., ..]);
505                let state = batch.states.row(i);
506                matrix.dot(&state)
507            })
508            .collect();
509
510        for (i, result) in results.into_iter().enumerate() {
511            result_states.row_mut(i).assign(&result);
512        }
513    } else {
514        // Sequential for small batches
515        for i in 0..batch_size {
516            let matrix = matrices.slice(s![i, .., ..]);
517            let state = batch.states.row(i);
518            let result = matrix.dot(&state);
519            result_states.row_mut(i).assign(&result);
520        }
521    }
522
523    BatchStateVector::from_states(result_states, batch.config.clone())
524}
525
526/// Parallel expectation value computation
527pub fn compute_expectation_values_batch(
528    batch: &BatchStateVector,
529    observable_matrix: &Array2<Complex64>,
530) -> QuantRS2Result<Vec<f64>> {
531    let batch_size = batch.batch_size();
532
533    // Use parallel computation for large batches
534    if batch_size > 16 {
535        let expectations: Vec<f64> = (0..batch_size)
536            .into_par_iter()
537            .map(|i| {
538                let state = batch.states.row(i);
539                compute_expectation_value(&state.to_owned(), observable_matrix)
540            })
541            .collect();
542
543        Ok(expectations)
544    } else {
545        // Sequential for small batches
546        let mut expectations = Vec::with_capacity(batch_size);
547        for i in 0..batch_size {
548            let state = batch.states.row(i);
549            expectations.push(compute_expectation_value(
550                &state.to_owned(),
551                observable_matrix,
552            ));
553        }
554        Ok(expectations)
555    }
556}
557
558/// Compute expectation value for a single state
559fn compute_expectation_value(state: &Array1<Complex64>, observable: &Array2<Complex64>) -> f64 {
560    // <ψ|O|ψ>
561    let temp = observable.dot(state);
562    let expectation = state
563        .iter()
564        .zip(temp.iter())
565        .map(|(a, b)| a.conj() * b)
566        .sum::<Complex64>();
567
568    expectation.re
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use scirs2_core::ndarray::array;
575
576    #[test]
577    fn test_batch_hadamard() {
578        let mut batch = BatchStateVector::new(3, 1, Default::default())
579            .expect("Failed to create batch state vector for Hadamard test");
580        let h = Hadamard { target: QubitId(0) };
581
582        h.apply_batch(&mut batch, &[QubitId(0)])
583            .expect("Failed to apply Hadamard gate to batch");
584
585        // Check all states are in superposition
586        for i in 0..3 {
587            let state = batch.get_state(i).expect("Failed to get state from batch");
588            assert!((state[0].re - 1.0 / std::f64::consts::SQRT_2).abs() < 1e-10);
589            assert!((state[1].re - 1.0 / std::f64::consts::SQRT_2).abs() < 1e-10);
590        }
591    }
592
593    #[test]
594    fn test_batch_pauli_x() {
595        let mut batch = BatchStateVector::new(2, 1, Default::default())
596            .expect("Failed to create batch state vector for Pauli X test");
597        let x = PauliX { target: QubitId(0) };
598
599        x.apply_batch(&mut batch, &[QubitId(0)])
600            .expect("Failed to apply Pauli X gate to batch");
601
602        // Check all states are flipped
603        for i in 0..2 {
604            let state = batch.get_state(i).expect("Failed to get state from batch");
605            assert_eq!(state[0], Complex64::new(0.0, 0.0));
606            assert_eq!(state[1], Complex64::new(1.0, 0.0));
607        }
608    }
609
610    #[test]
611    fn test_expectation_values_batch() {
612        let batch = BatchStateVector::new(5, 1, Default::default())
613            .expect("Failed to create batch state vector for expectation test");
614
615        // Pauli Z observable
616        let z_observable = array![
617            [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
618            [Complex64::new(0.0, 0.0), Complex64::new(-1.0, 0.0)]
619        ];
620
621        let expectations = compute_expectation_values_batch(&batch, &z_observable)
622            .expect("Failed to compute expectation values");
623
624        // All states are |0>, so expectation of Z should be 1
625        for exp in expectations {
626            assert!((exp - 1.0).abs() < 1e-10);
627        }
628    }
629
630    #[test]
631    fn test_apply_gate_sequence_batch_matches_manual() {
632        use crate::gate::multi::CNOT;
633
634        let make_batch = || {
635            BatchStateVector::new(4, 3, Default::default())
636                .expect("Failed to create batch state vector for sequence test")
637        };
638        let mut seq_batch = make_batch();
639        let mut ref_batch = make_batch();
640
641        // Exercises every dispatch path: a fixed single-qubit gate repeated
642        // (H — cache hit), a fixed two-qubit gate repeated (CNOT chain — cache
643        // hit on "CNOT"), and a parameterized gate (RotationZ — recompiled).
644        let gates: Vec<(Box<dyn GateOp>, Vec<QubitId>)> = vec![
645            (Box::new(Hadamard { target: QubitId(0) }), vec![QubitId(0)]),
646            (
647                Box::new(CNOT {
648                    control: QubitId(0),
649                    target: QubitId(1),
650                }),
651                vec![QubitId(0), QubitId(1)],
652            ),
653            (
654                Box::new(CNOT {
655                    control: QubitId(1),
656                    target: QubitId(2),
657                }),
658                vec![QubitId(1), QubitId(2)],
659            ),
660            (
661                Box::new(RotationZ {
662                    target: QubitId(2),
663                    theta: 0.7,
664                }),
665                vec![QubitId(2)],
666            ),
667            (Box::new(Hadamard { target: QubitId(0) }), vec![QubitId(0)]),
668        ];
669
670        // Path 1: optimized sequence application (name-detection + caching).
671        apply_gate_sequence_batch(&mut seq_batch, &gates)
672            .expect("optimized sequence application failed");
673
674        // Path 2: naive per-gate application via the low-level kernels.
675        for (gate, qubits) in &gates {
676            match qubits.len() {
677                1 => {
678                    let arr = compile_single_qubit_matrix(gate.as_ref())
679                        .expect("single-qubit matrix compile failed");
680                    apply_single_qubit_gate_batch(&mut ref_batch, &arr, qubits[0])
681                        .expect("single-qubit apply failed");
682                }
683                2 => {
684                    let arr = compile_two_qubit_matrix(gate.as_ref())
685                        .expect("two-qubit matrix compile failed");
686                    apply_two_qubit_gate_batch(&mut ref_batch, &arr, qubits[0], qubits[1])
687                        .expect("two-qubit apply failed");
688                }
689                _ => unreachable!(),
690            }
691        }
692
693        // The two paths must agree exactly.
694        for i in 0..seq_batch.batch_size() {
695            let a = seq_batch.get_state(i).expect("seq batch state");
696            let b = ref_batch.get_state(i).expect("ref batch state");
697            assert_eq!(a.len(), b.len());
698            for (x, y) in a.iter().zip(b.iter()) {
699                assert!(
700                    (x - y).norm() < 1e-12,
701                    "optimized dispatch diverged from manual application at state {i}"
702                );
703            }
704        }
705    }
706}