1use std::collections::HashMap;
7
8use crate::error::{SymEngineError, SymEngineResult};
9use crate::expr::{ExprLang, Expression};
10
11#[derive(Clone, Debug)]
13pub enum Pattern {
14 Wildcard(String),
16 Constant(f64),
18 Symbol(String),
20 Zero,
22 One,
24 Add(Box<Self>, Box<Self>),
26 Mul(Box<Self>, Box<Self>),
28 Pow(Box<Self>, Box<Self>),
30 Neg(Box<Self>),
32 Sin(Box<Self>),
34 Cos(Box<Self>),
36 Exp(Box<Self>),
38 Log(Box<Self>),
40 Commutator(Box<Self>, Box<Self>),
42 Anticommutator(Box<Self>, Box<Self>),
44 TensorProduct(Box<Self>, Box<Self>),
46 Dagger(Box<Self>),
48}
49
50#[allow(clippy::should_implement_trait)]
51impl Pattern {
52 #[must_use]
54 pub fn wildcard(name: &str) -> Self {
55 Self::Wildcard(name.to_string())
56 }
57
58 #[must_use]
60 pub fn symbol(name: &str) -> Self {
61 Self::Symbol(name.to_string())
62 }
63
64 #[must_use]
66 pub const fn constant(value: f64) -> Self {
67 Self::Constant(value)
68 }
69
70 #[must_use]
72 pub fn add(left: Self, right: Self) -> Self {
73 Self::Add(Box::new(left), Box::new(right))
74 }
75
76 #[must_use]
78 pub fn mul(left: Self, right: Self) -> Self {
79 Self::Mul(Box::new(left), Box::new(right))
80 }
81
82 #[must_use]
84 pub fn pow(base: Self, exp: Self) -> Self {
85 Self::Pow(Box::new(base), Box::new(exp))
86 }
87
88 #[must_use]
90 pub fn sin(arg: Self) -> Self {
91 Self::Sin(Box::new(arg))
92 }
93
94 #[must_use]
96 pub fn cos(arg: Self) -> Self {
97 Self::Cos(Box::new(arg))
98 }
99
100 #[must_use]
102 pub fn commutator(a: Self, b: Self) -> Self {
103 Self::Commutator(Box::new(a), Box::new(b))
104 }
105
106 #[must_use]
108 pub fn anticommutator(a: Self, b: Self) -> Self {
109 Self::Anticommutator(Box::new(a), Box::new(b))
110 }
111
112 #[must_use]
114 pub fn tensor(a: Self, b: Self) -> Self {
115 Self::TensorProduct(Box::new(a), Box::new(b))
116 }
117
118 #[must_use]
120 pub fn dagger(a: Self) -> Self {
121 Self::Dagger(Box::new(a))
122 }
123}
124
125pub type Captures = HashMap<String, Expression>;
127
128pub 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#[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 if let Some(existing) = captures.get(name) {
145 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 _ => match_compound_pattern(pattern, expr, captures),
171 }
172}
173
174fn 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 Pattern::Wildcard(_)
198 | Pattern::Constant(_)
199 | Pattern::Symbol(_)
200 | Pattern::Zero
201 | Pattern::One => unreachable!(),
202 }
203}
204
205fn 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
210fn 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
223fn extract_unary_arg(expr: &Expression, op: &str) -> Option<Expression> {
227 expr.unary_arg(op)
228}
229
230fn extract_binary_args(expr: &Expression, op: &str) -> Option<(Expression, Expression)> {
234 expr.binary_args(op)
235}
236
237#[must_use]
256pub fn is_rotation_gate(expr: &Expression) -> Option<(Expression, Expression)> {
257 let arg = expr.unary_arg("exp")?;
259
260 let inner = arg.unary_arg("neg").unwrap_or(arg);
263
264 let factors = flatten_factors(&inner);
266
267 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 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 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
304fn 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
319pub fn is_hermitian_form(expr: &Expression) -> bool {
321 if expr.is_number() {
324 return true;
325 }
326 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#[must_use]
343pub const fn is_projector_form(_expr: &Expression) -> bool {
344 false
345}
346
347const 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
374const BINARY_OPS: &[&str] = &["+", "*", "/", "^", "comm", "anticomm", "tensor"];
377
378fn 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
403fn 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 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#[must_use]
436pub fn is_pure_imaginary(expr: &Expression) -> bool {
437 let stripped = expr.unary_arg("neg").unwrap_or_else(|| expr.clone());
438
439 if stripped.as_symbol() == Some("I") {
441 return true;
442 }
443
444 if stripped.binary_args("*").is_none() {
449 return false;
450 }
451
452 is_imaginary_times_real(&flatten_factors(&stripped))
453}
454
455#[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 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#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum QuantumGatePattern {
488 PauliX,
490 PauliY,
492 PauliZ,
494 Hadamard,
496 SGate,
498 TGate,
500 Rx(Expression),
502 Ry(Expression),
504 Rz(Expression),
506 Rotation(Expression, Expression, Expression), Unknown,
510}
511
512#[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#[derive(Debug, Clone)]
566pub enum VariationalPattern {
567 SingleRotation {
569 axis: char, param: Expression,
571 },
572 EntanglingLayer { params: Vec<Expression> },
576 VqeAnsatz { params: Vec<Expression> },
580 QaoaMixer { beta: Expression },
582 QaoaCost { gamma: Expression },
584}
585
586#[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#[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#[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 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 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 let sum = Expression::symbol("x") + Expression::symbol("y");
756
757 let pattern = Pattern::add(Pattern::symbol("x"), Pattern::symbol("y"));
759 assert!(match_pattern(&pattern, &sum).is_some());
760
761 let reversed = Pattern::add(Pattern::symbol("y"), Pattern::symbol("x"));
763 assert!(match_pattern(&reversed, &sum).is_none());
764
765 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 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 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 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 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 assert!(is_rotation_gate(&crate::ops::trig::exp(&x)).is_none());
813
814 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 assert!(is_rotation_gate(&x).is_none());
822 }
823
824 #[test]
825 fn test_is_pure_imaginary_structural() {
826 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 assert!(is_pure_imaginary(&(-(Expression::i() * r.clone()))));
833
834 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 let nested = x * (Expression::i() + r);
848 assert!(!is_pure_imaginary(&nested));
849
850 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 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 assert!(is_unit_complex_form(&crate::ops::trig::exp(
870 &(-(Expression::i() * theta.clone()))
871 )));
872
873 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 assert!(!is_unit_complex_form(&crate::ops::trig::exp(
888 &Expression::symbol("x")
889 )));
890
891 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 assert_eq!(
927 recognize_gate_pattern(&Expression::symbol("H")),
928 QuantumGatePattern::Hadamard
929 );
930
931 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 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 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 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 assert!(recognize_variational_pattern(&Expression::symbol("H")).is_none());
977 }
978}