1use laddu_expr::{
2 BinaryOp, ExprGraph, ExprId, ExprMetadata, ExprNode, ExprSourceKind, UnaryOp, ValueKind,
3};
4use num::complex::Complex64;
5
6use crate::{CompileError, CompileResult, CompiledModel, GraphFacts};
7
8const DEFAULT_EXPANSION_BUDGET: usize = 4_096;
9
10#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12pub enum NormalizationStrategy {
13 Hermitian,
15 LinearStatistics,
17 Hybrid,
19 General,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum NormalizationFallbackReason {
26 NonScalarIntensity,
28 UnsupportedMixedOperation {
30 node: ExprId,
32 operation: &'static str,
34 },
35 ExpansionBudgetExceeded {
37 budget: usize,
39 },
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct NormalizationDiagnostics {
45 strategy: NormalizationStrategy,
46 basis_count: usize,
47 coherent_group_count: usize,
48 has_residual: bool,
49 fallback_reason: Option<NormalizationFallbackReason>,
50}
51
52impl NormalizationDiagnostics {
53 pub fn strategy(&self) -> NormalizationStrategy {
55 self.strategy
56 }
57
58 pub fn basis_count(&self) -> usize {
60 self.basis_count
61 }
62
63 pub fn coherent_group_count(&self) -> usize {
65 self.coherent_group_count
66 }
67
68 pub fn has_residual(&self) -> bool {
70 self.has_residual
71 }
72
73 pub fn fallback_reason(&self) -> Option<&NormalizationFallbackReason> {
75 self.fallback_reason.as_ref()
76 }
77}
78
79#[derive(Copy, Clone, Debug)]
80struct SeparableTerm {
81 coefficient: ExprId,
82 basis: ExprId,
83}
84
85#[doc(hidden)]
87#[derive(Clone, Debug)]
88pub struct NormalizationPlan {
89 graph: ExprGraph,
90 terms: Vec<SeparableTerm>,
91 residual: Option<ExprId>,
92 diagnostics: NormalizationDiagnostics,
93 proven_nonnegative: bool,
94}
95
96impl NormalizationPlan {
97 pub(crate) fn analyze_disabled(graph: &ExprGraph) -> Self {
98 Self::general(
99 graph,
100 NormalizationFallbackReason::UnsupportedMixedOperation {
101 node: graph.root(),
102 operation: "normalization analysis disabled",
103 },
104 )
105 }
106
107 pub(crate) fn analyze(graph: &ExprGraph, facts: &GraphFacts) -> Self {
108 if !matches!(
109 facts.get(graph.root()).map(|facts| facts.value_kind),
110 Some(ValueKind::Real | ValueKind::Complex)
111 ) {
112 return Self::general(graph, NormalizationFallbackReason::NonScalarIntensity);
113 }
114
115 let mut analyzer = Analyzer::new(graph, facts, DEFAULT_EXPANSION_BUDGET);
116 let mut terms = Vec::new();
117 let mut residuals = Vec::new();
118 for root in analyzer.additive_roots(graph.root()) {
119 match analyzer.decompose(root) {
120 Ok(mut extracted) => terms.append(&mut extracted),
121 Err(reason) => {
122 analyzer.last_reason = Some(reason);
123 residuals.push(root);
124 }
125 }
126 }
127
128 if terms.is_empty() {
129 return Self::general(
130 graph,
131 analyzer.last_reason.unwrap_or(
132 NormalizationFallbackReason::UnsupportedMixedOperation {
133 node: graph.root(),
134 operation: "root",
135 },
136 ),
137 );
138 }
139
140 let residual = analyzer.sum_roots(&residuals);
141 let strategy = if residual.is_some() {
142 NormalizationStrategy::Hybrid
143 } else if analyzer.coherent_groups > 0 {
144 NormalizationStrategy::Hermitian
145 } else {
146 NormalizationStrategy::LinearStatistics
147 };
148 let diagnostics = NormalizationDiagnostics {
149 strategy,
150 basis_count: terms.len(),
151 coherent_group_count: analyzer.coherent_groups,
152 has_residual: residual.is_some(),
153 fallback_reason: analyzer.last_reason.clone(),
154 };
155 Self {
156 graph: analyzer.finish(),
157 terms,
158 residual,
159 diagnostics,
160 proven_nonnegative: proves_nonnegative(graph, facts, graph.root()),
161 }
162 }
163
164 fn general(graph: &ExprGraph, reason: NormalizationFallbackReason) -> Self {
165 Self {
166 graph: graph.clone(),
167 terms: Vec::new(),
168 residual: None,
169 diagnostics: NormalizationDiagnostics {
170 strategy: NormalizationStrategy::General,
171 basis_count: 0,
172 coherent_group_count: 0,
173 has_residual: false,
174 fallback_reason: Some(reason),
175 },
176 proven_nonnegative: false,
177 }
178 }
179
180 pub fn diagnostics(&self) -> &NormalizationDiagnostics {
182 &self.diagnostics
183 }
184
185 pub fn proven_nonnegative(&self) -> bool {
187 self.proven_nonnegative
188 }
189
190 pub fn basis_models(&self) -> CompileResult<Vec<CompiledModel>> {
196 self.terms
197 .iter()
198 .map(|term| {
199 CompiledModel::from_graph_without_normalization(compact_root(
200 &self.graph,
201 term.basis,
202 ))
203 })
204 .collect()
205 }
206
207 pub fn evaluator_model(&self, statistics: &[Complex64]) -> CompileResult<CompiledModel> {
214 if statistics.len() != self.terms.len() {
215 return Err(CompileError::InvalidExecutablePlan(format!(
216 "normalization expected {} statistics, got {}",
217 self.terms.len(),
218 statistics.len()
219 )));
220 }
221 let mut nodes = self.graph.nodes().to_vec();
222 let mut metadata = graph_metadata(&self.graph);
223 let mut products = Vec::with_capacity(self.terms.len());
224 for (term, statistic) in self.terms.iter().zip(statistics) {
225 let constant = push_node(
226 &mut nodes,
227 &mut metadata,
228 ExprNode::from_folded_const(*statistic),
229 ExprSourceKind::Const,
230 );
231 products.push(push_node(
232 &mut nodes,
233 &mut metadata,
234 ExprNode::NaryMul {
235 factors: vec![constant, term.coefficient],
236 },
237 ExprSourceKind::Binary,
238 ));
239 }
240 let sum = push_node(
241 &mut nodes,
242 &mut metadata,
243 ExprNode::NaryAdd { terms: products },
244 ExprSourceKind::Binary,
245 );
246 let root = push_node(
247 &mut nodes,
248 &mut metadata,
249 ExprNode::Unary {
250 op: UnaryOp::Real,
251 input: sum,
252 },
253 ExprSourceKind::Unary,
254 );
255 let graph = ExprGraph::from_parts(root, nodes, metadata)?;
256 CompiledModel::from_graph_without_normalization(compact_root(&graph, root))
257 }
258
259 pub fn residual_model(&self) -> CompileResult<Option<CompiledModel>> {
265 self.residual
266 .map(|root| {
267 CompiledModel::from_graph_without_normalization(compact_root(&self.graph, root))
268 })
269 .transpose()
270 }
271}
272
273fn proves_nonnegative(graph: &ExprGraph, facts: &GraphFacts, id: ExprId) -> bool {
274 match graph.node(id).expect("normalization node exists") {
275 ExprNode::RealConst(value) => value.is_finite() && *value >= 0.0,
276 ExprNode::ComplexConst(value) => value.im == 0.0 && value.re.is_finite() && value.re >= 0.0,
277 ExprNode::Unary {
278 op: UnaryOp::NormSqr,
279 ..
280 } => true,
281 ExprNode::Unary {
282 op: UnaryOp::Real,
283 input,
284 } => proves_nonnegative(graph, facts, *input),
285 ExprNode::Unary {
286 op: UnaryOp::PowI(power),
287 input,
288 } => {
289 *power >= 0
290 && power % 2 == 0
291 && facts
292 .get(*input)
293 .is_some_and(|facts| facts.value_kind == ValueKind::Real)
294 }
295 ExprNode::Binary {
296 op: BinaryOp::Add,
297 lhs,
298 rhs,
299 } => proves_nonnegative(graph, facts, *lhs) && proves_nonnegative(graph, facts, *rhs),
300 ExprNode::Binary {
301 op: BinaryOp::Mul,
302 lhs,
303 rhs,
304 } => proves_nonnegative(graph, facts, *lhs) && proves_nonnegative(graph, facts, *rhs),
305 ExprNode::NaryAdd { terms } => terms
306 .iter()
307 .all(|term| proves_nonnegative(graph, facts, *term)),
308 ExprNode::NaryMul { factors } => factors
309 .iter()
310 .all(|factor| proves_nonnegative(graph, facts, *factor)),
311 _ => false,
312 }
313}
314
315struct Analyzer<'a> {
316 facts: &'a GraphFacts,
317 nodes: Vec<ExprNode>,
318 metadata: Vec<ExprMetadata>,
319 one: ExprId,
320 budget: usize,
321 coherent_groups: usize,
322 last_reason: Option<NormalizationFallbackReason>,
323}
324
325impl<'a> Analyzer<'a> {
326 fn new(graph: &ExprGraph, facts: &'a GraphFacts, budget: usize) -> Self {
327 let mut nodes = graph.nodes().to_vec();
328 let mut metadata = graph_metadata(graph);
329 let one = push_node(
330 &mut nodes,
331 &mut metadata,
332 ExprNode::RealConst(1.0),
333 ExprSourceKind::Const,
334 );
335 Self {
336 facts,
337 nodes,
338 metadata,
339 one,
340 budget,
341 coherent_groups: 0,
342 last_reason: None,
343 }
344 }
345
346 fn finish(&self) -> ExprGraph {
347 ExprGraph::from_parts(
348 ExprId::from_index(0),
349 self.nodes.clone(),
350 self.metadata.clone(),
351 )
352 .expect("augmented normalization graph is valid")
353 }
354
355 fn dependency(&self, id: ExprId) -> crate::DependencyFacts {
356 if id.index() < self.facts.nodes().len() {
357 return self.facts.get(id).expect("facts are complete").dependency;
358 }
359 self.nodes[id.index()].child_ids().into_iter().fold(
360 crate::DependencyFacts::per_compile(),
361 |dependency, child| dependency.union(self.dependency(child)),
362 )
363 }
364
365 fn additive_roots(&self, root: ExprId) -> Vec<ExprId> {
366 match &self.nodes[root.index()] {
367 ExprNode::NaryAdd { terms } => terms.clone(),
368 ExprNode::Binary {
369 op: BinaryOp::Add,
370 lhs,
371 rhs,
372 } => {
373 let mut roots = self.additive_roots(*lhs);
374 roots.extend(self.additive_roots(*rhs));
375 roots
376 }
377 _ => vec![root],
378 }
379 }
380
381 fn decompose(&mut self, id: ExprId) -> Result<Vec<SeparableTerm>, NormalizationFallbackReason> {
382 let dependency = self.dependency(id);
383 if !dependency.depends_on_event {
384 return Ok(vec![SeparableTerm {
385 coefficient: id,
386 basis: self.one,
387 }]);
388 }
389 if !dependency.depends_on_free_params {
390 return Ok(vec![SeparableTerm {
391 coefficient: self.one,
392 basis: id,
393 }]);
394 }
395
396 match self.nodes[id.index()].clone() {
397 ExprNode::Binary { op, lhs, rhs } => self.decompose_binary(id, op, lhs, rhs),
398 ExprNode::NaryAdd { terms } => {
399 let mut result = Vec::new();
400 for term in terms {
401 result.extend(self.decompose(term)?);
402 self.ensure_budget(&result)?;
403 }
404 Ok(result)
405 }
406 ExprNode::NaryMul { factors } => {
407 let mut result = vec![SeparableTerm {
408 coefficient: self.one,
409 basis: self.one,
410 }];
411 for factor in factors {
412 let factor_terms = self.decompose(factor)?;
413 result = self.multiply_terms(&result, &factor_terms)?;
414 }
415 Ok(result)
416 }
417 ExprNode::Unary { op, input } => self.decompose_unary(id, op, input),
418 _ => Err(self.unsupported(id, "structured mixed operation")),
419 }
420 }
421
422 fn decompose_binary(
423 &mut self,
424 id: ExprId,
425 op: BinaryOp,
426 lhs: ExprId,
427 rhs: ExprId,
428 ) -> Result<Vec<SeparableTerm>, NormalizationFallbackReason> {
429 match op {
430 BinaryOp::Add => {
431 let mut terms = self.decompose(lhs)?;
432 terms.extend(self.decompose(rhs)?);
433 self.ensure_budget(&terms)?;
434 Ok(terms)
435 }
436 BinaryOp::Sub => {
437 let mut terms = self.decompose(lhs)?;
438 for mut term in self.decompose(rhs)? {
439 term.coefficient = self.unary(UnaryOp::Neg, term.coefficient);
440 terms.push(term);
441 }
442 self.ensure_budget(&terms)?;
443 Ok(terms)
444 }
445 BinaryOp::Mul => {
446 let left = self.decompose(lhs)?;
447 let right = self.decompose(rhs)?;
448 self.multiply_terms(&left, &right)
449 }
450 BinaryOp::Div => {
451 let denominator = self.dependency(rhs);
452 let mut terms = self.decompose(lhs)?;
453 if !denominator.depends_on_event {
454 for term in &mut terms {
455 term.coefficient = self.binary(BinaryOp::Div, term.coefficient, rhs);
456 }
457 Ok(terms)
458 } else if !denominator.depends_on_free_params {
459 for term in &mut terms {
460 term.basis = self.binary(BinaryOp::Div, term.basis, rhs);
461 }
462 Ok(terms)
463 } else {
464 Err(self.unsupported(id, "mixed division"))
465 }
466 }
467 BinaryOp::Atan2 => Err(self.unsupported(id, "atan2")),
468 }
469 }
470
471 fn decompose_unary(
472 &mut self,
473 id: ExprId,
474 op: UnaryOp,
475 input: ExprId,
476 ) -> Result<Vec<SeparableTerm>, NormalizationFallbackReason> {
477 match op {
478 UnaryOp::Neg => {
479 let mut terms = self.decompose(input)?;
480 for term in &mut terms {
481 term.coefficient = self.unary(UnaryOp::Neg, term.coefficient);
482 }
483 Ok(terms)
484 }
485 UnaryOp::Conj => {
486 let mut terms = self.decompose(input)?;
487 for term in &mut terms {
488 term.coefficient = self.unary(UnaryOp::Conj, term.coefficient);
489 term.basis = self.unary(UnaryOp::Conj, term.basis);
490 }
491 Ok(terms)
492 }
493 UnaryOp::NormSqr => {
494 self.coherent_groups += 1;
495 let terms = self.decompose(input)?;
496 let packed_len = terms.len().saturating_mul(terms.len().saturating_add(1)) / 2;
497 if packed_len > self.budget {
498 return Err(NormalizationFallbackReason::ExpansionBudgetExceeded {
499 budget: self.budget,
500 });
501 }
502 let two = self.constant(Complex64::new(2.0, 0.0));
503 let mut packed = Vec::with_capacity(packed_len);
504 for (row, left) in terms.iter().enumerate() {
505 for (column, right) in terms.iter().enumerate().skip(row) {
506 let right_coefficient = self.unary(UnaryOp::Conj, right.coefficient);
507 let right_basis = self.unary(UnaryOp::Conj, right.basis);
508 let mut coefficient = self.product(&[left.coefficient, right_coefficient]);
509 if column != row {
510 coefficient = self.product(&[two, coefficient]);
511 }
512 packed.push(SeparableTerm {
513 coefficient,
514 basis: self.product(&[left.basis, right_basis]),
515 });
516 }
517 }
518 Ok(packed)
519 }
520 UnaryOp::PowI(power) if power >= 0 => {
521 let base = self.decompose(input)?;
522 let mut result = vec![SeparableTerm {
523 coefficient: self.one,
524 basis: self.one,
525 }];
526 for _ in 0..power {
527 result = self.multiply_terms(&result, &base)?;
528 }
529 Ok(result)
530 }
531 UnaryOp::Real | UnaryOp::Imag => self.decompose_projection(op, input),
532 UnaryOp::Sqrt
533 | UnaryOp::Exp
534 | UnaryOp::Sin
535 | UnaryOp::Cos
536 | UnaryOp::Log
537 | UnaryOp::PowI(_) => Err(self.unsupported(id, "nonlinear unary operation")),
538 }
539 }
540
541 fn decompose_projection(
542 &mut self,
543 op: UnaryOp,
544 input: ExprId,
545 ) -> Result<Vec<SeparableTerm>, NormalizationFallbackReason> {
546 let terms = self.decompose(input)?;
547 let mut result = Vec::with_capacity(terms.len() * 2);
548 let factor = if op == UnaryOp::Real {
549 Complex64::new(0.5, 0.0)
550 } else {
551 Complex64::new(0.0, -0.5)
552 };
553 let conjugate_factor = if op == UnaryOp::Real { factor } else { -factor };
554 let factor = self.constant(factor);
555 let conjugate_factor = self.constant(conjugate_factor);
556 for term in terms {
557 let coefficient = self.product(&[factor, term.coefficient]);
558 result.push(SeparableTerm {
559 coefficient,
560 basis: term.basis,
561 });
562 let conjugated_coefficient = self.unary(UnaryOp::Conj, term.coefficient);
563 let conjugated_basis = self.unary(UnaryOp::Conj, term.basis);
564 let coefficient = self.product(&[conjugate_factor, conjugated_coefficient]);
565 result.push(SeparableTerm {
566 coefficient,
567 basis: conjugated_basis,
568 });
569 }
570 self.ensure_budget(&result)?;
571 Ok(result)
572 }
573
574 fn multiply_terms(
575 &mut self,
576 lhs: &[SeparableTerm],
577 rhs: &[SeparableTerm],
578 ) -> Result<Vec<SeparableTerm>, NormalizationFallbackReason> {
579 let count = lhs.len().saturating_mul(rhs.len());
580 if count > self.budget {
581 return Err(NormalizationFallbackReason::ExpansionBudgetExceeded {
582 budget: self.budget,
583 });
584 }
585 let mut result = Vec::with_capacity(count);
586 for lhs in lhs {
587 for rhs in rhs {
588 let coefficient = self.product(&[lhs.coefficient, rhs.coefficient]);
589 let basis = self.product(&[lhs.basis, rhs.basis]);
590 result.push(SeparableTerm { coefficient, basis });
591 }
592 }
593 Ok(result)
594 }
595
596 fn ensure_budget(&self, terms: &[SeparableTerm]) -> Result<(), NormalizationFallbackReason> {
597 if terms.len() > self.budget {
598 Err(NormalizationFallbackReason::ExpansionBudgetExceeded {
599 budget: self.budget,
600 })
601 } else {
602 Ok(())
603 }
604 }
605
606 fn unsupported(&self, node: ExprId, operation: &'static str) -> NormalizationFallbackReason {
607 NormalizationFallbackReason::UnsupportedMixedOperation { node, operation }
608 }
609
610 fn constant(&mut self, value: Complex64) -> ExprId {
611 self.push(ExprNode::from_folded_const(value), ExprSourceKind::Const)
612 }
613
614 fn unary(&mut self, op: UnaryOp, input: ExprId) -> ExprId {
615 self.push(ExprNode::Unary { op, input }, ExprSourceKind::Unary)
616 }
617
618 fn binary(&mut self, op: BinaryOp, lhs: ExprId, rhs: ExprId) -> ExprId {
619 self.push(ExprNode::Binary { op, lhs, rhs }, ExprSourceKind::Binary)
620 }
621
622 fn product(&mut self, factors: &[ExprId]) -> ExprId {
623 match factors {
624 [] => self.one,
625 [only] => *only,
626 _ => self.push(
627 ExprNode::NaryMul {
628 factors: factors.to_vec(),
629 },
630 ExprSourceKind::Binary,
631 ),
632 }
633 }
634
635 fn sum_roots(&mut self, roots: &[ExprId]) -> Option<ExprId> {
636 match roots {
637 [] => None,
638 [only] => Some(*only),
639 _ => Some(self.push(
640 ExprNode::NaryAdd {
641 terms: roots.to_vec(),
642 },
643 ExprSourceKind::Binary,
644 )),
645 }
646 }
647
648 fn push(&mut self, node: ExprNode, source: ExprSourceKind) -> ExprId {
649 push_node(&mut self.nodes, &mut self.metadata, node, source)
650 }
651}
652
653fn graph_metadata(graph: &ExprGraph) -> Vec<ExprMetadata> {
654 (0..graph.nodes().len())
655 .map(|index| {
656 graph
657 .metadata(ExprId::from_index(index))
658 .expect("normalization graph metadata is complete")
659 .clone()
660 })
661 .collect()
662}
663
664fn push_node(
665 nodes: &mut Vec<ExprNode>,
666 metadata: &mut Vec<ExprMetadata>,
667 node: ExprNode,
668 source: ExprSourceKind,
669) -> ExprId {
670 let id = ExprId::from_index(nodes.len());
671 nodes.push(node);
672 metadata.push(ExprMetadata::new(source));
673 id
674}
675
676fn compact_root(graph: &ExprGraph, root: ExprId) -> ExprGraph {
677 let mut required = vec![false; graph.nodes().len()];
678 mark_required(graph, root, &mut required);
679 let mut remap = vec![None; graph.nodes().len()];
680 let mut nodes = Vec::new();
681 let mut metadata = Vec::new();
682 for (index, node) in graph.nodes().iter().enumerate() {
683 if !required[index] {
684 continue;
685 }
686 let id = ExprId::from_index(nodes.len());
687 remap[index] = Some(id);
688 nodes.push(remap_node(node, &remap));
689 metadata.push(
690 graph
691 .metadata(ExprId::from_index(index))
692 .expect("normalization graph metadata is complete")
693 .clone(),
694 );
695 }
696 ExprGraph::from_parts(
697 remap[root.index()].expect("normalization root is required"),
698 nodes,
699 metadata,
700 )
701 .expect("compacted normalization graph is valid")
702}
703
704fn mark_required(graph: &ExprGraph, id: ExprId, required: &mut [bool]) {
705 if required[id.index()] {
706 return;
707 }
708 required[id.index()] = true;
709 for child in graph
710 .node(id)
711 .expect("normalization node exists")
712 .child_ids()
713 {
714 mark_required(graph, child, required);
715 }
716}
717
718fn remap_node(node: &ExprNode, remap: &[Option<ExprId>]) -> ExprNode {
719 let id = |id: ExprId| remap[id.index()].expect("children precede normalization nodes");
720 match node {
721 ExprNode::RealConst(value) => ExprNode::RealConst(*value),
722 ExprNode::ComplexConst(value) => ExprNode::ComplexConst(*value),
723 ExprNode::ScalarParam(parameter) => ExprNode::ScalarParam(parameter.clone()),
724 ExprNode::EventScalar(name) => ExprNode::EventScalar(name.clone()),
725 ExprNode::EventP4Component { name, component } => ExprNode::EventP4Component {
726 name: name.clone(),
727 component: *component,
728 },
729 ExprNode::Unary { op, input } => ExprNode::Unary {
730 op: *op,
731 input: id(*input),
732 },
733 ExprNode::Binary { op, lhs, rhs } => ExprNode::Binary {
734 op: *op,
735 lhs: id(*lhs),
736 rhs: id(*rhs),
737 },
738 ExprNode::NaryAdd { terms } => ExprNode::NaryAdd {
739 terms: terms.iter().map(|term| id(*term)).collect(),
740 },
741 ExprNode::NaryMul { factors } => ExprNode::NaryMul {
742 factors: factors.iter().map(|factor| id(*factor)).collect(),
743 },
744 ExprNode::Complex { re, im } => ExprNode::Complex {
745 re: id(*re),
746 im: id(*im),
747 },
748 ExprNode::Vector { elements } => ExprNode::Vector {
749 elements: elements.iter().map(|element| id(*element)).collect(),
750 },
751 ExprNode::Matrix {
752 rows,
753 cols,
754 elements,
755 } => ExprNode::Matrix {
756 rows: *rows,
757 cols: *cols,
758 elements: elements.iter().map(|element| id(*element)).collect(),
759 },
760 ExprNode::Component { input, index } => ExprNode::Component {
761 input: id(*input),
762 index: *index,
763 },
764 ExprNode::MatrixElement { input, row, col } => ExprNode::MatrixElement {
765 input: id(*input),
766 row: *row,
767 col: *col,
768 },
769 ExprNode::MatMul { lhs, rhs } => ExprNode::MatMul {
770 lhs: id(*lhs),
771 rhs: id(*rhs),
772 },
773 ExprNode::MatVec { matrix, vector } => ExprNode::MatVec {
774 matrix: id(*matrix),
775 vector: id(*vector),
776 },
777 ExprNode::Dot { lhs, rhs } => ExprNode::Dot {
778 lhs: id(*lhs),
779 rhs: id(*rhs),
780 },
781 ExprNode::Solve { matrix, rhs } => ExprNode::Solve {
782 matrix: id(*matrix),
783 rhs: id(*rhs),
784 },
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use laddu_expr::{Expr, complex, event_scalar, parameter, polar_complex};
791
792 use super::*;
793
794 fn diagnostics(expression: &Expr) -> NormalizationDiagnostics {
795 CompiledModel::from_expr(expression)
796 .unwrap()
797 .normalization_diagnostics()
798 .clone()
799 }
800
801 #[test]
802 fn extracts_rectangular_and_polar_coherent_models() {
803 let basis = complex(event_scalar("x"), event_scalar("y"));
804 let rectangular = (complex(parameter!("re"), parameter!("im")) * basis.clone()).norm_sqr();
805 let polar = (polar_complex(parameter!("mag"), parameter!("phase")) * basis).norm_sqr();
806 for model in [&rectangular, &polar] {
807 let diagnostics = diagnostics(model);
808 assert_eq!(
809 diagnostics.strategy(),
810 NormalizationStrategy::Hermitian,
811 "{diagnostics:?}"
812 );
813 assert!(!diagnostics.has_residual());
814 assert!(diagnostics.basis_count() >= 1);
815 }
816 }
817
818 #[test]
819 fn decomposes_separable_and_nonseparable_additive_parts() {
820 let x = event_scalar("x");
821 let scale = Expr::from(parameter!("scale"));
822 let expression = scale.clone() * x.clone() + (scale * x).sin();
823 let diagnostics = diagnostics(&expression);
824 assert_eq!(diagnostics.strategy(), NormalizationStrategy::Hybrid);
825 assert!(diagnostics.has_residual());
826 }
827}