Skip to main content

quantrs2_symengine_pure/pattern/
mod.rs

1//! Pattern matching for quantum expressions.
2//!
3//! This module provides utilities for recognizing and extracting
4//! common patterns in quantum computing expressions.
5
6use std::collections::HashMap;
7
8use crate::error::{SymEngineError, SymEngineResult};
9use crate::expr::{ExprLang, Expression};
10
11/// A pattern that can match against expressions.
12#[derive(Clone, Debug)]
13pub enum Pattern {
14    /// Match any expression and capture it
15    Wildcard(String),
16    /// Match a specific constant
17    Constant(f64),
18    /// Match a specific symbol
19    Symbol(String),
20    /// Match zero
21    Zero,
22    /// Match one
23    One,
24    /// Match an addition pattern
25    Add(Box<Self>, Box<Self>),
26    /// Match a multiplication pattern
27    Mul(Box<Self>, Box<Self>),
28    /// Match a power pattern
29    Pow(Box<Self>, Box<Self>),
30    /// Match a negation pattern
31    Neg(Box<Self>),
32    /// Match a sine pattern
33    Sin(Box<Self>),
34    /// Match a cosine pattern
35    Cos(Box<Self>),
36    /// Match an exponential pattern
37    Exp(Box<Self>),
38    /// Match a logarithm pattern
39    Log(Box<Self>),
40    /// Match a commutator pattern
41    Commutator(Box<Self>, Box<Self>),
42    /// Match an anticommutator pattern
43    Anticommutator(Box<Self>, Box<Self>),
44    /// Match a tensor product pattern
45    TensorProduct(Box<Self>, Box<Self>),
46    /// Match a dagger pattern
47    Dagger(Box<Self>),
48}
49
50#[allow(clippy::should_implement_trait)]
51impl Pattern {
52    /// Create a wildcard pattern with the given name
53    #[must_use]
54    pub fn wildcard(name: &str) -> Self {
55        Self::Wildcard(name.to_string())
56    }
57
58    /// Create a symbol pattern
59    #[must_use]
60    pub fn symbol(name: &str) -> Self {
61        Self::Symbol(name.to_string())
62    }
63
64    /// Create a constant pattern
65    #[must_use]
66    pub const fn constant(value: f64) -> Self {
67        Self::Constant(value)
68    }
69
70    /// Create an addition pattern
71    #[must_use]
72    pub fn add(left: Self, right: Self) -> Self {
73        Self::Add(Box::new(left), Box::new(right))
74    }
75
76    /// Create a multiplication pattern
77    #[must_use]
78    pub fn mul(left: Self, right: Self) -> Self {
79        Self::Mul(Box::new(left), Box::new(right))
80    }
81
82    /// Create a power pattern
83    #[must_use]
84    pub fn pow(base: Self, exp: Self) -> Self {
85        Self::Pow(Box::new(base), Box::new(exp))
86    }
87
88    /// Create a sine pattern
89    #[must_use]
90    pub fn sin(arg: Self) -> Self {
91        Self::Sin(Box::new(arg))
92    }
93
94    /// Create a cosine pattern
95    #[must_use]
96    pub fn cos(arg: Self) -> Self {
97        Self::Cos(Box::new(arg))
98    }
99
100    /// Create a commutator pattern [A, B]
101    #[must_use]
102    pub fn commutator(a: Self, b: Self) -> Self {
103        Self::Commutator(Box::new(a), Box::new(b))
104    }
105
106    /// Create an anticommutator pattern {A, B}
107    #[must_use]
108    pub fn anticommutator(a: Self, b: Self) -> Self {
109        Self::Anticommutator(Box::new(a), Box::new(b))
110    }
111
112    /// Create a tensor product pattern A ⊗ B
113    #[must_use]
114    pub fn tensor(a: Self, b: Self) -> Self {
115        Self::TensorProduct(Box::new(a), Box::new(b))
116    }
117
118    /// Create a dagger pattern A†
119    #[must_use]
120    pub fn dagger(a: Self) -> Self {
121        Self::Dagger(Box::new(a))
122    }
123}
124
125/// Result of pattern matching - captured expressions
126pub type Captures = HashMap<String, Expression>;
127
128/// Match a pattern against an expression
129pub fn match_pattern(pattern: &Pattern, expr: &Expression) -> Option<Captures> {
130    let mut captures = Captures::new();
131    if match_pattern_rec(pattern, expr, &mut captures) {
132        Some(captures)
133    } else {
134        None
135    }
136}
137
138/// Recursive pattern matching helper
139#[allow(clippy::option_if_let_else)]
140fn match_pattern_rec(pattern: &Pattern, expr: &Expression, captures: &mut Captures) -> bool {
141    match pattern {
142        Pattern::Wildcard(name) => {
143            // Check if already captured with different value
144            if let Some(existing) = captures.get(name) {
145                // Must match the same expression
146                existing == expr
147            } else {
148                captures.insert(name.clone(), expr.clone());
149                true
150            }
151        }
152
153        Pattern::Constant(value) => {
154            if let Some(v) = expr.to_f64() {
155                (v - value).abs() < 1e-15
156            } else {
157                false
158            }
159        }
160
161        Pattern::Symbol(name) => expr.as_symbol() == Some(name.as_str()),
162
163        Pattern::Zero => expr.is_zero(),
164
165        Pattern::One => expr.is_one(),
166
167        // For compound patterns, we need to access the internal structure
168        // This requires parsing the expression representation
169        // For now, use string-based matching as a simple implementation
170        _ => match_compound_pattern(pattern, expr, captures),
171    }
172}
173
174/// Match a compound pattern against the real AST structure of an expression.
175///
176/// Each branch extracts the operands of the corresponding `ExprLang` node via the
177/// structural accessors in [`crate::expr`] and recurses. Matching is performed on
178/// the actual `RecExpr` tree (not its textual rendering), so it is exact and works
179/// for arbitrarily nested expressions.
180fn match_compound_pattern(pattern: &Pattern, expr: &Expression, captures: &mut Captures) -> bool {
181    match pattern {
182        Pattern::Neg(inner) => match_unary(inner, expr, "neg", captures),
183        Pattern::Sin(inner) => match_unary(inner, expr, "sin", captures),
184        Pattern::Cos(inner) => match_unary(inner, expr, "cos", captures),
185        Pattern::Exp(inner) => match_unary(inner, expr, "exp", captures),
186        Pattern::Log(inner) => match_unary(inner, expr, "log", captures),
187        Pattern::Dagger(inner) => match_unary(inner, expr, "dagger", captures),
188
189        Pattern::Add(left, right) => match_binary(left, right, expr, "+", captures),
190        Pattern::Mul(left, right) => match_binary(left, right, expr, "*", captures),
191        Pattern::Pow(base, exp) => match_binary(base, exp, expr, "^", captures),
192        Pattern::Commutator(a, b) => match_binary(a, b, expr, "comm", captures),
193        Pattern::Anticommutator(a, b) => match_binary(a, b, expr, "anticomm", captures),
194        Pattern::TensorProduct(a, b) => match_binary(a, b, expr, "tensor", captures),
195
196        // These are handled in the main match
197        Pattern::Wildcard(_)
198        | Pattern::Constant(_)
199        | Pattern::Symbol(_)
200        | Pattern::Zero
201        | Pattern::One => unreachable!(),
202    }
203}
204
205/// Match a unary pattern: extract the operand of `op` and recurse on `inner`.
206fn match_unary(inner: &Pattern, expr: &Expression, op: &str, captures: &mut Captures) -> bool {
207    extract_unary_arg(expr, op).is_some_and(|arg| match_pattern_rec(inner, &arg, captures))
208}
209
210/// Match a binary pattern: extract both operands of `op` and recurse on each.
211fn match_binary(
212    left: &Pattern,
213    right: &Pattern,
214    expr: &Expression,
215    op: &str,
216    captures: &mut Captures,
217) -> bool {
218    extract_binary_args(expr, op).is_some_and(|(l, r)| {
219        match_pattern_rec(left, &l, captures) && match_pattern_rec(right, &r, captures)
220    })
221}
222
223/// Extract the operand of a unary node whose operator is `op`.
224///
225/// Returns `None` when the expression's root node is not that unary operator.
226fn extract_unary_arg(expr: &Expression, op: &str) -> Option<Expression> {
227    expr.unary_arg(op)
228}
229
230/// Extract both operands of a binary node whose operator is `op`.
231///
232/// Returns `None` when the expression's root node is not that binary operator.
233fn extract_binary_args(expr: &Expression, op: &str) -> Option<(Expression, Expression)> {
234    expr.binary_args(op)
235}
236
237// =========================================================================
238// Common Quantum Pattern Recognizers
239// =========================================================================
240
241/// Check if an expression is a rotation gate form: `exp(-i * θ * G / 2)`.
242///
243/// Returns `Some((angle, generator))` when the expression is structurally a
244/// rotation gate, where `angle` is the rotation angle `θ` (the `1/2` factor and
245/// the imaginary unit are stripped out) and `generator` is the Hermitian
246/// generator `G`. Returns `None` for any expression that is not of this form.
247///
248/// The recognizer operates on the real expression AST: it requires an `exp`
249/// node whose argument, after removing an outer negation, is a product
250/// containing the imaginary unit `I`. The remaining factors are split into the
251/// numeric/symbolic angle (multiplied by 2 to undo the conventional `/2`) and
252/// the generator (the factor that is recognised as a Hermitian operator). A
253/// genuine `None` here means "not a recognizable rotation form", which is a
254/// correct negative rather than a placeholder.
255#[must_use]
256pub fn is_rotation_gate(expr: &Expression) -> Option<(Expression, Expression)> {
257    // Must be exp(arg).
258    let arg = expr.unary_arg("exp")?;
259
260    // exp(-i θ G / 2): the conventional sign is negative, but accept either so
261    // that exp(i θ G / 2) (negative rotation) is also recognised.
262    let inner = arg.unary_arg("neg").unwrap_or(arg);
263
264    // Flatten the product into its factors.
265    let factors = flatten_factors(&inner);
266
267    // A rotation generator times an imaginary unit needs at least `I` and `G`.
268    let mut has_imaginary = false;
269    let mut generator: Option<Expression> = None;
270    let mut angle_factors: Vec<Expression> = Vec::with_capacity(factors.len());
271
272    for factor in factors {
273        if factor.as_symbol() == Some("I") {
274            has_imaginary = true;
275        } else if generator.is_none() && is_hermitian_form(&factor) && !factor.is_number() {
276            // The first non-numeric Hermitian factor is taken as the generator.
277            generator = Some(factor);
278        } else {
279            angle_factors.push(factor);
280        }
281    }
282
283    if !has_imaginary {
284        return None;
285    }
286    let generator = generator?;
287
288    // Reassemble the angle from the remaining factors and undo the `/2` so the
289    // returned angle is the physical rotation angle θ.
290    let angle = match angle_factors.split_first() {
291        Some((first, rest)) => {
292            let mut acc = first.clone();
293            for f in rest {
294                acc = acc * f.clone();
295            }
296            acc * Expression::int(2)
297        }
298        None => Expression::int(2),
299    };
300
301    Some((angle, generator))
302}
303
304/// Flatten a (possibly nested) product into a flat list of factors.
305///
306/// Division `a / b` contributes `a` and `inv(b)` is not expanded here; only
307/// multiplication nodes are descended into. Non-product expressions yield a
308/// single-element list.
309fn flatten_factors(expr: &Expression) -> Vec<Expression> {
310    if let Some((left, right)) = expr.binary_args("*") {
311        let mut factors = flatten_factors(&left);
312        factors.extend(flatten_factors(&right));
313        factors
314    } else {
315        vec![expr.clone()]
316    }
317}
318
319/// Check if an expression represents a Hermitian operator (A = A†)
320pub fn is_hermitian_form(expr: &Expression) -> bool {
321    // Simple check: if it's a symbol, it could be Hermitian
322    // Real numbers are Hermitian
323    if expr.is_number() {
324        return true;
325    }
326    // Pauli matrices are Hermitian
327    expr.as_symbol().is_some_and(|sym| {
328        matches!(
329            sym,
330            "sigma_x" | "sigma_y" | "sigma_z" | "X" | "Y" | "Z" | "I"
331        )
332    })
333}
334
335/// Check if an expression is a projector (P² = P).
336///
337/// The expression language has no outer-product / ket-bra (`|ψ⟩⟨ψ|`) node, so a
338/// projector cannot be represented syntactically in this AST. This therefore
339/// always returns `false`: it is an honest "cannot be a projector in this
340/// representation" rather than a heuristic guess. Projector recognition would
341/// require extending [`crate::expr::ExprLang`] with bra/ket constructs.
342#[must_use]
343pub const fn is_projector_form(_expr: &Expression) -> bool {
344    false
345}
346
347/// Recognized unary AST operator names (mirrors the tokens accepted by
348/// [`Expression::unary_arg`]).
349const UNARY_OPS: &[&str] = &[
350    "neg",
351    "inv",
352    "abs",
353    "sin",
354    "cos",
355    "tan",
356    "exp",
357    "log",
358    "sqrt",
359    "asin",
360    "acos",
361    "atan",
362    "sinh",
363    "cosh",
364    "tanh",
365    "re",
366    "im",
367    "conj",
368    "trace",
369    "dagger",
370    "det",
371    "transpose",
372];
373
374/// Recognized binary AST operator names (mirrors the tokens accepted by
375/// [`Expression::binary_args`]).
376const BINARY_OPS: &[&str] = &["+", "*", "/", "^", "comm", "anticomm", "tensor"];
377
378/// Check whether `expr` contains the symbol `name` anywhere in its AST,
379/// however deeply nested (inside sums, products, powers, trig/exp/log
380/// wrappers, commutators, tensor products, ...).
381///
382/// This performs a full structural traversal via the `unary_arg`/`binary_args`
383/// accessors rather than using [`Expression::free_symbols`], which
384/// deliberately excludes the special constant `I` from its result (it treats
385/// `I` like `pi`/`e`, not like a free variable).
386fn contains_symbol(expr: &Expression, name: &str) -> bool {
387    if expr.as_symbol() == Some(name) {
388        return true;
389    }
390    for op in UNARY_OPS {
391        if let Some(inner) = expr.unary_arg(op) {
392            return contains_symbol(&inner, name);
393        }
394    }
395    for op in BINARY_OPS {
396        if let Some((left, right)) = expr.binary_args(op) {
397            return contains_symbol(&left, name) || contains_symbol(&right, name);
398        }
399    }
400    false
401}
402
403/// Check whether a flattened list of multiplication factors decomposes as
404/// exactly one occurrence of the imaginary unit `I` times factors that are
405/// all real, i.e. none of them contain a nested occurrence of `I` (which
406/// would make the product genuinely complex rather than `i * real`).
407fn is_imaginary_times_real(factors: &[Expression]) -> bool {
408    let mut has_imaginary = false;
409    for factor in factors {
410        if factor.as_symbol() == Some("I") {
411            if has_imaginary {
412                // A second bare `I` factor (`I * I * rest`) collapses to a
413                // real value, so this is no longer "a single imaginary unit
414                // times a real factor".
415                return false;
416            }
417            has_imaginary = true;
418        } else if contains_symbol(factor, "I") {
419            return false;
420        }
421    }
422    has_imaginary
423}
424
425/// Check if an expression is a pure imaginary number, i.e. structurally
426/// `I * real` (in any factor order, and optionally negated as a whole),
427/// where `real` contains no nested occurrence of `I`.
428///
429/// This walks the real `RecExpr` structure (via [`flatten_factors`] and
430/// [`contains_symbol`]) rather than matching substrings of
431/// `expr.to_string()` (the previous implementation), which produced false
432/// positives on compound expressions such as `x*I + y`: that expression's
433/// string form `"(+ (* x I) y)"` contains the substring `"(* x I)"` even
434/// though the *outer* expression is a sum, not a pure imaginary number.
435#[must_use]
436pub fn is_pure_imaginary(expr: &Expression) -> bool {
437    let stripped = expr.unary_arg("neg").unwrap_or_else(|| expr.clone());
438
439    // The bare imaginary unit is trivially pure imaginary (real part = 1).
440    if stripped.as_symbol() == Some("I") {
441        return true;
442    }
443
444    // Anything that is not (structurally) a product node after stripping an
445    // outer negation cannot be `I * real` in this representation - notably
446    // an `Add` node such as `x*I + y` is rejected here rather than
447    // accidentally matching via substring search.
448    if stripped.binary_args("*").is_none() {
449        return false;
450    }
451
452    is_imaginary_times_real(&flatten_factors(&stripped))
453}
454
455/// Check if an expression is a unit-modulus complex exponential.
456///
457/// Structurally `exp(I * real)` (in any factor order, and with the whole
458/// exponent optionally negated), where `real` contains no nested occurrence
459/// of `I`. Such a "phase factor" `e^{iθ}` always has `|e^{iθ}| = 1` for real
460/// `θ`.
461///
462/// This walks the real `RecExpr` structure rather than matching a string
463/// prefix (the previous implementation), which both false-positived on
464/// `exp(I * (a + I*b))` (a genuinely complex, not real, angle - modulus != 1
465/// in general) and false-negatived on the commuted form `exp(θ * I)`.
466#[must_use]
467pub fn is_unit_complex_form(expr: &Expression) -> bool {
468    let Some(arg) = expr.unary_arg("exp") else {
469        return false;
470    };
471    let inner = arg.unary_arg("neg").unwrap_or(arg);
472
473    // A bare `exp(I)` (angle = 1) is still a unit-modulus phase factor.
474    if inner.as_symbol() == Some("I") {
475        return true;
476    }
477
478    if inner.binary_args("*").is_none() {
479        return false;
480    }
481
482    is_imaginary_times_real(&flatten_factors(&inner))
483}
484
485/// Recognize common quantum gate patterns
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum QuantumGatePattern {
488    /// Pauli X gate
489    PauliX,
490    /// Pauli Y gate
491    PauliY,
492    /// Pauli Z gate
493    PauliZ,
494    /// Hadamard gate
495    Hadamard,
496    /// S gate (phase gate)
497    SGate,
498    /// T gate
499    TGate,
500    /// Rx rotation with angle
501    Rx(Expression),
502    /// Ry rotation with angle
503    Ry(Expression),
504    /// Rz rotation with angle
505    Rz(Expression),
506    /// General rotation
507    Rotation(Expression, Expression, Expression), // θ, φ, λ
508    /// Unknown gate
509    Unknown,
510}
511
512/// Try to recognize a quantum gate from its matrix/generator expression.
513///
514/// Bare Pauli/Clifford symbols (`X`, `H`, `S`, ...) are recognized directly.
515/// Compound `exp(-i θ G / 2)` forms are recognized structurally via
516/// [`is_rotation_gate`]: when the generator `G` is (up to naming) one of the
517/// single-qubit Pauli operators, the angle is reported through the
518/// corresponding [`QuantumGatePattern::Rx`]/[`Ry`](QuantumGatePattern::Ry)/
519/// [`Rz`](QuantumGatePattern::Rz) variant; any other recognized rotation
520/// generator is reported as a general [`QuantumGatePattern::Rotation`] with
521/// the angle in the `θ` slot and `φ = λ = 0` (this expression language only
522/// carries a single rotation angle per generator, so the Euler `φ`/`λ`
523/// decomposition is not recoverable from a single `exp(...)` node).
524#[must_use]
525pub fn recognize_gate_pattern(expr: &Expression) -> QuantumGatePattern {
526    if let Some(sym) = expr.as_symbol() {
527        match sym {
528            "X" | "sigma_x" | "pauli_x" => return QuantumGatePattern::PauliX,
529            "Y" | "sigma_y" | "pauli_y" => return QuantumGatePattern::PauliY,
530            "Z" | "sigma_z" | "pauli_z" => return QuantumGatePattern::PauliZ,
531            "H" | "hadamard" => return QuantumGatePattern::Hadamard,
532            "S" | "s_gate" => return QuantumGatePattern::SGate,
533            "T" | "t_gate" => return QuantumGatePattern::TGate,
534            _ => {}
535        }
536    }
537
538    if let Some((angle, generator)) = is_rotation_gate(expr) {
539        return match generator.as_symbol() {
540            Some("X" | "sigma_x" | "pauli_x") => QuantumGatePattern::Rx(angle),
541            Some("Y" | "sigma_y" | "pauli_y") => QuantumGatePattern::Ry(angle),
542            Some("Z" | "sigma_z" | "pauli_z") => QuantumGatePattern::Rz(angle),
543            _ => QuantumGatePattern::Rotation(angle, Expression::zero(), Expression::zero()),
544        };
545    }
546
547    QuantumGatePattern::Unknown
548}
549
550/// Recognize variational quantum circuit parameter patterns.
551///
552/// [`VariationalPattern::SingleRotation`], [`VariationalPattern::QaoaMixer`]
553/// and [`VariationalPattern::QaoaCost`] are constructible from a single gate
554/// expression and are produced by [`recognize_variational_pattern`].
555///
556/// [`VariationalPattern::EntanglingLayer`] and
557/// [`VariationalPattern::VqeAnsatz`] are **forward-declared scaffolding for a
558/// future circuit-level pattern API**: recognizing an entangling layer or a
559/// full VQE ansatz genuinely requires looking at a *sequence* of gates (e.g.
560/// the CNOT/CZ ladder plus the per-qubit rotations that make up one ansatz
561/// layer), which cannot be read off a single [`Expression`]. No function in
562/// this crate constructs these two variants today; they exist purely as
563/// planned enum shape for when a multi-gate (`&[Expression]`-based)
564/// recognizer is added.
565#[derive(Debug, Clone)]
566pub enum VariationalPattern {
567    /// Single parameter rotation
568    SingleRotation {
569        axis: char, // 'x', 'y', or 'z'
570        param: Expression,
571    },
572    /// Parametric entangling layer.
573    ///
574    /// Not yet constructible: see the enum-level doc comment.
575    EntanglingLayer { params: Vec<Expression> },
576    /// VQE ansatz pattern.
577    ///
578    /// Not yet constructible: see the enum-level doc comment.
579    VqeAnsatz { params: Vec<Expression> },
580    /// QAOA mixer pattern
581    QaoaMixer { beta: Expression },
582    /// QAOA cost pattern
583    QaoaCost { gamma: Expression },
584}
585
586/// Check if expression matches a VQE parameter pattern
587#[must_use]
588pub fn is_vqe_parameter(expr: &Expression) -> bool {
589    expr.as_symbol().is_some_and(|sym| {
590        sym.starts_with("theta") || sym.starts_with("phi") || sym.starts_with("lambda")
591    })
592}
593
594/// Check if expression matches a QAOA parameter
595#[must_use]
596pub fn is_qaoa_parameter(expr: &Expression) -> bool {
597    expr.as_symbol()
598        .is_some_and(|sym| sym.starts_with("beta") || sym.starts_with("gamma"))
599}
600
601/// Try to recognize a single-gate variational-circuit pattern.
602///
603/// Structurally recognizes `exp(-i θ G / 2)` (via [`is_rotation_gate`]) where
604/// `G` is a single-qubit Pauli generator:
605///
606/// * if `θ` contains an [`is_qaoa_parameter`]-recognized `beta*` factor and
607///   `G` is the `X` generator, this is a [`VariationalPattern::QaoaMixer`];
608/// * if `θ` contains an [`is_qaoa_parameter`]-recognized `gamma*` factor and
609///   `G` is the `Z` generator, this is a [`VariationalPattern::QaoaCost`];
610/// * otherwise it is a generic [`VariationalPattern::SingleRotation`] around
611///   the recognized axis.
612///
613/// Returns `None` when `expr` is not a structurally recognizable single-axis
614/// rotation. This function never produces
615/// [`VariationalPattern::EntanglingLayer`]/[`VariationalPattern::VqeAnsatz`];
616/// see the enum-level doc comment on [`VariationalPattern`] for why those
617/// require multi-gate context that this function does not have.
618#[must_use]
619pub fn recognize_variational_pattern(expr: &Expression) -> Option<VariationalPattern> {
620    let (angle, generator) = is_rotation_gate(expr)?;
621    let axis = match generator.as_symbol() {
622        Some("X" | "sigma_x" | "pauli_x") => 'x',
623        Some("Y" | "sigma_y" | "pauli_y") => 'y',
624        Some("Z" | "sigma_z" | "pauli_z") => 'z',
625        _ => return None,
626    };
627
628    let angle_factors = flatten_factors(&angle);
629    let angle_has_qaoa_param = angle_factors.iter().any(is_qaoa_parameter);
630
631    if axis == 'x' && angle_has_qaoa_param {
632        return Some(VariationalPattern::QaoaMixer { beta: angle });
633    }
634    if axis == 'z' && angle_has_qaoa_param {
635        return Some(VariationalPattern::QaoaCost { gamma: angle });
636    }
637
638    Some(VariationalPattern::SingleRotation { axis, param: angle })
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    #[test]
646    fn test_wildcard_pattern() {
647        let x = Expression::symbol("x");
648        let pattern = Pattern::wildcard("a");
649
650        let result = match_pattern(&pattern, &x);
651        assert!(result.is_some());
652
653        let captures = result.expect("should match");
654        assert!(captures.contains_key("a"));
655        assert_eq!(captures.get("a").expect("has a").as_symbol(), Some("x"));
656    }
657
658    #[test]
659    fn test_symbol_pattern() {
660        let x = Expression::symbol("x");
661        let pattern = Pattern::symbol("x");
662
663        assert!(match_pattern(&pattern, &x).is_some());
664
665        let y = Expression::symbol("y");
666        assert!(match_pattern(&pattern, &y).is_none());
667    }
668
669    #[test]
670    fn test_constant_pattern() {
671        let expr = Expression::float_unchecked(2.5);
672        let pattern = Pattern::constant(2.5);
673
674        assert!(match_pattern(&pattern, &expr).is_some());
675
676        let pattern2 = Pattern::constant(3.0);
677        assert!(match_pattern(&pattern2, &expr).is_none());
678    }
679
680    #[test]
681    fn test_zero_one_patterns() {
682        let zero = Expression::zero();
683        let one = Expression::one();
684
685        assert!(match_pattern(&Pattern::Zero, &zero).is_some());
686        assert!(match_pattern(&Pattern::One, &one).is_some());
687        assert!(match_pattern(&Pattern::Zero, &one).is_none());
688        assert!(match_pattern(&Pattern::One, &zero).is_none());
689    }
690
691    #[test]
692    fn test_gate_recognition() {
693        let x = Expression::symbol("X");
694        assert_eq!(recognize_gate_pattern(&x), QuantumGatePattern::PauliX);
695
696        let y = Expression::symbol("sigma_y");
697        assert_eq!(recognize_gate_pattern(&y), QuantumGatePattern::PauliY);
698
699        let h = Expression::symbol("H");
700        assert_eq!(recognize_gate_pattern(&h), QuantumGatePattern::Hadamard);
701    }
702
703    #[test]
704    fn test_hermitian_recognition() {
705        let x = Expression::symbol("X");
706        assert!(is_hermitian_form(&x));
707
708        let num = Expression::float_unchecked(2.5);
709        assert!(is_hermitian_form(&num));
710    }
711
712    #[test]
713    fn test_vqe_parameter_recognition() {
714        let theta = Expression::symbol("theta_1");
715        assert!(is_vqe_parameter(&theta));
716
717        let x = Expression::symbol("x");
718        assert!(!is_vqe_parameter(&x));
719    }
720
721    #[test]
722    fn test_qaoa_parameter_recognition() {
723        let beta = Expression::symbol("beta_0");
724        assert!(is_qaoa_parameter(&beta));
725
726        let gamma = Expression::symbol("gamma_1");
727        assert!(is_qaoa_parameter(&gamma));
728
729        let x = Expression::symbol("x");
730        assert!(!is_qaoa_parameter(&x));
731    }
732
733    #[test]
734    fn test_unary_compound_pattern_matches_and_captures() {
735        // Regression test: compound (unary) patterns used to ALWAYS fail because
736        // the operand extractor returned `None`. They must now match and capture.
737        let x = Expression::symbol("x");
738        let sin_x = crate::ops::trig::sin(&x);
739
740        let pattern = Pattern::sin(Pattern::wildcard("inner"));
741        let captures = match_pattern(&pattern, &sin_x).expect("sin(x) must match Sin(?inner)");
742        assert_eq!(
743            captures.get("inner").and_then(Expression::as_symbol),
744            Some("x")
745        );
746
747        // A cos pattern must NOT match a sin expression.
748        let cos_pattern = Pattern::cos(Pattern::wildcard("inner"));
749        assert!(match_pattern(&cos_pattern, &sin_x).is_none());
750    }
751
752    #[test]
753    fn test_binary_compound_pattern_matches_operands() {
754        // Regression test: binary patterns used to always fail.
755        let sum = Expression::symbol("x") + Expression::symbol("y");
756
757        // Order-sensitive structural match: x is the left operand, y the right.
758        let pattern = Pattern::add(Pattern::symbol("x"), Pattern::symbol("y"));
759        assert!(match_pattern(&pattern, &sum).is_some());
760
761        // Reversed operands must not match the concrete structure.
762        let reversed = Pattern::add(Pattern::symbol("y"), Pattern::symbol("x"));
763        assert!(match_pattern(&reversed, &sum).is_none());
764
765        // A multiplication pattern must not match an addition.
766        let mul_pattern = Pattern::mul(Pattern::wildcard("a"), Pattern::wildcard("b"));
767        assert!(match_pattern(&mul_pattern, &sum).is_none());
768    }
769
770    #[test]
771    fn test_nested_compound_pattern_with_wildcard_consistency() {
772        // exp(sin(x)) must match Exp(Sin(?a)) and capture a = x.
773        let x = Expression::symbol("x");
774        let nested = crate::ops::trig::exp(&crate::ops::trig::sin(&x));
775
776        let pattern = Pattern::Exp(Box::new(Pattern::sin(Pattern::wildcard("a"))));
777        let captures = match_pattern(&pattern, &nested).expect("must match nested pattern");
778        assert_eq!(captures.get("a").and_then(Expression::as_symbol), Some("x"));
779
780        // Wildcard consistency: Add(?a, ?a) matches x + x but not x + y.
781        let same = Pattern::add(Pattern::wildcard("a"), Pattern::wildcard("a"));
782        assert!(match_pattern(&same, &(x.clone() + x.clone())).is_some());
783        let y = Expression::symbol("y");
784        assert!(match_pattern(&same, &(x + y)).is_none());
785    }
786
787    #[test]
788    fn test_is_rotation_gate_structural() {
789        // exp(-i * theta * X / 2) is a rotation gate with angle theta, generator X.
790        let theta = Expression::symbol("theta");
791        let generator = Expression::symbol("X");
792        let half = Expression::float_unchecked(0.5);
793        let arg = ((Expression::i() * theta) * generator) * half;
794        let rot = crate::ops::trig::exp(&(-arg));
795
796        let (angle, gen) =
797            is_rotation_gate(&rot).expect("exp(-i theta X / 2) must be a rotation gate");
798        assert_eq!(gen.as_symbol(), Some("X"));
799
800        // The recovered angle, evaluated at theta = 1.3, must equal 1.3 (the /2 is
801        // undone). This fails if the recognizer fabricates or drops the angle.
802        let mut values = std::collections::HashMap::new();
803        values.insert("theta".to_string(), 1.3_f64);
804        let angle_val = angle.eval(&values).expect("angle must evaluate");
805        assert!((angle_val - 1.3).abs() < 1e-10, "angle was {angle_val}");
806    }
807
808    #[test]
809    fn test_is_rotation_gate_rejects_non_rotations() {
810        let x = Expression::symbol("x");
811        // exp(x): not a rotation (no imaginary unit / generator).
812        assert!(is_rotation_gate(&crate::ops::trig::exp(&x)).is_none());
813
814        // exp(-theta * X / 2): missing the imaginary unit -> not a rotation.
815        let theta = Expression::symbol("theta");
816        let generator = Expression::symbol("X");
817        let arg = (theta * generator) * Expression::float_unchecked(0.5);
818        assert!(is_rotation_gate(&crate::ops::trig::exp(&(-arg))).is_none());
819
820        // A bare symbol is not an exp(...) at all.
821        assert!(is_rotation_gate(&x).is_none());
822    }
823
824    #[test]
825    fn test_is_pure_imaginary_structural() {
826        // Bare I and I * real are pure imaginary.
827        assert!(is_pure_imaginary(&Expression::i()));
828        let r = Expression::symbol("r");
829        assert!(is_pure_imaginary(&(Expression::i() * r.clone())));
830        assert!(is_pure_imaginary(&(r.clone() * Expression::i())));
831        // Negated forms are still pure imaginary.
832        assert!(is_pure_imaginary(&(-(Expression::i() * r.clone()))));
833
834        // Regression: x*I + y is a SUM containing an imaginary term, not a
835        // pure imaginary number. The old substring-based implementation
836        // returned `true` here because "(* x I)" appears in the string form
837        // "(+ (* x I) y)".
838        let x = Expression::symbol("x");
839        let y = Expression::symbol("y");
840        let sum = (x.clone() * Expression::i()) + y;
841        assert!(
842            !is_pure_imaginary(&sum),
843            "x*I + y must not be recognized as pure imaginary"
844        );
845
846        // A factor that itself nests `I` (e.g. x * (I + y)) is not `I * real`.
847        let nested = x * (Expression::i() + r);
848        assert!(!is_pure_imaginary(&nested));
849
850        // A plain real symbol is not pure imaginary.
851        assert!(!is_pure_imaginary(&Expression::symbol("z")));
852    }
853
854    #[test]
855    fn test_is_unit_complex_form_structural() {
856        let theta = Expression::symbol("theta");
857
858        // exp(I * theta) and the commuted exp(theta * I) are both unit
859        // modulus phase factors; the old prefix-matching implementation
860        // false-negatived on the commuted form.
861        assert!(is_unit_complex_form(&crate::ops::trig::exp(
862            &(Expression::i() * theta.clone())
863        )));
864        assert!(is_unit_complex_form(&crate::ops::trig::exp(
865            &(theta.clone() * Expression::i())
866        )));
867
868        // exp(-i * theta) is also unit modulus.
869        assert!(is_unit_complex_form(&crate::ops::trig::exp(
870            &(-(Expression::i() * theta.clone()))
871        )));
872
873        // Regression: exp(I * (a + I*b)) has a genuinely COMPLEX angle
874        // (a + i*b), so |exp(i*(a+ib))| = e^{-b} != 1 in general. The old
875        // prefix-matching implementation returned `true` because the string
876        // still started with "(exp (* I ".
877        let a = Expression::symbol("a");
878        let b = Expression::symbol("b");
879        let complex_angle = a + Expression::i() * b;
880        let fake_phase = crate::ops::trig::exp(&(Expression::i() * complex_angle));
881        assert!(
882            !is_unit_complex_form(&fake_phase),
883            "exp(I * (a + I*b)) must not be recognized as unit modulus"
884        );
885
886        // exp(x) with no imaginary unit at all is not a phase factor.
887        assert!(!is_unit_complex_form(&crate::ops::trig::exp(
888            &Expression::symbol("x")
889        )));
890
891        // A bare (non-exp) expression is never a unit complex form.
892        assert!(!is_unit_complex_form(&theta));
893    }
894
895    #[test]
896    fn test_recognize_gate_pattern_rotations() {
897        let theta = Expression::symbol("theta");
898        let half = Expression::float_unchecked(0.5);
899
900        let make_rotation = |generator: Expression| {
901            crate::ops::trig::exp(
902                &(-(((Expression::i() * theta.clone()) * generator) * half.clone())),
903            )
904        };
905
906        match recognize_gate_pattern(&make_rotation(Expression::symbol("X"))) {
907            QuantumGatePattern::Rx(angle) => {
908                let mut values = std::collections::HashMap::new();
909                values.insert("theta".to_string(), 0.7_f64);
910                let v = angle.eval(&values).expect("angle must evaluate");
911                assert!((v - 0.7).abs() < 1e-10, "angle was {v}");
912            }
913            other => panic!("expected Rx, got {other:?}"),
914        }
915
916        assert!(matches!(
917            recognize_gate_pattern(&make_rotation(Expression::symbol("Y"))),
918            QuantumGatePattern::Ry(_)
919        ));
920        assert!(matches!(
921            recognize_gate_pattern(&make_rotation(Expression::symbol("Z"))),
922            QuantumGatePattern::Rz(_)
923        ));
924
925        // Bare symbols still resolve to their fixed-gate variants.
926        assert_eq!(
927            recognize_gate_pattern(&Expression::symbol("H")),
928            QuantumGatePattern::Hadamard
929        );
930
931        // A non-rotation, non-symbol expression is Unknown.
932        assert_eq!(
933            recognize_gate_pattern(&(Expression::symbol("x") + Expression::symbol("y"))),
934            QuantumGatePattern::Unknown
935        );
936    }
937
938    #[test]
939    fn test_recognize_variational_pattern() {
940        let half = Expression::float_unchecked(0.5);
941
942        // exp(-i * beta_0 * X / 2) is a QAOA mixer term.
943        let beta = Expression::symbol("beta_0");
944        let mixer = crate::ops::trig::exp(
945            &(-(((Expression::i() * beta) * Expression::symbol("X")) * half.clone())),
946        );
947        match recognize_variational_pattern(&mixer) {
948            Some(VariationalPattern::QaoaMixer { beta }) => {
949                assert!(is_qaoa_parameter(&flatten_factors(&beta)[0]));
950            }
951            other => panic!("expected QaoaMixer, got {other:?}"),
952        }
953
954        // exp(-i * gamma_1 * Z / 2) is a QAOA cost term.
955        let gamma = Expression::symbol("gamma_1");
956        let cost = crate::ops::trig::exp(
957            &(-(((Expression::i() * gamma) * Expression::symbol("Z")) * half.clone())),
958        );
959        assert!(matches!(
960            recognize_variational_pattern(&cost),
961            Some(VariationalPattern::QaoaCost { .. })
962        ));
963
964        // exp(-i * theta_0 * Y / 2) with a non-QAOA-named angle is a plain
965        // single rotation, not a QAOA variant.
966        let theta = Expression::symbol("theta_0");
967        let single = crate::ops::trig::exp(
968            &(-(((Expression::i() * theta) * Expression::symbol("Y")) * half)),
969        );
970        match recognize_variational_pattern(&single) {
971            Some(VariationalPattern::SingleRotation { axis, .. }) => assert_eq!(axis, 'y'),
972            other => panic!("expected SingleRotation, got {other:?}"),
973        }
974
975        // A non-rotation expression yields no variational pattern.
976        assert!(recognize_variational_pattern(&Expression::symbol("H")).is_none());
977    }
978}