Skip to main content

laddu_runtime/
cpu.rs

1use std::{
2    collections::HashMap,
3    mem::size_of,
4    sync::{Arc, OnceLock},
5};
6
7use laddu_autodiff::{AutodiffMode, AutodiffPlan, AutodiffResult, gradient_ir};
8use laddu_compile::{
9    CachePlan, CompiledModel, ExecutablePlan, ReductionPlan, ReductionTransform,
10    SolveComponentPlan, SolveRowMatrixPlan,
11};
12#[cfg(test)]
13use laddu_data::data::accurate::AccurateComplex64;
14use laddu_data::{
15    data::accurate::AccurateF64,
16    data::{CacheStorage, Dataset, EventBatch, MemoryPolicy},
17    schema::Schema,
18};
19use laddu_expr::{
20    BinaryOp, ExprGraph, ExprId, ExprNode, P4Component, UnaryOp, ValueKind,
21    parameters::{ParamId, ParamLayout, ParamValues},
22};
23use laddu_kernel::ir::{
24    GradientKernelIr, KernelInstruction, KernelValue, KernelValueClass, KernelValueId,
25    KernelValueKind, OutputComponent, ScalarKernelIr,
26};
27use nalgebra::{DMatrix, DVector, Dyn, LU};
28use num::{
29    complex::{Complex, Complex32, Complex64},
30    traits::Float,
31};
32use rayon::prelude::*;
33
34use crate::{
35    JitPolicy, MemoryDecision, MemoryLease, Precision, RuntimeError, RuntimeResult,
36    execution::Execution,
37};
38
39mod gradient_interpreter;
40use gradient_interpreter::GradientInterpreter;
41
42#[cfg(feature = "jit")]
43use crate::jit::{JitCacheView, JitGradientKernel, JitPrecision, JitScalarKernel};
44
45const SCALAR_BLOCK_SIZE: usize = 32;
46
47/// Supplies event-dependent scalar values for direct CPU evaluation.
48pub trait EventLookup {
49    /// Returns the scalar named `name`, or `None` when it is unavailable.
50    fn scalar(&self, name: &str) -> Option<f64>;
51
52    /// Returns one component of a named four-momentum.
53    fn p4_component(&self, name: &str, component: P4Component) -> Option<f64> {
54        let key = format!("{}.{}", name, component.label());
55        self.scalar(&key)
56    }
57}
58
59impl<F> EventLookup for F
60where
61    F: for<'a> Fn(&'a str) -> Option<f64>,
62{
63    fn scalar(&self, name: &str) -> Option<f64> {
64        self(name)
65    }
66}
67
68impl EventLookup for HashMap<String, f64> {
69    fn scalar(&self, name: &str) -> Option<f64> {
70        self.get(name).copied()
71    }
72}
73
74/// Prepares compiled models for CPU execution.
75#[derive(Clone, Debug, Default)]
76pub struct CpuBackend;
77
78/// CPU scalar-kernel execution strategy.
79#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
80pub enum CpuExecutionMode {
81    /// Prefer JIT execution when available and fall back to interpretation.
82    #[default]
83    Auto,
84    /// Always interpret the scalar kernel.
85    Interpreter,
86}
87
88/// A compiled model prepared for CPU evaluation.
89#[derive(Clone, Debug)]
90pub struct CpuPlan {
91    precision: Precision,
92    graph: ExprGraph,
93    params: ParamLayout,
94    parameter_slots: Vec<Option<ParamId>>,
95    autodiff: AutodiffPlan,
96    cache_plan: CachePlan,
97    cache_slots: Vec<Option<usize>>,
98    cached_evaluation_nodes: Vec<ExprId>,
99    cached_value_slots: Vec<Option<usize>>,
100    scalar_kernel: Option<ScalarKernelIr>,
101    scalar_executor: Option<ScalarExecutor>,
102    #[cfg_attr(not(feature = "jit"), allow(dead_code))]
103    gradient_executor: GradientExecutor,
104    // Direct EventLookup evaluation cannot use block JIT kernels, so f32 plans retain
105    // an interpreter fallback even when cached and invariant gradients use the JIT.
106    f32_gradient_fallback_real: Option<GradientKernelIr>,
107    f32_gradient_fallback_imag: Option<GradientKernelIr>,
108    cache_materialization_nodes: Vec<ExprId>,
109    solve_components: Vec<Option<SolveComponentPlan>>,
110    solve_rhs_elements: Vec<Option<Vec<ExprId>>>,
111    solve_row_matrices: Vec<SolveRowMatrixPlan>,
112    solve_row_keys: Vec<(ExprId, usize, usize)>,
113    factor_matrix_slots: Vec<Option<usize>>,
114    factor_matrices: Vec<(ExprId, usize)>,
115    constant_factor_slots: Vec<Option<usize>>,
116    constant_factors: Vec<Arc<OnceLock<DynamicLu>>>,
117}
118
119#[derive(Clone, Debug)]
120enum ScalarExecutor {
121    Interpreter(ScalarEvaluationPlan),
122    #[cfg(feature = "jit")]
123    Jit(JitScalarKernel),
124}
125
126impl ScalarExecutor {
127    fn prepare(
128        plan: &ScalarKernelIr,
129        mode: CpuExecutionMode,
130        precision: Precision,
131    ) -> Option<Self> {
132        #[cfg(not(feature = "jit"))]
133        let _ = precision;
134        match mode {
135            CpuExecutionMode::Auto => {
136                #[cfg(feature = "jit")]
137                {
138                    let jit_precision = match precision {
139                        Precision::F32 => JitPrecision::F32,
140                        Precision::Auto | Precision::F64 => JitPrecision::F64,
141                    };
142                    if let Ok(Some(kernel)) =
143                        JitScalarKernel::compile_with_precision(plan, jit_precision)
144                    {
145                        return Some(Self::Jit(kernel));
146                    }
147                }
148                ScalarEvaluationPlan::from_kernel_ir(plan).map(Self::Interpreter)
149            }
150            CpuExecutionMode::Interpreter => {
151                ScalarEvaluationPlan::from_kernel_ir(plan).map(Self::Interpreter)
152            }
153        }
154    }
155}
156
157#[derive(Clone, Debug)]
158enum GradientExecutor {
159    Interpreter(Option<GradientInterpreter>),
160    #[cfg(feature = "jit")]
161    Jit(JitGradientKernel),
162}
163
164impl GradientExecutor {
165    fn prepare(
166        plan: Option<&ScalarKernelIr>,
167        params: &ParamLayout,
168        mode: CpuExecutionMode,
169        precision: Precision,
170        gradient_ir: Option<(&GradientKernelIr, Option<&GradientKernelIr>)>,
171    ) -> AutodiffResult<Self> {
172        #[cfg(not(feature = "jit"))]
173        let _ = (plan, params, mode, precision, gradient_ir);
174        #[cfg(feature = "jit")]
175        if mode == CpuExecutionMode::Auto
176            && let Some(plan) = plan
177            && let Ok(kernel) = (if let Some((real, imag)) = gradient_ir {
178                JitGradientKernel::compile_gradient_ir(real, imag, JitPrecision::F32)
179            } else {
180                JitGradientKernel::compile_with_precision(
181                    plan,
182                    params.free_params(),
183                    match precision {
184                        Precision::F32 => JitPrecision::F32,
185                        Precision::Auto | Precision::F64 => JitPrecision::F64,
186                    },
187                )
188                .and_then(|kernel| kernel.ok_or_else(|| "missing gradient kernel".into()))
189            })
190        {
191            return Ok(Self::Jit(kernel));
192        }
193        Ok(Self::Interpreter(
194            plan.map(|plan| GradientInterpreter::new(plan, params.free_params()))
195                .transpose()?,
196        ))
197    }
198}
199
200#[derive(Clone, Debug)]
201struct ScalarEvaluationPlan {
202    invariant_instructions: Vec<ScalarInvariantInstruction>,
203    invariant_real_slot_count: usize,
204    invariant_complex_slot_count: usize,
205    event_instructions: Vec<ScalarEventInstruction>,
206    event_real_slot_count: usize,
207    event_complex_slot_count: usize,
208    outputs: Vec<ScalarOperand>,
209}
210
211impl ScalarEvaluationPlan {
212    fn from_kernel_ir(ir: &ScalarKernelIr) -> Option<Self> {
213        Self::from_kernel_values(ir.values(), &[ir.root()])
214    }
215
216    fn from_kernel_values(values: &[KernelValue], outputs: &[KernelValueId]) -> Option<Self> {
217        let mut required = vec![false; values.len()];
218        let mut pending = outputs.to_vec();
219        while let Some(id) = pending.pop() {
220            if required[id.index()] {
221                continue;
222            }
223            required[id.index()] = true;
224            pending.extend(values[id.index()].instruction.operands());
225        }
226        let mut operands = Vec::with_capacity(values.len());
227        let mut invariant_instructions = Vec::new();
228        let mut invariant_real_slots = 0;
229        let mut invariant_complex_slots = 0;
230        let mut event_instructions = Vec::new();
231
232        for (index, value) in values.iter().enumerate() {
233            if !required[index] {
234                operands.push(None);
235                continue;
236            }
237            if !matches!(value.kind, KernelValueKind::Real | KernelValueKind::Complex) {
238                return None;
239            }
240            let instruction = ScalarInstruction::from_kernel(&value.instruction, &operands);
241            let operand = match value.class {
242                KernelValueClass::Invariant => match value.kind {
243                    KernelValueKind::Real => {
244                        let slot = invariant_real_slots;
245                        invariant_real_slots += 1;
246                        invariant_instructions.push((ScalarSlot::Real(slot), instruction));
247                        ScalarOperand::InvariantReal(slot)
248                    }
249                    KernelValueKind::Complex => {
250                        let slot = invariant_complex_slots;
251                        invariant_complex_slots += 1;
252                        invariant_instructions.push((ScalarSlot::Complex(slot), instruction));
253                        ScalarOperand::InvariantComplex(slot)
254                    }
255                    KernelValueKind::Vector { .. } | KernelValueKind::Matrix { .. } => {
256                        unreachable!("aggregate values were rejected before scalar lowering")
257                    }
258                },
259                KernelValueClass::Event => {
260                    let slot = event_instructions.len();
261                    let output = match value.kind {
262                        KernelValueKind::Real => ScalarSlot::Real(slot),
263                        KernelValueKind::Complex => ScalarSlot::Complex(slot),
264                        KernelValueKind::Vector { .. } | KernelValueKind::Matrix { .. } => {
265                            unreachable!("aggregate values were rejected before scalar lowering")
266                        }
267                    };
268                    event_instructions.push((output, instruction));
269                    match value.kind {
270                        KernelValueKind::Real => ScalarOperand::EventReal(slot),
271                        KernelValueKind::Complex => ScalarOperand::EventComplex(slot),
272                        KernelValueKind::Vector { .. } | KernelValueKind::Matrix { .. } => {
273                            unreachable!("aggregate values were rejected before scalar lowering")
274                        }
275                    }
276                }
277            };
278            operands.push(Some(operand));
279        }
280
281        Some(Self::new(
282            invariant_instructions,
283            event_instructions,
284            outputs
285                .iter()
286                .map(|output| operands[output.index()].expect("kernel output is required"))
287                .collect(),
288            invariant_real_slots,
289            invariant_complex_slots,
290        ))
291    }
292
293    fn new(
294        invariant_instructions: Vec<(ScalarSlot, ScalarInstruction)>,
295        event_instructions: Vec<(ScalarSlot, ScalarInstruction)>,
296        outputs: Vec<ScalarOperand>,
297        invariant_real_slot_count: usize,
298        invariant_complex_slot_count: usize,
299    ) -> Self {
300        let mut last_use = vec![0; event_instructions.len()];
301        for (index, (_, instruction)) in event_instructions.iter().enumerate() {
302            instruction.record_event_uses(&mut last_use, index);
303        }
304        for output in &outputs {
305            output.record_event_use(&mut last_use, event_instructions.len());
306        }
307
308        let mut logical_to_physical = vec![usize::MAX; event_instructions.len()];
309        let mut free_real_slots = Vec::new();
310        let mut free_complex_slots = Vec::new();
311        let mut next_real_slot = 0;
312        let mut next_complex_slot = 0;
313        let mut slotted_instructions = Vec::with_capacity(event_instructions.len());
314
315        for (index, (output, instruction)) in event_instructions.into_iter().enumerate() {
316            let output_slot = match output {
317                ScalarSlot::Real(_) => {
318                    let slot = if let Some(slot) = free_real_slots.pop() {
319                        slot
320                    } else {
321                        let slot = next_real_slot;
322                        next_real_slot += 1;
323                        slot
324                    };
325                    ScalarSlot::Real(slot)
326                }
327                ScalarSlot::Complex(_) => {
328                    let slot = if let Some(slot) = free_complex_slots.pop() {
329                        slot
330                    } else {
331                        let slot = next_complex_slot;
332                        next_complex_slot += 1;
333                        slot
334                    };
335                    ScalarSlot::Complex(slot)
336                }
337            };
338            logical_to_physical[index] = output_slot.index();
339
340            let mut event_inputs = Vec::new();
341            instruction.collect_event_slots(&mut event_inputs);
342            event_inputs.sort_unstable();
343            event_inputs.dedup();
344
345            slotted_instructions.push(ScalarEventInstruction {
346                output_slot,
347                instruction: instruction.remap_event_operands(&logical_to_physical),
348            });
349
350            for input in event_inputs {
351                if last_use[input] == index {
352                    match output_slot_for_event(&slotted_instructions, input) {
353                        ScalarSlot::Real(slot) => free_real_slots.push(slot),
354                        ScalarSlot::Complex(slot) => free_complex_slots.push(slot),
355                    }
356                }
357            }
358        }
359
360        Self {
361            invariant_instructions: invariant_instructions
362                .into_iter()
363                .map(|(output_slot, instruction)| ScalarInvariantInstruction {
364                    output_slot,
365                    instruction,
366                })
367                .collect(),
368            invariant_real_slot_count,
369            invariant_complex_slot_count,
370            event_instructions: slotted_instructions,
371            event_real_slot_count: next_real_slot,
372            event_complex_slot_count: next_complex_slot,
373            outputs: outputs
374                .into_iter()
375                .map(|output| output.remap_event(&logical_to_physical))
376                .collect(),
377        }
378    }
379
380    fn root(&self) -> ScalarOperand {
381        self.outputs[0]
382    }
383}
384
385fn output_slot_for_event(
386    instructions: &[ScalarEventInstruction],
387    logical_slot: usize,
388) -> ScalarSlot {
389    instructions
390        .get(logical_slot)
391        .map(|instruction| instruction.output_slot)
392        .expect("event input is produced by an earlier event instruction")
393}
394
395#[derive(Copy, Clone, Debug)]
396enum ScalarSlot {
397    Real(usize),
398    Complex(usize),
399}
400
401impl ScalarSlot {
402    fn index(self) -> usize {
403        match self {
404            Self::Real(slot) | Self::Complex(slot) => slot,
405        }
406    }
407}
408
409#[derive(Clone, Default)]
410struct ScalarInvariantValues {
411    real: Vec<f64>,
412    complex: Vec<Complex64>,
413}
414
415#[derive(Clone, Default)]
416struct ScalarEventWorkspace {
417    real: Vec<[f64; SCALAR_BLOCK_SIZE]>,
418    complex: Vec<[Complex64; SCALAR_BLOCK_SIZE]>,
419}
420
421#[derive(Copy, Clone, Debug)]
422enum ScalarOperand {
423    InvariantReal(usize),
424    InvariantComplex(usize),
425    EventReal(usize),
426    EventComplex(usize),
427}
428
429impl ScalarOperand {
430    fn complex_value(
431        self,
432        invariant: &ScalarInvariantValues,
433        event: &ScalarEventWorkspace,
434    ) -> Complex64 {
435        match self {
436            Self::InvariantReal(slot) => Complex64::from(invariant.real[slot]),
437            Self::InvariantComplex(slot) => invariant.complex[slot],
438            Self::EventReal(slot) => Complex64::from(event.real[slot][0]),
439            Self::EventComplex(slot) => event.complex[slot][0],
440        }
441    }
442
443    fn real_value(self, invariant: &ScalarInvariantValues, event: &ScalarEventWorkspace) -> f64 {
444        match self {
445            Self::InvariantReal(slot) => invariant.real[slot],
446            Self::InvariantComplex(slot) => invariant.complex[slot].re,
447            Self::EventReal(slot) => event.real[slot][0],
448            Self::EventComplex(slot) => event.complex[slot][0].re,
449        }
450    }
451
452    fn block_complex_value(
453        self,
454        invariant: &ScalarInvariantValues,
455        event: &ScalarEventWorkspace,
456        lane: usize,
457    ) -> Complex64 {
458        match self {
459            Self::InvariantReal(slot) => Complex64::from(invariant.real[slot]),
460            Self::InvariantComplex(slot) => invariant.complex[slot],
461            Self::EventReal(slot) => Complex64::from(event.real[slot][lane]),
462            Self::EventComplex(slot) => event.complex[slot][lane],
463        }
464    }
465
466    fn block_real_value(
467        self,
468        invariant: &ScalarInvariantValues,
469        event: &ScalarEventWorkspace,
470        lane: usize,
471    ) -> f64 {
472        match self {
473            Self::InvariantReal(slot) => invariant.real[slot],
474            Self::InvariantComplex(slot) => invariant.complex[slot].re,
475            Self::EventReal(slot) => event.real[slot][lane],
476            Self::EventComplex(slot) => event.complex[slot][lane].re,
477        }
478    }
479
480    fn collect_event_slot(self, slots: &mut Vec<usize>) {
481        if let Self::EventReal(slot) | Self::EventComplex(slot) = self {
482            slots.push(slot);
483        }
484    }
485
486    fn record_event_use(self, last_use: &mut [usize], instruction_index: usize) {
487        if let Self::EventReal(slot) | Self::EventComplex(slot) = self {
488            last_use[slot] = instruction_index;
489        }
490    }
491
492    fn remap_event(self, logical_to_physical: &[usize]) -> Self {
493        match self {
494            Self::InvariantReal(slot) => Self::InvariantReal(slot),
495            Self::InvariantComplex(slot) => Self::InvariantComplex(slot),
496            Self::EventReal(slot) => Self::EventReal(logical_to_physical[slot]),
497            Self::EventComplex(slot) => Self::EventComplex(logical_to_physical[slot]),
498        }
499    }
500}
501
502#[derive(Clone, Debug)]
503enum OperandRun {
504    InvariantReal(Vec<usize>),
505    InvariantComplex(Vec<usize>),
506    EventReal(Vec<usize>),
507    EventComplex(Vec<usize>),
508}
509
510impl OperandRun {
511    fn from_operands(operands: impl IntoIterator<Item = ScalarOperand>) -> Vec<Self> {
512        let mut runs = Vec::new();
513        for operand in operands {
514            match (runs.last_mut(), operand) {
515                (Some(Self::InvariantReal(slots)), ScalarOperand::InvariantReal(slot))
516                | (Some(Self::InvariantComplex(slots)), ScalarOperand::InvariantComplex(slot))
517                | (Some(Self::EventReal(slots)), ScalarOperand::EventReal(slot))
518                | (Some(Self::EventComplex(slots)), ScalarOperand::EventComplex(slot)) => {
519                    slots.push(slot)
520                }
521                (_, ScalarOperand::InvariantReal(slot)) => {
522                    runs.push(Self::InvariantReal(vec![slot]))
523                }
524                (_, ScalarOperand::InvariantComplex(slot)) => {
525                    runs.push(Self::InvariantComplex(vec![slot]))
526                }
527                (_, ScalarOperand::EventReal(slot)) => runs.push(Self::EventReal(vec![slot])),
528                (_, ScalarOperand::EventComplex(slot)) => runs.push(Self::EventComplex(vec![slot])),
529            }
530        }
531        runs
532    }
533
534    fn add_to_complex(
535        &self,
536        value: &mut Complex64,
537        invariant: &ScalarInvariantValues,
538        event: &ScalarEventWorkspace,
539    ) {
540        match self {
541            Self::InvariantReal(slots) => {
542                for slot in slots {
543                    *value += invariant.real[*slot];
544                }
545            }
546            Self::InvariantComplex(slots) => {
547                for slot in slots {
548                    *value += invariant.complex[*slot];
549                }
550            }
551            Self::EventReal(slots) => {
552                for slot in slots {
553                    *value += event.real[*slot][0];
554                }
555            }
556            Self::EventComplex(slots) => {
557                for slot in slots {
558                    *value += event.complex[*slot][0];
559                }
560            }
561        }
562    }
563
564    fn add_to_real(
565        &self,
566        value: &mut f64,
567        invariant: &ScalarInvariantValues,
568        event: &ScalarEventWorkspace,
569    ) {
570        match self {
571            Self::InvariantReal(slots) => {
572                for slot in slots {
573                    *value += invariant.real[*slot];
574                }
575            }
576            Self::EventReal(slots) => {
577                for slot in slots {
578                    *value += event.real[*slot][0];
579                }
580            }
581            Self::InvariantComplex(_) | Self::EventComplex(_) => {
582                unreachable!("complex operand appeared in real add instruction")
583            }
584        }
585    }
586
587    fn multiply_into_complex(
588        &self,
589        value: &mut Complex64,
590        invariant: &ScalarInvariantValues,
591        event: &ScalarEventWorkspace,
592    ) {
593        match self {
594            Self::InvariantReal(slots) => {
595                for slot in slots {
596                    *value *= invariant.real[*slot];
597                }
598            }
599            Self::InvariantComplex(slots) => {
600                for slot in slots {
601                    *value *= invariant.complex[*slot];
602                }
603            }
604            Self::EventReal(slots) => {
605                for slot in slots {
606                    *value *= event.real[*slot][0];
607                }
608            }
609            Self::EventComplex(slots) => {
610                for slot in slots {
611                    *value *= event.complex[*slot][0];
612                }
613            }
614        }
615    }
616
617    fn multiply_into_real(
618        &self,
619        value: &mut f64,
620        invariant: &ScalarInvariantValues,
621        event: &ScalarEventWorkspace,
622    ) {
623        match self {
624            Self::InvariantReal(slots) => {
625                for slot in slots {
626                    *value *= invariant.real[*slot];
627                }
628            }
629            Self::EventReal(slots) => {
630                for slot in slots {
631                    *value *= event.real[*slot][0];
632                }
633            }
634            Self::InvariantComplex(_) | Self::EventComplex(_) => {
635                unreachable!("complex operand appeared in real multiply instruction")
636            }
637        }
638    }
639
640    fn collect_event_slots(&self, slots: &mut Vec<usize>) {
641        if let Self::EventReal(event_slots) | Self::EventComplex(event_slots) = self {
642            slots.extend(event_slots);
643        }
644    }
645
646    fn record_event_uses(&self, last_use: &mut [usize], instruction_index: usize) {
647        if let Self::EventReal(event_slots) | Self::EventComplex(event_slots) = self {
648            for slot in event_slots {
649                last_use[*slot] = instruction_index;
650            }
651        }
652    }
653
654    fn remap_events(&self, logical_to_physical: &[usize]) -> Self {
655        match self {
656            Self::InvariantReal(slots) => Self::InvariantReal(slots.clone()),
657            Self::InvariantComplex(slots) => Self::InvariantComplex(slots.clone()),
658            Self::EventReal(slots) => Self::EventReal(
659                slots
660                    .iter()
661                    .map(|slot| logical_to_physical[*slot])
662                    .collect(),
663            ),
664            Self::EventComplex(slots) => Self::EventComplex(
665                slots
666                    .iter()
667                    .map(|slot| logical_to_physical[*slot])
668                    .collect(),
669            ),
670        }
671    }
672}
673
674#[derive(Clone, Debug)]
675enum ScalarInstruction {
676    Cached(usize),
677    Constant(Complex64),
678    Parameter(ParamId),
679    Unary {
680        op: UnaryOp,
681        input: ScalarOperand,
682    },
683    Binary {
684        op: BinaryOp,
685        lhs: ScalarOperand,
686        rhs: ScalarOperand,
687    },
688    Add(Vec<OperandRun>),
689    Mul(Vec<OperandRun>),
690    Complex {
691        re: ScalarOperand,
692        im: ScalarOperand,
693    },
694    SolveRow {
695        row_slot: usize,
696        rhs: Vec<ScalarOperand>,
697    },
698    SolveRowAdjointElement {
699        row_slot: usize,
700        index: usize,
701        len: usize,
702        adjoint: ScalarOperand,
703    },
704}
705
706impl ScalarInstruction {
707    fn from_kernel(instruction: &KernelInstruction, operands: &[Option<ScalarOperand>]) -> Self {
708        let operand = |id: KernelValueId| {
709            operands[id.index()].expect("required instruction operand was lowered")
710        };
711        match instruction {
712            KernelInstruction::Cached(slot) => Self::Cached(*slot),
713            KernelInstruction::RealConstant(value) => Self::Constant(Complex64::from(*value)),
714            KernelInstruction::ComplexConstant(value) => Self::Constant(*value),
715            KernelInstruction::Parameter(id) => Self::Parameter(*id),
716            KernelInstruction::Unary { op, input } => Self::Unary {
717                op: *op,
718                input: operand(*input),
719            },
720            KernelInstruction::Binary { op, lhs, rhs } => Self::Binary {
721                op: *op,
722                lhs: operand(*lhs),
723                rhs: operand(*rhs),
724            },
725            KernelInstruction::Add(terms) => Self::Add(OperandRun::from_operands(
726                terms.iter().map(|id| operand(*id)),
727            )),
728            KernelInstruction::Mul(factors) => Self::Mul(OperandRun::from_operands(
729                factors.iter().map(|id| operand(*id)),
730            )),
731            KernelInstruction::Complex { re, im } => Self::Complex {
732                re: operand(*re),
733                im: operand(*im),
734            },
735            KernelInstruction::SolveRow { row_slot, rhs } => Self::SolveRow {
736                row_slot: *row_slot,
737                rhs: rhs.iter().map(|id| operand(*id)).collect(),
738            },
739            KernelInstruction::SolveRowAdjointElement {
740                row_slot,
741                index,
742                len,
743                adjoint,
744            } => Self::SolveRowAdjointElement {
745                row_slot: *row_slot,
746                index: *index,
747                len: *len,
748                adjoint: operand(*adjoint),
749            },
750            KernelInstruction::Vector(_)
751            | KernelInstruction::Matrix { .. }
752            | KernelInstruction::Component { .. }
753            | KernelInstruction::MatrixElement { .. }
754            | KernelInstruction::MatMul { .. }
755            | KernelInstruction::MatVec { .. }
756            | KernelInstruction::Dot { .. }
757            | KernelInstruction::Solve { .. } => {
758                unreachable!("aggregate instruction cannot enter the scalar interpreter")
759            }
760        }
761    }
762
763    fn collect_event_slots(&self, slots: &mut Vec<usize>) {
764        match self {
765            Self::Cached(_) | Self::Constant(_) | Self::Parameter(_) => {}
766            Self::Unary { input, .. } => input.collect_event_slot(slots),
767            Self::Binary { lhs, rhs, .. } => {
768                lhs.collect_event_slot(slots);
769                rhs.collect_event_slot(slots);
770            }
771            Self::Add(runs) | Self::Mul(runs) => {
772                for run in runs {
773                    run.collect_event_slots(slots);
774                }
775            }
776            Self::Complex { re, im } => {
777                re.collect_event_slot(slots);
778                im.collect_event_slot(slots);
779            }
780            Self::SolveRow { rhs, .. } => {
781                for operand in rhs {
782                    operand.collect_event_slot(slots);
783                }
784            }
785            Self::SolveRowAdjointElement { adjoint, .. } => {
786                adjoint.collect_event_slot(slots);
787            }
788        }
789    }
790
791    fn record_event_uses(&self, last_use: &mut [usize], instruction_index: usize) {
792        match self {
793            Self::Cached(_) | Self::Constant(_) | Self::Parameter(_) => {}
794            Self::Unary { input, .. } => input.record_event_use(last_use, instruction_index),
795            Self::Binary { lhs, rhs, .. } => {
796                lhs.record_event_use(last_use, instruction_index);
797                rhs.record_event_use(last_use, instruction_index);
798            }
799            Self::Add(runs) | Self::Mul(runs) => {
800                for run in runs {
801                    run.record_event_uses(last_use, instruction_index);
802                }
803            }
804            Self::Complex { re, im } => {
805                re.record_event_use(last_use, instruction_index);
806                im.record_event_use(last_use, instruction_index);
807            }
808            Self::SolveRow { rhs, .. } => {
809                for operand in rhs {
810                    operand.record_event_use(last_use, instruction_index);
811                }
812            }
813            Self::SolveRowAdjointElement { adjoint, .. } => {
814                adjoint.record_event_use(last_use, instruction_index);
815            }
816        }
817    }
818
819    fn remap_event_operands(self, logical_to_physical: &[usize]) -> Self {
820        match self {
821            Self::Cached(slot) => Self::Cached(slot),
822            Self::Constant(value) => Self::Constant(value),
823            Self::Parameter(id) => Self::Parameter(id),
824            Self::Unary { op, input } => Self::Unary {
825                op,
826                input: input.remap_event(logical_to_physical),
827            },
828            Self::Binary { op, lhs, rhs } => Self::Binary {
829                op,
830                lhs: lhs.remap_event(logical_to_physical),
831                rhs: rhs.remap_event(logical_to_physical),
832            },
833            Self::Add(runs) => Self::Add(
834                runs.iter()
835                    .map(|run| run.remap_events(logical_to_physical))
836                    .collect(),
837            ),
838            Self::Mul(runs) => Self::Mul(
839                runs.iter()
840                    .map(|run| run.remap_events(logical_to_physical))
841                    .collect(),
842            ),
843            Self::Complex { re, im } => Self::Complex {
844                re: re.remap_event(logical_to_physical),
845                im: im.remap_event(logical_to_physical),
846            },
847            Self::SolveRow { row_slot, rhs } => Self::SolveRow {
848                row_slot,
849                rhs: rhs
850                    .iter()
851                    .map(|operand| operand.remap_event(logical_to_physical))
852                    .collect(),
853            },
854            Self::SolveRowAdjointElement {
855                row_slot,
856                index,
857                len,
858                adjoint,
859            } => Self::SolveRowAdjointElement {
860                row_slot,
861                index,
862                len,
863                adjoint: adjoint.remap_event(logical_to_physical),
864            },
865        }
866    }
867
868    fn evaluate_real(
869        &self,
870        params: Option<&ParamValues>,
871        cache: Option<(&CpuBatchCache, usize)>,
872        invariant: &ScalarInvariantValues,
873        event: &ScalarEventWorkspace,
874    ) -> RuntimeResult<f64> {
875        Ok(match self {
876            Self::Cached(slot) => {
877                cache
878                    .expect("cached instruction requires an event cache")
879                    .0
880                    .scalar(
881                        *slot,
882                        cache.expect("cached instruction requires an event cache").1,
883                    )?
884                    .re
885            }
886            Self::Constant(value) => value.re,
887            Self::Parameter(id) => params
888                .expect("parameter instruction requires parameter values")
889                .get(*id)
890                .map_err(|err| RuntimeError::Parameter(err.to_string()))?,
891            Self::Unary { op, input } => match op {
892                UnaryOp::Neg => -input.real_value(invariant, event),
893                UnaryOp::Real | UnaryOp::Conj => input.complex_value(invariant, event).re,
894                UnaryOp::Imag => input.complex_value(invariant, event).im,
895                UnaryOp::NormSqr => input.complex_value(invariant, event).norm_sqr(),
896                UnaryOp::Sqrt => input.real_value(invariant, event).sqrt(),
897                UnaryOp::Exp => input.real_value(invariant, event).exp(),
898                UnaryOp::Sin => input.real_value(invariant, event).sin(),
899                UnaryOp::Cos => input.real_value(invariant, event).cos(),
900                UnaryOp::Log => input.real_value(invariant, event).ln(),
901                UnaryOp::PowI(power) => input.real_value(invariant, event).powi(*power),
902            },
903            Self::Binary { op, lhs, rhs } => {
904                let lhs = lhs.real_value(invariant, event);
905                let rhs = rhs.real_value(invariant, event);
906                match op {
907                    BinaryOp::Add => lhs + rhs,
908                    BinaryOp::Sub => lhs - rhs,
909                    BinaryOp::Mul => lhs * rhs,
910                    BinaryOp::Div => lhs / rhs,
911                    BinaryOp::Atan2 => lhs.atan2(rhs),
912                }
913            }
914            Self::Add(runs) => {
915                let mut value = 0.0;
916                for run in runs {
917                    run.add_to_real(&mut value, invariant, event);
918                }
919                value
920            }
921            Self::Mul(runs) => {
922                let mut value = 1.0;
923                for run in runs {
924                    run.multiply_into_real(&mut value, invariant, event);
925                }
926                value
927            }
928            Self::Complex { .. } | Self::SolveRow { .. } | Self::SolveRowAdjointElement { .. } => {
929                unreachable!("complex-only instruction appeared in real scalar slot")
930            }
931        })
932    }
933
934    fn evaluate_complex(
935        &self,
936        params: Option<&ParamValues>,
937        cache: Option<(&CpuBatchCache, usize)>,
938        invariant: &ScalarInvariantValues,
939        event: &ScalarEventWorkspace,
940    ) -> RuntimeResult<Complex64> {
941        Ok(match self {
942            Self::Cached(slot) => cache
943                .expect("cached instruction requires an event cache")
944                .0
945                .scalar(
946                    *slot,
947                    cache.expect("cached instruction requires an event cache").1,
948                )?,
949            Self::Constant(value) => *value,
950            Self::Parameter(id) => Complex64::from(
951                params
952                    .expect("parameter instruction requires parameter values")
953                    .get(*id)
954                    .map_err(|err| RuntimeError::Parameter(err.to_string()))?,
955            ),
956            Self::Unary { op, input } => eval_unary(*op, input.complex_value(invariant, event)),
957            Self::Binary { op, lhs, rhs } => eval_binary(
958                *op,
959                lhs.complex_value(invariant, event),
960                rhs.complex_value(invariant, event),
961            ),
962            Self::Add(runs) => {
963                let mut value = Complex64::ZERO;
964                for run in runs {
965                    run.add_to_complex(&mut value, invariant, event);
966                }
967                value
968            }
969            Self::Mul(runs) => {
970                let mut value = Complex64::ONE;
971                for run in runs {
972                    run.multiply_into_complex(&mut value, invariant, event);
973                }
974                value
975            }
976            Self::Complex { re, im } => Complex64::new(
977                re.real_value(invariant, event),
978                im.real_value(invariant, event),
979            ),
980            Self::SolveRow { row_slot, rhs } => {
981                let (cache, row) = cache.expect("solve row instruction requires an event cache");
982                let inverse_row = cache.solve_row(*row_slot, row)?;
983                if inverse_row.len() != rhs.len() {
984                    return Err(RuntimeError::InvalidShape {
985                        index: row,
986                        message: format!(
987                            "specialized solve row has len {}, expected {}",
988                            inverse_row.len(),
989                            rhs.len()
990                        ),
991                    });
992                }
993                inverse_row
994                    .iter()
995                    .zip(rhs)
996                    .map(|(lhs, operand)| lhs * operand.complex_value(invariant, event))
997                    .sum()
998            }
999            Self::SolveRowAdjointElement {
1000                row_slot,
1001                index,
1002                len,
1003                adjoint,
1004            } => {
1005                let (cache, row) =
1006                    cache.expect("solve-row adjoint instruction requires an event cache");
1007                let inverse_row = cache.solve_row(*row_slot, row)?;
1008                if inverse_row.len() != *len {
1009                    return Err(RuntimeError::InvalidShape {
1010                        index: row,
1011                        message: format!(
1012                            "specialized solve row has len {}, expected {len}",
1013                            inverse_row.len()
1014                        ),
1015                    });
1016                }
1017                adjoint.complex_value(invariant, event) * inverse_row[*index].conj()
1018            }
1019        })
1020    }
1021}
1022
1023#[derive(Clone, Debug)]
1024struct ScalarEventInstruction {
1025    output_slot: ScalarSlot,
1026    instruction: ScalarInstruction,
1027}
1028
1029#[derive(Clone, Debug)]
1030struct ScalarInvariantInstruction {
1031    output_slot: ScalarSlot,
1032    instruction: ScalarInstruction,
1033}
1034
1035impl CpuBackend {
1036    /// Prepares a model using the policies resolved by an execution context.
1037    ///
1038    /// # Errors
1039    ///
1040    /// Returns [`RuntimeError`] when model lowering or differentiation fails,
1041    /// or the requested precision is unsupported for the model.
1042    pub fn prepare_for_execution(
1043        &self,
1044        model: &CompiledModel,
1045        execution: &Execution,
1046    ) -> RuntimeResult<CpuPlan> {
1047        let mode = match execution.jit_policy() {
1048            JitPolicy::Auto | JitPolicy::Enabled => CpuExecutionMode::Auto,
1049            JitPolicy::Disabled => CpuExecutionMode::Interpreter,
1050        };
1051        let plan = self
1052            .prepare_with_modes_precision(
1053                model,
1054                execution.autodiff_mode(),
1055                mode,
1056                execution.precision(),
1057            )
1058            .map_err(|error| RuntimeError::Data(error.to_string()))?;
1059        if execution.precision() == Precision::F32 && !plan.supports_f32_scalar_execution() {
1060            return Err(crate::ExecutionError::UnsupportedCpuF32Model.into());
1061        }
1062        Ok(plan)
1063    }
1064
1065    /// Prepares a model with forward autodiff and automatic execution-mode selection.
1066    ///
1067    /// # Panics
1068    ///
1069    /// Panics if forward differentiation or executable-plan construction fails
1070    /// for the compiled model.
1071    pub fn prepare(&self, model: &CompiledModel) -> CpuPlan {
1072        self.prepare_with_modes(model, AutodiffMode::Forward, CpuExecutionMode::Auto)
1073            .expect("forward autodiff supports every compiled expression node")
1074    }
1075
1076    /// Prepares a model with an explicit scalar-kernel execution mode.
1077    ///
1078    /// # Panics
1079    ///
1080    /// Panics if forward differentiation or executable-plan construction fails
1081    /// for the compiled model.
1082    pub fn prepare_with_execution_mode(
1083        &self,
1084        model: &CompiledModel,
1085        execution_mode: CpuExecutionMode,
1086    ) -> CpuPlan {
1087        self.prepare_with_modes(model, AutodiffMode::Forward, execution_mode)
1088            .expect("forward autodiff supports every compiled expression node")
1089    }
1090
1091    /// Prepares a model with an explicit automatic-differentiation mode.
1092    ///
1093    /// # Errors
1094    ///
1095    /// Returns [`laddu_autodiff::AutodiffError`] when model lowering or
1096    /// differentiation fails.
1097    pub fn prepare_with_autodiff_mode(
1098        &self,
1099        model: &CompiledModel,
1100        mode: AutodiffMode,
1101    ) -> AutodiffResult<CpuPlan> {
1102        self.prepare_with_modes(model, mode, CpuExecutionMode::Auto)
1103    }
1104
1105    /// Prepares a model with explicit autodiff and scalar execution modes.
1106    ///
1107    /// # Errors
1108    ///
1109    /// Returns [`laddu_autodiff::AutodiffError`] when model lowering or
1110    /// differentiation fails.
1111    pub fn prepare_with_modes(
1112        &self,
1113        model: &CompiledModel,
1114        autodiff_mode: AutodiffMode,
1115        execution_mode: CpuExecutionMode,
1116    ) -> AutodiffResult<CpuPlan> {
1117        self.prepare_with_modes_precision(model, autodiff_mode, execution_mode, Precision::F64)
1118    }
1119
1120    fn prepare_with_modes_precision(
1121        &self,
1122        model: &CompiledModel,
1123        autodiff_mode: AutodiffMode,
1124        execution_mode: CpuExecutionMode,
1125        precision: Precision,
1126    ) -> AutodiffResult<CpuPlan> {
1127        let executable = ExecutablePlan::from_model(model)
1128            .map_err(|error| laddu_autodiff::AutodiffError::InvalidKernel(error.to_string()))?;
1129        let scalar_kernel = executable.scalar_kernel().cloned();
1130        let scalar_executor = scalar_kernel
1131            .as_ref()
1132            .and_then(|kernel| ScalarExecutor::prepare(kernel, execution_mode, precision));
1133        let f32_gradient_fallback_real = if precision == Precision::F32 {
1134            scalar_kernel
1135                .as_ref()
1136                .map(|kernel| {
1137                    gradient_ir(kernel, model.params().free_params(), OutputComponent::Real)
1138                })
1139                .transpose()?
1140        } else {
1141            None
1142        };
1143        let f32_gradient_fallback_imag = if precision == Precision::F32
1144            && scalar_kernel.as_ref().is_some_and(|kernel| {
1145                kernel.values()[kernel.root().index()].kind == KernelValueKind::Complex
1146            }) {
1147            scalar_kernel
1148                .as_ref()
1149                .map(|kernel| {
1150                    gradient_ir(kernel, model.params().free_params(), OutputComponent::Imag)
1151                })
1152                .transpose()?
1153        } else {
1154            None
1155        };
1156        let gradient_executor = GradientExecutor::prepare(
1157            scalar_kernel.as_ref(),
1158            model.params(),
1159            execution_mode,
1160            precision,
1161            f32_gradient_fallback_real
1162                .as_ref()
1163                .map(|real| (real, f32_gradient_fallback_imag.as_ref())),
1164        )?;
1165        let constant_factors = executable
1166            .constant_factor_matrices()
1167            .iter()
1168            .map(|_| Arc::new(OnceLock::new()))
1169            .collect();
1170        Ok(CpuPlan {
1171            precision,
1172            graph: executable.graph().clone(),
1173            params: executable.params().clone(),
1174            parameter_slots: executable.parameter_slots().to_vec(),
1175            autodiff: AutodiffPlan::from_model(model, autodiff_mode)?,
1176            cache_plan: executable.cache_plan().clone(),
1177            cache_slots: executable.cache_slots().to_vec(),
1178            cached_evaluation_nodes: executable.evaluation_nodes().to_vec(),
1179            cached_value_slots: executable.value_slots().to_vec(),
1180            scalar_kernel,
1181            scalar_executor,
1182            gradient_executor,
1183            f32_gradient_fallback_real,
1184            f32_gradient_fallback_imag,
1185            cache_materialization_nodes: executable.cache_materialization_nodes().to_vec(),
1186            solve_components: executable.solve_components().to_vec(),
1187            solve_rhs_elements: executable.solve_rhs_elements().to_vec(),
1188            solve_row_matrices: executable.solve_row_matrices().to_vec(),
1189            solve_row_keys: executable.solve_row_keys().to_vec(),
1190            factor_matrix_slots: executable.factor_matrix_slots().to_vec(),
1191            factor_matrices: executable.factor_matrices().to_vec(),
1192            constant_factor_slots: executable.constant_factor_slots().to_vec(),
1193            constant_factors,
1194        })
1195    }
1196}
1197
1198/// A complex model value and its derivatives with respect to free parameters.
1199#[derive(Clone, Debug, PartialEq)]
1200pub struct ValueGradient {
1201    value: Complex64,
1202    gradient: Vec<Complex64>,
1203}
1204
1205/// The scalar value and free-parameter gradient produced by a reduction.
1206#[derive(Clone, Debug, PartialEq)]
1207pub struct ReductionEvaluation {
1208    value: f64,
1209    gradient: Vec<f64>,
1210}
1211
1212impl ReductionEvaluation {
1213    #[cfg(feature = "wgpu")]
1214    pub(crate) fn new(value: f64, gradient: Vec<f64>) -> Self {
1215        Self { value, gradient }
1216    }
1217
1218    /// Returns the reduced scalar value.
1219    pub fn value(&self) -> f64 {
1220        self.value
1221    }
1222
1223    /// Returns derivatives in free-parameter order.
1224    pub fn gradient(&self) -> &[f64] {
1225        &self.gradient
1226    }
1227
1228    /// Consumes the evaluation and returns its value and gradient.
1229    pub fn into_parts(self) -> (f64, Vec<f64>) {
1230        (self.value, self.gradient)
1231    }
1232}
1233
1234impl ValueGradient {
1235    /// Returns the complex model value.
1236    pub fn value(&self) -> Complex64 {
1237        self.value
1238    }
1239
1240    /// Returns complex derivatives in free-parameter order.
1241    pub fn gradient(&self) -> &[Complex64] {
1242        &self.gradient
1243    }
1244
1245    /// Consumes the evaluation and returns its value and gradient.
1246    pub fn into_parts(self) -> (Complex64, Vec<Complex64>) {
1247        (self.value, self.gradient)
1248    }
1249}
1250
1251struct RealGradientAccumulator {
1252    value: AccurateF64,
1253    gradient: Vec<AccurateF64>,
1254}
1255
1256impl RealGradientAccumulator {
1257    fn zero(parameter_count: usize) -> Self {
1258        Self {
1259            value: AccurateF64::zero(),
1260            gradient: (0..parameter_count).map(|_| AccurateF64::zero()).collect(),
1261        }
1262    }
1263
1264    fn push(&mut self, weight: f64, value: f64, derivative: f64, model_gradient: &[Complex64]) {
1265        self.value.push(weight * value);
1266        for (sum, model_derivative) in self.gradient.iter_mut().zip(model_gradient) {
1267            sum.push(weight * derivative * model_derivative.re);
1268        }
1269    }
1270
1271    fn push_f32(&mut self, weight: f64, value: f64, derivative: f64, model_gradient: &[f32]) {
1272        self.value.push(weight * value);
1273        for (sum, model_derivative) in self.gradient.iter_mut().zip(model_gradient) {
1274            sum.push(weight * derivative * f64::from(*model_derivative));
1275        }
1276    }
1277
1278    fn merge(&mut self, other: Self) {
1279        self.value.merge(other.value);
1280        for (target, source) in self.gradient.iter_mut().zip(other.gradient) {
1281            target.merge(source);
1282        }
1283    }
1284
1285    fn finish(self) -> (f64, Vec<f64>) {
1286        (
1287            self.value.finish(),
1288            self.gradient.into_iter().map(AccurateF64::finish).collect(),
1289        )
1290    }
1291}
1292
1293impl CpuPlan {
1294    fn supports_f32_scalar_execution(&self) -> bool {
1295        self.scalar_kernel.as_ref().is_some_and(|kernel| {
1296            kernel.values().iter().all(|value| {
1297                matches!(value.kind, KernelValueKind::Real | KernelValueKind::Complex)
1298                    || !matches!(
1299                        value.instruction,
1300                        KernelInstruction::SolveRowAdjointElement { .. }
1301                    )
1302            })
1303        })
1304    }
1305
1306    fn scalar_interpreter_plan(&self) -> Option<&ScalarEvaluationPlan> {
1307        match (&self.scalar_kernel, &self.scalar_executor) {
1308            (Some(_), Some(ScalarExecutor::Interpreter(plan))) => Some(plan),
1309            #[cfg(feature = "jit")]
1310            (Some(_), Some(ScalarExecutor::Jit(_))) => None,
1311            (Some(_), None) | (None, None) => None,
1312            (None, Some(_)) => unreachable!("executor requires kernel IR"),
1313        }
1314    }
1315
1316    #[cfg(feature = "jit")]
1317    fn scalar_jit_kernel(&self) -> Option<&JitScalarKernel> {
1318        match (&self.scalar_kernel, &self.scalar_executor) {
1319            (Some(_), Some(ScalarExecutor::Jit(kernel))) => Some(kernel),
1320            (Some(_), Some(ScalarExecutor::Interpreter(_))) | (Some(_), None) | (None, None) => {
1321                None
1322            }
1323            (None, Some(_)) => unreachable!("executor requires kernel IR"),
1324        }
1325    }
1326
1327    #[cfg(feature = "jit")]
1328    fn gradient_jit_kernel(&self) -> Option<&JitGradientKernel> {
1329        match &self.gradient_executor {
1330            GradientExecutor::Jit(kernel) => Some(kernel),
1331            GradientExecutor::Interpreter(_) => None,
1332        }
1333    }
1334
1335    fn gradient_interpreter(&self) -> Option<&GradientInterpreter> {
1336        match &self.gradient_executor {
1337            GradientExecutor::Interpreter(interpreter) => interpreter.as_ref(),
1338            #[cfg(feature = "jit")]
1339            GradientExecutor::Jit(_) => None,
1340        }
1341    }
1342
1343    fn parameter_value(&self, params: &ParamValues, node: usize) -> RuntimeResult<f64> {
1344        let id = self.parameter_slots[node].ok_or_else(|| RuntimeError::InvalidShape {
1345            index: node,
1346            message: "node is not a parameter".into(),
1347        })?;
1348        params
1349            .get(id)
1350            .map_err(|err| RuntimeError::Parameter(err.to_string()))
1351    }
1352
1353    /// Returns the number of parameters, including fixed parameters.
1354    pub fn parameter_count(&self) -> usize {
1355        self.params.len()
1356    }
1357
1358    /// Returns the number of free parameters.
1359    pub fn free_parameter_count(&self) -> usize {
1360        self.params.n_free()
1361    }
1362
1363    /// Returns the event-cache layout required by this plan.
1364    pub fn cache_plan(&self) -> &CachePlan {
1365        &self.cache_plan
1366    }
1367
1368    /// Evaluates a model that has no event-dependent inputs.
1369    ///
1370    /// # Errors
1371    ///
1372    /// Returns [`RuntimeError`] when parameters are incompatible, the model
1373    /// requires event data, evaluation fails, or a matrix is singular.
1374    pub fn evaluate(&self, params: &ParamValues) -> RuntimeResult<Complex64> {
1375        self.evaluate_inner(params, None)
1376    }
1377
1378    /// Evaluates an event-independent model and its free-parameter gradient.
1379    ///
1380    /// # Errors
1381    ///
1382    /// Returns [`RuntimeError`] when parameters are incompatible, the model
1383    /// requires event data, differentiation or evaluation fails, or a solve is
1384    /// singular.
1385    pub fn evaluate_with_gradient(&self, params: &ParamValues) -> RuntimeResult<ValueGradient> {
1386        #[cfg(feature = "jit")]
1387        if let (Some(value_kernel), Some(gradient_kernel)) =
1388            (self.scalar_jit_kernel(), self.gradient_jit_kernel())
1389        {
1390            let value = value_kernel.evaluate_invariant(params)?;
1391            let mut real = Vec::new();
1392            let mut imag = Vec::new();
1393            gradient_kernel.evaluate_invariant_component(params, 0, &mut real)?;
1394            gradient_kernel.evaluate_invariant_component(params, 1, &mut imag)?;
1395            let gradient = real
1396                .into_iter()
1397                .zip(imag)
1398                .map(|(re, im)| Complex64::new(re, im))
1399                .collect();
1400            return Ok(ValueGradient { value, gradient });
1401        }
1402        if self.precision == Precision::F32 {
1403            return self.evaluate_f32_gradient(params, F32KernelInput::Cache(None));
1404        }
1405        self.require_f64_gradient()?;
1406        if let Some(interpreter) = self.gradient_interpreter() {
1407            let (value, gradient) = interpreter.evaluate(params, None)?;
1408            return Ok(ValueGradient { value, gradient });
1409        }
1410        let values = self.evaluate_values(params, None)?;
1411        self.value_gradient(values, None)
1412    }
1413
1414    /// Evaluates the model using values supplied by an event lookup.
1415    ///
1416    /// # Errors
1417    ///
1418    /// Returns [`RuntimeError`] when a required event value is missing,
1419    /// parameters are incompatible, evaluation fails, or a solve is singular.
1420    pub fn evaluate_with_event(
1421        &self,
1422        params: &ParamValues,
1423        event: &impl EventLookup,
1424    ) -> RuntimeResult<Complex64> {
1425        self.evaluate_inner(params, Some(event))
1426    }
1427
1428    /// Evaluates the model and gradient using values supplied by an event lookup.
1429    ///
1430    /// # Errors
1431    ///
1432    /// Returns [`RuntimeError`] when a required event value is missing,
1433    /// parameters are incompatible, or differentiation or evaluation fails.
1434    pub fn evaluate_with_event_and_gradient(
1435        &self,
1436        params: &ParamValues,
1437        event: &impl EventLookup,
1438    ) -> RuntimeResult<ValueGradient> {
1439        if self.precision == Precision::F32 {
1440            return self.evaluate_f32_gradient(params, F32KernelInput::Event(event));
1441        }
1442        self.require_f64_gradient()?;
1443        let values = self.evaluate_values(params, Some(event))?;
1444        self.value_gradient(values, None)
1445    }
1446
1447    /// Materializes the event-dependent cache for a batch.
1448    ///
1449    /// # Errors
1450    ///
1451    /// Returns [`RuntimeError`] when required columns are missing, expression
1452    /// shapes are invalid, cache construction fails, or a matrix is singular.
1453    ///
1454    /// # Panics
1455    ///
1456    /// Panics if a node selected by the validated cache plan was not evaluated.
1457    pub fn cache_event_batch(&self, batch: &EventBatch) -> RuntimeResult<CpuBatchCache> {
1458        let event_columns = self.event_columns(batch.schema())?;
1459        let mut cache = CpuBatchCache::new(
1460            &self.cache_plan,
1461            &self.factor_matrices,
1462            &self.solve_row_keys,
1463            batch.len(),
1464        );
1465        for row in 0..batch.len() {
1466            let values = self.evaluate_cache_values_for_row(batch, row, &event_columns)?;
1467            for (slot, entry) in self.cache_plan.entries().iter().enumerate() {
1468                let value = values[entry.node().index()]
1469                    .as_ref()
1470                    .expect("cacheable node should have been evaluated")
1471                    .clone();
1472                cache.push(slot, value)?;
1473            }
1474            for plan in &self.solve_row_matrices {
1475                let (rows, cols, values) = matrix_at_optional(&values, plan.matrix().index())?;
1476                if rows != plan.dimension() || cols != plan.dimension() {
1477                    return Err(RuntimeError::InvalidShape {
1478                        index: plan.matrix().index(),
1479                        message: format!(
1480                            "specialized solve expected a {}x{} matrix, got {rows}x{cols}",
1481                            plan.dimension(),
1482                            plan.dimension()
1483                        ),
1484                    });
1485                }
1486                let transpose_factor = DMatrix::from_row_slice(rows, cols, values).transpose().lu();
1487                for (slot, index) in plan.rows() {
1488                    let mut basis = DVector::zeros(plan.dimension());
1489                    basis[*index] = Complex64::ONE;
1490                    let inverse_row = transpose_factor
1491                        .solve(&basis)
1492                        .ok_or(RuntimeError::SingularMatrix(plan.matrix().index()))?;
1493                    cache.push_solve_row(*slot, inverse_row.iter().copied())?;
1494                }
1495            }
1496            for (slot, (matrix, _)) in self.factor_matrices.iter().enumerate() {
1497                let (rows, cols, values) = matrix_at_optional(&values, matrix.index())?;
1498                cache.push_factor(slot, DMatrix::from_row_slice(rows, cols, values).lu())?;
1499            }
1500        }
1501        cache.set_weights((0..batch.len()).map(|row| batch.weights_at(row)).collect());
1502        Ok(cache)
1503    }
1504
1505    /// Evaluates every row in a materialized batch cache.
1506    ///
1507    /// # Errors
1508    ///
1509    /// Returns [`RuntimeError`] when parameters or cache layout are
1510    /// incompatible, evaluation fails, or a matrix is singular.
1511    pub fn evaluate_cache(
1512        &self,
1513        params: &ParamValues,
1514        cache: &CpuBatchCache,
1515    ) -> RuntimeResult<Vec<Complex64>> {
1516        self.check_batch_cache(cache)?;
1517        #[cfg(feature = "jit")]
1518        if let Some(kernel) = self.scalar_jit_kernel() {
1519            let mut output = Vec::with_capacity(cache.len());
1520            kernel.evaluate(params, cache, 0, cache.len(), &mut output)?;
1521            return Ok(output);
1522        }
1523        let invariant = self.scalar_invariant_values(params)?;
1524        let mut out = Vec::with_capacity(cache.len());
1525        let mut workspace = ScalarEventWorkspace::default();
1526        for row in 0..cache.len() {
1527            out.push(self.evaluate_cache_row_prepared(
1528                params,
1529                cache,
1530                row,
1531                invariant.as_ref(),
1532                &mut workspace,
1533            )?);
1534        }
1535        Ok(out)
1536    }
1537
1538    /// Evaluates one row in a materialized batch cache.
1539    ///
1540    /// # Errors
1541    ///
1542    /// Returns [`RuntimeError`] when `row` is out of range, parameters or cache
1543    /// layout are incompatible, evaluation fails, or a matrix is singular.
1544    pub fn evaluate_cache_row(
1545        &self,
1546        params: &ParamValues,
1547        cache: &CpuBatchCache,
1548        row: usize,
1549    ) -> RuntimeResult<Complex64> {
1550        self.check_batch_cache(cache)?;
1551        self.evaluate_cache_row_unchecked(params, cache, row)
1552    }
1553
1554    fn evaluate_cache_row_unchecked(
1555        &self,
1556        params: &ParamValues,
1557        cache: &CpuBatchCache,
1558        row: usize,
1559    ) -> RuntimeResult<Complex64> {
1560        let invariant = self.scalar_invariant_values(params)?;
1561        self.evaluate_cache_row_prepared(
1562            params,
1563            cache,
1564            row,
1565            invariant.as_ref(),
1566            &mut ScalarEventWorkspace::default(),
1567        )
1568    }
1569
1570    fn evaluate_cache_row_prepared(
1571        &self,
1572        params: &ParamValues,
1573        cache: &CpuBatchCache,
1574        row: usize,
1575        invariant: Option<&ScalarInvariantValues>,
1576        workspace: &mut ScalarEventWorkspace,
1577    ) -> RuntimeResult<Complex64> {
1578        #[cfg(feature = "jit")]
1579        if let Some(kernel) = self.scalar_jit_kernel() {
1580            let mut output = Vec::with_capacity(1);
1581            kernel.evaluate(params, cache, row, row + 1, &mut output)?;
1582            return Ok(output[0]);
1583        }
1584        if self.precision == Precision::F32 {
1585            return self.evaluate_f32_scalar(params, F32KernelInput::Cache(Some((cache, row))));
1586        }
1587        if let (Some(plan), Some(invariant)) = (self.scalar_interpreter_plan(), invariant) {
1588            return self.evaluate_scalar_cache_row(cache, row, plan, invariant, workspace);
1589        }
1590        let values = self.evaluate_values_from_cache(params, cache, row)?;
1591        self.cached_scalar_at(&values, self.graph.root())
1592    }
1593
1594    fn scalar_invariant_values(
1595        &self,
1596        params: &ParamValues,
1597    ) -> RuntimeResult<Option<ScalarInvariantValues>> {
1598        if self.precision == Precision::F32 {
1599            return Ok(None);
1600        }
1601        let Some(plan) = self.scalar_interpreter_plan() else {
1602            return Ok(None);
1603        };
1604        let mut values = ScalarInvariantValues {
1605            real: vec![0.0; plan.invariant_real_slot_count],
1606            complex: vec![Complex64::ZERO; plan.invariant_complex_slot_count],
1607        };
1608        let event = ScalarEventWorkspace::default();
1609        for instruction in &plan.invariant_instructions {
1610            match instruction.output_slot {
1611                ScalarSlot::Real(slot) => {
1612                    values.real[slot] = instruction.instruction.evaluate_real(
1613                        Some(params),
1614                        None,
1615                        &values,
1616                        &event,
1617                    )?;
1618                }
1619                ScalarSlot::Complex(slot) => {
1620                    values.complex[slot] = instruction.instruction.evaluate_complex(
1621                        Some(params),
1622                        None,
1623                        &values,
1624                        &event,
1625                    )?;
1626                }
1627            }
1628        }
1629        Ok(Some(values))
1630    }
1631
1632    fn evaluate_scalar_cache_row(
1633        &self,
1634        cache: &CpuBatchCache,
1635        row: usize,
1636        plan: &ScalarEvaluationPlan,
1637        invariant: &ScalarInvariantValues,
1638        values: &mut ScalarEventWorkspace,
1639    ) -> RuntimeResult<Complex64> {
1640        values.real.clear();
1641        values
1642            .real
1643            .resize(plan.event_real_slot_count, [0.0; SCALAR_BLOCK_SIZE]);
1644        values.complex.clear();
1645        values.complex.resize(
1646            plan.event_complex_slot_count,
1647            [Complex64::ZERO; SCALAR_BLOCK_SIZE],
1648        );
1649        for event_instruction in &plan.event_instructions {
1650            match event_instruction.output_slot {
1651                ScalarSlot::Real(slot) => {
1652                    values.real[slot][0] = event_instruction.instruction.evaluate_real(
1653                        None,
1654                        Some((cache, row)),
1655                        invariant,
1656                        values,
1657                    )?;
1658                }
1659                ScalarSlot::Complex(slot) => {
1660                    values.complex[slot][0] = event_instruction.instruction.evaluate_complex(
1661                        None,
1662                        Some((cache, row)),
1663                        invariant,
1664                        values,
1665                    )?;
1666                }
1667            }
1668        }
1669        Ok(plan.root().complex_value(invariant, values))
1670    }
1671
1672    #[allow(clippy::too_many_arguments)]
1673    fn evaluate_cache_block_prepared(
1674        &self,
1675        params: &ParamValues,
1676        cache: &CpuBatchCache,
1677        start: usize,
1678        end: usize,
1679        invariant: Option<&ScalarInvariantValues>,
1680        workspace: &mut ScalarEventWorkspace,
1681        output: &mut Vec<Complex64>,
1682        #[cfg(feature = "jit")] jit_cache: Option<&JitCacheView>,
1683    ) -> RuntimeResult<()> {
1684        #[cfg(feature = "jit")]
1685        if let Some(kernel) = self.scalar_jit_kernel() {
1686            let owned;
1687            let jit_cache = if let Some(jit_cache) = jit_cache {
1688                jit_cache
1689            } else {
1690                owned = JitScalarKernel::prepare_cache(cache);
1691                &owned
1692            };
1693            return kernel.evaluate_prepared(params, jit_cache, start, end, output);
1694        }
1695        if self.precision == Precision::F32 {
1696            output.clear();
1697            output.reserve(end - start);
1698            for row in start..end {
1699                output.push(
1700                    self.evaluate_f32_scalar(params, F32KernelInput::Cache(Some((cache, row))))?,
1701                );
1702            }
1703            return Ok(());
1704        }
1705        if let (Some(plan), Some(invariant)) = (self.scalar_interpreter_plan(), invariant) {
1706            return evaluate_scalar_cache_block(
1707                cache, start, end, plan, invariant, workspace, output,
1708            );
1709        }
1710        output.clear();
1711        for row in start..end {
1712            output
1713                .push(self.evaluate_cache_row_prepared(params, cache, row, invariant, workspace)?);
1714        }
1715        Ok(())
1716    }
1717}
1718
1719fn evaluate_scalar_cache_block(
1720    cache: &CpuBatchCache,
1721    start: usize,
1722    end: usize,
1723    plan: &ScalarEvaluationPlan,
1724    invariant: &ScalarInvariantValues,
1725    workspace: &mut ScalarEventWorkspace,
1726    output: &mut Vec<Complex64>,
1727) -> RuntimeResult<()> {
1728    let block_len = end - start;
1729    workspace
1730        .real
1731        .resize(plan.event_real_slot_count, [0.0; SCALAR_BLOCK_SIZE]);
1732    workspace.complex.resize(
1733        plan.event_complex_slot_count,
1734        [Complex64::ZERO; SCALAR_BLOCK_SIZE],
1735    );
1736
1737    for event_instruction in &plan.event_instructions {
1738        match event_instruction.output_slot {
1739            ScalarSlot::Real(slot) => {
1740                let output_slot = slot;
1741                match &event_instruction.instruction {
1742                    ScalarInstruction::Cached(slot) => {
1743                        workspace.real[output_slot][..block_len]
1744                            .copy_from_slice(cache.real_range(*slot, start, end)?);
1745                    }
1746                    ScalarInstruction::Unary { op, input } => {
1747                        for lane in 0..block_len {
1748                            workspace.real[output_slot][lane] = match op {
1749                                UnaryOp::Neg => -input.block_real_value(invariant, workspace, lane),
1750                                UnaryOp::Real | UnaryOp::Conj => {
1751                                    input.block_complex_value(invariant, workspace, lane).re
1752                                }
1753                                UnaryOp::Imag => {
1754                                    input.block_complex_value(invariant, workspace, lane).im
1755                                }
1756                                UnaryOp::NormSqr => input
1757                                    .block_complex_value(invariant, workspace, lane)
1758                                    .norm_sqr(),
1759                                UnaryOp::Sqrt => {
1760                                    input.block_real_value(invariant, workspace, lane).sqrt()
1761                                }
1762                                UnaryOp::Exp => {
1763                                    input.block_real_value(invariant, workspace, lane).exp()
1764                                }
1765                                UnaryOp::Sin => {
1766                                    input.block_real_value(invariant, workspace, lane).sin()
1767                                }
1768                                UnaryOp::Cos => {
1769                                    input.block_real_value(invariant, workspace, lane).cos()
1770                                }
1771                                UnaryOp::Log => {
1772                                    input.block_real_value(invariant, workspace, lane).ln()
1773                                }
1774                                UnaryOp::PowI(power) => input
1775                                    .block_real_value(invariant, workspace, lane)
1776                                    .powi(*power),
1777                            };
1778                        }
1779                    }
1780                    ScalarInstruction::Binary { op, lhs, rhs } => {
1781                        for lane in 0..block_len {
1782                            let lhs = lhs.block_real_value(invariant, workspace, lane);
1783                            let rhs = rhs.block_real_value(invariant, workspace, lane);
1784                            workspace.real[output_slot][lane] = match op {
1785                                BinaryOp::Add => lhs + rhs,
1786                                BinaryOp::Sub => lhs - rhs,
1787                                BinaryOp::Mul => lhs * rhs,
1788                                BinaryOp::Div => lhs / rhs,
1789                                BinaryOp::Atan2 => lhs.atan2(rhs),
1790                            };
1791                        }
1792                    }
1793                    ScalarInstruction::Add(runs) => {
1794                        workspace.real[output_slot][..block_len].fill(0.0);
1795                        for run in runs {
1796                            match run {
1797                                OperandRun::InvariantReal(slots) => {
1798                                    for slot in slots {
1799                                        let operand = invariant.real[*slot];
1800                                        for lane in 0..block_len {
1801                                            workspace.real[output_slot][lane] += operand;
1802                                        }
1803                                    }
1804                                }
1805                                OperandRun::EventReal(slots) => {
1806                                    for slot in slots {
1807                                        for lane in 0..block_len {
1808                                            workspace.real[output_slot][lane] +=
1809                                                workspace.real[*slot][lane];
1810                                        }
1811                                    }
1812                                }
1813                                OperandRun::InvariantComplex(_) | OperandRun::EventComplex(_) => {
1814                                    unreachable!("complex operand appeared in real add")
1815                                }
1816                            }
1817                        }
1818                    }
1819                    ScalarInstruction::Mul(runs) => {
1820                        workspace.real[output_slot][..block_len].fill(1.0);
1821                        for run in runs {
1822                            match run {
1823                                OperandRun::InvariantReal(slots) => {
1824                                    for slot in slots {
1825                                        let operand = invariant.real[*slot];
1826                                        for lane in 0..block_len {
1827                                            workspace.real[output_slot][lane] *= operand;
1828                                        }
1829                                    }
1830                                }
1831                                OperandRun::EventReal(slots) => {
1832                                    for slot in slots {
1833                                        for lane in 0..block_len {
1834                                            workspace.real[output_slot][lane] *=
1835                                                workspace.real[*slot][lane];
1836                                        }
1837                                    }
1838                                }
1839                                OperandRun::InvariantComplex(_) | OperandRun::EventComplex(_) => {
1840                                    unreachable!("complex operand appeared in real multiply")
1841                                }
1842                            }
1843                        }
1844                    }
1845                    ScalarInstruction::Constant(_)
1846                    | ScalarInstruction::Parameter(_)
1847                    | ScalarInstruction::Complex { .. }
1848                    | ScalarInstruction::SolveRow { .. }
1849                    | ScalarInstruction::SolveRowAdjointElement { .. } => {
1850                        unreachable!("non-real event instruction appeared in a real slot")
1851                    }
1852                }
1853            }
1854            ScalarSlot::Complex(slot) => {
1855                let output_slot = slot;
1856                match &event_instruction.instruction {
1857                    ScalarInstruction::Cached(slot) => {
1858                        workspace.complex[output_slot][..block_len]
1859                            .copy_from_slice(cache.complex_range(*slot, start, end)?);
1860                    }
1861                    ScalarInstruction::Unary { op, input } => {
1862                        for lane in 0..block_len {
1863                            let input = input.block_complex_value(invariant, workspace, lane);
1864                            workspace.complex[output_slot][lane] = eval_unary(*op, input);
1865                        }
1866                    }
1867                    ScalarInstruction::Binary { op, lhs, rhs } => {
1868                        for lane in 0..block_len {
1869                            let lhs = lhs.block_complex_value(invariant, workspace, lane);
1870                            let rhs = rhs.block_complex_value(invariant, workspace, lane);
1871                            workspace.complex[output_slot][lane] = eval_binary(*op, lhs, rhs);
1872                        }
1873                    }
1874                    ScalarInstruction::Add(runs) => {
1875                        workspace.complex[output_slot][..block_len].fill(Complex64::ZERO);
1876                        for run in runs {
1877                            match run {
1878                                OperandRun::InvariantReal(slots) => {
1879                                    for slot in slots {
1880                                        let operand = invariant.real[*slot];
1881                                        for lane in 0..block_len {
1882                                            workspace.complex[output_slot][lane] += operand;
1883                                        }
1884                                    }
1885                                }
1886                                OperandRun::InvariantComplex(slots) => {
1887                                    for slot in slots {
1888                                        let operand = invariant.complex[*slot];
1889                                        for lane in 0..block_len {
1890                                            workspace.complex[output_slot][lane] += operand;
1891                                        }
1892                                    }
1893                                }
1894                                OperandRun::EventReal(slots) => {
1895                                    for slot in slots {
1896                                        for lane in 0..block_len {
1897                                            workspace.complex[output_slot][lane] +=
1898                                                workspace.real[*slot][lane];
1899                                        }
1900                                    }
1901                                }
1902                                OperandRun::EventComplex(slots) => {
1903                                    for slot in slots {
1904                                        for lane in 0..block_len {
1905                                            let operand = workspace.complex[*slot][lane];
1906                                            workspace.complex[output_slot][lane] += operand;
1907                                        }
1908                                    }
1909                                }
1910                            }
1911                        }
1912                    }
1913                    ScalarInstruction::Mul(runs) => {
1914                        workspace.complex[output_slot][..block_len].fill(Complex64::ONE);
1915                        for run in runs {
1916                            match run {
1917                                OperandRun::InvariantReal(slots) => {
1918                                    for slot in slots {
1919                                        let operand = invariant.real[*slot];
1920                                        for lane in 0..block_len {
1921                                            workspace.complex[output_slot][lane] *= operand;
1922                                        }
1923                                    }
1924                                }
1925                                OperandRun::InvariantComplex(slots) => {
1926                                    for slot in slots {
1927                                        let operand = invariant.complex[*slot];
1928                                        for lane in 0..block_len {
1929                                            workspace.complex[output_slot][lane] *= operand;
1930                                        }
1931                                    }
1932                                }
1933                                OperandRun::EventReal(slots) => {
1934                                    for slot in slots {
1935                                        for lane in 0..block_len {
1936                                            workspace.complex[output_slot][lane] *=
1937                                                workspace.real[*slot][lane];
1938                                        }
1939                                    }
1940                                }
1941                                OperandRun::EventComplex(slots) => {
1942                                    for slot in slots {
1943                                        for lane in 0..block_len {
1944                                            let operand = workspace.complex[*slot][lane];
1945                                            workspace.complex[output_slot][lane] *= operand;
1946                                        }
1947                                    }
1948                                }
1949                            }
1950                        }
1951                    }
1952                    ScalarInstruction::Complex { re, im } => {
1953                        for lane in 0..block_len {
1954                            workspace.complex[output_slot][lane] = Complex64::new(
1955                                re.block_real_value(invariant, workspace, lane),
1956                                im.block_real_value(invariant, workspace, lane),
1957                            );
1958                        }
1959                    }
1960                    ScalarInstruction::SolveRow { row_slot, rhs } => {
1961                        for lane in 0..block_len {
1962                            let inverse_row = cache.solve_row(*row_slot, start + lane)?;
1963                            if inverse_row.len() != rhs.len() {
1964                                return Err(RuntimeError::InvalidShape {
1965                                    index: start + lane,
1966                                    message: format!(
1967                                        "specialized solve row has len {}, expected {}",
1968                                        inverse_row.len(),
1969                                        rhs.len()
1970                                    ),
1971                                });
1972                            }
1973                            workspace.complex[output_slot][lane] = inverse_row
1974                                .iter()
1975                                .zip(rhs)
1976                                .map(|(lhs, operand)| {
1977                                    lhs * operand.block_complex_value(invariant, workspace, lane)
1978                                })
1979                                .sum();
1980                        }
1981                    }
1982                    ScalarInstruction::SolveRowAdjointElement {
1983                        row_slot,
1984                        index,
1985                        len,
1986                        adjoint,
1987                    } => {
1988                        for lane in 0..block_len {
1989                            let inverse_row = cache.solve_row(*row_slot, start + lane)?;
1990                            if inverse_row.len() != *len {
1991                                return Err(RuntimeError::InvalidShape {
1992                                    index: start + lane,
1993                                    message: format!(
1994                                        "specialized solve row has len {}, expected {len}",
1995                                        inverse_row.len()
1996                                    ),
1997                                });
1998                            }
1999                            workspace.complex[output_slot][lane] = adjoint
2000                                .block_complex_value(invariant, workspace, lane)
2001                                * inverse_row[*index].conj();
2002                        }
2003                    }
2004                    ScalarInstruction::Constant(_) | ScalarInstruction::Parameter(_) => {
2005                        unreachable!("invariant instruction appeared in the event tape")
2006                    }
2007                }
2008            }
2009        }
2010    }
2011
2012    output.clear();
2013    output.reserve(block_len * plan.outputs.len());
2014    for lane in 0..block_len {
2015        for output_operand in &plan.outputs {
2016            output.push(output_operand.block_complex_value(invariant, workspace, lane));
2017        }
2018    }
2019    Ok(())
2020}
2021
2022impl CpuPlan {
2023    /// Evaluates one cached row and its free-parameter gradient.
2024    ///
2025    /// # Errors
2026    ///
2027    /// Returns [`RuntimeError`] when `row` is out of range, parameters or cache
2028    /// layout are incompatible, or differentiation or evaluation fails.
2029    pub fn evaluate_cache_row_with_gradient(
2030        &self,
2031        params: &ParamValues,
2032        cache: &CpuBatchCache,
2033        row: usize,
2034    ) -> RuntimeResult<ValueGradient> {
2035        self.check_batch_cache(cache)?;
2036        self.evaluate_cache_row_with_gradient_unchecked(params, cache, row)
2037    }
2038
2039    fn evaluate_cache_row_with_gradient_unchecked(
2040        &self,
2041        params: &ParamValues,
2042        cache: &CpuBatchCache,
2043        row: usize,
2044    ) -> RuntimeResult<ValueGradient> {
2045        #[cfg(feature = "jit")]
2046        if self.gradient_jit_kernel().is_some() {
2047            return self
2048                .evaluate_cache_gradient_jit(params, cache, row, row + 1)?
2049                .pop()
2050                .ok_or_else(|| RuntimeError::InvalidShape {
2051                    index: row,
2052                    message: "single-row JIT gradient produced no value".into(),
2053                });
2054        }
2055        if self.precision == Precision::F32 {
2056            return self.evaluate_f32_gradient(params, F32KernelInput::Cache(Some((cache, row))));
2057        }
2058        if self.autodiff.mode() == AutodiffMode::Reverse {
2059            self.require_f64_gradient()?;
2060            let values = self.evaluate_values_from_cache(params, cache, row)?;
2061            return self.value_gradient(values, Some((cache, row)));
2062        }
2063        if let Some(interpreter) = self.gradient_interpreter() {
2064            let (value, gradient) = interpreter.evaluate(params, Some((cache, row)))?;
2065            return Ok(ValueGradient { value, gradient });
2066        }
2067        let values = self.evaluate_values_from_cache(params, cache, row)?;
2068        self.value_gradient(values, Some((cache, row)))
2069    }
2070
2071    /// Evaluates every cached row and its free-parameter gradient.
2072    ///
2073    /// # Errors
2074    ///
2075    /// Returns [`RuntimeError`] when parameters or cache layout are
2076    /// incompatible, or differentiation or evaluation fails.
2077    pub fn evaluate_cache_with_gradient(
2078        &self,
2079        params: &ParamValues,
2080        cache: &CpuBatchCache,
2081    ) -> RuntimeResult<Vec<ValueGradient>> {
2082        self.check_batch_cache(cache)?;
2083        #[cfg(feature = "jit")]
2084        if self.gradient_jit_kernel().is_some() {
2085            return self.evaluate_cache_gradient_jit(params, cache, 0, cache.len());
2086        }
2087        if self.precision == Precision::F32 {
2088            return (0..cache.len())
2089                .map(|row| {
2090                    self.evaluate_f32_gradient(params, F32KernelInput::Cache(Some((cache, row))))
2091                })
2092                .collect();
2093        }
2094        self.require_f64_gradient()?;
2095        (0..cache.len())
2096            .map(|row| self.evaluate_cache_row_with_gradient_unchecked(params, cache, row))
2097            .collect()
2098    }
2099
2100    #[cfg(feature = "jit")]
2101    fn evaluate_cache_gradient_jit(
2102        &self,
2103        params: &ParamValues,
2104        cache: &CpuBatchCache,
2105        start: usize,
2106        end: usize,
2107    ) -> RuntimeResult<Vec<ValueGradient>> {
2108        let (Some(value_kernel), Some(gradient_kernel)) =
2109            (self.scalar_jit_kernel(), self.gradient_jit_kernel())
2110        else {
2111            return Err(RuntimeError::InvalidShape {
2112                index: self.graph.root().index(),
2113                message: "JIT gradient evaluation requires both scalar and gradient kernels".into(),
2114            });
2115        };
2116        let view = JitScalarKernel::prepare_cache(cache);
2117        let mut values = Vec::new();
2118        let mut real = Vec::new();
2119        let mut imag = Vec::new();
2120        value_kernel.evaluate_prepared(params, &view, start, end, &mut values)?;
2121        gradient_kernel.evaluate_prepared(params, &view, start, end, 0, &mut real)?;
2122        gradient_kernel.evaluate_prepared(params, &view, start, end, 1, &mut imag)?;
2123        let parameter_count = self.free_parameter_count();
2124        Ok(values
2125            .into_iter()
2126            .enumerate()
2127            .map(|(row, value)| ValueGradient {
2128                value,
2129                gradient: (0..parameter_count)
2130                    .map(|parameter| {
2131                        let index = row * parameter_count + parameter;
2132                        Complex64::new(real[index], imag[index])
2133                    })
2134                    .collect(),
2135            })
2136            .collect())
2137    }
2138
2139    /// Evaluates the model for every event in a batch.
2140    ///
2141    /// # Errors
2142    ///
2143    /// Returns [`RuntimeError`] when cache materialization or evaluation fails.
2144    pub fn evaluate_batch(
2145        &self,
2146        params: &ParamValues,
2147        batch: &EventBatch,
2148    ) -> RuntimeResult<Vec<Complex64>> {
2149        let cache = self.cache_event_batch(batch)?;
2150        self.evaluate_cache(params, &cache)
2151    }
2152
2153    /// Evaluates the model and gradient for every event in a batch.
2154    ///
2155    /// # Errors
2156    ///
2157    /// Returns [`RuntimeError`] when cache materialization, differentiation, or
2158    /// evaluation fails.
2159    pub fn evaluate_batch_with_gradient(
2160        &self,
2161        params: &ParamValues,
2162        batch: &EventBatch,
2163    ) -> RuntimeResult<Vec<ValueGradient>> {
2164        let cache = self.cache_event_batch(batch)?;
2165        self.evaluate_cache_with_gradient(params, &cache)
2166    }
2167
2168    /// Materializes all event-dependent caches for a dataset.
2169    ///
2170    /// # Errors
2171    ///
2172    /// Returns [`RuntimeError`] when the dataset cannot be read, a batch schema
2173    /// is incompatible, cache construction fails, or a matrix is singular.
2174    pub fn cache_dataset(&self, dataset: &Dataset) -> RuntimeResult<CpuCachedDataset> {
2175        self.cache_dataset_with_plan(dataset, dataset.read_plan())
2176    }
2177
2178    /// Estimates retained compiled-cache bytes for `events`.
2179    pub fn cache_memory_estimate(&self, events: usize) -> usize {
2180        let fixed = self.cache_plan.entries().len() * size_of::<CachedSlot>()
2181            + self.factor_matrices.len() * size_of::<CachedFactorSlot>()
2182            + self.solve_row_keys.len() * size_of::<CachedSolveRowSlot>()
2183            + self.cache_plan.entries().len() * size_of::<ExprId>()
2184            + self.factor_matrices.len() * size_of::<ExprId>()
2185            + self.solve_row_keys.len() * size_of::<(ExprId, usize, usize)>();
2186        let slot_bytes = self
2187            .cache_plan
2188            .entries()
2189            .iter()
2190            .map(|entry| match entry.value_kind() {
2191                ValueKind::Real => size_of::<f64>(),
2192                ValueKind::Complex => size_of::<Complex64>(),
2193                ValueKind::Vector { len } => len * size_of::<Complex64>(),
2194                ValueKind::Matrix { rows, cols } => rows * cols * size_of::<Complex64>(),
2195            })
2196            .sum::<usize>();
2197        let factor_bytes = self
2198            .factor_matrices
2199            .iter()
2200            .map(|(_, dimension)| {
2201                size_of::<DynamicLu>()
2202                    + dimension * dimension * size_of::<Complex64>()
2203                    + dimension * size_of::<usize>()
2204            })
2205            .sum::<usize>();
2206        let solve_bytes = self
2207            .solve_row_keys
2208            .iter()
2209            .map(|(_, _, dimension)| dimension * size_of::<Complex64>())
2210            .sum::<usize>();
2211        fixed.saturating_add(
2212            events.saturating_mul(
2213                size_of::<f64>()
2214                    .saturating_add(slot_bytes)
2215                    .saturating_add(factor_bytes)
2216                    .saturating_add(solve_bytes),
2217            ),
2218        )
2219    }
2220
2221    fn cache_dataset_with_plan(
2222        &self,
2223        dataset: &Dataset,
2224        read_plan: laddu_data::io::ReadPlan,
2225    ) -> RuntimeResult<CpuCachedDataset> {
2226        let mut batches = Vec::new();
2227        let mut sum_weights = 0.0;
2228        for batch in dataset
2229            .batches_with_plan(read_plan)
2230            .map_err(|err| RuntimeError::Data(err.to_string()))?
2231        {
2232            let batch = batch.map_err(|err| RuntimeError::Data(err.to_string()))?;
2233            let cached = CpuCachedBatch {
2234                cache: self.cache_event_batch(&batch)?,
2235            };
2236            sum_weights += cached.sum_weights();
2237            batches.push(cached);
2238        }
2239        Ok(CpuCachedDataset {
2240            batches,
2241            sum_weights,
2242        })
2243    }
2244
2245    /// Prepares a dataset according to its cache-storage policy.
2246    ///
2247    /// # Errors
2248    ///
2249    /// Returns [`RuntimeError`] when dataset reading or cache construction
2250    /// fails, or another distributed worker reports failure.
2251    pub fn prepare_dataset(
2252        &self,
2253        execution: &Execution,
2254        dataset: &Dataset,
2255    ) -> RuntimeResult<CpuPreparedDataset> {
2256        let mut read_plan = execution.read_plan(dataset.read_plan());
2257        let schema = dataset
2258            .schema()
2259            .map_err(|error| RuntimeError::Data(error.to_string()))?;
2260        let source_bytes_per_event =
2261            (4 * schema.n_p4s() + schema.n_scalars() + usize::from(schema.has_weight()))
2262                * size_of::<f64>();
2263        let cache_one = self.cache_memory_estimate(1);
2264        let cache_zero = self.cache_memory_estimate(0);
2265        let cache_bytes_per_event = cache_one.saturating_sub(cache_zero);
2266        let local_event_limit = dataset
2267            .num_events()
2268            .map_err(|error| RuntimeError::Data(error.to_string()))?
2269            .and_then(|events| usize::try_from(events).ok())
2270            .unwrap_or(usize::MAX);
2271        let host_remaining = execution.host_memory().remaining();
2272        let resident_plan = resident_cache_plan(
2273            cache_zero,
2274            cache_bytes_per_event,
2275            source_bytes_per_event.saturating_mul(2),
2276            local_event_limit,
2277            usize::try_from(host_remaining).unwrap_or(usize::MAX),
2278        );
2279        let requested_storage = match dataset.memory_policy() {
2280            MemoryPolicy::Streaming => CacheStorage::Streaming,
2281            MemoryPolicy::Resident => {
2282                if resident_plan.is_none() {
2283                    return Err(laddu_memory::MemoryError::BudgetExceeded {
2284                        resource: "host".into(),
2285                        requested: u64::try_from(
2286                            cache_bytes_per_event
2287                                .saturating_mul(local_event_limit)
2288                                .saturating_add(cache_zero)
2289                                .saturating_add(source_bytes_per_event.saturating_mul(2)),
2290                        )
2291                        .unwrap_or(u64::MAX),
2292                        remaining: host_remaining,
2293                    }
2294                    .into());
2295                }
2296                CacheStorage::Resident
2297            }
2298            MemoryPolicy::Fastest if resident_plan.is_some() => CacheStorage::Resident,
2299            MemoryPolicy::Fastest => CacheStorage::Streaming,
2300        };
2301        let persistent_lease = if requested_storage == CacheStorage::Resident {
2302            let (resident_bytes, _) = resident_plan
2303                .ok_or_else(|| RuntimeError::Data("resident cache plan was not resolved".into()))?;
2304            Some(
2305                execution
2306                    .host_memory()
2307                    .reserve(u64::try_from(resident_bytes).unwrap_or(u64::MAX))?,
2308            )
2309        } else {
2310            None
2311        };
2312        let available_for_batch = execution.host_memory().remaining();
2313        // Sources may hold the current decoded batch plus one bounded
2314        // prefetched batch. A resident cache is already covered by its
2315        // persistent lease; streaming additionally needs one transient cache.
2316        let (fixed_peak, per_event_peak) = if requested_storage == CacheStorage::Streaming {
2317            (
2318                cache_zero,
2319                source_bytes_per_event
2320                    .saturating_mul(2)
2321                    .saturating_add(cache_bytes_per_event),
2322            )
2323        } else {
2324            (0, source_bytes_per_event.saturating_mul(2))
2325        };
2326        let decision = MemoryDecision::fit(
2327            "CPU prepared dataset",
2328            u64::try_from(fixed_peak).unwrap_or(u64::MAX),
2329            u64::try_from(per_event_peak).unwrap_or(u64::MAX),
2330            available_for_batch,
2331            local_event_limit,
2332            if requested_storage == CacheStorage::Resident {
2333                "resident"
2334            } else {
2335                "streaming"
2336            },
2337        )?;
2338        read_plan.chunk_size = Some(
2339            read_plan
2340                .chunk_size
2341                .map_or(decision.chunk_events, |manual| {
2342                    manual.min(decision.chunk_events)
2343                })
2344                .max(1),
2345        );
2346        execution.record_memory_decision(decision.clone());
2347        match requested_storage {
2348            CacheStorage::Resident => {
2349                let local = self.cache_dataset_with_plan(dataset, read_plan);
2350                if !execution.all_succeeded(local.is_ok()) {
2351                    return local.and(Err(RuntimeError::DistributedPeerFailure));
2352                }
2353                let dataset = local?;
2354                let stats = PreparedDatasetStats {
2355                    local_events: dataset.len(),
2356                    global_events: execution.sum_usize(dataset.len()),
2357                    local_batches: dataset.batches().len(),
2358                    sum_weights: execution.sum_f64(dataset.sum_weights()),
2359                    resident_bytes: dataset.resident_bytes(),
2360                    storage: CacheStorage::Resident,
2361                };
2362                let memory_lease = persistent_lease.ok_or_else(|| {
2363                    RuntimeError::Data(
2364                        "resident dataset preparation did not reserve host memory".into(),
2365                    )
2366                })?;
2367                Ok(CpuPreparedDataset::Resident {
2368                    dataset,
2369                    stats,
2370                    memory_lease,
2371                })
2372            }
2373            CacheStorage::Streaming => {
2374                let local = (|| {
2375                    let mut local_events = 0;
2376                    let mut local_batches = 0;
2377                    let mut sum_weights = AccurateF64::zero();
2378                    for batch in dataset
2379                        .batches_with_plan(read_plan)
2380                        .map_err(|error| RuntimeError::Data(error.to_string()))?
2381                    {
2382                        let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
2383                        local_events += batch.len();
2384                        local_batches += 1;
2385                        for row in 0..batch.len() {
2386                            sum_weights.push(batch.weights_at(row));
2387                        }
2388                    }
2389                    Ok::<_, RuntimeError>((local_events, local_batches, sum_weights.finish()))
2390                })();
2391                if !execution.all_succeeded(local.is_ok()) {
2392                    return local.and(Err(RuntimeError::DistributedPeerFailure));
2393                }
2394                let (local_events, local_batches, sum_weights) = local?;
2395                Ok(CpuPreparedDataset::Streaming {
2396                    dataset: dataset.clone(),
2397                    stats: PreparedDatasetStats {
2398                        local_events,
2399                        global_events: execution.sum_usize(local_events),
2400                        local_batches,
2401                        sum_weights: execution.sum_f64(sum_weights),
2402                        resident_bytes: 0,
2403                        storage: CacheStorage::Streaming,
2404                    },
2405                    read_plan,
2406                    transient_bytes: decision.estimated_peak_bytes,
2407                })
2408            }
2409        }
2410    }
2411
2412    /// Execute a weighted reduction over a prepared dataset.
2413    ///
2414    /// # Errors
2415    ///
2416    /// Returns [`RuntimeError`] when streaming, cache validation, evaluation,
2417    /// or reduction fails, or another distributed worker reports failure.
2418    pub fn reduce(
2419        &self,
2420        execution: &Execution,
2421        params: &ParamValues,
2422        dataset: &CpuPreparedDataset,
2423        reduction: ReductionPlan,
2424    ) -> RuntimeResult<f64> {
2425        let local = match dataset {
2426            CpuPreparedDataset::Resident { dataset, .. } => {
2427                self.reduce_cached(execution, params, dataset, reduction)
2428            }
2429            CpuPreparedDataset::Streaming {
2430                dataset,
2431                read_plan,
2432                transient_bytes,
2433                ..
2434            } => (|| {
2435                let _memory = execution
2436                    .host_memory()
2437                    .reserve(*transient_bytes)
2438                    .map_err(RuntimeError::from)?;
2439                let mut total = AccurateF64::zero();
2440                for batch in dataset
2441                    .batches_with_plan(*read_plan)
2442                    .map_err(|error| RuntimeError::Data(error.to_string()))?
2443                {
2444                    let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
2445                    let cached = CpuCachedDataset {
2446                        sum_weights: (0..batch.len()).map(|row| batch.weights_at(row)).sum(),
2447                        batches: vec![CpuCachedBatch {
2448                            cache: self.cache_event_batch(&batch)?,
2449                        }],
2450                    };
2451                    total.push(self.reduce_cached(execution, params, &cached, reduction)?);
2452                }
2453                Ok(total.finish())
2454            })(),
2455        };
2456        if !execution.all_succeeded(local.is_ok()) {
2457            return local.and(Err(RuntimeError::DistributedPeerFailure));
2458        }
2459        Ok(execution.sum_f64(local?))
2460    }
2461
2462    /// Execute a weighted reduction and its free-parameter gradient.
2463    ///
2464    /// # Errors
2465    ///
2466    /// Returns [`RuntimeError`] when streaming, cache validation,
2467    /// differentiation, evaluation, or reduction fails, or another distributed
2468    /// worker reports failure.
2469    pub fn reduce_with_gradient(
2470        &self,
2471        execution: &Execution,
2472        params: &ParamValues,
2473        dataset: &CpuPreparedDataset,
2474        reduction: ReductionPlan,
2475    ) -> RuntimeResult<ReductionEvaluation> {
2476        let (value, gradient) =
2477            self.try_reduce_weighted_with_gradient(execution, params, dataset, |value| {
2478                reduction
2479                    .apply(value)
2480                    .map(|output| output.into_parts())
2481                    .map_err(RuntimeError::from)
2482            })?;
2483        Ok(ReductionEvaluation { value, gradient })
2484    }
2485
2486    fn try_reduce_weighted_with_gradient<E, F>(
2487        &self,
2488        execution: &Execution,
2489        params: &ParamValues,
2490        dataset: &CpuPreparedDataset,
2491        transform: F,
2492    ) -> Result<(f64, Vec<f64>), E>
2493    where
2494        E: From<RuntimeError> + Send,
2495        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
2496    {
2497        let local = match dataset {
2498            CpuPreparedDataset::Resident { dataset, .. } => {
2499                self.try_reduce_weighted_with_gradient_cached(execution, params, dataset, transform)
2500            }
2501            CpuPreparedDataset::Streaming {
2502                dataset,
2503                read_plan,
2504                transient_bytes,
2505                ..
2506            } => (|| {
2507                let _memory = execution
2508                    .host_memory()
2509                    .reserve(*transient_bytes)
2510                    .map_err(RuntimeError::from)
2511                    .map_err(E::from)?;
2512                let mut value = AccurateF64::zero();
2513                let mut gradient = (0..self.free_parameter_count())
2514                    .map(|_| AccurateF64::zero())
2515                    .collect::<Vec<_>>();
2516                for batch in dataset
2517                    .batches_with_plan(*read_plan)
2518                    .map_err(|error| E::from(RuntimeError::Data(error.to_string())))?
2519                {
2520                    let batch =
2521                        batch.map_err(|error| E::from(RuntimeError::Data(error.to_string())))?;
2522                    let cached = CpuCachedDataset {
2523                        sum_weights: (0..batch.len()).map(|row| batch.weights_at(row)).sum(),
2524                        batches: vec![CpuCachedBatch {
2525                            cache: self.cache_event_batch(&batch)?,
2526                        }],
2527                    };
2528                    let (partial_value, partial_gradient) = self
2529                        .try_reduce_weighted_with_gradient_cached(
2530                            execution, params, &cached, &transform,
2531                        )?;
2532                    value.push(partial_value);
2533                    for (sum, partial) in gradient.iter_mut().zip(partial_gradient) {
2534                        sum.push(partial);
2535                    }
2536                }
2537                Ok::<_, E>((
2538                    value.finish(),
2539                    gradient.into_iter().map(AccurateF64::finish).collect(),
2540                ))
2541            })(),
2542        };
2543        if !execution.all_succeeded(local.is_ok()) {
2544            return local.and(Err(E::from(RuntimeError::DistributedPeerFailure)));
2545        }
2546        let (local_value, local_gradient) = local?;
2547        Ok((
2548            execution.sum_f64(local_value),
2549            execution.sum_slice(&local_gradient),
2550        ))
2551    }
2552
2553    /// Evaluates every event in a fully cached dataset.
2554    ///
2555    /// # Errors
2556    ///
2557    /// Returns [`RuntimeError`] when parameters or a cache layout are
2558    /// incompatible, evaluation fails, or a matrix is singular.
2559    pub fn evaluate_cached_dataset(
2560        &self,
2561        params: &ParamValues,
2562        dataset: &CpuCachedDataset,
2563    ) -> RuntimeResult<Vec<Complex64>> {
2564        let total_len = dataset.batches.iter().map(CpuCachedBatch::len).sum();
2565        let mut out = Vec::with_capacity(total_len);
2566        let invariant = self.scalar_invariant_values(params)?;
2567        let mut workspace = ScalarEventWorkspace::default();
2568        for batch in &dataset.batches {
2569            self.check_batch_cache(batch.cache())?;
2570            for row in 0..batch.len() {
2571                out.push(self.evaluate_cache_row_prepared(
2572                    params,
2573                    batch.cache(),
2574                    row,
2575                    invariant.as_ref(),
2576                    &mut workspace,
2577                )?);
2578            }
2579        }
2580        Ok(out)
2581    }
2582
2583    /// Evaluates every event and gradient in a fully cached dataset.
2584    ///
2585    /// # Errors
2586    ///
2587    /// Returns [`RuntimeError`] when parameters or a cache layout are
2588    /// incompatible, or differentiation or evaluation fails.
2589    pub fn evaluate_cached_dataset_with_gradient(
2590        &self,
2591        params: &ParamValues,
2592        dataset: &CpuCachedDataset,
2593    ) -> RuntimeResult<Vec<ValueGradient>> {
2594        let total_len = dataset.batches.iter().map(CpuCachedBatch::len).sum();
2595        let mut out = Vec::with_capacity(total_len);
2596        for batch in &dataset.batches {
2597            out.extend(self.evaluate_cache_with_gradient(params, batch.cache())?);
2598        }
2599        Ok(out)
2600    }
2601
2602    fn try_weighted_sum_cached<E, F>(
2603        &self,
2604        params: &ParamValues,
2605        dataset: &CpuCachedDataset,
2606        mut f: F,
2607    ) -> Result<f64, E>
2608    where
2609        E: From<RuntimeError>,
2610        F: FnMut(Complex64) -> Result<f64, E>,
2611    {
2612        let mut sum = 0.0;
2613        let invariant = self.scalar_invariant_values(params)?;
2614        let mut workspace = ScalarEventWorkspace::default();
2615        for batch in dataset.batches() {
2616            self.check_batch_cache(batch.cache())?;
2617            for row in 0..batch.len() {
2618                let value = self.evaluate_cache_row_prepared(
2619                    params,
2620                    batch.cache(),
2621                    row,
2622                    invariant.as_ref(),
2623                    &mut workspace,
2624                )?;
2625                sum += batch.weights()[row] * f(value)?;
2626            }
2627        }
2628        Ok(sum)
2629    }
2630
2631    #[cfg(test)]
2632    fn weighted_sum_cached<F>(
2633        &self,
2634        params: &ParamValues,
2635        dataset: &CpuCachedDataset,
2636        mut f: F,
2637    ) -> RuntimeResult<f64>
2638    where
2639        F: FnMut(Complex64) -> f64,
2640    {
2641        self.try_weighted_sum_cached(params, dataset, |value| Ok(f(value)))
2642    }
2643
2644    fn try_weighted_real_sum_with_gradient_cached<E, F>(
2645        &self,
2646        params: &ParamValues,
2647        dataset: &CpuCachedDataset,
2648        mut transform: F,
2649    ) -> Result<(f64, Vec<f64>), E>
2650    where
2651        E: From<RuntimeError>,
2652        F: FnMut(Complex64) -> Result<(f64, f64), E>,
2653    {
2654        #[cfg(feature = "jit")]
2655        if let (Some(value_kernel), Some(gradient_kernel)) =
2656            (self.scalar_jit_kernel(), self.gradient_jit_kernel())
2657        {
2658            return self.try_weighted_real_sum_with_jit_gradient_cached(
2659                params,
2660                dataset,
2661                transform,
2662                value_kernel,
2663                gradient_kernel,
2664            );
2665        }
2666        if self.precision != Precision::F32
2667            && let Some(interpreter) = self.gradient_interpreter()
2668            && let Some(mut state) = interpreter.prepare_real_blocks(params)?
2669        {
2670            let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2671            let output_count = state.output_count();
2672            for batch in dataset.batches() {
2673                self.check_batch_cache(batch.cache())?;
2674                for block in 0..batch.len().div_ceil(SCALAR_BLOCK_SIZE) {
2675                    let start = block * SCALAR_BLOCK_SIZE;
2676                    let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2677                    let outputs = state.evaluate(batch.cache(), start, end)?;
2678                    for (lane, row) in outputs.chunks_exact(output_count).enumerate() {
2679                        let (value, derivative) = transform(row[0])?;
2680                        total.push(batch.weights()[start + lane], value, derivative, &row[1..]);
2681                    }
2682                }
2683            }
2684            return Ok(total.finish());
2685        }
2686        if self.precision == Precision::F32
2687            && let Some(ir) = self.f32_gradient_fallback_real.as_ref()
2688        {
2689            let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2690            let mut gradient = Vec::new();
2691            for batch in dataset.batches() {
2692                self.check_batch_cache(batch.cache())?;
2693                for row in 0..batch.len() {
2694                    let (value, model_gradient) = self.evaluate_f32_gradient_component_prepared(
2695                        ir,
2696                        params,
2697                        F32KernelInput::Cache(Some((batch.cache(), row))),
2698                        &mut gradient,
2699                    )?;
2700                    let (value, derivative) = transform(value)?;
2701                    total.push_f32(batch.weights()[row], value, derivative, model_gradient);
2702                }
2703            }
2704            return Ok(total.finish());
2705        }
2706        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2707        for batch in dataset.batches() {
2708            self.check_batch_cache(batch.cache())?;
2709            for row in 0..batch.len() {
2710                let evaluation =
2711                    self.evaluate_cache_row_with_gradient_unchecked(params, batch.cache(), row)?;
2712                let (value, derivative) = transform(evaluation.value())?;
2713                total.push(
2714                    batch.weights()[row],
2715                    value,
2716                    derivative,
2717                    evaluation.gradient(),
2718                );
2719            }
2720        }
2721        Ok(total.finish())
2722    }
2723
2724    #[cfg(feature = "jit")]
2725    fn try_weighted_real_sum_with_jit_gradient_cached<E, F>(
2726        &self,
2727        params: &ParamValues,
2728        dataset: &CpuCachedDataset,
2729        mut transform: F,
2730        value_kernel: &JitScalarKernel,
2731        gradient_kernel: &JitGradientKernel,
2732    ) -> Result<(f64, Vec<f64>), E>
2733    where
2734        E: From<RuntimeError>,
2735        F: FnMut(Complex64) -> Result<(f64, f64), E>,
2736    {
2737        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2738        let mut values = Vec::new();
2739        let mut tangents = Vec::new();
2740        let mut derivatives = Vec::new();
2741        for batch in dataset.batches() {
2742            self.check_batch_cache(batch.cache())?;
2743            let cache = JitScalarKernel::prepare_cache(batch.cache());
2744            for block in 0..batch.len().div_ceil(SCALAR_BLOCK_SIZE) {
2745                let start = block * SCALAR_BLOCK_SIZE;
2746                let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2747                value_kernel.evaluate_prepared(params, &cache, start, end, &mut values)?;
2748                derivatives.clear();
2749                derivatives.reserve(values.len());
2750                for (lane, value) in values.iter().copied().enumerate() {
2751                    let (value, derivative) = transform(value)?;
2752                    let weight = batch.weights()[start + lane];
2753                    total.value.push(weight * value);
2754                    derivatives.push(weight * derivative);
2755                }
2756                gradient_kernel.evaluate_prepared(params, &cache, start, end, 0, &mut tangents)?;
2757                for (lane, factor) in derivatives.iter().enumerate() {
2758                    for free_index in 0..self.free_parameter_count() {
2759                        total.gradient[free_index].push(
2760                            factor * tangents[lane * self.free_parameter_count() + free_index],
2761                        );
2762                    }
2763                }
2764            }
2765        }
2766        Ok(total.finish())
2767    }
2768
2769    #[cfg(test)]
2770    fn try_weighted_complex_sum_cached<E, F>(
2771        &self,
2772        params: &ParamValues,
2773        dataset: &CpuCachedDataset,
2774        mut f: F,
2775    ) -> Result<Complex64, E>
2776    where
2777        E: From<RuntimeError>,
2778        F: FnMut(Complex64) -> Result<Complex64, E>,
2779    {
2780        let mut sum = Complex64::default();
2781        let invariant = self.scalar_invariant_values(params)?;
2782        let mut workspace = ScalarEventWorkspace::default();
2783        for batch in dataset.batches() {
2784            self.check_batch_cache(batch.cache())?;
2785            for row in 0..batch.len() {
2786                let value = self.evaluate_cache_row_prepared(
2787                    params,
2788                    batch.cache(),
2789                    row,
2790                    invariant.as_ref(),
2791                    &mut workspace,
2792                )?;
2793                sum += f(value)? * batch.weights()[row];
2794            }
2795        }
2796        Ok(sum)
2797    }
2798
2799    #[cfg(test)]
2800    fn weighted_complex_sum_cached<F>(
2801        &self,
2802        params: &ParamValues,
2803        dataset: &CpuCachedDataset,
2804        mut f: F,
2805    ) -> RuntimeResult<Complex64>
2806    where
2807        F: FnMut(Complex64) -> Complex64,
2808    {
2809        self.try_weighted_complex_sum_cached(params, dataset, |value| Ok(f(value)))
2810    }
2811
2812    fn reduce_cached(
2813        &self,
2814        execution: &Execution,
2815        params: &ParamValues,
2816        dataset: &CpuCachedDataset,
2817        reduction: ReductionPlan,
2818    ) -> RuntimeResult<f64> {
2819        if execution.is_parallel() {
2820            execution.install(|| {
2821                self.par_try_weighted_sum_cached(params, dataset, |value| {
2822                    self.apply_reduction(reduction, value)
2823                })
2824            })
2825        } else {
2826            self.try_weighted_sum_cached(params, dataset, |value| {
2827                self.apply_reduction(reduction, value)
2828            })
2829        }
2830    }
2831
2832    fn apply_reduction(&self, reduction: ReductionPlan, value: Complex64) -> RuntimeResult<f64> {
2833        if self.precision != Precision::F32 {
2834            return reduction
2835                .apply(value)
2836                .map(|output| output.value())
2837                .map_err(RuntimeError::from);
2838        }
2839        let real = value.re as f32;
2840        match reduction.transform() {
2841            ReductionTransform::Real => Ok(real as f64),
2842            ReductionTransform::PositiveReal if real > 0.0 => Ok(real as f64),
2843            ReductionTransform::LogPositiveReal if real > 0.0 => Ok(real.ln() as f64),
2844            ReductionTransform::PositiveReal | ReductionTransform::LogPositiveReal => reduction
2845                .apply(Complex64::from(real as f64))
2846                .map(|output| output.value())
2847                .map_err(RuntimeError::from),
2848        }
2849    }
2850
2851    fn try_reduce_weighted_with_gradient_cached<E, F>(
2852        &self,
2853        execution: &Execution,
2854        params: &ParamValues,
2855        dataset: &CpuCachedDataset,
2856        transform: F,
2857    ) -> Result<(f64, Vec<f64>), E>
2858    where
2859        E: From<RuntimeError> + Send,
2860        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
2861    {
2862        if execution.is_parallel() {
2863            execution.install(|| {
2864                self.par_try_weighted_real_sum_with_gradient_cached(params, dataset, transform)
2865            })
2866        } else {
2867            self.try_weighted_real_sum_with_gradient_cached(params, dataset, transform)
2868        }
2869    }
2870
2871    pub(crate) fn par_try_weighted_sum_cached<E, F>(
2872        &self,
2873        params: &ParamValues,
2874        dataset: &CpuCachedDataset,
2875        f: F,
2876    ) -> Result<f64, E>
2877    where
2878        E: From<RuntimeError> + Send,
2879        F: Fn(Complex64) -> Result<f64, E> + Send + Sync,
2880    {
2881        let mut total = AccurateF64::zero();
2882        let invariant = self.scalar_invariant_values(params)?;
2883        for batch in dataset.batches() {
2884            self.check_batch_cache(batch.cache())?;
2885            #[cfg(feature = "jit")]
2886            let jit_cache = self
2887                .scalar_jit_kernel()
2888                .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
2889            let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
2890            let partial = (0..n_blocks)
2891                .into_par_iter()
2892                .try_fold(
2893                    || {
2894                        (
2895                            AccurateF64::zero(),
2896                            ScalarEventWorkspace::default(),
2897                            Vec::new(),
2898                        )
2899                    },
2900                    |(mut acc, mut workspace, mut output), block| {
2901                        let start = block * SCALAR_BLOCK_SIZE;
2902                        let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2903                        self.evaluate_cache_block_prepared(
2904                            params,
2905                            batch.cache(),
2906                            start,
2907                            end,
2908                            invariant.as_ref(),
2909                            &mut workspace,
2910                            &mut output,
2911                            #[cfg(feature = "jit")]
2912                            jit_cache.as_ref(),
2913                        )?;
2914                        for (lane, value) in output.iter().copied().enumerate() {
2915                            acc.push(batch.weights()[start + lane] * f(value)?);
2916                        }
2917                        Ok::<_, E>((acc, workspace, output))
2918                    },
2919                )
2920                .try_reduce(
2921                    || {
2922                        (
2923                            AccurateF64::zero(),
2924                            ScalarEventWorkspace::default(),
2925                            Vec::new(),
2926                        )
2927                    },
2928                    |(mut lhs, workspace, output), (rhs, _, _)| {
2929                        lhs.merge(rhs);
2930                        Ok::<_, E>((lhs, workspace, output))
2931                    },
2932                )?;
2933            total.merge(partial.0);
2934        }
2935        Ok(total.finish())
2936    }
2937
2938    #[cfg(test)]
2939    pub(crate) fn par_weighted_sum_cached<F>(
2940        &self,
2941        params: &ParamValues,
2942        dataset: &CpuCachedDataset,
2943        f: F,
2944    ) -> RuntimeResult<f64>
2945    where
2946        F: Fn(Complex64) -> f64 + Send + Sync,
2947    {
2948        self.par_try_weighted_sum_cached(params, dataset, |value| Ok(f(value)))
2949    }
2950
2951    pub(crate) fn par_try_weighted_real_sum_with_gradient_cached<E, F>(
2952        &self,
2953        params: &ParamValues,
2954        dataset: &CpuCachedDataset,
2955        transform: F,
2956    ) -> Result<(f64, Vec<f64>), E>
2957    where
2958        E: From<RuntimeError> + Send,
2959        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
2960    {
2961        #[cfg(feature = "jit")]
2962        if let (Some(value_kernel), Some(gradient_kernel)) =
2963            (self.scalar_jit_kernel(), self.gradient_jit_kernel())
2964        {
2965            return self.par_try_weighted_real_sum_with_jit_gradient_cached(
2966                params,
2967                dataset,
2968                transform,
2969                value_kernel,
2970                gradient_kernel,
2971            );
2972        }
2973        if self.precision != Precision::F32
2974            && let Some(interpreter) = self.gradient_interpreter()
2975            && let Some(state) = interpreter.prepare_real_blocks(params)?
2976        {
2977            let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2978            let output_count = state.output_count();
2979            for batch in dataset.batches() {
2980                self.check_batch_cache(batch.cache())?;
2981                let partial = (0..batch.len().div_ceil(SCALAR_BLOCK_SIZE))
2982                    .into_par_iter()
2983                    .try_fold(
2984                        || {
2985                            (
2986                                RealGradientAccumulator::zero(self.free_parameter_count()),
2987                                state.clone(),
2988                            )
2989                        },
2990                        |(mut accumulator, mut state), block| {
2991                            let start = block * SCALAR_BLOCK_SIZE;
2992                            let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2993                            let outputs = state.evaluate(batch.cache(), start, end)?;
2994                            for (lane, row) in outputs.chunks_exact(output_count).enumerate() {
2995                                let (value, derivative) = transform(row[0])?;
2996                                accumulator.push(
2997                                    batch.weights()[start + lane],
2998                                    value,
2999                                    derivative,
3000                                    &row[1..],
3001                                );
3002                            }
3003                            Ok::<_, E>((accumulator, state))
3004                        },
3005                    )
3006                    .try_reduce(
3007                        || {
3008                            (
3009                                RealGradientAccumulator::zero(self.free_parameter_count()),
3010                                state.clone(),
3011                            )
3012                        },
3013                        |(mut lhs, state), (rhs, _)| {
3014                            lhs.merge(rhs);
3015                            Ok::<_, E>((lhs, state))
3016                        },
3017                    )?;
3018                total.merge(partial.0);
3019            }
3020            return Ok(total.finish());
3021        }
3022        if self.precision == Precision::F32
3023            && let Some(ir) = self.f32_gradient_fallback_real.as_ref()
3024        {
3025            let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3026            for batch in dataset.batches() {
3027                self.check_batch_cache(batch.cache())?;
3028                let partial = (0..batch.len())
3029                    .into_par_iter()
3030                    .try_fold(
3031                        || {
3032                            (
3033                                RealGradientAccumulator::zero(self.free_parameter_count()),
3034                                Vec::new(),
3035                            )
3036                        },
3037                        |(mut accumulator, mut gradient), row| {
3038                            let (value, model_gradient) = self
3039                                .evaluate_f32_gradient_component_prepared(
3040                                    ir,
3041                                    params,
3042                                    F32KernelInput::Cache(Some((batch.cache(), row))),
3043                                    &mut gradient,
3044                                )?;
3045                            let (value, derivative) = transform(value)?;
3046                            accumulator.push_f32(
3047                                batch.weights()[row],
3048                                value,
3049                                derivative,
3050                                model_gradient,
3051                            );
3052                            Ok::<_, E>((accumulator, gradient))
3053                        },
3054                    )
3055                    .try_reduce(
3056                        || {
3057                            (
3058                                RealGradientAccumulator::zero(self.free_parameter_count()),
3059                                Vec::new(),
3060                            )
3061                        },
3062                        |(mut lhs, gradient), (rhs, _)| {
3063                            lhs.merge(rhs);
3064                            Ok::<_, E>((lhs, gradient))
3065                        },
3066                    )?;
3067                total.merge(partial.0);
3068            }
3069            return Ok(total.finish());
3070        }
3071        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3072        for batch in dataset.batches() {
3073            self.check_batch_cache(batch.cache())?;
3074            let partial = (0..batch.len())
3075                .into_par_iter()
3076                .try_fold(
3077                    || RealGradientAccumulator::zero(self.free_parameter_count()),
3078                    |mut accumulator, row| {
3079                        let evaluation = self.evaluate_cache_row_with_gradient_unchecked(
3080                            params,
3081                            batch.cache(),
3082                            row,
3083                        )?;
3084                        let (value, derivative) = transform(evaluation.value())?;
3085                        accumulator.push(
3086                            batch.weights()[row],
3087                            value,
3088                            derivative,
3089                            evaluation.gradient(),
3090                        );
3091                        Ok::<_, E>(accumulator)
3092                    },
3093                )
3094                .try_reduce(
3095                    || RealGradientAccumulator::zero(self.free_parameter_count()),
3096                    |mut lhs, rhs| {
3097                        lhs.merge(rhs);
3098                        Ok::<_, E>(lhs)
3099                    },
3100                )?;
3101            total.merge(partial);
3102        }
3103        Ok(total.finish())
3104    }
3105
3106    #[cfg(feature = "jit")]
3107    fn par_try_weighted_real_sum_with_jit_gradient_cached<E, F>(
3108        &self,
3109        params: &ParamValues,
3110        dataset: &CpuCachedDataset,
3111        transform: F,
3112        value_kernel: &JitScalarKernel,
3113        gradient_kernel: &JitGradientKernel,
3114    ) -> Result<(f64, Vec<f64>), E>
3115    where
3116        E: From<RuntimeError> + Send,
3117        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
3118    {
3119        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3120        for batch in dataset.batches() {
3121            self.check_batch_cache(batch.cache())?;
3122            let cache = JitScalarKernel::prepare_cache(batch.cache());
3123            let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
3124            let partial = (0..n_blocks)
3125                .into_par_iter()
3126                .try_fold(
3127                    || {
3128                        (
3129                            RealGradientAccumulator::zero(self.free_parameter_count()),
3130                            Vec::new(),
3131                            Vec::new(),
3132                            Vec::new(),
3133                        )
3134                    },
3135                    |(mut accumulator, mut values, mut tangents, mut derivatives), block| {
3136                        let start = block * SCALAR_BLOCK_SIZE;
3137                        let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
3138                        value_kernel.evaluate_prepared(params, &cache, start, end, &mut values)?;
3139                        derivatives.clear();
3140                        derivatives.reserve(values.len());
3141                        for (lane, value) in values.iter().copied().enumerate() {
3142                            let (value, derivative) = transform(value)?;
3143                            let weight = batch.weights()[start + lane];
3144                            accumulator.value.push(weight * value);
3145                            derivatives.push(weight * derivative);
3146                        }
3147                        gradient_kernel.evaluate_prepared(
3148                            params,
3149                            &cache,
3150                            start,
3151                            end,
3152                            0,
3153                            &mut tangents,
3154                        )?;
3155                        for (lane, factor) in derivatives.iter().enumerate() {
3156                            for free_index in 0..self.free_parameter_count() {
3157                                accumulator.gradient[free_index].push(
3158                                    factor
3159                                        * tangents[lane * self.free_parameter_count() + free_index],
3160                                );
3161                            }
3162                        }
3163                        Ok::<_, E>((accumulator, values, tangents, derivatives))
3164                    },
3165                )
3166                .try_reduce(
3167                    || {
3168                        (
3169                            RealGradientAccumulator::zero(self.free_parameter_count()),
3170                            Vec::new(),
3171                            Vec::new(),
3172                            Vec::new(),
3173                        )
3174                    },
3175                    |(mut lhs, values, tangents, derivatives), (rhs, _, _, _)| {
3176                        lhs.merge(rhs);
3177                        Ok::<_, E>((lhs, values, tangents, derivatives))
3178                    },
3179                )?;
3180            total.merge(partial.0);
3181        }
3182        Ok(total.finish())
3183    }
3184
3185    #[cfg(test)]
3186    pub(crate) fn par_try_weighted_complex_sum_cached<E, F>(
3187        &self,
3188        params: &ParamValues,
3189        dataset: &CpuCachedDataset,
3190        f: F,
3191    ) -> Result<Complex64, E>
3192    where
3193        E: From<RuntimeError> + Send,
3194        F: Fn(Complex64) -> Result<Complex64, E> + Send + Sync,
3195    {
3196        let mut total = AccurateComplex64::zero();
3197        let invariant = self.scalar_invariant_values(params)?;
3198        for batch in dataset.batches() {
3199            self.check_batch_cache(batch.cache())?;
3200            #[cfg(feature = "jit")]
3201            let jit_cache = self
3202                .scalar_jit_kernel()
3203                .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
3204            let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
3205            let partial = (0..n_blocks)
3206                .into_par_iter()
3207                .try_fold(
3208                    || {
3209                        (
3210                            AccurateComplex64::zero(),
3211                            ScalarEventWorkspace::default(),
3212                            Vec::new(),
3213                        )
3214                    },
3215                    |(mut acc, mut workspace, mut output), block| {
3216                        let start = block * SCALAR_BLOCK_SIZE;
3217                        let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
3218                        self.evaluate_cache_block_prepared(
3219                            params,
3220                            batch.cache(),
3221                            start,
3222                            end,
3223                            invariant.as_ref(),
3224                            &mut workspace,
3225                            &mut output,
3226                            #[cfg(feature = "jit")]
3227                            jit_cache.as_ref(),
3228                        )?;
3229                        for (lane, value) in output.iter().copied().enumerate() {
3230                            acc.push(f(value)? * batch.weights()[start + lane]);
3231                        }
3232                        Ok::<_, E>((acc, workspace, output))
3233                    },
3234                )
3235                .try_reduce(
3236                    || {
3237                        (
3238                            AccurateComplex64::zero(),
3239                            ScalarEventWorkspace::default(),
3240                            Vec::new(),
3241                        )
3242                    },
3243                    |(mut lhs, workspace, output), (rhs, _, _)| {
3244                        lhs.merge(rhs);
3245                        Ok::<_, E>((lhs, workspace, output))
3246                    },
3247                )?;
3248            total.merge(partial.0);
3249        }
3250        Ok(total.finish())
3251    }
3252
3253    #[cfg(test)]
3254    pub(crate) fn par_weighted_complex_sum_cached<F>(
3255        &self,
3256        params: &ParamValues,
3257        dataset: &CpuCachedDataset,
3258        f: F,
3259    ) -> RuntimeResult<Complex64>
3260    where
3261        F: Fn(Complex64) -> Complex64 + Send + Sync,
3262    {
3263        self.par_try_weighted_complex_sum_cached(params, dataset, |value| Ok(f(value)))
3264    }
3265
3266    fn evaluate_inner(
3267        &self,
3268        params: &ParamValues,
3269        event: Option<&dyn EventLookup>,
3270    ) -> RuntimeResult<Complex64> {
3271        #[cfg(feature = "jit")]
3272        if event.is_none()
3273            && let Some(kernel) = self.scalar_jit_kernel()
3274        {
3275            if params.as_slice().len() != self.params.len() {
3276                return Err(RuntimeError::Parameter(format!(
3277                    "expected {} parameter values, got {}",
3278                    self.params.len(),
3279                    params.as_slice().len()
3280                )));
3281            }
3282            return kernel.evaluate_invariant(params);
3283        }
3284        if self.precision == Precision::F32 {
3285            let input = match event {
3286                Some(event) => F32KernelInput::Event(event),
3287                None => F32KernelInput::Cache(None),
3288            };
3289            return self.evaluate_f32_scalar(params, input);
3290        }
3291        let values = self.evaluate_values(params, event)?;
3292        scalar_at(&values, self.graph.root().index())
3293    }
3294
3295    fn require_f64_gradient(&self) -> RuntimeResult<()> {
3296        if self.precision == Precision::F32 {
3297            return Err(crate::ExecutionError::UnsupportedCpuF32Gradient.into());
3298        }
3299        Ok(())
3300    }
3301
3302    fn evaluate_f32_scalar(
3303        &self,
3304        params: &ParamValues,
3305        input: F32KernelInput<'_>,
3306    ) -> RuntimeResult<Complex64> {
3307        let kernel = self
3308            .scalar_kernel
3309            .as_ref()
3310            .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3311        let values = self.evaluate_f32_kernel_values(kernel.values(), params, input)?;
3312        let value = f32_scalar_at(&values, kernel.root())?;
3313        Ok(Complex64::new(value.re as f64, value.im as f64))
3314    }
3315
3316    fn evaluate_f32_kernel_values(
3317        &self,
3318        kernel_values: &[KernelValue],
3319        params: &ParamValues,
3320        input: F32KernelInput<'_>,
3321    ) -> RuntimeResult<Vec<F32Value>> {
3322        let mut values = Vec::with_capacity(kernel_values.len());
3323        for (index, value) in kernel_values.iter().enumerate() {
3324            let result = match &value.instruction {
3325                KernelInstruction::Cached(slot) => self.evaluate_f32_cached_value(
3326                    *slot,
3327                    params,
3328                    input,
3329                    crate::ExecutionError::UnsupportedCpuF32Model,
3330                )?,
3331                KernelInstruction::RealConstant(value) => {
3332                    F32Value::Scalar(Complex32::from(*value as f32))
3333                }
3334                KernelInstruction::ComplexConstant(value) => {
3335                    F32Value::Scalar(Complex32::new(value.re as f32, value.im as f32))
3336                }
3337                KernelInstruction::Parameter(id) => F32Value::Scalar(Complex32::from(
3338                    params
3339                        .get(*id)
3340                        .map_err(|error| RuntimeError::Parameter(error.to_string()))?
3341                        as f32,
3342                )),
3343                KernelInstruction::Unary { op, input } => {
3344                    F32Value::Scalar(eval_unary(*op, f32_scalar_at(&values, *input)?))
3345                }
3346                KernelInstruction::Binary { op, lhs, rhs } => F32Value::Scalar(eval_binary(
3347                    *op,
3348                    f32_scalar_at(&values, *lhs)?,
3349                    f32_scalar_at(&values, *rhs)?,
3350                )),
3351                KernelInstruction::Add(terms) => F32Value::Scalar(
3352                    terms
3353                        .iter()
3354                        .map(|id| f32_scalar_at(&values, *id))
3355                        .sum::<RuntimeResult<Complex32>>()?,
3356                ),
3357                KernelInstruction::Mul(factors) => {
3358                    F32Value::Scalar(factors.iter().try_fold(Complex32::ONE, |product, id| {
3359                        Ok::<_, RuntimeError>(product * f32_scalar_at(&values, *id)?)
3360                    })?)
3361                }
3362                KernelInstruction::Complex { re, im } => F32Value::Scalar(Complex32::new(
3363                    f32_scalar_at(&values, *re)?.re,
3364                    f32_scalar_at(&values, *im)?.re,
3365                )),
3366                KernelInstruction::Vector(elements) => F32Value::Vector(
3367                    elements
3368                        .iter()
3369                        .map(|id| f32_scalar_at(&values, *id))
3370                        .collect::<RuntimeResult<_>>()?,
3371                ),
3372                KernelInstruction::Matrix {
3373                    rows,
3374                    cols,
3375                    elements,
3376                } => F32Value::Matrix {
3377                    rows: *rows,
3378                    cols: *cols,
3379                    values: elements
3380                        .iter()
3381                        .map(|id| f32_scalar_at(&values, *id))
3382                        .collect::<RuntimeResult<_>>()?,
3383                },
3384                KernelInstruction::Component {
3385                    input,
3386                    index: element,
3387                } => {
3388                    let vector = f32_vector_at(&values, *input)?;
3389                    F32Value::Scalar(*vector.get(*element).ok_or_else(|| {
3390                        RuntimeError::InvalidShape {
3391                            index,
3392                            message: format!(
3393                                "component index {element} out of bounds for len {}",
3394                                vector.len()
3395                            ),
3396                        }
3397                    })?)
3398                }
3399                KernelInstruction::MatrixElement { input, row, col } => {
3400                    let (rows, cols, matrix) = f32_matrix_at(&values, *input)?;
3401                    if *row >= rows || *col >= cols {
3402                        return Err(RuntimeError::InvalidShape {
3403                            index,
3404                            message: format!(
3405                                "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
3406                            ),
3407                        });
3408                    }
3409                    F32Value::Scalar(matrix[row * cols + col])
3410                }
3411                KernelInstruction::Dot { lhs, rhs } => {
3412                    let lhs = f32_vector_at(&values, *lhs)?;
3413                    let rhs = f32_vector_at(&values, *rhs)?;
3414                    if lhs.len() != rhs.len() {
3415                        return Err(RuntimeError::InvalidShape {
3416                            index,
3417                            message: format!(
3418                                "cannot dot len {} vector with len {} vector",
3419                                lhs.len(),
3420                                rhs.len()
3421                            ),
3422                        });
3423                    }
3424                    F32Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
3425                }
3426                KernelInstruction::MatVec { matrix, vector } => {
3427                    let (rows, cols, matrix) = f32_matrix_at(&values, *matrix)?;
3428                    let vector = f32_vector_at(&values, *vector)?;
3429                    if cols != vector.len() {
3430                        return Err(RuntimeError::InvalidShape {
3431                            index,
3432                            message: format!(
3433                                "cannot multiply {rows}x{cols} matrix by len {} vector",
3434                                vector.len()
3435                            ),
3436                        });
3437                    }
3438                    let output = DMatrix::from_row_slice(rows, cols, matrix)
3439                        * DVector::from_row_slice(vector);
3440                    F32Value::Vector(output.iter().copied().collect())
3441                }
3442                KernelInstruction::MatMul { lhs, rhs } => {
3443                    let (lhs_rows, lhs_cols, lhs) = f32_matrix_at(&values, *lhs)?;
3444                    let (rhs_rows, rhs_cols, rhs) = f32_matrix_at(&values, *rhs)?;
3445                    if lhs_cols != rhs_rows {
3446                        return Err(RuntimeError::InvalidShape {
3447                            index,
3448                            message: format!(
3449                                "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
3450                            ),
3451                        });
3452                    }
3453                    let output = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs)
3454                        * DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
3455                    F32Value::Matrix {
3456                        rows: output.nrows(),
3457                        cols: output.ncols(),
3458                        values: matrix_values_row_major_f32(&output),
3459                    }
3460                }
3461                KernelInstruction::Solve { matrix, rhs } => {
3462                    let (rows, cols, matrix) = f32_matrix_at(&values, *matrix)?;
3463                    let rhs = f32_vector_at(&values, *rhs)?;
3464                    if rows != cols || rows != rhs.len() {
3465                        return Err(RuntimeError::InvalidShape {
3466                            index,
3467                            message: format!(
3468                                "cannot solve {rows}x{cols} matrix against len {} vector",
3469                                rhs.len()
3470                            ),
3471                        });
3472                    }
3473                    let solution = DMatrix::from_row_slice(rows, cols, matrix)
3474                        .lu()
3475                        .solve(&DVector::from_row_slice(rhs))
3476                        .ok_or(RuntimeError::SingularMatrix(index))?;
3477                    F32Value::Vector(solution.iter().copied().collect())
3478                }
3479                KernelInstruction::SolveRow { row_slot, rhs } => {
3480                    let (cache, row) = input
3481                        .cache()
3482                        .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3483                    let inverse = cache.solve_row(*row_slot, row)?;
3484                    if inverse.len() != rhs.len() {
3485                        return Err(RuntimeError::InvalidShape {
3486                            index,
3487                            message: format!(
3488                                "specialized solve row has len {}, expected {}",
3489                                inverse.len(),
3490                                rhs.len()
3491                            ),
3492                        });
3493                    }
3494                    F32Value::Scalar(
3495                        inverse
3496                            .iter()
3497                            .zip(rhs)
3498                            .map(|(coefficient, rhs)| {
3499                                Ok::<_, RuntimeError>(
3500                                    Complex32::new(coefficient.re as f32, coefficient.im as f32)
3501                                        * f32_scalar_at(&values, *rhs)?,
3502                                )
3503                            })
3504                            .sum::<RuntimeResult<Complex32>>()?,
3505                    )
3506                }
3507                KernelInstruction::SolveRowAdjointElement {
3508                    row_slot,
3509                    index: element,
3510                    len,
3511                    adjoint,
3512                } => {
3513                    let (cache, row) = input
3514                        .cache()
3515                        .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3516                    let inverse = cache.solve_row(*row_slot, row)?;
3517                    if inverse.len() != *len {
3518                        return Err(RuntimeError::InvalidShape {
3519                            index,
3520                            message: format!(
3521                                "specialized solve row has len {}, expected {len}",
3522                                inverse.len()
3523                            ),
3524                        });
3525                    }
3526                    let coefficient = inverse[*element];
3527                    F32Value::Scalar(
3528                        f32_scalar_at(&values, *adjoint)?
3529                            * Complex32::new(coefficient.re as f32, coefficient.im as f32).conj(),
3530                    )
3531                }
3532            };
3533            values.push(result);
3534        }
3535        Ok(values)
3536    }
3537
3538    fn evaluate_f32_cached_value(
3539        &self,
3540        slot: usize,
3541        params: &ParamValues,
3542        input: F32KernelInput<'_>,
3543        missing_input: crate::ExecutionError,
3544    ) -> RuntimeResult<F32Value> {
3545        match input {
3546            F32KernelInput::Cache(Some((cache, row))) => {
3547                Ok(F32Value::from_value(cache.value(slot, row)?))
3548            }
3549            F32KernelInput::Cache(None) => Err(missing_input.into()),
3550            F32KernelInput::Event(event) => {
3551                let entry =
3552                    self.cache_plan
3553                        .entries()
3554                        .get(slot)
3555                        .ok_or(RuntimeError::InvalidShape {
3556                            index: self.graph.root().index(),
3557                            message: format!("cache slot {slot} is out of bounds"),
3558                        })?;
3559                let values = self.evaluate_values(params, Some(event))?;
3560                let value = values
3561                    .get(entry.node().index())
3562                    .ok_or(RuntimeError::InvalidShape {
3563                        index: entry.node().index(),
3564                        message: "cached node is out of bounds".into(),
3565                    })?
3566                    .clone();
3567                Ok(F32Value::from_value(value))
3568            }
3569        }
3570    }
3571
3572    fn evaluate_f32_gradient(
3573        &self,
3574        params: &ParamValues,
3575        input: F32KernelInput<'_>,
3576    ) -> RuntimeResult<ValueGradient> {
3577        let real_ir = self
3578            .f32_gradient_fallback_real
3579            .as_ref()
3580            .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3581        let mut real = Vec::new();
3582        let (value, _) =
3583            self.evaluate_f32_gradient_component_prepared(real_ir, params, input, &mut real)?;
3584        let imag = if let Some(imag_ir) = self.f32_gradient_fallback_imag.as_ref() {
3585            let mut imag = Vec::new();
3586            self.evaluate_f32_gradient_component_prepared(imag_ir, params, input, &mut imag)?;
3587            imag
3588        } else {
3589            vec![0.0; real.len()]
3590        };
3591        Ok(ValueGradient {
3592            value,
3593            gradient: real
3594                .into_iter()
3595                .zip(imag)
3596                .map(|(re, im)| Complex64::new(re as f64, im as f64))
3597                .collect(),
3598        })
3599    }
3600
3601    fn evaluate_f32_gradient_component_prepared<'a>(
3602        &self,
3603        ir: &GradientKernelIr,
3604        params: &ParamValues,
3605        input: F32KernelInput<'_>,
3606        gradient: &'a mut Vec<f32>,
3607    ) -> RuntimeResult<(Complex64, &'a [f32])> {
3608        let values = self.evaluate_f32_kernel_values(ir.values(), params, input)?;
3609        let value = f32_scalar_at(&values, ir.primal_root())?;
3610        gradient.clear();
3611        gradient.reserve(ir.outputs().len());
3612        for output in ir.outputs() {
3613            gradient.push(f32_scalar_at(&values, *output)?.re);
3614        }
3615        Ok((Complex64::new(value.re as f64, value.im as f64), gradient))
3616    }
3617
3618    fn value_gradient(
3619        &self,
3620        values: Vec<Value>,
3621        cached_factors: Option<(&CpuBatchCache, usize)>,
3622    ) -> RuntimeResult<ValueGradient> {
3623        let value = if cached_factors.is_some() {
3624            self.cached_scalar_at(&values, self.graph.root())?
3625        } else {
3626            scalar_at(&values, self.graph.root().index())?
3627        };
3628        let gradient = match self.autodiff.mode() {
3629            AutodiffMode::Forward => {
3630                DerivativeWorkspace::new(self, &values, cached_factors).gradient()?
3631            }
3632            AutodiffMode::Reverse => {
3633                ReverseDerivativeWorkspace::new(self, &values, cached_factors).gradient()?
3634            }
3635        };
3636        Ok(ValueGradient { value, gradient })
3637    }
3638
3639    fn solve_primal(
3640        &self,
3641        matrix_id: ExprId,
3642        dimension: usize,
3643        matrix: &[Complex64],
3644        rhs: &DVector<Complex64>,
3645        node_index: usize,
3646        cached: Option<(&CpuBatchCache, usize)>,
3647    ) -> RuntimeResult<DVector<Complex64>> {
3648        let solution = if let (Some(slot), Some((cache, row))) =
3649            (self.factor_matrix_slots[matrix_id.index()], cached)
3650        {
3651            cache.factor(slot, row)?.solve(rhs)
3652        } else if let Some(slot) = self.constant_factor_slots[matrix_id.index()] {
3653            self.constant_factors[slot]
3654                .get_or_init(|| DMatrix::from_row_slice(dimension, dimension, matrix).lu())
3655                .solve(rhs)
3656        } else {
3657            DMatrix::from_row_slice(dimension, dimension, matrix)
3658                .lu()
3659                .solve(rhs)
3660        };
3661        solution.ok_or(RuntimeError::SingularMatrix(node_index))
3662    }
3663
3664    fn event_columns(&self, schema: &Schema) -> RuntimeResult<Vec<Option<EventColumn>>> {
3665        self.graph
3666            .nodes()
3667            .iter()
3668            .map(|node| {
3669                if let ExprNode::EventScalar(name) = node {
3670                    Ok(Some(EventColumn::Scalar(
3671                        schema
3672                            .scalar_index(name)
3673                            .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?,
3674                    )))
3675                } else if let ExprNode::EventP4Component { name, component } = node {
3676                    Ok(Some(EventColumn::P4Component {
3677                        col: schema
3678                            .p4_index(name)
3679                            .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?,
3680                        component: *component,
3681                    }))
3682                } else {
3683                    Ok(None)
3684                }
3685            })
3686            .collect()
3687    }
3688
3689    fn evaluate_cache_values_for_row(
3690        &self,
3691        batch: &EventBatch,
3692        row: usize,
3693        event_columns: &[Option<EventColumn>],
3694    ) -> RuntimeResult<Vec<Option<Value>>> {
3695        let mut values = vec![None; self.graph.nodes().len()];
3696
3697        for id in &self.cache_materialization_nodes {
3698            let index = id.index();
3699            let node = &self.graph.nodes()[index];
3700            let value = match node {
3701                ExprNode::RealConst(value) => Value::Scalar(Complex64::from(*value)),
3702                ExprNode::ComplexConst(value) => Value::Scalar(*value),
3703                ExprNode::EventScalar(name) => {
3704                    let col = event_columns[index]
3705                        .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?;
3706                    let EventColumn::Scalar(col) = col else {
3707                        return Err(RuntimeError::MissingEventColumn(name.to_string()));
3708                    };
3709                    Value::Scalar(Complex64::from(batch.scalar_at(col, row)))
3710                }
3711                ExprNode::EventP4Component { name, component } => {
3712                    let col = event_columns[index]
3713                        .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?;
3714                    let EventColumn::P4Component {
3715                        col,
3716                        component: actual,
3717                    } = col
3718                    else {
3719                        return Err(RuntimeError::MissingEventColumn(name.to_string()));
3720                    };
3721                    debug_assert_eq!(actual, *component);
3722                    let p4 = batch.p4_at(col, row);
3723                    let value = match component {
3724                        P4Component::Px => p4.px,
3725                        P4Component::Py => p4.py,
3726                        P4Component::Pz => p4.pz,
3727                        P4Component::E => p4.e,
3728                    };
3729                    Value::Scalar(Complex64::from(value))
3730                }
3731                ExprNode::Unary { op, input } => {
3732                    let input = scalar_at_optional(&values, input.index())?;
3733                    Value::Scalar(eval_unary(*op, input))
3734                }
3735                ExprNode::Binary { op, lhs, rhs } => {
3736                    let lhs = scalar_at_optional(&values, lhs.index())?;
3737                    let rhs = scalar_at_optional(&values, rhs.index())?;
3738                    Value::Scalar(eval_binary(*op, lhs, rhs))
3739                }
3740                ExprNode::NaryAdd { terms } => {
3741                    let mut sum = Complex64::ZERO;
3742                    for term in terms {
3743                        sum += scalar_at_optional(&values, term.index())?;
3744                    }
3745                    Value::Scalar(sum)
3746                }
3747                ExprNode::NaryMul { factors } => {
3748                    let mut product = Complex64::ONE;
3749                    for factor in factors {
3750                        product *= scalar_at_optional(&values, factor.index())?;
3751                    }
3752                    Value::Scalar(product)
3753                }
3754                ExprNode::Complex { re, im } => {
3755                    let re = scalar_at_optional(&values, re.index())?;
3756                    let im = scalar_at_optional(&values, im.index())?;
3757                    Value::Scalar(Complex64::new(re.re, im.re))
3758                }
3759                ExprNode::Vector { elements } => Value::Vector(
3760                    elements
3761                        .iter()
3762                        .map(|id| scalar_at_optional(&values, id.index()))
3763                        .collect::<RuntimeResult<_>>()?,
3764                ),
3765                ExprNode::Matrix {
3766                    rows,
3767                    cols,
3768                    elements,
3769                } => {
3770                    if elements.len() != rows * cols {
3771                        return Err(RuntimeError::InvalidShape {
3772                            index,
3773                            message: format!(
3774                                "matrix has {} elements for shape {rows}x{cols}",
3775                                elements.len()
3776                            ),
3777                        });
3778                    }
3779                    Value::Matrix {
3780                        rows: *rows,
3781                        cols: *cols,
3782                        values: elements
3783                            .iter()
3784                            .map(|id| scalar_at_optional(&values, id.index()))
3785                            .collect::<RuntimeResult<_>>()?,
3786                    }
3787                }
3788                ExprNode::Component { input, index: i } => {
3789                    let vector = vector_at_optional(&values, input.index())?;
3790                    Value::Scalar(*vector.get(*i).ok_or_else(|| RuntimeError::InvalidShape {
3791                        index,
3792                        message: format!(
3793                            "component index {i} out of bounds for len {}",
3794                            vector.len()
3795                        ),
3796                    })?)
3797                }
3798                ExprNode::MatrixElement { input, row, col } => {
3799                    let (rows, cols, matrix) = matrix_at_optional(&values, input.index())?;
3800                    if *row >= rows || *col >= cols {
3801                        return Err(RuntimeError::InvalidShape {
3802                            index,
3803                            message: format!(
3804                                "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
3805                            ),
3806                        });
3807                    }
3808                    Value::Scalar(matrix[row * cols + col])
3809                }
3810                ExprNode::MatMul { lhs, rhs } => {
3811                    let (lhs_rows, lhs_cols, lhs) = matrix_at_optional(&values, lhs.index())?;
3812                    let (rhs_rows, rhs_cols, rhs) = matrix_at_optional(&values, rhs.index())?;
3813                    if lhs_cols != rhs_rows {
3814                        return Err(RuntimeError::InvalidShape {
3815                            index,
3816                            message: format!(
3817                                "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
3818                            ),
3819                        });
3820                    }
3821                    let lhs = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs);
3822                    let rhs = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
3823                    let out = lhs * rhs;
3824                    Value::Matrix {
3825                        rows: out.nrows(),
3826                        cols: out.ncols(),
3827                        values: matrix_values_row_major(&out),
3828                    }
3829                }
3830                ExprNode::MatVec { matrix, vector } => {
3831                    let (rows, cols, matrix) = matrix_at_optional(&values, matrix.index())?;
3832                    let vector = vector_at_optional(&values, vector.index())?;
3833                    if cols != vector.len() {
3834                        return Err(RuntimeError::InvalidShape {
3835                            index,
3836                            message: format!(
3837                                "cannot multiply {rows}x{cols} matrix by len {} vector",
3838                                vector.len()
3839                            ),
3840                        });
3841                    }
3842                    let matrix = DMatrix::from_row_slice(rows, cols, matrix);
3843                    let vector = DVector::from_row_slice(vector);
3844                    Value::Vector((matrix * vector).iter().copied().collect())
3845                }
3846                ExprNode::Dot { lhs, rhs } => {
3847                    let lhs = vector_at_optional(&values, lhs.index())?;
3848                    let rhs = vector_at_optional(&values, rhs.index())?;
3849                    if lhs.len() != rhs.len() {
3850                        return Err(RuntimeError::InvalidShape {
3851                            index,
3852                            message: format!(
3853                                "cannot dot len {} vector with len {} vector",
3854                                lhs.len(),
3855                                rhs.len()
3856                            ),
3857                        });
3858                    }
3859                    Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
3860                }
3861                ExprNode::Solve { matrix, rhs } => {
3862                    let matrix_id = *matrix;
3863                    let (rows, cols, matrix) = matrix_at_optional(&values, matrix_id.index())?;
3864                    let rhs = vector_at_optional(&values, rhs.index())?;
3865                    if rows != cols || rows != rhs.len() {
3866                        return Err(RuntimeError::InvalidShape {
3867                            index,
3868                            message: format!(
3869                                "cannot solve {rows}x{cols} matrix against len {} vector",
3870                                rhs.len()
3871                            ),
3872                        });
3873                    }
3874                    let rhs = DVector::from_row_slice(rhs);
3875                    let solution = self.solve_primal(matrix_id, rows, matrix, &rhs, index, None)?;
3876                    Value::Vector(solution.iter().copied().collect())
3877                }
3878                ExprNode::ScalarParam(_) => {
3879                    return Err(RuntimeError::InvalidShape {
3880                        index,
3881                        message: "parameter-dependent node cannot be part of an event cache".into(),
3882                    });
3883                }
3884            };
3885            values[index] = Some(value);
3886        }
3887
3888        Ok(values)
3889    }
3890
3891    fn evaluate_values(
3892        &self,
3893        params: &ParamValues,
3894        event: Option<&dyn EventLookup>,
3895    ) -> RuntimeResult<Vec<Value>> {
3896        let mut values = Vec::with_capacity(self.graph.nodes().len());
3897
3898        for (index, node) in self.graph.nodes().iter().enumerate() {
3899            let value = match node {
3900                ExprNode::RealConst(value) => Value::Scalar(Complex64::from(*value)),
3901                ExprNode::ComplexConst(value) => Value::Scalar(*value),
3902                ExprNode::ScalarParam(_) => {
3903                    Value::Scalar(Complex64::from(self.parameter_value(params, index)?))
3904                }
3905                ExprNode::EventScalar(name) => {
3906                    let Some(event) = event else {
3907                        return Err(RuntimeError::MissingEventScalar(name.to_string()));
3908                    };
3909                    Value::Scalar(Complex64::from(
3910                        event
3911                            .scalar(name)
3912                            .ok_or_else(|| RuntimeError::MissingEventScalar(name.to_string()))?,
3913                    ))
3914                }
3915                ExprNode::EventP4Component { name, component } => {
3916                    let Some(event) = event else {
3917                        return Err(RuntimeError::MissingEventScalar(format!(
3918                            "{name}.{}",
3919                            component.label()
3920                        )));
3921                    };
3922                    Value::Scalar(Complex64::from(
3923                        event.p4_component(name, *component).ok_or_else(|| {
3924                            RuntimeError::MissingEventScalar(format!(
3925                                "{name}.{}",
3926                                component.label()
3927                            ))
3928                        })?,
3929                    ))
3930                }
3931                ExprNode::Unary { op, input } => {
3932                    let input = scalar_at(&values, input.index())?;
3933                    Value::Scalar(eval_unary(*op, input))
3934                }
3935                ExprNode::Binary { op, lhs, rhs } => {
3936                    let lhs = scalar_at(&values, lhs.index())?;
3937                    let rhs = scalar_at(&values, rhs.index())?;
3938                    Value::Scalar(eval_binary(*op, lhs, rhs))
3939                }
3940                ExprNode::NaryAdd { terms } => {
3941                    let mut sum = Complex64::ZERO;
3942                    for term in terms {
3943                        sum += scalar_at(&values, term.index())?;
3944                    }
3945                    Value::Scalar(sum)
3946                }
3947                ExprNode::NaryMul { factors } => {
3948                    let mut product = Complex64::ONE;
3949                    for factor in factors {
3950                        product *= scalar_at(&values, factor.index())?;
3951                    }
3952                    Value::Scalar(product)
3953                }
3954                ExprNode::Complex { re, im } => {
3955                    let re = scalar_at(&values, re.index())?;
3956                    let im = scalar_at(&values, im.index())?;
3957                    Value::Scalar(Complex64::new(re.re, im.re))
3958                }
3959                ExprNode::Vector { elements } => Value::Vector(
3960                    elements
3961                        .iter()
3962                        .map(|id| scalar_at(&values, id.index()))
3963                        .collect::<RuntimeResult<_>>()?,
3964                ),
3965                ExprNode::Matrix {
3966                    rows,
3967                    cols,
3968                    elements,
3969                } => {
3970                    if elements.len() != rows * cols {
3971                        return Err(RuntimeError::InvalidShape {
3972                            index,
3973                            message: format!(
3974                                "matrix has {} elements for shape {rows}x{cols}",
3975                                elements.len()
3976                            ),
3977                        });
3978                    }
3979                    Value::Matrix {
3980                        rows: *rows,
3981                        cols: *cols,
3982                        values: elements
3983                            .iter()
3984                            .map(|id| scalar_at(&values, id.index()))
3985                            .collect::<RuntimeResult<_>>()?,
3986                    }
3987                }
3988                ExprNode::Component { input, index: i } => {
3989                    let vector = vector_at(&values, input.index())?;
3990                    Value::Scalar(*vector.get(*i).ok_or_else(|| RuntimeError::InvalidShape {
3991                        index,
3992                        message: format!(
3993                            "component index {i} out of bounds for len {}",
3994                            vector.len()
3995                        ),
3996                    })?)
3997                }
3998                ExprNode::MatrixElement { input, row, col } => {
3999                    let (rows, cols, matrix) = matrix_at(&values, input.index())?;
4000                    if *row >= rows || *col >= cols {
4001                        return Err(RuntimeError::InvalidShape {
4002                            index,
4003                            message: format!(
4004                                "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
4005                            ),
4006                        });
4007                    }
4008                    Value::Scalar(matrix[row * cols + col])
4009                }
4010                ExprNode::MatMul { lhs, rhs } => {
4011                    let (lhs_rows, lhs_cols, lhs) = matrix_at(&values, lhs.index())?;
4012                    let (rhs_rows, rhs_cols, rhs) = matrix_at(&values, rhs.index())?;
4013                    if lhs_cols != rhs_rows {
4014                        return Err(RuntimeError::InvalidShape {
4015                            index,
4016                            message: format!(
4017                                "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
4018                            ),
4019                        });
4020                    }
4021                    let lhs = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs);
4022                    let rhs = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
4023                    let out = lhs * rhs;
4024                    Value::Matrix {
4025                        rows: out.nrows(),
4026                        cols: out.ncols(),
4027                        values: matrix_values_row_major(&out),
4028                    }
4029                }
4030                ExprNode::MatVec { matrix, vector } => {
4031                    let (rows, cols, matrix) = matrix_at(&values, matrix.index())?;
4032                    let vector = vector_at(&values, vector.index())?;
4033                    if cols != vector.len() {
4034                        return Err(RuntimeError::InvalidShape {
4035                            index,
4036                            message: format!(
4037                                "cannot multiply {rows}x{cols} matrix by len {} vector",
4038                                vector.len()
4039                            ),
4040                        });
4041                    }
4042                    let matrix = DMatrix::from_row_slice(rows, cols, matrix);
4043                    let vector = DVector::from_row_slice(vector);
4044                    Value::Vector((matrix * vector).iter().copied().collect())
4045                }
4046                ExprNode::Dot { lhs, rhs } => {
4047                    let lhs = vector_at(&values, lhs.index())?;
4048                    let rhs = vector_at(&values, rhs.index())?;
4049                    if lhs.len() != rhs.len() {
4050                        return Err(RuntimeError::InvalidShape {
4051                            index,
4052                            message: format!(
4053                                "cannot dot len {} vector with len {} vector",
4054                                lhs.len(),
4055                                rhs.len()
4056                            ),
4057                        });
4058                    }
4059                    Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
4060                }
4061                ExprNode::Solve { matrix, rhs } => {
4062                    let matrix_id = *matrix;
4063                    let (rows, cols, matrix) = matrix_at(&values, matrix_id.index())?;
4064                    let rhs = vector_at(&values, rhs.index())?;
4065                    if rows != cols || rows != rhs.len() {
4066                        return Err(RuntimeError::InvalidShape {
4067                            index,
4068                            message: format!(
4069                                "cannot solve {rows}x{cols} matrix against len {} vector",
4070                                rhs.len()
4071                            ),
4072                        });
4073                    }
4074                    let rhs = DVector::from_row_slice(rhs);
4075                    let solution = self.solve_primal(matrix_id, rows, matrix, &rhs, index, None)?;
4076                    Value::Vector(solution.iter().copied().collect())
4077                }
4078            };
4079            values.push(value);
4080        }
4081
4082        Ok(values)
4083    }
4084
4085    fn evaluate_values_from_cache(
4086        &self,
4087        params: &ParamValues,
4088        cache: &CpuBatchCache,
4089        row: usize,
4090    ) -> RuntimeResult<Vec<Value>> {
4091        let mut values = Vec::with_capacity(self.cached_evaluation_nodes.len());
4092
4093        for id in &self.cached_evaluation_nodes {
4094            let index = id.index();
4095            let node = &self.graph.nodes()[index];
4096            if let Some(slot) = self.cache_slots[index] {
4097                values.push(cache.value(slot, row)?);
4098                continue;
4099            }
4100            let value = match node {
4101                ExprNode::RealConst(value) => Value::Scalar(Complex64::from(*value)),
4102                ExprNode::ComplexConst(value) => Value::Scalar(*value),
4103                ExprNode::ScalarParam(_) => {
4104                    Value::Scalar(Complex64::from(self.parameter_value(params, index)?))
4105                }
4106                ExprNode::EventScalar(name) => {
4107                    return Err(RuntimeError::MissingEventScalar(name.to_string()));
4108                }
4109                ExprNode::EventP4Component { name, component } => {
4110                    return Err(RuntimeError::MissingEventScalar(format!(
4111                        "{name}.{}",
4112                        component.label()
4113                    )));
4114                }
4115                ExprNode::Unary { op, input } => {
4116                    let input = self.cached_scalar_at(&values, *input)?;
4117                    Value::Scalar(eval_unary(*op, input))
4118                }
4119                ExprNode::Binary { op, lhs, rhs } => {
4120                    let lhs = self.cached_scalar_at(&values, *lhs)?;
4121                    let rhs = self.cached_scalar_at(&values, *rhs)?;
4122                    Value::Scalar(eval_binary(*op, lhs, rhs))
4123                }
4124                ExprNode::NaryAdd { terms } => {
4125                    let mut sum = Complex64::ZERO;
4126                    for term in terms {
4127                        sum += self.cached_scalar_at(&values, *term)?;
4128                    }
4129                    Value::Scalar(sum)
4130                }
4131                ExprNode::NaryMul { factors } => {
4132                    let mut product = Complex64::ONE;
4133                    for factor in factors {
4134                        product *= self.cached_scalar_at(&values, *factor)?;
4135                    }
4136                    Value::Scalar(product)
4137                }
4138                ExprNode::Complex { re, im } => {
4139                    let re = self.cached_scalar_at(&values, *re)?;
4140                    let im = self.cached_scalar_at(&values, *im)?;
4141                    Value::Scalar(Complex64::new(re.re, im.re))
4142                }
4143                ExprNode::Vector { elements } => Value::Vector(
4144                    elements
4145                        .iter()
4146                        .map(|id| self.cached_scalar_at(&values, *id))
4147                        .collect::<RuntimeResult<_>>()?,
4148                ),
4149                ExprNode::Matrix {
4150                    rows,
4151                    cols,
4152                    elements,
4153                } => {
4154                    if elements.len() != rows * cols {
4155                        return Err(RuntimeError::InvalidShape {
4156                            index,
4157                            message: format!(
4158                                "matrix has {} elements for shape {rows}x{cols}",
4159                                elements.len()
4160                            ),
4161                        });
4162                    }
4163                    Value::Matrix {
4164                        rows: *rows,
4165                        cols: *cols,
4166                        values: elements
4167                            .iter()
4168                            .map(|id| self.cached_scalar_at(&values, *id))
4169                            .collect::<RuntimeResult<_>>()?,
4170                    }
4171                }
4172                ExprNode::Component { input, index: i } => {
4173                    if let Some(plan) = self.solve_components[index] {
4174                        let inverse_row = cache.solve_row(plan.row_slot(), row)?;
4175                        if inverse_row.len() != plan.dimension() {
4176                            return Err(RuntimeError::InvalidShape {
4177                                index,
4178                                message: format!(
4179                                    "specialized solve expected row len {}, got {}",
4180                                    plan.dimension(),
4181                                    inverse_row.len()
4182                                ),
4183                            });
4184                        }
4185                        if let Some(elements) = &self.solve_rhs_elements[plan.rhs().index()] {
4186                            if elements.len() != plan.dimension() {
4187                                return Err(RuntimeError::InvalidShape {
4188                                    index,
4189                                    message: format!(
4190                                        "specialized solve expected {} RHS elements, got {}",
4191                                        plan.dimension(),
4192                                        elements.len()
4193                                    ),
4194                                });
4195                            }
4196                            Value::Scalar(
4197                                inverse_row
4198                                    .iter()
4199                                    .zip(elements)
4200                                    .map(|(lhs, rhs)| {
4201                                        Ok(lhs * self.cached_scalar_at(&values, *rhs)?)
4202                                    })
4203                                    .sum::<RuntimeResult<Complex64>>()?,
4204                            )
4205                        } else {
4206                            let rhs = self.cached_vector_at(&values, plan.rhs())?;
4207                            if rhs.len() != plan.dimension() {
4208                                return Err(RuntimeError::InvalidShape {
4209                                    index,
4210                                    message: format!(
4211                                        "specialized solve expected RHS len {}, got {}",
4212                                        plan.dimension(),
4213                                        rhs.len()
4214                                    ),
4215                                });
4216                            }
4217                            Value::Scalar(
4218                                inverse_row
4219                                    .iter()
4220                                    .zip(rhs)
4221                                    .map(|(lhs, rhs)| lhs * rhs)
4222                                    .sum(),
4223                            )
4224                        }
4225                    } else {
4226                        let vector = self.cached_vector_at(&values, *input)?;
4227                        Value::Scalar(*vector.get(*i).ok_or_else(|| {
4228                            RuntimeError::InvalidShape {
4229                                index,
4230                                message: format!(
4231                                    "component index {i} out of bounds for len {}",
4232                                    vector.len()
4233                                ),
4234                            }
4235                        })?)
4236                    }
4237                }
4238                ExprNode::MatrixElement { input, row, col } => {
4239                    let (rows, cols, matrix) = self.cached_matrix_at(&values, *input)?;
4240                    if *row >= rows || *col >= cols {
4241                        return Err(RuntimeError::InvalidShape {
4242                            index,
4243                            message: format!(
4244                                "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
4245                            ),
4246                        });
4247                    }
4248                    Value::Scalar(matrix[row * cols + col])
4249                }
4250                ExprNode::MatMul { lhs, rhs } => {
4251                    let (lhs_rows, lhs_cols, lhs) = self.cached_matrix_at(&values, *lhs)?;
4252                    let (rhs_rows, rhs_cols, rhs) = self.cached_matrix_at(&values, *rhs)?;
4253                    if lhs_cols != rhs_rows {
4254                        return Err(RuntimeError::InvalidShape {
4255                            index,
4256                            message: format!(
4257                                "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
4258                            ),
4259                        });
4260                    }
4261                    let lhs = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs);
4262                    let rhs = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
4263                    let out = lhs * rhs;
4264                    Value::Matrix {
4265                        rows: out.nrows(),
4266                        cols: out.ncols(),
4267                        values: matrix_values_row_major(&out),
4268                    }
4269                }
4270                ExprNode::MatVec { matrix, vector } => {
4271                    let (rows, cols, matrix) = self.cached_matrix_at(&values, *matrix)?;
4272                    let vector = self.cached_vector_at(&values, *vector)?;
4273                    if cols != vector.len() {
4274                        return Err(RuntimeError::InvalidShape {
4275                            index,
4276                            message: format!(
4277                                "cannot multiply {rows}x{cols} matrix by len {} vector",
4278                                vector.len()
4279                            ),
4280                        });
4281                    }
4282                    let matrix = DMatrix::from_row_slice(rows, cols, matrix);
4283                    let vector = DVector::from_row_slice(vector);
4284                    Value::Vector((matrix * vector).iter().copied().collect())
4285                }
4286                ExprNode::Dot { lhs, rhs } => {
4287                    let lhs = self.cached_vector_at(&values, *lhs)?;
4288                    let rhs = self.cached_vector_at(&values, *rhs)?;
4289                    if lhs.len() != rhs.len() {
4290                        return Err(RuntimeError::InvalidShape {
4291                            index,
4292                            message: format!(
4293                                "cannot dot len {} vector with len {} vector",
4294                                lhs.len(),
4295                                rhs.len()
4296                            ),
4297                        });
4298                    }
4299                    Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
4300                }
4301                ExprNode::Solve { matrix, rhs } => {
4302                    let matrix_id = *matrix;
4303                    let (rows, cols, matrix) = self.cached_matrix_at(&values, matrix_id)?;
4304                    let rhs = self.cached_vector_at(&values, *rhs)?;
4305                    if rows != cols || rows != rhs.len() {
4306                        return Err(RuntimeError::InvalidShape {
4307                            index,
4308                            message: format!(
4309                                "cannot solve {rows}x{cols} matrix against len {} vector",
4310                                rhs.len()
4311                            ),
4312                        });
4313                    }
4314                    let rhs = DVector::from_row_slice(rhs);
4315                    let solution = self.solve_primal(
4316                        matrix_id,
4317                        rows,
4318                        matrix,
4319                        &rhs,
4320                        index,
4321                        Some((cache, row)),
4322                    )?;
4323                    Value::Vector(solution.iter().copied().collect())
4324                }
4325            };
4326            values.push(value);
4327        }
4328
4329        Ok(values)
4330    }
4331
4332    fn cached_value_slot(&self, id: ExprId) -> RuntimeResult<usize> {
4333        self.cached_value_slots[id.index()].ok_or_else(|| RuntimeError::InvalidShape {
4334            index: id.index(),
4335            message: "node is not part of the cached evaluation schedule".into(),
4336        })
4337    }
4338
4339    fn cached_scalar_at(&self, values: &[Value], id: ExprId) -> RuntimeResult<Complex64> {
4340        scalar_at(values, self.cached_value_slot(id)?)
4341    }
4342
4343    fn cached_vector_at<'a>(
4344        &self,
4345        values: &'a [Value],
4346        id: ExprId,
4347    ) -> RuntimeResult<&'a [Complex64]> {
4348        vector_at(values, self.cached_value_slot(id)?)
4349    }
4350
4351    fn cached_matrix_at<'a>(
4352        &self,
4353        values: &'a [Value],
4354        id: ExprId,
4355    ) -> RuntimeResult<(usize, usize, &'a [Complex64])> {
4356        matrix_at(values, self.cached_value_slot(id)?)
4357    }
4358
4359    fn check_batch_cache(&self, cache: &CpuBatchCache) -> RuntimeResult<()> {
4360        if cache.nodes
4361            == self
4362                .cache_plan
4363                .entries()
4364                .iter()
4365                .map(|entry| entry.node())
4366                .collect::<Vec<_>>()
4367            && cache.factor_nodes
4368                == self
4369                    .factor_matrices
4370                    .iter()
4371                    .map(|(node, _)| *node)
4372                    .collect::<Vec<_>>()
4373            && cache.solve_row_keys == self.solve_row_keys
4374        {
4375            Ok(())
4376        } else {
4377            Err(RuntimeError::InvalidCacheLayout)
4378        }
4379    }
4380}
4381
4382fn resident_cache_plan(
4383    fixed_per_batch: usize,
4384    cache_bytes_per_event: usize,
4385    source_bytes_per_event: usize,
4386    events: usize,
4387    available: usize,
4388) -> Option<(usize, usize)> {
4389    if events == 0 {
4390        return Some((fixed_per_batch, 1));
4391    }
4392    let event_cache = cache_bytes_per_event.checked_mul(events)?;
4393    let minimum = event_cache
4394        .checked_add(fixed_per_batch)?
4395        .checked_add(source_bytes_per_event)?;
4396    if minimum > available {
4397        return None;
4398    }
4399    let mut chunk = events;
4400    for _ in 0..16 {
4401        let batches = events.saturating_add(chunk - 1) / chunk;
4402        let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
4403        let next = available
4404            .saturating_sub(resident)
4405            .checked_div(source_bytes_per_event.max(1))?
4406            .min(events);
4407        if next == 0 {
4408            return None;
4409        }
4410        if next == chunk {
4411            return Some((resident, chunk));
4412        }
4413        chunk = next;
4414    }
4415    let batches = events.saturating_add(chunk - 1) / chunk;
4416    let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
4417    (resident.checked_add(source_bytes_per_event.checked_mul(chunk)?)? <= available)
4418        .then_some((resident, chunk))
4419}
4420
4421#[derive(Clone, Debug, PartialEq)]
4422enum Value {
4423    Scalar(Complex64),
4424    Vector(Vec<Complex64>),
4425    Matrix {
4426        rows: usize,
4427        cols: usize,
4428        values: Vec<Complex64>,
4429    },
4430}
4431
4432#[derive(Clone, Copy)]
4433enum F32KernelInput<'a> {
4434    Cache(Option<(&'a CpuBatchCache, usize)>),
4435    Event(&'a dyn EventLookup),
4436}
4437
4438impl<'a> F32KernelInput<'a> {
4439    fn cache(self) -> Option<(&'a CpuBatchCache, usize)> {
4440        match self {
4441            Self::Cache(cache) => cache,
4442            Self::Event(_) => None,
4443        }
4444    }
4445}
4446
4447#[derive(Clone, Debug, PartialEq)]
4448enum F32Value {
4449    Scalar(Complex32),
4450    Vector(Vec<Complex32>),
4451    Matrix {
4452        rows: usize,
4453        cols: usize,
4454        values: Vec<Complex32>,
4455    },
4456}
4457
4458impl F32Value {
4459    fn from_value(value: Value) -> Self {
4460        match value {
4461            Value::Scalar(value) => Self::Scalar(Complex32::new(value.re as f32, value.im as f32)),
4462            Value::Vector(values) => Self::Vector(
4463                values
4464                    .into_iter()
4465                    .map(|value| Complex32::new(value.re as f32, value.im as f32))
4466                    .collect(),
4467            ),
4468            Value::Matrix { rows, cols, values } => Self::Matrix {
4469                rows,
4470                cols,
4471                values: values
4472                    .into_iter()
4473                    .map(|value| Complex32::new(value.re as f32, value.im as f32))
4474                    .collect(),
4475            },
4476        }
4477    }
4478
4479    fn kind(&self) -> &'static str {
4480        match self {
4481            Self::Scalar(_) => "scalar",
4482            Self::Vector(_) => "vector",
4483            Self::Matrix { .. } => "matrix",
4484        }
4485    }
4486}
4487
4488type DynamicLu = LU<Complex64, Dyn, Dyn>;
4489
4490struct DerivativeWorkspace<'a> {
4491    plan: &'a CpuPlan,
4492    primals: &'a [Value],
4493    tangents: Vec<Option<Value>>,
4494    factors: HashMap<usize, DynamicLu>,
4495    cached_factors: Option<(&'a CpuBatchCache, usize)>,
4496}
4497
4498impl<'a> DerivativeWorkspace<'a> {
4499    fn new(
4500        plan: &'a CpuPlan,
4501        primals: &'a [Value],
4502        cached_factors: Option<(&'a CpuBatchCache, usize)>,
4503    ) -> Self {
4504        Self {
4505            plan,
4506            primals,
4507            tangents: vec![None; plan.graph.nodes().len()],
4508            factors: HashMap::new(),
4509            cached_factors,
4510        }
4511    }
4512
4513    fn gradient(&mut self) -> RuntimeResult<Vec<Complex64>> {
4514        let mut gradient = Vec::with_capacity(self.plan.autodiff.parameter_count());
4515        for parameter in 0..self.plan.autodiff.parameter_count() {
4516            let active = self
4517                .plan
4518                .autodiff
4519                .active_nodes(parameter)
4520                .expect("free parameter index is valid");
4521            for id in active {
4522                self.differentiate_node(*id)?;
4523            }
4524            gradient.push(self.scalar_tangent(self.plan.graph.root())?);
4525            for id in active {
4526                self.tangents[id.index()] = None;
4527            }
4528        }
4529        Ok(gradient)
4530    }
4531
4532    fn differentiate_node(&mut self, id: ExprId) -> RuntimeResult<()> {
4533        let index = id.index();
4534        let node = self.plan.graph.nodes()[index].clone();
4535        let tangent = match node {
4536            ExprNode::ScalarParam(_) => Value::Scalar(Complex64::ONE),
4537            ExprNode::Unary { op, input } => {
4538                let input_value = self.primal_scalar(input)?;
4539                let output_value = self.primal_scalar(id)?;
4540                let input_tangent = self.scalar_tangent(input)?;
4541                let value = match op {
4542                    UnaryOp::Neg => -input_tangent,
4543                    UnaryOp::Real => Complex64::from(input_tangent.re),
4544                    UnaryOp::Imag => Complex64::from(input_tangent.im),
4545                    UnaryOp::Conj => input_tangent.conj(),
4546                    UnaryOp::NormSqr => {
4547                        Complex64::from(2.0 * (input_value.conj() * input_tangent).re)
4548                    }
4549                    UnaryOp::Sqrt => input_tangent / (2.0 * output_value),
4550                    UnaryOp::Exp => output_value * input_tangent,
4551                    UnaryOp::Sin => input_value.cos() * input_tangent,
4552                    UnaryOp::Cos => -input_value.sin() * input_tangent,
4553                    UnaryOp::Log => input_tangent / input_value,
4554                    UnaryOp::PowI(power) => {
4555                        if power == 0 {
4556                            Complex64::ZERO
4557                        } else if power == i32::MIN {
4558                            power as f64 * output_value * input_tangent / input_value
4559                        } else {
4560                            power as f64 * input_value.powi(power - 1) * input_tangent
4561                        }
4562                    }
4563                };
4564                Value::Scalar(value)
4565            }
4566            ExprNode::Binary { op, lhs, rhs } => {
4567                let lhs_value = self.primal_scalar(lhs)?;
4568                let rhs_value = self.primal_scalar(rhs)?;
4569                let lhs_tangent = self.scalar_tangent(lhs)?;
4570                let rhs_tangent = self.scalar_tangent(rhs)?;
4571                let value = match op {
4572                    BinaryOp::Add => lhs_tangent + rhs_tangent,
4573                    BinaryOp::Sub => lhs_tangent - rhs_tangent,
4574                    BinaryOp::Mul => lhs_tangent * rhs_value + lhs_value * rhs_tangent,
4575                    BinaryOp::Div => {
4576                        (lhs_tangent * rhs_value - lhs_value * rhs_tangent) / rhs_value.powi(2)
4577                    }
4578                    BinaryOp::Atan2 => {
4579                        let denominator = lhs_value.re.powi(2) + rhs_value.re.powi(2);
4580                        Complex64::from(
4581                            (rhs_value.re * lhs_tangent.re - lhs_value.re * rhs_tangent.re)
4582                                / denominator,
4583                        )
4584                    }
4585                };
4586                Value::Scalar(value)
4587            }
4588            ExprNode::NaryAdd { terms } => {
4589                Value::Scalar(terms.into_iter().try_fold(Complex64::ZERO, |sum, term| {
4590                    Ok::<_, RuntimeError>(sum + self.scalar_tangent(term)?)
4591                })?)
4592            }
4593            ExprNode::NaryMul { factors } => {
4594                let mut product = Complex64::ONE;
4595                let mut derivative = Complex64::ZERO;
4596                for factor in factors {
4597                    let value = self.primal_scalar(factor)?;
4598                    derivative = derivative * value + product * self.scalar_tangent(factor)?;
4599                    product *= value;
4600                }
4601                Value::Scalar(derivative)
4602            }
4603            ExprNode::Complex { re, im } => Value::Scalar(Complex64::new(
4604                self.scalar_tangent(re)?.re,
4605                self.scalar_tangent(im)?.re,
4606            )),
4607            ExprNode::Vector { .. }
4608                if self.cached_factors.is_some()
4609                    && self.plan.cached_value_slots[index].is_none()
4610                    && self.plan.solve_rhs_elements[index].is_some() =>
4611            {
4612                Value::Vector(Vec::new())
4613            }
4614            ExprNode::Vector { elements } => Value::Vector(
4615                elements
4616                    .into_iter()
4617                    .map(|element| self.scalar_tangent(element))
4618                    .collect::<RuntimeResult<_>>()?,
4619            ),
4620            ExprNode::Matrix {
4621                rows,
4622                cols,
4623                elements,
4624            } => {
4625                if elements.len() != rows * cols {
4626                    return Err(RuntimeError::InvalidShape {
4627                        index,
4628                        message: format!(
4629                            "matrix has {} elements for shape {rows}x{cols}",
4630                            elements.len()
4631                        ),
4632                    });
4633                }
4634                Value::Matrix {
4635                    rows,
4636                    cols,
4637                    values: elements
4638                        .into_iter()
4639                        .map(|element| self.scalar_tangent(element))
4640                        .collect::<RuntimeResult<_>>()?,
4641                }
4642            }
4643            ExprNode::Component { input, index: i } => {
4644                if let (Some(plan), Some((cache, row))) =
4645                    (self.plan.solve_components[index], self.cached_factors)
4646                {
4647                    let inverse_row = cache.solve_row(plan.row_slot(), row)?;
4648                    if let Some(elements) = &self.plan.solve_rhs_elements[plan.rhs().index()] {
4649                        Value::Scalar(
4650                            inverse_row
4651                                .iter()
4652                                .zip(elements)
4653                                .map(|(lhs, rhs)| Ok(lhs * self.scalar_tangent(*rhs)?))
4654                                .sum::<RuntimeResult<Complex64>>()?,
4655                        )
4656                    } else {
4657                        let rhs_tangent =
4658                            self.vector_tangent_value(plan.rhs(), plan.dimension())?;
4659                        Value::Scalar(
4660                            inverse_row
4661                                .iter()
4662                                .zip(rhs_tangent)
4663                                .map(|(lhs, rhs)| lhs * rhs)
4664                                .sum(),
4665                        )
4666                    }
4667                } else {
4668                    let vector = self.vector_tangent(input)?;
4669                    Value::Scalar(*vector.get(i).ok_or_else(|| RuntimeError::InvalidShape {
4670                        index,
4671                        message: format!(
4672                            "component index {i} out of bounds for len {}",
4673                            vector.len()
4674                        ),
4675                    })?)
4676                }
4677            }
4678            ExprNode::MatrixElement { input, row, col } => {
4679                let (rows, cols, matrix) = self.matrix_tangent(input)?;
4680                if row >= rows || col >= cols {
4681                    return Err(RuntimeError::InvalidShape {
4682                        index,
4683                        message: format!(
4684                            "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
4685                        ),
4686                    });
4687                }
4688                Value::Scalar(matrix[row * cols + col])
4689            }
4690            ExprNode::MatMul { lhs, rhs } => {
4691                let (lhs_rows, lhs_cols, lhs_value) = self.primal_matrix(lhs)?;
4692                let (rhs_rows, rhs_cols, rhs_value) = self.primal_matrix(rhs)?;
4693                if lhs_cols != rhs_rows {
4694                    return Err(RuntimeError::InvalidShape {
4695                        index,
4696                        message: format!(
4697                            "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
4698                        ),
4699                    });
4700                }
4701                let lhs_value = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs_value);
4702                let rhs_value = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs_value);
4703                let lhs_tangent = self.matrix_tangent_value(lhs, lhs_rows, lhs_cols)?;
4704                let rhs_tangent = self.matrix_tangent_value(rhs, rhs_rows, rhs_cols)?;
4705                let output = lhs_tangent * &rhs_value + lhs_value * rhs_tangent;
4706                Value::Matrix {
4707                    rows: output.nrows(),
4708                    cols: output.ncols(),
4709                    values: matrix_values_row_major(&output),
4710                }
4711            }
4712            ExprNode::MatVec { matrix, vector } => {
4713                let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
4714                let vector_value = self.primal_vector(vector)?;
4715                if cols != vector_value.len() {
4716                    return Err(RuntimeError::InvalidShape {
4717                        index,
4718                        message: format!(
4719                            "cannot multiply {rows}x{cols} matrix by len {} vector",
4720                            vector_value.len()
4721                        ),
4722                    });
4723                }
4724                let matrix_value = DMatrix::from_row_slice(rows, cols, matrix_value);
4725                let vector_value = DVector::from_row_slice(vector_value);
4726                let matrix_tangent = self.matrix_tangent_value(matrix, rows, cols)?;
4727                let vector_tangent = DVector::from_vec(self.vector_tangent_value(vector, cols)?);
4728                Value::Vector(
4729                    (matrix_tangent * vector_value + matrix_value * vector_tangent)
4730                        .iter()
4731                        .copied()
4732                        .collect(),
4733                )
4734            }
4735            ExprNode::Dot { lhs, rhs } => {
4736                let lhs_value = self.primal_vector(lhs)?;
4737                let rhs_value = self.primal_vector(rhs)?;
4738                if lhs_value.len() != rhs_value.len() {
4739                    return Err(RuntimeError::InvalidShape {
4740                        index,
4741                        message: format!(
4742                            "cannot dot len {} vector with len {} vector",
4743                            lhs_value.len(),
4744                            rhs_value.len()
4745                        ),
4746                    });
4747                }
4748                let lhs_tangent = self.vector_tangent_value(lhs, lhs_value.len())?;
4749                let rhs_tangent = self.vector_tangent_value(rhs, rhs_value.len())?;
4750                Value::Scalar(
4751                    lhs_tangent
4752                        .iter()
4753                        .zip(rhs_value)
4754                        .map(|(lhs, rhs)| lhs * rhs)
4755                        .sum::<Complex64>()
4756                        + lhs_value
4757                            .iter()
4758                            .zip(rhs_tangent)
4759                            .map(|(lhs, rhs)| lhs * rhs)
4760                            .sum::<Complex64>(),
4761                )
4762            }
4763            ExprNode::Solve { matrix, rhs } => {
4764                if self.cached_factors.is_some() && self.plan.cached_value_slots[index].is_none() {
4765                    // Specialized components differentiate the RHS directly and never read this.
4766                    self.tangents[index] = Some(Value::Vector(Vec::new()));
4767                    return Ok(());
4768                }
4769                let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
4770                let solution = self.primal_vector(id)?;
4771                let rhs_value = self.primal_vector(rhs)?;
4772                if rows != cols || rows != rhs_value.len() {
4773                    return Err(RuntimeError::InvalidShape {
4774                        index,
4775                        message: format!(
4776                            "cannot solve {rows}x{cols} matrix against len {} vector",
4777                            rhs_value.len()
4778                        ),
4779                    });
4780                }
4781                let matrix_tangent = self.matrix_tangent_value(matrix, rows, cols)?;
4782                let rhs_tangent = DVector::from_vec(self.vector_tangent_value(rhs, rows)?);
4783                let solution = DVector::from_row_slice(solution);
4784                let tangent_rhs = rhs_tangent - matrix_tangent * solution;
4785                let tangent = if let (Some(slot), Some((cache, row))) = (
4786                    self.plan.factor_matrix_slots[matrix.index()],
4787                    self.cached_factors,
4788                ) {
4789                    cache
4790                        .factor(slot, row)?
4791                        .solve(&tangent_rhs)
4792                        .ok_or(RuntimeError::SingularMatrix(index))?
4793                } else if let Some(slot) = self.plan.constant_factor_slots[matrix.index()] {
4794                    self.plan.constant_factors[slot]
4795                        .get_or_init(|| DMatrix::from_row_slice(rows, cols, matrix_value).lu())
4796                        .solve(&tangent_rhs)
4797                        .ok_or(RuntimeError::SingularMatrix(index))?
4798                } else {
4799                    let matrix_value = DMatrix::from_row_slice(rows, cols, matrix_value);
4800                    self.factors
4801                        .entry(matrix.index())
4802                        .or_insert_with(|| matrix_value.lu())
4803                        .solve(&tangent_rhs)
4804                        .ok_or(RuntimeError::SingularMatrix(index))?
4805                };
4806                Value::Vector(tangent.iter().copied().collect())
4807            }
4808            ExprNode::RealConst(_)
4809            | ExprNode::ComplexConst(_)
4810            | ExprNode::EventScalar(_)
4811            | ExprNode::EventP4Component { .. } => {
4812                return Err(RuntimeError::InvalidShape {
4813                    index,
4814                    message: "parameter-independent node appeared in a derivative lane".into(),
4815                });
4816            }
4817        };
4818        self.tangents[index] = Some(tangent);
4819        Ok(())
4820    }
4821
4822    fn primal_scalar(&self, id: ExprId) -> RuntimeResult<Complex64> {
4823        if self.cached_factors.is_some() {
4824            self.plan.cached_scalar_at(self.primals, id)
4825        } else {
4826            scalar_at(self.primals, id.index())
4827        }
4828    }
4829
4830    fn primal_vector(&self, id: ExprId) -> RuntimeResult<&[Complex64]> {
4831        if self.cached_factors.is_some() {
4832            self.plan.cached_vector_at(self.primals, id)
4833        } else {
4834            vector_at(self.primals, id.index())
4835        }
4836    }
4837
4838    fn primal_matrix(&self, id: ExprId) -> RuntimeResult<(usize, usize, &[Complex64])> {
4839        if self.cached_factors.is_some() {
4840            self.plan.cached_matrix_at(self.primals, id)
4841        } else {
4842            matrix_at(self.primals, id.index())
4843        }
4844    }
4845
4846    fn scalar_tangent(&self, id: ExprId) -> RuntimeResult<Complex64> {
4847        match &self.tangents[id.index()] {
4848            Some(Value::Scalar(value)) => Ok(*value),
4849            Some(value) => Err(RuntimeError::TypeMismatch {
4850                index: id.index(),
4851                expected: "scalar tangent",
4852                actual: value.kind(),
4853            }),
4854            None => Ok(Complex64::ZERO),
4855        }
4856    }
4857
4858    fn vector_tangent(&self, id: ExprId) -> RuntimeResult<&[Complex64]> {
4859        match &self.tangents[id.index()] {
4860            Some(Value::Vector(values)) => Ok(values),
4861            Some(value) => Err(RuntimeError::TypeMismatch {
4862                index: id.index(),
4863                expected: "vector tangent",
4864                actual: value.kind(),
4865            }),
4866            None => Err(RuntimeError::InvalidShape {
4867                index: id.index(),
4868                message: "inactive vector tangent requested without a target length".into(),
4869            }),
4870        }
4871    }
4872
4873    fn vector_tangent_value(&self, id: ExprId, len: usize) -> RuntimeResult<Vec<Complex64>> {
4874        match &self.tangents[id.index()] {
4875            Some(Value::Vector(values)) if values.len() == len => Ok(values.clone()),
4876            Some(Value::Vector(values)) => Err(RuntimeError::InvalidShape {
4877                index: id.index(),
4878                message: format!("vector tangent has len {}, expected {len}", values.len()),
4879            }),
4880            Some(value) => Err(RuntimeError::TypeMismatch {
4881                index: id.index(),
4882                expected: "vector tangent",
4883                actual: value.kind(),
4884            }),
4885            None => Ok(vec![Complex64::ZERO; len]),
4886        }
4887    }
4888
4889    fn matrix_tangent(&self, id: ExprId) -> RuntimeResult<(usize, usize, &[Complex64])> {
4890        match &self.tangents[id.index()] {
4891            Some(Value::Matrix { rows, cols, values }) => Ok((*rows, *cols, values)),
4892            Some(value) => Err(RuntimeError::TypeMismatch {
4893                index: id.index(),
4894                expected: "matrix tangent",
4895                actual: value.kind(),
4896            }),
4897            None => Err(RuntimeError::InvalidShape {
4898                index: id.index(),
4899                message: "inactive matrix tangent requested without a target shape".into(),
4900            }),
4901        }
4902    }
4903
4904    fn matrix_tangent_value(
4905        &self,
4906        id: ExprId,
4907        rows: usize,
4908        cols: usize,
4909    ) -> RuntimeResult<DMatrix<Complex64>> {
4910        match &self.tangents[id.index()] {
4911            Some(Value::Matrix {
4912                rows: actual_rows,
4913                cols: actual_cols,
4914                values,
4915            }) if *actual_rows == rows && *actual_cols == cols => {
4916                Ok(DMatrix::from_row_slice(rows, cols, values))
4917            }
4918            Some(Value::Matrix {
4919                rows: actual_rows,
4920                cols: actual_cols,
4921                ..
4922            }) => Err(RuntimeError::InvalidShape {
4923                index: id.index(),
4924                message: format!(
4925                    "matrix tangent has shape {actual_rows}x{actual_cols}, expected {rows}x{cols}"
4926                ),
4927            }),
4928            Some(value) => Err(RuntimeError::TypeMismatch {
4929                index: id.index(),
4930                expected: "matrix tangent",
4931                actual: value.kind(),
4932            }),
4933            None => Ok(DMatrix::zeros(rows, cols)),
4934        }
4935    }
4936}
4937
4938#[derive(Clone, Copy, Debug, Default, PartialEq)]
4939struct ScalarAdjoint {
4940    dz: Complex64,
4941    dz_conj: Complex64,
4942}
4943
4944impl ScalarAdjoint {
4945    fn seed() -> Self {
4946        Self {
4947            dz: Complex64::ONE,
4948            dz_conj: Complex64::ZERO,
4949        }
4950    }
4951
4952    fn gradient(self) -> Complex64 {
4953        self.dz + self.dz_conj
4954    }
4955}
4956
4957#[derive(Clone, Debug, PartialEq)]
4958enum ReverseAdjoint {
4959    Scalar(ScalarAdjoint),
4960    Vector(Vec<ScalarAdjoint>),
4961    Matrix {
4962        rows: usize,
4963        cols: usize,
4964        values: Vec<ScalarAdjoint>,
4965    },
4966}
4967
4968impl ReverseAdjoint {
4969    fn kind(&self) -> &'static str {
4970        match self {
4971            Self::Scalar(_) => "scalar adjoint",
4972            Self::Vector(_) => "vector adjoint",
4973            Self::Matrix { .. } => "matrix adjoint",
4974        }
4975    }
4976}
4977
4978struct ReverseDerivativeWorkspace<'a> {
4979    plan: &'a CpuPlan,
4980    primals: &'a [Value],
4981    adjoints: Vec<Option<ReverseAdjoint>>,
4982    cached_factors: Option<(&'a CpuBatchCache, usize)>,
4983}
4984
4985impl<'a> ReverseDerivativeWorkspace<'a> {
4986    fn new(
4987        plan: &'a CpuPlan,
4988        primals: &'a [Value],
4989        cached_factors: Option<(&'a CpuBatchCache, usize)>,
4990    ) -> Self {
4991        Self {
4992            plan,
4993            primals,
4994            adjoints: vec![None; plan.graph.nodes().len()],
4995            cached_factors,
4996        }
4997    }
4998
4999    fn gradient(&mut self) -> RuntimeResult<Vec<Complex64>> {
5000        self.accumulate_scalar(self.plan.graph.root(), ScalarAdjoint::seed())?;
5001        if self.cached_factors.is_some() {
5002            for id in self.plan.cached_evaluation_nodes.iter().rev().copied() {
5003                self.propagate_node(id)?;
5004            }
5005        } else {
5006            for index in (0..self.plan.graph.nodes().len()).rev() {
5007                let id = ExprId::from_index(index);
5008                self.propagate_node(id)?;
5009            }
5010        }
5011
5012        let mut gradient = vec![Complex64::ZERO; self.plan.autodiff.parameter_count()];
5013        for (index, parameter) in self.plan.parameter_slots.iter().enumerate() {
5014            let Some(parameter) = parameter else {
5015                continue;
5016            };
5017            let Ok(Some(free_id)) = self.plan.params.free_id(*parameter) else {
5018                continue;
5019            };
5020            if let Some(adjoint) = self.scalar_adjoint_at(index)? {
5021                gradient[free_id.index()] += adjoint.gradient();
5022            }
5023        }
5024        Ok(gradient)
5025    }
5026
5027    fn propagate_node(&mut self, id: ExprId) -> RuntimeResult<()> {
5028        let index = id.index();
5029        if matches!(self.plan.graph.nodes()[index], ExprNode::ScalarParam(_)) {
5030            return Ok(());
5031        }
5032        let Some(adjoint) = self.adjoints[index].take() else {
5033            return Ok(());
5034        };
5035        if self.cached_factors.is_some() && self.plan.cache_slots[index].is_some() {
5036            return Ok(());
5037        }
5038        let node = self.plan.graph.nodes()[index].clone();
5039        match node {
5040            ExprNode::Unary { op, input } => {
5041                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5042                let input_value = self.primal_scalar(input)?;
5043                let output_value = self.primal_scalar(id)?;
5044                self.propagate_unary(op, input, input_value, output_value, adjoint)?;
5045            }
5046            ExprNode::Binary { op, lhs, rhs } => {
5047                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5048                let lhs_value = self.primal_scalar(lhs)?;
5049                let rhs_value = self.primal_scalar(rhs)?;
5050                self.propagate_binary(op, lhs, rhs, lhs_value, rhs_value, adjoint)?;
5051            }
5052            ExprNode::NaryAdd { terms } => {
5053                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5054                for term in terms {
5055                    self.accumulate_scalar(term, adjoint)?;
5056                }
5057            }
5058            ExprNode::NaryMul { factors } => {
5059                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5060                let values = factors
5061                    .iter()
5062                    .map(|factor| self.primal_scalar(*factor))
5063                    .collect::<RuntimeResult<Vec<_>>>()?;
5064                for (target, _) in factors.iter().enumerate() {
5065                    let mut derivative = Complex64::ONE;
5066                    for (source, value) in values.iter().copied().enumerate() {
5067                        if source != target {
5068                            derivative *= value;
5069                        }
5070                    }
5071                    self.accumulate_analytic_scalar(factors[target], adjoint, derivative)?;
5072                }
5073            }
5074            ExprNode::Complex { re, im } => {
5075                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5076                let re_part = (adjoint.dz + adjoint.dz_conj) * 0.5;
5077                let im_part = Complex64::I * (adjoint.dz - adjoint.dz_conj) * 0.5;
5078                self.accumulate_scalar(
5079                    re,
5080                    ScalarAdjoint {
5081                        dz: re_part,
5082                        dz_conj: re_part,
5083                    },
5084                )?;
5085                self.accumulate_scalar(
5086                    im,
5087                    ScalarAdjoint {
5088                        dz: im_part,
5089                        dz_conj: im_part,
5090                    },
5091                )?;
5092            }
5093            ExprNode::Vector { elements } => {
5094                let adjoint = Self::expect_vector_adjoint(index, adjoint)?;
5095                if elements.len() != adjoint.len() {
5096                    return Err(RuntimeError::InvalidShape {
5097                        index,
5098                        message: format!(
5099                            "vector adjoint has len {}, expected {}",
5100                            adjoint.len(),
5101                            elements.len()
5102                        ),
5103                    });
5104                }
5105                for (element, contribution) in elements.into_iter().zip(adjoint) {
5106                    self.accumulate_scalar(element, contribution)?;
5107                }
5108            }
5109            ExprNode::Matrix {
5110                rows,
5111                cols,
5112                elements,
5113            } => {
5114                let adjoint = Self::expect_matrix_adjoint(index, adjoint)?;
5115                if adjoint.0 != rows || adjoint.1 != cols || elements.len() != adjoint.2.len() {
5116                    return Err(RuntimeError::InvalidShape {
5117                        index,
5118                        message: format!(
5119                            "matrix adjoint has shape {}x{}, expected {rows}x{cols}",
5120                            adjoint.0, adjoint.1
5121                        ),
5122                    });
5123                }
5124                for (element, contribution) in elements.into_iter().zip(adjoint.2) {
5125                    self.accumulate_scalar(element, contribution)?;
5126                }
5127            }
5128            ExprNode::Component { input, index: i } => {
5129                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5130                if let (Some(plan), Some((cache, row))) =
5131                    (self.plan.solve_components[index], self.cached_factors)
5132                {
5133                    let inverse_row = cache.solve_row(plan.row_slot(), row)?;
5134                    if inverse_row.len() != plan.dimension() {
5135                        return Err(RuntimeError::InvalidShape {
5136                            index,
5137                            message: format!(
5138                                "specialized solve expected row len {}, got {}",
5139                                plan.dimension(),
5140                                inverse_row.len()
5141                            ),
5142                        });
5143                    }
5144                    let rhs_contributions = inverse_row
5145                        .iter()
5146                        .map(|value| ScalarAdjoint {
5147                            dz: adjoint.dz * value,
5148                            dz_conj: adjoint.dz_conj * value.conj(),
5149                        })
5150                        .collect::<Vec<_>>();
5151                    self.accumulate_solve_rhs_adjoint(
5152                        plan.rhs(),
5153                        plan.dimension(),
5154                        rhs_contributions,
5155                    )?;
5156                } else {
5157                    self.accumulate_vector_element(input, i, adjoint)?;
5158                }
5159            }
5160            ExprNode::MatrixElement { input, row, col } => {
5161                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5162                let (rows, cols, _) = self.primal_matrix(input)?;
5163                if row >= rows || col >= cols {
5164                    return Err(RuntimeError::InvalidShape {
5165                        index,
5166                        message: format!(
5167                            "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
5168                        ),
5169                    });
5170                }
5171                self.accumulate_matrix_element(input, rows, cols, row, col, adjoint)?;
5172            }
5173            ExprNode::MatMul { lhs, rhs } => {
5174                let (out_rows, out_cols, adjoint) = Self::expect_matrix_adjoint(index, adjoint)?;
5175                self.propagate_matmul(index, lhs, rhs, out_rows, out_cols, &adjoint)?;
5176            }
5177            ExprNode::MatVec { matrix, vector } => {
5178                let adjoint = Self::expect_vector_adjoint(index, adjoint)?;
5179                self.propagate_matvec(index, matrix, vector, &adjoint)?;
5180            }
5181            ExprNode::Dot { lhs, rhs } => {
5182                let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5183                self.propagate_dot(index, lhs, rhs, adjoint)?;
5184            }
5185            ExprNode::Solve { matrix, rhs } => {
5186                let adjoint = Self::expect_vector_adjoint(index, adjoint)?;
5187                self.propagate_solve(index, matrix, rhs, &adjoint)?;
5188            }
5189            ExprNode::RealConst(_)
5190            | ExprNode::ComplexConst(_)
5191            | ExprNode::ScalarParam(_)
5192            | ExprNode::EventScalar(_)
5193            | ExprNode::EventP4Component { .. } => {}
5194        }
5195        Ok(())
5196    }
5197
5198    fn propagate_unary(
5199        &mut self,
5200        op: UnaryOp,
5201        input: ExprId,
5202        input_value: Complex64,
5203        output_value: Complex64,
5204        adjoint: ScalarAdjoint,
5205    ) -> RuntimeResult<()> {
5206        match op {
5207            UnaryOp::Neg => self.accumulate_scalar(
5208                input,
5209                ScalarAdjoint {
5210                    dz: -adjoint.dz,
5211                    dz_conj: -adjoint.dz_conj,
5212                },
5213            ),
5214            UnaryOp::Real => {
5215                let contribution = (adjoint.dz + adjoint.dz_conj) * 0.5;
5216                self.accumulate_scalar(
5217                    input,
5218                    ScalarAdjoint {
5219                        dz: contribution,
5220                        dz_conj: contribution,
5221                    },
5222                )
5223            }
5224            UnaryOp::Imag => {
5225                let contribution = -Complex64::I * (adjoint.dz + adjoint.dz_conj) * 0.5;
5226                self.accumulate_scalar(
5227                    input,
5228                    ScalarAdjoint {
5229                        dz: contribution,
5230                        dz_conj: -contribution,
5231                    },
5232                )
5233            }
5234            UnaryOp::Conj => self.accumulate_scalar(
5235                input,
5236                ScalarAdjoint {
5237                    dz: adjoint.dz_conj,
5238                    dz_conj: adjoint.dz,
5239                },
5240            ),
5241            UnaryOp::NormSqr => {
5242                let sum = adjoint.dz + adjoint.dz_conj;
5243                self.accumulate_scalar(
5244                    input,
5245                    ScalarAdjoint {
5246                        dz: sum * input_value.conj(),
5247                        dz_conj: sum * input_value,
5248                    },
5249                )
5250            }
5251            UnaryOp::Sqrt => {
5252                self.accumulate_analytic_scalar(input, adjoint, 1.0 / (2.0 * output_value))
5253            }
5254            UnaryOp::Exp => self.accumulate_analytic_scalar(input, adjoint, output_value),
5255            UnaryOp::Sin => self.accumulate_analytic_scalar(input, adjoint, input_value.cos()),
5256            UnaryOp::Cos => self.accumulate_analytic_scalar(input, adjoint, -input_value.sin()),
5257            UnaryOp::Log => self.accumulate_analytic_scalar(input, adjoint, 1.0 / input_value),
5258            UnaryOp::PowI(power) => {
5259                let derivative = if power == 0 {
5260                    Complex64::ZERO
5261                } else if power == i32::MIN {
5262                    power as f64 * output_value / input_value
5263                } else {
5264                    power as f64 * input_value.powi(power - 1)
5265                };
5266                self.accumulate_analytic_scalar(input, adjoint, derivative)
5267            }
5268        }
5269    }
5270
5271    fn propagate_binary(
5272        &mut self,
5273        op: BinaryOp,
5274        lhs: ExprId,
5275        rhs: ExprId,
5276        lhs_value: Complex64,
5277        rhs_value: Complex64,
5278        adjoint: ScalarAdjoint,
5279    ) -> RuntimeResult<()> {
5280        match op {
5281            BinaryOp::Add => {
5282                self.accumulate_scalar(lhs, adjoint)?;
5283                self.accumulate_scalar(rhs, adjoint)
5284            }
5285            BinaryOp::Sub => {
5286                self.accumulate_scalar(lhs, adjoint)?;
5287                self.accumulate_scalar(
5288                    rhs,
5289                    ScalarAdjoint {
5290                        dz: -adjoint.dz,
5291                        dz_conj: -adjoint.dz_conj,
5292                    },
5293                )
5294            }
5295            BinaryOp::Mul => {
5296                self.accumulate_analytic_scalar(lhs, adjoint, rhs_value)?;
5297                self.accumulate_analytic_scalar(rhs, adjoint, lhs_value)
5298            }
5299            BinaryOp::Div => {
5300                self.accumulate_analytic_scalar(lhs, adjoint, 1.0 / rhs_value)?;
5301                self.accumulate_analytic_scalar(rhs, adjoint, -lhs_value / rhs_value.powi(2))
5302            }
5303            BinaryOp::Atan2 => {
5304                let denominator = lhs_value.re.powi(2) + rhs_value.re.powi(2);
5305                let sum = adjoint.dz + adjoint.dz_conj;
5306                self.accumulate_real_linear_scalar(lhs, sum * rhs_value.re / denominator)?;
5307                self.accumulate_real_linear_scalar(rhs, -sum * lhs_value.re / denominator)
5308            }
5309        }
5310    }
5311
5312    fn propagate_matmul(
5313        &mut self,
5314        index: usize,
5315        lhs: ExprId,
5316        rhs: ExprId,
5317        out_rows: usize,
5318        out_cols: usize,
5319        adjoint: &[ScalarAdjoint],
5320    ) -> RuntimeResult<()> {
5321        let (lhs_rows, lhs_cols, lhs_value) = self.primal_matrix(lhs)?;
5322        let (rhs_rows, rhs_cols, rhs_value) = self.primal_matrix(rhs)?;
5323        if lhs_cols != rhs_rows || lhs_rows != out_rows || rhs_cols != out_cols {
5324            return Err(RuntimeError::InvalidShape {
5325                index,
5326                message: format!(
5327                    "matmul adjoint has shape {out_rows}x{out_cols} for {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
5328                ),
5329            });
5330        }
5331        let mut lhs_adjoint = vec![ScalarAdjoint::default(); lhs_rows * lhs_cols];
5332        let mut rhs_adjoint = vec![ScalarAdjoint::default(); rhs_rows * rhs_cols];
5333        for row in 0..lhs_rows {
5334            for col in 0..rhs_cols {
5335                let output_adjoint = adjoint[row * rhs_cols + col];
5336                for mid in 0..lhs_cols {
5337                    let rhs_entry = rhs_value[mid * rhs_cols + col];
5338                    let lhs_entry = lhs_value[row * lhs_cols + mid];
5339                    let lhs_target = &mut lhs_adjoint[row * lhs_cols + mid];
5340                    lhs_target.dz += output_adjoint.dz * rhs_entry;
5341                    lhs_target.dz_conj += output_adjoint.dz_conj * rhs_entry.conj();
5342                    let rhs_target = &mut rhs_adjoint[mid * rhs_cols + col];
5343                    rhs_target.dz += output_adjoint.dz * lhs_entry;
5344                    rhs_target.dz_conj += output_adjoint.dz_conj * lhs_entry.conj();
5345                }
5346            }
5347        }
5348        self.accumulate_matrix(lhs, lhs_rows, lhs_cols, lhs_adjoint)?;
5349        self.accumulate_matrix(rhs, rhs_rows, rhs_cols, rhs_adjoint)
5350    }
5351
5352    fn propagate_matvec(
5353        &mut self,
5354        index: usize,
5355        matrix: ExprId,
5356        vector: ExprId,
5357        adjoint: &[ScalarAdjoint],
5358    ) -> RuntimeResult<()> {
5359        let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
5360        let vector_value = self.primal_vector(vector)?;
5361        if cols != vector_value.len() || rows != adjoint.len() {
5362            return Err(RuntimeError::InvalidShape {
5363                index,
5364                message: format!(
5365                    "matvec adjoint has len {}, expected {rows} for matrix {rows}x{cols} and vector len {}",
5366                    adjoint.len(),
5367                    vector_value.len()
5368                ),
5369            });
5370        }
5371        let mut matrix_adjoint = vec![ScalarAdjoint::default(); rows * cols];
5372        let mut vector_adjoint = vec![ScalarAdjoint::default(); cols];
5373        for row in 0..rows {
5374            let output_adjoint = adjoint[row];
5375            for col in 0..cols {
5376                let vector_entry = vector_value[col];
5377                let matrix_entry = matrix_value[row * cols + col];
5378                let matrix_target = &mut matrix_adjoint[row * cols + col];
5379                matrix_target.dz += output_adjoint.dz * vector_entry;
5380                matrix_target.dz_conj += output_adjoint.dz_conj * vector_entry.conj();
5381                vector_adjoint[col].dz += output_adjoint.dz * matrix_entry;
5382                vector_adjoint[col].dz_conj += output_adjoint.dz_conj * matrix_entry.conj();
5383            }
5384        }
5385        self.accumulate_matrix(matrix, rows, cols, matrix_adjoint)?;
5386        self.accumulate_vector(vector, vector_adjoint)
5387    }
5388
5389    fn propagate_dot(
5390        &mut self,
5391        index: usize,
5392        lhs: ExprId,
5393        rhs: ExprId,
5394        adjoint: ScalarAdjoint,
5395    ) -> RuntimeResult<()> {
5396        let lhs_value = self.primal_vector(lhs)?;
5397        let rhs_value = self.primal_vector(rhs)?;
5398        if lhs_value.len() != rhs_value.len() {
5399            return Err(RuntimeError::InvalidShape {
5400                index,
5401                message: format!(
5402                    "cannot dot len {} vector with len {} vector",
5403                    lhs_value.len(),
5404                    rhs_value.len()
5405                ),
5406            });
5407        }
5408        let lhs_adjoint = rhs_value
5409            .iter()
5410            .map(|value| ScalarAdjoint {
5411                dz: adjoint.dz * value,
5412                dz_conj: adjoint.dz_conj * value.conj(),
5413            })
5414            .collect();
5415        let rhs_adjoint = lhs_value
5416            .iter()
5417            .map(|value| ScalarAdjoint {
5418                dz: adjoint.dz * value,
5419                dz_conj: adjoint.dz_conj * value.conj(),
5420            })
5421            .collect();
5422        self.accumulate_vector(lhs, lhs_adjoint)?;
5423        self.accumulate_vector(rhs, rhs_adjoint)
5424    }
5425
5426    fn propagate_solve(
5427        &mut self,
5428        index: usize,
5429        matrix: ExprId,
5430        rhs: ExprId,
5431        adjoint: &[ScalarAdjoint],
5432    ) -> RuntimeResult<()> {
5433        let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
5434        let rhs_value = self.primal_vector(rhs)?;
5435        let solution = self.primal_vector(ExprId::from_index(index))?;
5436        if rows != cols || rows != rhs_value.len() || rows != adjoint.len() {
5437            return Err(RuntimeError::InvalidShape {
5438                index,
5439                message: format!(
5440                    "solve adjoint has len {}, expected {rows} for {rows}x{cols} solve",
5441                    adjoint.len()
5442                ),
5443            });
5444        }
5445        let matrix_value = DMatrix::from_row_slice(rows, cols, matrix_value);
5446        let transposed = matrix_value.transpose();
5447        let conjugate_transposed = matrix_value.map(|value| value.conj()).transpose();
5448        let alpha = DVector::from_iterator(rows, adjoint.iter().map(|adjoint| adjoint.dz));
5449        let beta = DVector::from_iterator(rows, adjoint.iter().map(|adjoint| adjoint.dz_conj));
5450        let lambda = transposed
5451            .lu()
5452            .solve(&alpha)
5453            .ok_or(RuntimeError::SingularMatrix(index))?;
5454        let lambda_conj = conjugate_transposed
5455            .lu()
5456            .solve(&beta)
5457            .ok_or(RuntimeError::SingularMatrix(index))?;
5458        let solution = DVector::from_row_slice(solution);
5459        let mut matrix_adjoint = vec![ScalarAdjoint::default(); rows * cols];
5460        for row in 0..rows {
5461            for col in 0..cols {
5462                matrix_adjoint[row * cols + col].dz -= lambda[row] * solution[col];
5463                matrix_adjoint[row * cols + col].dz_conj -= lambda_conj[row] * solution[col].conj();
5464            }
5465        }
5466        let rhs_adjoint = (0..rows)
5467            .map(|row| ScalarAdjoint {
5468                dz: lambda[row],
5469                dz_conj: lambda_conj[row],
5470            })
5471            .collect();
5472        self.accumulate_matrix(matrix, rows, cols, matrix_adjoint)?;
5473        self.accumulate_vector(rhs, rhs_adjoint)
5474    }
5475
5476    fn accumulate_analytic_scalar(
5477        &mut self,
5478        id: ExprId,
5479        adjoint: ScalarAdjoint,
5480        derivative: Complex64,
5481    ) -> RuntimeResult<()> {
5482        self.accumulate_scalar(
5483            id,
5484            ScalarAdjoint {
5485                dz: adjoint.dz * derivative,
5486                dz_conj: adjoint.dz_conj * derivative.conj(),
5487            },
5488        )
5489    }
5490
5491    fn accumulate_real_linear_scalar(
5492        &mut self,
5493        id: ExprId,
5494        contribution: Complex64,
5495    ) -> RuntimeResult<()> {
5496        self.accumulate_scalar(
5497            id,
5498            ScalarAdjoint {
5499                dz: contribution * 0.5,
5500                dz_conj: contribution * 0.5,
5501            },
5502        )
5503    }
5504
5505    fn accumulate_solve_rhs_adjoint(
5506        &mut self,
5507        rhs: ExprId,
5508        len: usize,
5509        contributions: Vec<ScalarAdjoint>,
5510    ) -> RuntimeResult<()> {
5511        if let Some(elements) = &self.plan.solve_rhs_elements[rhs.index()] {
5512            if elements.len() != len || elements.len() != contributions.len() {
5513                return Err(RuntimeError::InvalidShape {
5514                    index: rhs.index(),
5515                    message: format!(
5516                        "specialized solve expected {len} RHS elements, got {}",
5517                        elements.len()
5518                    ),
5519                });
5520            }
5521            for (element, contribution) in elements.iter().copied().zip(contributions) {
5522                self.accumulate_scalar(element, contribution)?;
5523            }
5524            Ok(())
5525        } else {
5526            self.accumulate_vector(rhs, contributions)
5527        }
5528    }
5529
5530    fn accumulate_scalar(&mut self, id: ExprId, contribution: ScalarAdjoint) -> RuntimeResult<()> {
5531        match &mut self.adjoints[id.index()] {
5532            Some(ReverseAdjoint::Scalar(adjoint)) => {
5533                adjoint.dz += contribution.dz;
5534                adjoint.dz_conj += contribution.dz_conj;
5535            }
5536            Some(value) => {
5537                return Err(RuntimeError::TypeMismatch {
5538                    index: id.index(),
5539                    expected: "scalar adjoint",
5540                    actual: value.kind(),
5541                });
5542            }
5543            None => {
5544                self.adjoints[id.index()] = Some(ReverseAdjoint::Scalar(contribution));
5545            }
5546        }
5547        Ok(())
5548    }
5549
5550    fn accumulate_vector(
5551        &mut self,
5552        id: ExprId,
5553        contributions: Vec<ScalarAdjoint>,
5554    ) -> RuntimeResult<()> {
5555        match &mut self.adjoints[id.index()] {
5556            Some(ReverseAdjoint::Vector(adjoint)) if adjoint.len() == contributions.len() => {
5557                for (target, source) in adjoint.iter_mut().zip(contributions) {
5558                    target.dz += source.dz;
5559                    target.dz_conj += source.dz_conj;
5560                }
5561            }
5562            Some(ReverseAdjoint::Vector(adjoint)) => {
5563                return Err(RuntimeError::InvalidShape {
5564                    index: id.index(),
5565                    message: format!(
5566                        "vector adjoint has len {}, expected {}",
5567                        adjoint.len(),
5568                        contributions.len()
5569                    ),
5570                });
5571            }
5572            Some(value) => {
5573                return Err(RuntimeError::TypeMismatch {
5574                    index: id.index(),
5575                    expected: "vector adjoint",
5576                    actual: value.kind(),
5577                });
5578            }
5579            None => {
5580                self.adjoints[id.index()] = Some(ReverseAdjoint::Vector(contributions));
5581            }
5582        }
5583        Ok(())
5584    }
5585
5586    fn accumulate_vector_element(
5587        &mut self,
5588        id: ExprId,
5589        element: usize,
5590        contribution: ScalarAdjoint,
5591    ) -> RuntimeResult<()> {
5592        let len = self.primal_vector(id)?.len();
5593        if element >= len {
5594            return Err(RuntimeError::InvalidShape {
5595                index: id.index(),
5596                message: format!("component index {element} out of bounds for len {len}"),
5597            });
5598        }
5599        let mut contributions = vec![ScalarAdjoint::default(); len];
5600        contributions[element] = contribution;
5601        self.accumulate_vector(id, contributions)
5602    }
5603
5604    fn accumulate_matrix(
5605        &mut self,
5606        id: ExprId,
5607        rows: usize,
5608        cols: usize,
5609        contributions: Vec<ScalarAdjoint>,
5610    ) -> RuntimeResult<()> {
5611        match &mut self.adjoints[id.index()] {
5612            Some(ReverseAdjoint::Matrix {
5613                rows: actual_rows,
5614                cols: actual_cols,
5615                values,
5616            }) if *actual_rows == rows
5617                && *actual_cols == cols
5618                && values.len() == contributions.len() =>
5619            {
5620                for (target, source) in values.iter_mut().zip(contributions) {
5621                    target.dz += source.dz;
5622                    target.dz_conj += source.dz_conj;
5623                }
5624            }
5625            Some(ReverseAdjoint::Matrix {
5626                rows: actual_rows,
5627                cols: actual_cols,
5628                ..
5629            }) => {
5630                return Err(RuntimeError::InvalidShape {
5631                    index: id.index(),
5632                    message: format!(
5633                        "matrix adjoint has shape {actual_rows}x{actual_cols}, expected {rows}x{cols}"
5634                    ),
5635                });
5636            }
5637            Some(value) => {
5638                return Err(RuntimeError::TypeMismatch {
5639                    index: id.index(),
5640                    expected: "matrix adjoint",
5641                    actual: value.kind(),
5642                });
5643            }
5644            None => {
5645                self.adjoints[id.index()] = Some(ReverseAdjoint::Matrix {
5646                    rows,
5647                    cols,
5648                    values: contributions,
5649                });
5650            }
5651        }
5652        Ok(())
5653    }
5654
5655    fn accumulate_matrix_element(
5656        &mut self,
5657        id: ExprId,
5658        rows: usize,
5659        cols: usize,
5660        row: usize,
5661        col: usize,
5662        contribution: ScalarAdjoint,
5663    ) -> RuntimeResult<()> {
5664        let mut contributions = vec![ScalarAdjoint::default(); rows * cols];
5665        contributions[row * cols + col] = contribution;
5666        self.accumulate_matrix(id, rows, cols, contributions)
5667    }
5668
5669    fn scalar_adjoint_at(&self, index: usize) -> RuntimeResult<Option<ScalarAdjoint>> {
5670        match &self.adjoints[index] {
5671            Some(ReverseAdjoint::Scalar(adjoint)) => Ok(Some(*adjoint)),
5672            Some(value) => Err(RuntimeError::TypeMismatch {
5673                index,
5674                expected: "scalar adjoint",
5675                actual: value.kind(),
5676            }),
5677            None => Ok(None),
5678        }
5679    }
5680
5681    fn expect_scalar_adjoint(
5682        index: usize,
5683        adjoint: ReverseAdjoint,
5684    ) -> RuntimeResult<ScalarAdjoint> {
5685        match adjoint {
5686            ReverseAdjoint::Scalar(adjoint) => Ok(adjoint),
5687            value => Err(RuntimeError::TypeMismatch {
5688                index,
5689                expected: "scalar adjoint",
5690                actual: value.kind(),
5691            }),
5692        }
5693    }
5694
5695    fn expect_vector_adjoint(
5696        index: usize,
5697        adjoint: ReverseAdjoint,
5698    ) -> RuntimeResult<Vec<ScalarAdjoint>> {
5699        match adjoint {
5700            ReverseAdjoint::Vector(adjoint) => Ok(adjoint),
5701            value => Err(RuntimeError::TypeMismatch {
5702                index,
5703                expected: "vector adjoint",
5704                actual: value.kind(),
5705            }),
5706        }
5707    }
5708
5709    fn expect_matrix_adjoint(
5710        index: usize,
5711        adjoint: ReverseAdjoint,
5712    ) -> RuntimeResult<(usize, usize, Vec<ScalarAdjoint>)> {
5713        match adjoint {
5714            ReverseAdjoint::Matrix { rows, cols, values } => Ok((rows, cols, values)),
5715            value => Err(RuntimeError::TypeMismatch {
5716                index,
5717                expected: "matrix adjoint",
5718                actual: value.kind(),
5719            }),
5720        }
5721    }
5722
5723    fn primal_scalar(&self, id: ExprId) -> RuntimeResult<Complex64> {
5724        if self.cached_factors.is_some() {
5725            self.plan.cached_scalar_at(self.primals, id)
5726        } else {
5727            scalar_at(self.primals, id.index())
5728        }
5729    }
5730
5731    fn primal_vector(&self, id: ExprId) -> RuntimeResult<&[Complex64]> {
5732        if self.cached_factors.is_some() {
5733            self.plan.cached_vector_at(self.primals, id)
5734        } else {
5735            vector_at(self.primals, id.index())
5736        }
5737    }
5738
5739    fn primal_matrix(&self, id: ExprId) -> RuntimeResult<(usize, usize, &[Complex64])> {
5740        if self.cached_factors.is_some() {
5741            self.plan.cached_matrix_at(self.primals, id)
5742        } else {
5743            matrix_at(self.primals, id.index())
5744        }
5745    }
5746}
5747
5748#[derive(Copy, Clone, Debug, PartialEq, Eq)]
5749enum EventColumn {
5750    Scalar(usize),
5751    P4Component { col: usize, component: P4Component },
5752}
5753
5754/// Materialized event-dependent values for one batch.
5755#[derive(Clone, Debug)]
5756pub struct CpuBatchCache {
5757    len: usize,
5758    weights: Vec<f64>,
5759    sum_weights: f64,
5760    nodes: Vec<ExprId>,
5761    pub(crate) slots: Vec<CachedSlot>,
5762    factor_nodes: Vec<ExprId>,
5763    factor_slots: Vec<CachedFactorSlot>,
5764    solve_row_keys: Vec<(ExprId, usize, usize)>,
5765    pub(crate) solve_row_slots: Vec<CachedSolveRowSlot>,
5766}
5767
5768impl CpuBatchCache {
5769    fn new(
5770        cache_plan: &CachePlan,
5771        factor_matrices: &[(ExprId, usize)],
5772        solve_row_keys: &[(ExprId, usize, usize)],
5773        len: usize,
5774    ) -> Self {
5775        Self {
5776            len,
5777            weights: vec![1.0; len],
5778            sum_weights: len as f64,
5779            nodes: cache_plan
5780                .entries()
5781                .iter()
5782                .map(|entry| entry.node())
5783                .collect(),
5784            slots: cache_plan
5785                .entries()
5786                .iter()
5787                .map(|entry| CachedSlot::new(entry.value_kind(), len))
5788                .collect(),
5789            factor_nodes: factor_matrices.iter().map(|(node, _)| *node).collect(),
5790            factor_slots: factor_matrices
5791                .iter()
5792                .map(|(_, dimension)| CachedFactorSlot::new(*dimension))
5793                .collect(),
5794            solve_row_keys: solve_row_keys.to_vec(),
5795            solve_row_slots: solve_row_keys
5796                .iter()
5797                .map(|(_, _, dimension)| CachedSolveRowSlot::new(*dimension))
5798                .collect(),
5799        }
5800    }
5801
5802    /// Returns the number of cached events.
5803    pub fn len(&self) -> usize {
5804        self.len
5805    }
5806
5807    /// Returns whether the cache contains no events.
5808    pub fn is_empty(&self) -> bool {
5809        self.len == 0
5810    }
5811
5812    /// Returns per-event weights.
5813    pub fn weights(&self) -> &[f64] {
5814        &self.weights
5815    }
5816
5817    /// Returns the sum of event weights.
5818    pub fn sum_weights(&self) -> f64 {
5819        self.sum_weights
5820    }
5821
5822    /// Estimates heap memory retained by this cache, in bytes.
5823    pub fn resident_bytes(&self) -> usize {
5824        self.weights.capacity() * size_of::<f64>()
5825            + self.nodes.capacity() * size_of::<ExprId>()
5826            + self
5827                .slots
5828                .iter()
5829                .map(CachedSlot::resident_bytes)
5830                .sum::<usize>()
5831            + self.factor_nodes.capacity() * size_of::<ExprId>()
5832            + self
5833                .factor_slots
5834                .iter()
5835                .map(CachedFactorSlot::resident_bytes)
5836                .sum::<usize>()
5837            + self.solve_row_keys.capacity() * size_of::<(ExprId, usize, usize)>()
5838            + self
5839                .solve_row_slots
5840                .iter()
5841                .map(CachedSolveRowSlot::resident_bytes)
5842                .sum::<usize>()
5843    }
5844
5845    fn set_weights(&mut self, weights: Vec<f64>) {
5846        self.sum_weights = weights.iter().sum();
5847        self.weights = weights;
5848    }
5849
5850    fn push(&mut self, slot: usize, value: Value) -> RuntimeResult<()> {
5851        let len = self.slots.len();
5852        self.slots
5853            .get_mut(slot)
5854            .ok_or(RuntimeError::InvalidCache {
5855                expected: len,
5856                actual: slot + 1,
5857            })?
5858            .push(value)
5859    }
5860
5861    fn value(&self, slot: usize, row: usize) -> RuntimeResult<Value> {
5862        if row >= self.len {
5863            return Err(RuntimeError::InvalidShape {
5864                index: row,
5865                message: format!("cache row {row} out of bounds for len {}", self.len),
5866            });
5867        }
5868        self.slots
5869            .get(slot)
5870            .ok_or(RuntimeError::InvalidCache {
5871                expected: self.slots.len(),
5872                actual: slot + 1,
5873            })?
5874            .value(row)
5875    }
5876
5877    fn scalar(&self, slot: usize, row: usize) -> RuntimeResult<Complex64> {
5878        if row >= self.len {
5879            return Err(RuntimeError::InvalidShape {
5880                index: row,
5881                message: format!("cache row {row} out of bounds for len {}", self.len),
5882            });
5883        }
5884        self.slots
5885            .get(slot)
5886            .ok_or(RuntimeError::InvalidCache {
5887                expected: self.slots.len(),
5888                actual: slot + 1,
5889            })?
5890            .scalar(row)
5891    }
5892
5893    fn real_range(&self, slot: usize, start: usize, end: usize) -> RuntimeResult<&[f64]> {
5894        if start > end || end > self.len {
5895            return Err(RuntimeError::InvalidShape {
5896                index: start,
5897                message: format!(
5898                    "cache range {start}..{end} out of bounds for len {}",
5899                    self.len
5900                ),
5901            });
5902        }
5903        self.slots
5904            .get(slot)
5905            .ok_or(RuntimeError::InvalidCache {
5906                expected: self.slots.len(),
5907                actual: slot + 1,
5908            })?
5909            .real_range(start, end)
5910    }
5911
5912    fn complex_range(&self, slot: usize, start: usize, end: usize) -> RuntimeResult<&[Complex64]> {
5913        if start > end || end > self.len {
5914            return Err(RuntimeError::InvalidShape {
5915                index: start,
5916                message: format!(
5917                    "cache range {start}..{end} out of bounds for len {}",
5918                    self.len
5919                ),
5920            });
5921        }
5922        self.slots
5923            .get(slot)
5924            .ok_or(RuntimeError::InvalidCache {
5925                expected: self.slots.len(),
5926                actual: slot + 1,
5927            })?
5928            .complex_range(start, end)
5929    }
5930
5931    fn push_factor(&mut self, slot: usize, factor: DynamicLu) -> RuntimeResult<()> {
5932        let len = self.factor_slots.len();
5933        self.factor_slots
5934            .get_mut(slot)
5935            .ok_or(RuntimeError::InvalidCache {
5936                expected: len,
5937                actual: slot + 1,
5938            })?
5939            .push(factor)
5940    }
5941
5942    fn factor(&self, slot: usize, row: usize) -> RuntimeResult<&DynamicLu> {
5943        self.factor_slots
5944            .get(slot)
5945            .ok_or(RuntimeError::InvalidCache {
5946                expected: self.factor_slots.len(),
5947                actual: slot + 1,
5948            })?
5949            .factor(row)
5950    }
5951
5952    fn push_solve_row(
5953        &mut self,
5954        slot: usize,
5955        values: impl IntoIterator<Item = Complex64>,
5956    ) -> RuntimeResult<()> {
5957        let len = self.solve_row_slots.len();
5958        self.solve_row_slots
5959            .get_mut(slot)
5960            .ok_or(RuntimeError::InvalidCache {
5961                expected: len,
5962                actual: slot + 1,
5963            })?
5964            .push(values)
5965    }
5966
5967    fn solve_row(&self, slot: usize, row: usize) -> RuntimeResult<&[Complex64]> {
5968        self.solve_row_slots
5969            .get(slot)
5970            .ok_or(RuntimeError::InvalidCache {
5971                expected: self.solve_row_slots.len(),
5972                actual: slot + 1,
5973            })?
5974            .row(row)
5975    }
5976}
5977
5978/// A cached event batch and its associated weights.
5979#[derive(Clone, Debug)]
5980pub struct CpuCachedBatch {
5981    cache: CpuBatchCache,
5982}
5983
5984impl CpuCachedBatch {
5985    /// Returns the underlying materialized cache.
5986    pub fn cache(&self) -> &CpuBatchCache {
5987        &self.cache
5988    }
5989
5990    /// Returns the number of events.
5991    pub fn len(&self) -> usize {
5992        self.cache.len()
5993    }
5994
5995    /// Returns whether the batch contains no events.
5996    pub fn is_empty(&self) -> bool {
5997        self.cache.is_empty()
5998    }
5999
6000    /// Returns per-event weights.
6001    pub fn weights(&self) -> &[f64] {
6002        self.cache.weights()
6003    }
6004
6005    /// Returns the sum of event weights.
6006    pub fn sum_weights(&self) -> f64 {
6007        self.cache.sum_weights()
6008    }
6009
6010    /// Estimates retained heap memory, in bytes.
6011    pub fn resident_bytes(&self) -> usize {
6012        self.cache.resident_bytes()
6013    }
6014}
6015
6016/// A dataset whose event-dependent model values are fully cached in memory.
6017#[derive(Clone, Debug, Default)]
6018pub struct CpuCachedDataset {
6019    batches: Vec<CpuCachedBatch>,
6020    sum_weights: f64,
6021}
6022
6023#[derive(Copy, Clone, Debug, PartialEq)]
6024/// Fixed statistics collected when a dataset is prepared for repeated evaluation.
6025pub struct PreparedDatasetStats {
6026    local_events: usize,
6027    global_events: usize,
6028    local_batches: usize,
6029    sum_weights: f64,
6030    resident_bytes: usize,
6031    storage: CacheStorage,
6032}
6033
6034impl PreparedDatasetStats {
6035    #[cfg(feature = "wgpu")]
6036    pub(crate) fn new(
6037        local_events: usize,
6038        global_events: usize,
6039        local_batches: usize,
6040        sum_weights: f64,
6041        resident_bytes: usize,
6042        storage: CacheStorage,
6043    ) -> Self {
6044        Self {
6045            local_events,
6046            global_events,
6047            local_batches,
6048            sum_weights,
6049            resident_bytes,
6050            storage,
6051        }
6052    }
6053
6054    /// Returns the number of events assigned to this rank.
6055    pub fn local_events(&self) -> usize {
6056        self.local_events
6057    }
6058
6059    /// Returns the total number of events across all ranks.
6060    pub fn global_events(&self) -> usize {
6061        self.global_events
6062    }
6063
6064    /// Returns the number of batches assigned to this rank.
6065    pub fn local_batches(&self) -> usize {
6066        self.local_batches
6067    }
6068
6069    /// Returns the total event-weight sum across all ranks.
6070    pub fn sum_weights(&self) -> f64 {
6071        self.sum_weights
6072    }
6073
6074    /// Returns the number of bytes retained for prepared data on this rank.
6075    pub fn resident_bytes(&self) -> usize {
6076        self.resident_bytes
6077    }
6078
6079    /// Returns the dataset's cache-storage policy.
6080    pub fn storage(&self) -> CacheStorage {
6081        self.storage
6082    }
6083}
6084
6085#[derive(Clone)]
6086/// A dataset prepared according to its [`CacheStorage`] policy.
6087///
6088/// Resident datasets own all event-dependent cache values. Streaming datasets retain the source
6089/// and read plan and rebuild transient batch caches on every reduction.
6090pub enum CpuPreparedDataset {
6091    /// A dataset whose event caches are resident in memory.
6092    Resident {
6093        /// Fully cached dataset.
6094        dataset: CpuCachedDataset,
6095        /// Preparation statistics.
6096        stats: PreparedDatasetStats,
6097        /// Persistent host-memory reservation shared by clones.
6098        memory_lease: MemoryLease,
6099    },
6100    /// A dataset whose event caches are rebuilt while streaming.
6101    Streaming {
6102        /// Source dataset.
6103        dataset: Dataset,
6104        /// Read plan used for each pass.
6105        read_plan: laddu_data::io::ReadPlan,
6106        /// Preparation statistics.
6107        stats: PreparedDatasetStats,
6108        /// Peak transient bytes reserved during each reduction.
6109        transient_bytes: u64,
6110    },
6111}
6112
6113impl std::fmt::Debug for CpuPreparedDataset {
6114    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6115        formatter
6116            .debug_struct("CpuPreparedDataset")
6117            .field("stats", self.stats())
6118            .finish_non_exhaustive()
6119    }
6120}
6121
6122impl CpuPreparedDataset {
6123    /// Returns statistics collected while preparing the dataset.
6124    pub fn stats(&self) -> &PreparedDatasetStats {
6125        match self {
6126            Self::Resident { stats, .. } | Self::Streaming { stats, .. } => stats,
6127        }
6128    }
6129}
6130
6131impl CpuCachedDataset {
6132    /// Returns the cached batches.
6133    pub fn batches(&self) -> &[CpuCachedBatch] {
6134        &self.batches
6135    }
6136
6137    /// Returns the total number of cached events.
6138    pub fn len(&self) -> usize {
6139        self.batches.iter().map(CpuCachedBatch::len).sum()
6140    }
6141
6142    /// Returns whether the dataset contains no events.
6143    pub fn is_empty(&self) -> bool {
6144        self.batches.iter().all(CpuCachedBatch::is_empty)
6145    }
6146
6147    /// Returns the sum of all event weights.
6148    pub fn sum_weights(&self) -> f64 {
6149        self.sum_weights
6150    }
6151
6152    /// Estimates retained heap memory, in bytes.
6153    pub fn resident_bytes(&self) -> usize {
6154        self.batches
6155            .iter()
6156            .map(CpuCachedBatch::resident_bytes)
6157            .sum()
6158    }
6159}
6160
6161#[derive(Clone, Debug)]
6162struct CachedFactorSlot {
6163    dimension: usize,
6164    factors: Vec<DynamicLu>,
6165}
6166
6167#[derive(Clone, Debug)]
6168pub(crate) struct CachedSolveRowSlot {
6169    pub(crate) dimension: usize,
6170    pub(crate) values: Vec<Complex64>,
6171}
6172
6173impl CachedSolveRowSlot {
6174    fn new(dimension: usize) -> Self {
6175        Self {
6176            dimension,
6177            values: Vec::new(),
6178        }
6179    }
6180
6181    fn push(&mut self, values: impl IntoIterator<Item = Complex64>) -> RuntimeResult<()> {
6182        let start = self.values.len();
6183        self.values.extend(values);
6184        let actual = self.values.len() - start;
6185        if actual != self.dimension {
6186            return Err(RuntimeError::InvalidShape {
6187                index: start / self.dimension,
6188                message: format!(
6189                    "cached solve row has len {actual}, expected {}",
6190                    self.dimension
6191                ),
6192            });
6193        }
6194        Ok(())
6195    }
6196
6197    fn row(&self, row: usize) -> RuntimeResult<&[Complex64]> {
6198        let start = row
6199            .checked_mul(self.dimension)
6200            .ok_or_else(|| RuntimeError::InvalidShape {
6201                index: row,
6202                message: "cached solve row offset overflowed".into(),
6203            })?;
6204        self.values
6205            .get(start..start + self.dimension)
6206            .ok_or_else(|| RuntimeError::InvalidShape {
6207                index: row,
6208                message: format!(
6209                    "cached solve row {row} out of bounds for len {}",
6210                    self.values.len() / self.dimension
6211                ),
6212            })
6213    }
6214
6215    fn resident_bytes(&self) -> usize {
6216        self.values.capacity() * size_of::<Complex64>()
6217    }
6218}
6219
6220impl CachedFactorSlot {
6221    fn new(dimension: usize) -> Self {
6222        Self {
6223            dimension,
6224            factors: Vec::new(),
6225        }
6226    }
6227
6228    fn push(&mut self, factor: DynamicLu) -> RuntimeResult<()> {
6229        self.factors.push(factor);
6230        Ok(())
6231    }
6232
6233    fn factor(&self, row: usize) -> RuntimeResult<&DynamicLu> {
6234        self.factors
6235            .get(row)
6236            .ok_or_else(|| RuntimeError::InvalidShape {
6237                index: row,
6238                message: format!(
6239                    "factor row {row} out of bounds for len {}",
6240                    self.factors.len()
6241                ),
6242            })
6243    }
6244
6245    fn resident_bytes(&self) -> usize {
6246        self.factors.capacity()
6247            * (self.dimension * self.dimension * size_of::<Complex64>()
6248                + self.dimension * size_of::<usize>())
6249    }
6250}
6251
6252#[derive(Clone, Debug, PartialEq)]
6253pub(crate) enum CachedSlot {
6254    Real(Vec<f64>),
6255    Complex(Vec<Complex64>),
6256    Vector {
6257        len: usize,
6258        values: Vec<Complex64>,
6259    },
6260    Matrix {
6261        rows: usize,
6262        cols: usize,
6263        values: Vec<Complex64>,
6264    },
6265}
6266
6267impl CachedSlot {
6268    #[cfg(feature = "jit")]
6269    pub(crate) fn values_ptr(&self) -> *const u8 {
6270        match self {
6271            Self::Real(values) => values.as_ptr().cast(),
6272            Self::Complex(values) | Self::Vector { values, .. } | Self::Matrix { values, .. } => {
6273                values.as_ptr().cast()
6274            }
6275        }
6276    }
6277
6278    #[cfg(feature = "jit")]
6279    pub(crate) fn width(&self) -> usize {
6280        match self {
6281            Self::Real(_) | Self::Complex(_) => 1,
6282            Self::Vector { len, .. } => *len,
6283            Self::Matrix { rows, cols, .. } => rows * cols,
6284        }
6285    }
6286
6287    fn new(kind: ValueKind, events: usize) -> Self {
6288        match kind {
6289            ValueKind::Real => Self::Real(Vec::with_capacity(events)),
6290            ValueKind::Complex => Self::Complex(Vec::with_capacity(events)),
6291            ValueKind::Vector { len } => Self::Vector {
6292                len,
6293                values: Vec::with_capacity(events.saturating_mul(len)),
6294            },
6295            ValueKind::Matrix { rows, cols } => Self::Matrix {
6296                rows,
6297                cols,
6298                values: Vec::with_capacity(events.saturating_mul(rows).saturating_mul(cols)),
6299            },
6300        }
6301    }
6302
6303    fn resident_bytes(&self) -> usize {
6304        match self {
6305            Self::Real(values) => values.capacity() * size_of::<f64>(),
6306            Self::Complex(values) => values.capacity() * size_of::<Complex64>(),
6307            Self::Vector { values, .. } | Self::Matrix { values, .. } => {
6308                values.capacity() * size_of::<Complex64>()
6309            }
6310        }
6311    }
6312
6313    fn push(&mut self, value: Value) -> RuntimeResult<()> {
6314        match (self, value) {
6315            (Self::Real(values), Value::Scalar(value)) => {
6316                values.push(value.re);
6317                Ok(())
6318            }
6319            (Self::Complex(values), Value::Scalar(value)) => {
6320                values.push(value);
6321                Ok(())
6322            }
6323            (Self::Vector { len, values }, Value::Vector(value)) if *len == value.len() => {
6324                values.extend(value);
6325                Ok(())
6326            }
6327            (
6328                Self::Matrix { rows, cols, values },
6329                Value::Matrix {
6330                    rows: value_rows,
6331                    cols: value_cols,
6332                    values: value,
6333                },
6334            ) if *rows == value_rows && *cols == value_cols => {
6335                values.extend(value);
6336                Ok(())
6337            }
6338            (_, value) => Err(RuntimeError::InvalidShape {
6339                index: 0,
6340                message: format!("cached value kind did not match slot: {}", value.kind()),
6341            }),
6342        }
6343    }
6344
6345    fn value(&self, row: usize) -> RuntimeResult<Value> {
6346        match self {
6347            Self::Real(values) => values
6348                .get(row)
6349                .copied()
6350                .map(Complex64::from)
6351                .map(Value::Scalar)
6352                .ok_or_else(|| RuntimeError::InvalidShape {
6353                    index: row,
6354                    message: format!("cache row {row} out of bounds"),
6355                }),
6356            Self::Complex(values) => values.get(row).copied().map(Value::Scalar).ok_or_else(|| {
6357                RuntimeError::InvalidShape {
6358                    index: row,
6359                    message: format!("cache row {row} out of bounds"),
6360                }
6361            }),
6362            Self::Vector { len, values } => {
6363                let start = row
6364                    .checked_mul(*len)
6365                    .ok_or_else(|| RuntimeError::InvalidShape {
6366                        index: row,
6367                        message: "cache vector row offset overflowed".into(),
6368                    })?;
6369                let end = start + *len;
6370                values
6371                    .get(start..end)
6372                    .map(|value| Value::Vector(value.to_vec()))
6373                    .ok_or_else(|| RuntimeError::InvalidShape {
6374                        index: row,
6375                        message: format!("cache row {row} out of bounds"),
6376                    })
6377            }
6378            Self::Matrix { rows, cols, values } => {
6379                let len = rows * cols;
6380                let start = row
6381                    .checked_mul(len)
6382                    .ok_or_else(|| RuntimeError::InvalidShape {
6383                        index: row,
6384                        message: "cache matrix row offset overflowed".into(),
6385                    })?;
6386                let end = start + len;
6387                values
6388                    .get(start..end)
6389                    .map(|value| Value::Matrix {
6390                        rows: *rows,
6391                        cols: *cols,
6392                        values: value.to_vec(),
6393                    })
6394                    .ok_or_else(|| RuntimeError::InvalidShape {
6395                        index: row,
6396                        message: format!("cache row {row} out of bounds"),
6397                    })
6398            }
6399        }
6400    }
6401
6402    fn scalar(&self, row: usize) -> RuntimeResult<Complex64> {
6403        match self {
6404            Self::Real(values) => values
6405                .get(row)
6406                .copied()
6407                .map(Complex64::from)
6408                .ok_or_else(|| RuntimeError::InvalidShape {
6409                    index: row,
6410                    message: format!("cache row {row} out of bounds"),
6411                }),
6412            Self::Complex(values) => {
6413                values
6414                    .get(row)
6415                    .copied()
6416                    .ok_or_else(|| RuntimeError::InvalidShape {
6417                        index: row,
6418                        message: format!("cache row {row} out of bounds"),
6419                    })
6420            }
6421            Self::Vector { .. } | Self::Matrix { .. } => Err(RuntimeError::TypeMismatch {
6422                index: row,
6423                expected: "scalar",
6424                actual: match self {
6425                    Self::Vector { .. } => "vector",
6426                    Self::Matrix { .. } => "matrix",
6427                    Self::Real(_) | Self::Complex(_) => unreachable!(),
6428                },
6429            }),
6430        }
6431    }
6432
6433    fn real_range(&self, start: usize, end: usize) -> RuntimeResult<&[f64]> {
6434        match self {
6435            Self::Real(values) => {
6436                values
6437                    .get(start..end)
6438                    .ok_or_else(|| RuntimeError::InvalidShape {
6439                        index: start,
6440                        message: format!("cache range {start}..{end} out of bounds"),
6441                    })
6442            }
6443            Self::Complex(_) | Self::Vector { .. } | Self::Matrix { .. } => {
6444                Err(RuntimeError::TypeMismatch {
6445                    index: start,
6446                    expected: "real scalar",
6447                    actual: match self {
6448                        Self::Complex(_) => "complex scalar",
6449                        Self::Vector { .. } => "vector",
6450                        Self::Matrix { .. } => "matrix",
6451                        Self::Real(_) => unreachable!(),
6452                    },
6453                })
6454            }
6455        }
6456    }
6457
6458    fn complex_range(&self, start: usize, end: usize) -> RuntimeResult<&[Complex64]> {
6459        match self {
6460            Self::Complex(values) => {
6461                values
6462                    .get(start..end)
6463                    .ok_or_else(|| RuntimeError::InvalidShape {
6464                        index: start,
6465                        message: format!("cache range {start}..{end} out of bounds"),
6466                    })
6467            }
6468            Self::Real(_) | Self::Vector { .. } | Self::Matrix { .. } => {
6469                Err(RuntimeError::TypeMismatch {
6470                    index: start,
6471                    expected: "complex scalar",
6472                    actual: match self {
6473                        Self::Real(_) => "real scalar",
6474                        Self::Vector { .. } => "vector",
6475                        Self::Matrix { .. } => "matrix",
6476                        Self::Complex(_) => unreachable!(),
6477                    },
6478                })
6479            }
6480        }
6481    }
6482}
6483
6484impl Value {
6485    fn kind(&self) -> &'static str {
6486        match self {
6487            Self::Scalar(_) => "scalar",
6488            Self::Vector(_) => "vector",
6489            Self::Matrix { .. } => "matrix",
6490        }
6491    }
6492}
6493
6494fn f32_scalar_at(values: &[F32Value], id: KernelValueId) -> RuntimeResult<Complex32> {
6495    match &values[id.index()] {
6496        F32Value::Scalar(value) => Ok(*value),
6497        value => Err(RuntimeError::TypeMismatch {
6498            index: id.index(),
6499            expected: "scalar",
6500            actual: value.kind(),
6501        }),
6502    }
6503}
6504
6505fn f32_vector_at(values: &[F32Value], id: KernelValueId) -> RuntimeResult<&[Complex32]> {
6506    match &values[id.index()] {
6507        F32Value::Vector(values) => Ok(values),
6508        value => Err(RuntimeError::TypeMismatch {
6509            index: id.index(),
6510            expected: "vector",
6511            actual: value.kind(),
6512        }),
6513    }
6514}
6515
6516fn f32_matrix_at(
6517    values: &[F32Value],
6518    id: KernelValueId,
6519) -> RuntimeResult<(usize, usize, &[Complex32])> {
6520    match &values[id.index()] {
6521        F32Value::Matrix { rows, cols, values } => Ok((*rows, *cols, values)),
6522        value => Err(RuntimeError::TypeMismatch {
6523            index: id.index(),
6524            expected: "matrix",
6525            actual: value.kind(),
6526        }),
6527    }
6528}
6529
6530fn scalar_at(values: &[Value], index: usize) -> RuntimeResult<Complex64> {
6531    match &values[index] {
6532        Value::Scalar(value) => Ok(*value),
6533        value => Err(RuntimeError::TypeMismatch {
6534            index,
6535            expected: "scalar",
6536            actual: value.kind(),
6537        }),
6538    }
6539}
6540
6541fn vector_at(values: &[Value], index: usize) -> RuntimeResult<&[Complex64]> {
6542    match &values[index] {
6543        Value::Vector(value) => Ok(value),
6544        value => Err(RuntimeError::TypeMismatch {
6545            index,
6546            expected: "vector",
6547            actual: value.kind(),
6548        }),
6549    }
6550}
6551
6552fn matrix_at(values: &[Value], index: usize) -> RuntimeResult<(usize, usize, &[Complex64])> {
6553    match &values[index] {
6554        Value::Matrix { rows, cols, values } => Ok((*rows, *cols, values)),
6555        value => Err(RuntimeError::TypeMismatch {
6556            index,
6557            expected: "matrix",
6558            actual: value.kind(),
6559        }),
6560    }
6561}
6562
6563fn scalar_at_optional(values: &[Option<Value>], index: usize) -> RuntimeResult<Complex64> {
6564    match values.get(index).and_then(Option::as_ref) {
6565        Some(Value::Scalar(value)) => Ok(*value),
6566        Some(value) => Err(RuntimeError::TypeMismatch {
6567            index,
6568            expected: "scalar",
6569            actual: value.kind(),
6570        }),
6571        None => Err(RuntimeError::InvalidShape {
6572            index,
6573            message: "required cache prerequisite was not evaluated".into(),
6574        }),
6575    }
6576}
6577
6578fn vector_at_optional(values: &[Option<Value>], index: usize) -> RuntimeResult<&[Complex64]> {
6579    match values.get(index).and_then(Option::as_ref) {
6580        Some(Value::Vector(value)) => Ok(value),
6581        Some(value) => Err(RuntimeError::TypeMismatch {
6582            index,
6583            expected: "vector",
6584            actual: value.kind(),
6585        }),
6586        None => Err(RuntimeError::InvalidShape {
6587            index,
6588            message: "required cache prerequisite was not evaluated".into(),
6589        }),
6590    }
6591}
6592
6593fn matrix_at_optional(
6594    values: &[Option<Value>],
6595    index: usize,
6596) -> RuntimeResult<(usize, usize, &[Complex64])> {
6597    match values.get(index).and_then(Option::as_ref) {
6598        Some(Value::Matrix { rows, cols, values }) => Ok((*rows, *cols, values)),
6599        Some(value) => Err(RuntimeError::TypeMismatch {
6600            index,
6601            expected: "matrix",
6602            actual: value.kind(),
6603        }),
6604        None => Err(RuntimeError::InvalidShape {
6605            index,
6606            message: "required cache prerequisite was not evaluated".into(),
6607        }),
6608    }
6609}
6610
6611fn matrix_values_row_major(matrix: &DMatrix<Complex64>) -> Vec<Complex64> {
6612    let mut values = Vec::with_capacity(matrix.nrows() * matrix.ncols());
6613    for row in 0..matrix.nrows() {
6614        for col in 0..matrix.ncols() {
6615            values.push(matrix[(row, col)]);
6616        }
6617    }
6618    values
6619}
6620
6621fn matrix_values_row_major_f32(matrix: &DMatrix<Complex32>) -> Vec<Complex32> {
6622    let mut values = Vec::with_capacity(matrix.nrows() * matrix.ncols());
6623    for row in 0..matrix.nrows() {
6624        for col in 0..matrix.ncols() {
6625            values.push(matrix[(row, col)]);
6626        }
6627    }
6628    values
6629}
6630
6631fn eval_unary<T: Float>(op: UnaryOp, input: Complex<T>) -> Complex<T> {
6632    match op {
6633        UnaryOp::Neg => -input,
6634        UnaryOp::Real => Complex::from(input.re),
6635        UnaryOp::Imag => Complex::from(input.im),
6636        UnaryOp::Conj => input.conj(),
6637        UnaryOp::NormSqr => Complex::from(input.norm_sqr()),
6638        UnaryOp::Sqrt => input.sqrt(),
6639        UnaryOp::Exp => input.exp(),
6640        UnaryOp::Sin => input.sin(),
6641        UnaryOp::Cos => input.cos(),
6642        UnaryOp::Log => input.ln(),
6643        UnaryOp::PowI(power) => input.powi(power),
6644    }
6645}
6646
6647fn eval_binary<T: Float>(op: BinaryOp, lhs: Complex<T>, rhs: Complex<T>) -> Complex<T> {
6648    match op {
6649        BinaryOp::Add => lhs + rhs,
6650        BinaryOp::Sub => lhs - rhs,
6651        BinaryOp::Mul => lhs * rhs,
6652        BinaryOp::Div => lhs / rhs,
6653        BinaryOp::Atan2 => Complex::from(lhs.re.atan2(rhs.re)),
6654    }
6655}
6656
6657#[cfg(test)]
6658mod tests {
6659    use std::sync::Arc;
6660
6661    use laddu_compile::{CompileOptions, CompiledModel};
6662    use laddu_data::{
6663        RealVec4,
6664        data::{Dataset, EventBatch, OwnedEvent},
6665        schema::Schema,
6666    };
6667    use laddu_expr::{
6668        P4Component, atan2, complex, dot, event_p4_component, event_scalar, matmul, matrix, matvec,
6669        parameter, polar_complex, solve, vector,
6670    };
6671
6672    use super::*;
6673
6674    #[test]
6675    fn resident_cache_plan_accounts_for_batch_overhead_and_source_prefetch() {
6676        // 100 events require 1,000 cache bytes. With 100 bytes of fixed
6677        // overhead per cached batch and 20 source bytes per event, a
6678        // 2,000-byte budget first suggests 45 events, then accounts for three
6679        // fixed-overhead batches and converges to a 35-event chunk.
6680        let (resident, chunk) = resident_cache_plan(100, 10, 20, 100, 2_000).unwrap();
6681        let batches = 100_usize.div_ceil(chunk);
6682        assert_eq!(resident, 1_000 + 100 * batches);
6683        assert!(resident + 20 * chunk <= 2_000);
6684        assert!(resident_cache_plan(100, 10, 20, 100, 1_119).is_none());
6685    }
6686
6687    fn evaluate(expr: &laddu_expr::Expr) -> Complex64 {
6688        let model = CompiledModel::from_expr(expr).unwrap();
6689        let params = Arc::new(model.params().clone()).default_values();
6690        CpuBackend.prepare(&model).evaluate(&params).unwrap()
6691    }
6692
6693    fn finite_difference(plan: &CpuPlan, params: &ParamValues, parameter: usize) -> Complex64 {
6694        let h = 1.0e-6;
6695        let mut plus = params.clone();
6696        let mut minus = params.clone();
6697        let id = params.layout().free_params()[parameter];
6698        let free_id = params.layout().free_id(id).unwrap().unwrap();
6699        let value = params.get(id).unwrap();
6700        plus.set_free(free_id, value + h).unwrap();
6701        minus.set_free(free_id, value - h).unwrap();
6702        (plan.evaluate(&plus).unwrap() - plan.evaluate(&minus).unwrap()) / (2.0 * h)
6703    }
6704
6705    fn assert_gradient_close(actual: &[Complex64], expected: &[Complex64], tolerance: f64) {
6706        assert_eq!(actual.len(), expected.len());
6707        for (actual, expected) in actual.iter().zip(expected) {
6708            assert!(
6709                (actual - expected).norm() < tolerance,
6710                "{actual} != {expected}"
6711            );
6712        }
6713    }
6714
6715    #[cfg(feature = "jit")]
6716    fn f32_execution(jit: JitPolicy) -> Execution {
6717        Execution::local(crate::ExecutionOptions {
6718            device: crate::Device::Cpu(crate::CpuOptions {
6719                jit,
6720                ..crate::CpuOptions::default()
6721            }),
6722            precision: Precision::F32,
6723            ..crate::ExecutionOptions::default()
6724        })
6725        .unwrap()
6726    }
6727
6728    #[cfg(feature = "jit")]
6729    fn f32_jit_and_interpreter(model: &CompiledModel) -> (CpuPlan, CpuPlan) {
6730        let automatic = CpuBackend
6731            .prepare_for_execution(model, &f32_execution(JitPolicy::Auto))
6732            .unwrap();
6733        let interpreted = CpuBackend
6734            .prepare_for_execution(model, &f32_execution(JitPolicy::Disabled))
6735            .unwrap();
6736
6737        let Some(ScalarExecutor::Jit(kernel)) = &automatic.scalar_executor else {
6738            panic!("f32 auto execution should select scalar JIT");
6739        };
6740        assert_eq!(kernel.precision(), JitPrecision::F32);
6741        let GradientExecutor::Jit(kernel) = &automatic.gradient_executor else {
6742            panic!("f32 auto execution should select gradient JIT");
6743        };
6744        assert_eq!(kernel.precision(), JitPrecision::F32);
6745        if interpreted.scalar_executor.is_some() {
6746            assert!(matches!(
6747                interpreted.scalar_executor,
6748                Some(ScalarExecutor::Interpreter(_))
6749            ));
6750        }
6751        (automatic, interpreted)
6752    }
6753
6754    #[cfg(feature = "jit")]
6755    fn f64_jit_and_interpreter(model: &CompiledModel) -> (CpuPlan, CpuPlan) {
6756        let automatic = CpuBackend.prepare(model);
6757        let interpreted =
6758            CpuBackend.prepare_with_execution_mode(model, CpuExecutionMode::Interpreter);
6759
6760        if automatic.scalar_executor.is_some() {
6761            assert!(matches!(
6762                automatic.scalar_executor,
6763                Some(ScalarExecutor::Jit(_))
6764            ));
6765        }
6766        if interpreted.scalar_executor.is_some() {
6767            assert!(matches!(
6768                interpreted.scalar_executor,
6769                Some(ScalarExecutor::Interpreter(_))
6770            ));
6771        }
6772        (automatic, interpreted)
6773    }
6774
6775    #[cfg(feature = "jit")]
6776    fn assert_complex_close(actual: Complex64, expected: Complex64) {
6777        assert!(
6778            (actual - expected).norm() < 1.0e-6,
6779            "{actual} != {expected}"
6780        );
6781    }
6782
6783    #[cfg(feature = "jit")]
6784    fn assert_complex_slices_close(actual: &[Complex64], expected: &[Complex64]) {
6785        assert_eq!(actual.len(), expected.len());
6786        for (actual, expected) in actual.iter().zip(expected) {
6787            assert_complex_close(*actual, *expected);
6788        }
6789    }
6790
6791    #[cfg(feature = "jit")]
6792    fn assert_complex_close_f64(actual: Complex64, expected: Complex64) {
6793        assert!(
6794            (actual - expected).norm() < 1.0e-10,
6795            "{actual} != {expected}"
6796        );
6797    }
6798
6799    #[cfg(feature = "jit")]
6800    fn assert_complex_slices_close_f64(actual: &[Complex64], expected: &[Complex64]) {
6801        assert_eq!(actual.len(), expected.len());
6802        for (actual, expected) in actual.iter().zip(expected) {
6803            assert_complex_close_f64(*actual, *expected);
6804        }
6805    }
6806
6807    #[test]
6808    fn evaluates_scalar_expression_with_parameters() {
6809        let expr = (2.0 * parameter!("x", initial: 3.0)
6810            + complex(
6811                parameter!("re", initial: 1.0),
6812                parameter!("im", initial: 2.0),
6813            ))
6814        .norm_sqr();
6815
6816        assert_eq!(evaluate(&expr), Complex64::from(53.0));
6817    }
6818
6819    #[test]
6820    fn forward_gradients_match_scalar_complex_finite_differences() {
6821        let x = laddu_expr::Expr::from(parameter!("x", initial: 0.4));
6822        let y = laddu_expr::Expr::from(parameter!("y", initial: -0.2));
6823        let expression = complex(x.clone().sin(), y.clone().exp()).norm_sqr() + (x * y).cos();
6824        let model = CompiledModel::from_expr(&expression).unwrap();
6825        let params = Arc::new(model.params().clone()).default_values();
6826        let plan = CpuBackend.prepare(&model);
6827        let result = plan.evaluate_with_gradient(&params).unwrap();
6828        let ir_result = gradient_interpreter::GradientInterpreter::new(
6829            plan.scalar_kernel.as_ref().unwrap(),
6830            model.params().free_params(),
6831        )
6832        .unwrap()
6833        .evaluate(&params, None)
6834        .unwrap()
6835        .1;
6836
6837        for (actual, expected) in ir_result.iter().zip(result.gradient()) {
6838            assert!(
6839                (actual - expected).norm() < 1.0e-12,
6840                "{actual} != {expected}"
6841            );
6842        }
6843
6844        for (parameter, derivative) in result.gradient().iter().enumerate() {
6845            let expected = finite_difference(&plan, &params, parameter);
6846            assert!((derivative - expected).norm() < 1.0e-8);
6847        }
6848    }
6849
6850    #[test]
6851    fn reverse_gradients_match_forward_for_scalar_complex_operations() {
6852        let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
6853        let y = laddu_expr::Expr::from(parameter!("y", initial: -0.3));
6854        let z = complex(x.clone(), y.clone());
6855        let expression = x.clone().sqrt()
6856            + x.clone().log()
6857            + x.clone().powi(-2)
6858            + x.clone().sin()
6859            + x.clone().cos()
6860            + x.clone().exp()
6861            + z.clone().conj()
6862            + z.clone().real()
6863            + z.clone().imag()
6864            + z.norm_sqr()
6865            + atan2(y.clone(), x.clone())
6866            + x * y;
6867        let model = CompiledModel::from_expr(&expression).unwrap();
6868        let params = Arc::new(model.params().clone()).default_values();
6869        let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6870        let reverse = CpuBackend
6871            .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6872            .unwrap();
6873
6874        let expected = forward.evaluate_with_gradient(&params).unwrap();
6875        let actual = reverse.evaluate_with_gradient(&params).unwrap();
6876
6877        assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6878        assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6879    }
6880
6881    #[test]
6882    fn reverse_gradients_match_forward_for_structured_linear_algebra() {
6883        let a = laddu_expr::Expr::from(parameter!("a", initial: 0.7));
6884        let b = laddu_expr::Expr::from(parameter!("b", initial: -0.2));
6885        let c = laddu_expr::Expr::from(parameter!("c", initial: 1.1));
6886        let d = laddu_expr::Expr::from(parameter!("d", initial: 0.4));
6887        let x = laddu_expr::Expr::from(parameter!("x", initial: -0.3));
6888        let y = laddu_expr::Expr::from(parameter!("y", initial: 0.9));
6889        let left = matrix([
6890            [a.clone(), complex(b.clone(), 0.2)],
6891            [1.3.into(), c.clone()],
6892        ]);
6893        let right = matrix([[complex(0.5, -0.1), d.clone()], [b.clone(), 0.8.into()]]);
6894        let product = matmul(left, right);
6895        let input_vector = vector([x.clone(), complex(y.clone(), -0.4)]);
6896        let projected = matvec(product.clone(), input_vector);
6897        let expression = dot(projected.clone(), vector([complex(0.25, 0.3), c.clone()]))
6898            + product.matrix_element(1, 0)
6899            + projected.component(1);
6900        let model = CompiledModel::from_expr(&expression).unwrap();
6901        let params = Arc::new(model.params().clone()).default_values();
6902        let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6903        let reverse = CpuBackend
6904            .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6905            .unwrap();
6906
6907        let expected = forward.evaluate_with_gradient(&params).unwrap();
6908        let actual = reverse.evaluate_with_gradient(&params).unwrap();
6909
6910        assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6911        assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6912    }
6913
6914    #[test]
6915    fn reverse_gradients_match_forward_for_parameter_dependent_solve() {
6916        let a = laddu_expr::Expr::from(parameter!("a", initial: 2.0));
6917        let b = laddu_expr::Expr::from(parameter!("b", initial: 0.3));
6918        let r = laddu_expr::Expr::from(parameter!("r", initial: 1.2));
6919        let solution = solve(
6920            matrix([[a, complex(b.clone(), 0.1)], [b, 1.7.into()]]),
6921            vector([r, complex(0.5, -0.1)]),
6922        );
6923        let expression = dot(solution, vector([complex(1.0, 0.2), (-0.4).into()]));
6924        let model = CompiledModel::from_expr(&expression).unwrap();
6925        let params = Arc::new(model.params().clone()).default_values();
6926        let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6927        let reverse = CpuBackend
6928            .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6929            .unwrap();
6930
6931        let expected = forward.evaluate_with_gradient(&params).unwrap();
6932        let actual = reverse.evaluate_with_gradient(&params).unwrap();
6933
6934        assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6935        assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6936    }
6937
6938    #[test]
6939    fn reverse_cached_event_gradients_match_forward() {
6940        let x = event_scalar("x");
6941        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 0.4));
6942        let phase = laddu_expr::Expr::from(parameter!("phase", initial: -0.2));
6943        let expression =
6944            complex((x.clone() * &scale).sin(), (x.clone() + phase).cos()).norm_sqr() + x * scale;
6945        let model = CompiledModel::from_expr(&expression).unwrap();
6946        let params = Arc::new(model.params().clone()).default_values();
6947        let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6948        let reverse = CpuBackend
6949            .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6950            .unwrap();
6951        let batch = EventBatch::from_events(
6952            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
6953            [
6954                OwnedEvent::new(vec![], vec![0.25]),
6955                OwnedEvent::new(vec![], vec![0.75]),
6956                OwnedEvent::new(vec![], vec![1.25]),
6957            ],
6958        )
6959        .unwrap();
6960
6961        let expected = forward
6962            .evaluate_cache_with_gradient(&params, &forward.cache_event_batch(&batch).unwrap())
6963            .unwrap();
6964        let actual = reverse
6965            .evaluate_cache_with_gradient(&params, &reverse.cache_event_batch(&batch).unwrap())
6966            .unwrap();
6967
6968        for (actual, expected) in actual.iter().zip(&expected) {
6969            assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6970            assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6971        }
6972    }
6973
6974    #[test]
6975    fn reverse_cached_event_materialization_is_a_leaf() {
6976        let event_sum = event_scalar("x") + event_scalar("y").sin();
6977        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
6978        let expression = scale * event_sum;
6979        let model = CompiledModel::from_expr(&expression).unwrap();
6980        let params = Arc::new(model.params().clone()).default_values();
6981        let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6982        let reverse = CpuBackend
6983            .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6984            .unwrap();
6985        let batch = EventBatch::from_events(
6986            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], false).unwrap()),
6987            [OwnedEvent::new(vec![], vec![0.5, 0.25])],
6988        )
6989        .unwrap();
6990        assert!(
6991            reverse
6992                .cache_slots
6993                .iter()
6994                .enumerate()
6995                .any(|(index, slot)| slot.is_some() && reverse.cached_value_slots[index].is_some())
6996        );
6997
6998        let expected = forward
6999            .evaluate_cache_row_with_gradient(
7000                &params,
7001                &forward.cache_event_batch(&batch).unwrap(),
7002                0,
7003            )
7004            .unwrap();
7005        let actual = reverse
7006            .evaluate_cache_row_with_gradient(
7007                &params,
7008                &reverse.cache_event_batch(&batch).unwrap(),
7009                0,
7010            )
7011            .unwrap();
7012
7013        assert_eq!(actual.value(), expected.value());
7014        assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
7015    }
7016
7017    #[test]
7018    fn reverse_f32_gradients_match_forward() {
7019        let expression = laddu_expr::Expr::from(parameter!("x", initial: 0.4)).sin();
7020        let model = CompiledModel::from_expr(&expression).unwrap();
7021        let params = Arc::new(model.params().clone()).default_values();
7022        let reverse = CpuBackend
7023            .prepare_with_modes_precision(
7024                &model,
7025                AutodiffMode::Reverse,
7026                CpuExecutionMode::Interpreter,
7027                Precision::F32,
7028            )
7029            .unwrap();
7030        let forward = CpuBackend
7031            .prepare_with_modes_precision(
7032                &model,
7033                AutodiffMode::Forward,
7034                CpuExecutionMode::Interpreter,
7035                Precision::F32,
7036            )
7037            .unwrap();
7038
7039        assert_eq!(
7040            reverse.evaluate_with_gradient(&params).unwrap(),
7041            forward.evaluate_with_gradient(&params).unwrap()
7042        );
7043    }
7044
7045    #[cfg(feature = "jit")]
7046    #[test]
7047    fn reverse_jit_gradients_match_interpreters_in_both_precisions() {
7048        let event = event_scalar("x");
7049        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 0.4));
7050        let phase = laddu_expr::Expr::from(parameter!("phase", initial: -0.2));
7051        let expression = complex(
7052            (event.clone() * scale.clone()).sin(),
7053            (event.clone() + phase).cos(),
7054        )
7055        .norm_sqr()
7056            + event * scale;
7057        let model = CompiledModel::from_expr(&expression).unwrap();
7058        let params = Arc::new(model.params().clone()).default_values();
7059        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
7060        let batch = EventBatch::from_events(
7061            schema,
7062            [
7063                OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7064                OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7065            ],
7066        )
7067        .unwrap();
7068
7069        for (precision, tolerance) in [(Precision::F32, 1.0e-6), (Precision::F64, 1.0e-12)] {
7070            let jit = CpuBackend
7071                .prepare_with_modes_precision(
7072                    &model,
7073                    AutodiffMode::Reverse,
7074                    CpuExecutionMode::Auto,
7075                    precision,
7076                )
7077                .unwrap();
7078            let interpreter = CpuBackend
7079                .prepare_with_modes_precision(
7080                    &model,
7081                    AutodiffMode::Reverse,
7082                    CpuExecutionMode::Interpreter,
7083                    precision,
7084                )
7085                .unwrap();
7086            assert!(matches!(jit.gradient_executor, GradientExecutor::Jit(_)));
7087
7088            let actual = jit
7089                .evaluate_cache_with_gradient(&params, &jit.cache_event_batch(&batch).unwrap())
7090                .unwrap();
7091            let expected = interpreter
7092                .evaluate_cache_with_gradient(
7093                    &params,
7094                    &interpreter.cache_event_batch(&batch).unwrap(),
7095                )
7096                .unwrap();
7097            for (actual, expected) in actual.iter().zip(expected) {
7098                assert!((actual.value() - expected.value()).norm() < tolerance);
7099                assert_gradient_close(actual.gradient(), expected.gradient(), tolerance);
7100            }
7101        }
7102    }
7103
7104    #[cfg(feature = "jit")]
7105    #[test]
7106    fn jit_gradient_reduction_matches_interpreter() {
7107        let x = event_scalar("x").real();
7108        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 0.4));
7109        let phase = laddu_expr::Expr::from(parameter!("phase", initial: -0.2));
7110        let intensity =
7111            complex((x.clone() * &scale).sin(), (x.clone() + phase).cos()).norm_sqr() + 0.5;
7112        let expression = complex(intensity, x * scale);
7113        let model = CompiledModel::from_expr(&expression).unwrap();
7114        let params = Arc::new(model.params().clone()).default_values();
7115        let automatic = CpuBackend.prepare(&model);
7116        let interpreted =
7117            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7118        assert!(matches!(
7119            automatic.gradient_executor,
7120            GradientExecutor::Jit(_)
7121        ));
7122
7123        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
7124        let batch = EventBatch::from_events(
7125            schema,
7126            [
7127                OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7128                OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7129                OwnedEvent::weighted(vec![], vec![1.25], 2.0),
7130            ],
7131        )
7132        .unwrap();
7133        let actual = automatic
7134            .evaluate_cache_with_gradient(&params, &automatic.cache_event_batch(&batch).unwrap())
7135            .unwrap();
7136        let expected = interpreted
7137            .evaluate_cache_with_gradient(&params, &interpreted.cache_event_batch(&batch).unwrap())
7138            .unwrap();
7139        for (actual, expected) in actual.iter().zip(&expected) {
7140            assert!(
7141                (actual.value() - expected.value()).norm() < 1.0e-12,
7142                "{} != {}",
7143                actual.value(),
7144                expected.value()
7145            );
7146            for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
7147                assert!((actual - expected).norm() < 1.0e-12);
7148            }
7149        }
7150        let dataset = Dataset::from_batch(batch);
7151        let execution = Execution::local(crate::ExecutionOptions {
7152            device: crate::Device::Cpu(crate::CpuOptions {
7153                threads: crate::ThreadPolicy::Serial,
7154                ..crate::CpuOptions::default()
7155            }),
7156            ..crate::ExecutionOptions::default()
7157        })
7158        .unwrap();
7159        let automatic_data = automatic.prepare_dataset(&execution, &dataset).unwrap();
7160        let interpreted_data = interpreted.prepare_dataset(&execution, &dataset).unwrap();
7161        let automatic_result = automatic
7162            .reduce_with_gradient(
7163                &execution,
7164                &params,
7165                &automatic_data,
7166                ReductionPlan::weighted_log_positive_real(),
7167            )
7168            .unwrap();
7169        let interpreted_result = interpreted
7170            .reduce_with_gradient(
7171                &execution,
7172                &params,
7173                &interpreted_data,
7174                ReductionPlan::weighted_log_positive_real(),
7175            )
7176            .unwrap();
7177
7178        assert!((automatic_result.value() - interpreted_result.value()).abs() < 1.0e-12);
7179        for (actual, expected) in automatic_result
7180            .gradient()
7181            .iter()
7182            .zip(interpreted_result.gradient())
7183        {
7184            assert!((actual - expected).abs() < 1.0e-12);
7185        }
7186    }
7187
7188    #[test]
7189    fn forward_gradients_cover_unary_atan2_and_zero_products() {
7190        let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
7191        let y = laddu_expr::Expr::from(parameter!("y", initial: 0.0));
7192        let z = complex(x.clone(), y.clone());
7193        let expression = x.clone().sqrt()
7194            + x.clone().log()
7195            + x.clone().powi(-2)
7196            + x.clone().sin()
7197            + x.clone().cos()
7198            + x.clone().exp()
7199            + z.clone().conj().real()
7200            + z.clone().imag()
7201            + z.norm_sqr()
7202            + atan2(y.clone(), x.clone())
7203            + y * x;
7204        let model = CompiledModel::from_expr(&expression).unwrap();
7205        let params = Arc::new(model.params().clone()).default_values();
7206        let plan = CpuBackend.prepare(&model);
7207        let result = plan.evaluate_with_gradient(&params).unwrap();
7208
7209        for (parameter, derivative) in result.gradient().iter().enumerate() {
7210            let expected = finite_difference(&plan, &params, parameter);
7211            assert!((derivative - expected).norm() < 1.0e-7);
7212        }
7213    }
7214
7215    #[test]
7216    fn forward_gradients_cover_matrix_vector_and_dot_operations() {
7217        let a = laddu_expr::Expr::from(parameter!("a", initial: 0.7));
7218        let b = laddu_expr::Expr::from(parameter!("b", initial: -0.2));
7219        let c = laddu_expr::Expr::from(parameter!("c", initial: 1.1));
7220        let d = laddu_expr::Expr::from(parameter!("d", initial: 0.4));
7221        let x = laddu_expr::Expr::from(parameter!("x", initial: -0.3));
7222        let y = laddu_expr::Expr::from(parameter!("y", initial: 0.9));
7223        let left = matrix([
7224            [a.clone(), complex(b.clone(), 0.2)],
7225            [1.3.into(), c.clone()],
7226        ]);
7227        let right = matrix([[complex(0.5, -0.1), d.clone()], [b.clone(), 0.8.into()]]);
7228        let product = matmul(left, right);
7229        let input_vector = vector([x.clone(), complex(y.clone(), -0.4)]);
7230        let projected = matvec(product.clone(), input_vector);
7231        let expression = dot(projected.clone(), vector([complex(0.25, 0.3), c.clone()]))
7232            + product.matrix_element(1, 0)
7233            + projected.component(1);
7234        let model = CompiledModel::from_expr(&expression).unwrap();
7235        let params = Arc::new(model.params().clone()).default_values();
7236        let plan = CpuBackend.prepare(&model);
7237        let result = plan.evaluate_with_gradient(&params).unwrap();
7238
7239        for (parameter, derivative) in result.gradient().iter().enumerate() {
7240            let expected = finite_difference(&plan, &params, parameter);
7241            assert!(
7242                (derivative - expected).norm() < 1.0e-7,
7243                "{derivative} != {expected}"
7244            );
7245        }
7246    }
7247
7248    #[test]
7249    fn solve_gradients_match_finite_differences_for_matrix_and_rhs_parameters() {
7250        let a = laddu_expr::Expr::from(parameter!("a", initial: 2.0));
7251        let b = laddu_expr::Expr::from(parameter!("b", initial: 0.3));
7252        let r = laddu_expr::Expr::from(parameter!("r", initial: 1.2));
7253        let solution = solve(
7254            matrix([[a, b], [0.2.into(), 1.7.into()]]),
7255            vector([r, complex(0.5, -0.1)]),
7256        );
7257        let expression = dot(solution, vector([complex(1.0, 0.2), (-0.4).into()]));
7258        let model = CompiledModel::from_expr(&expression).unwrap();
7259        let params = Arc::new(model.params().clone()).default_values();
7260        let plan = CpuBackend.prepare(&model);
7261        let result = plan.evaluate_with_gradient(&params).unwrap();
7262
7263        for (parameter, derivative) in result.gradient().iter().enumerate() {
7264            let expected = finite_difference(&plan, &params, parameter);
7265            assert!((derivative - expected).norm() < 1.0e-8);
7266        }
7267    }
7268
7269    #[test]
7270    fn evaluates_event_scalars() {
7271        let expr = laddu_expr::event_scalar("x") * 2.0;
7272        let model = CompiledModel::from_expr(&expr).unwrap();
7273        let params = Arc::new(model.params().clone()).default_values();
7274        let plan = CpuBackend.prepare(&model);
7275        let event = HashMap::from([("x".to_owned(), 3.0)]);
7276
7277        assert_eq!(
7278            plan.evaluate_with_event(&params, &event).unwrap(),
7279            Complex64::from(6.0)
7280        );
7281    }
7282
7283    #[test]
7284    fn scalar_kernel_ir_preserves_typed_dependency_classes() {
7285        let coefficient = complex(parameter!("re", initial: 2.0), 1.0);
7286        let expr = coefficient * event_scalar("x");
7287        let model = CompiledModel::from_expr(&expr).unwrap();
7288        let plan = CpuBackend.prepare(&model);
7289        let kernel = plan.scalar_kernel.as_ref().unwrap();
7290
7291        assert!(
7292            kernel
7293                .values()
7294                .iter()
7295                .any(|value| value.class == KernelValueClass::Invariant)
7296        );
7297        assert!(
7298            kernel
7299                .values()
7300                .iter()
7301                .any(|value| value.class == KernelValueClass::Event)
7302        );
7303        let root = &kernel.values()[kernel.root().index()];
7304        assert_eq!(root.kind, KernelValueKind::Complex);
7305        assert_eq!(root.class, KernelValueClass::Event);
7306        assert!(matches!(root.instruction, KernelInstruction::Mul(_)));
7307    }
7308
7309    #[test]
7310    fn cpu_execution_mode_selects_retained_interpreter() {
7311        let expr = laddu_expr::Expr::from(parameter!("x", initial: 2.0)).exp() + 1.0;
7312        let model = CompiledModel::from_expr(&expr).unwrap();
7313        let params = Arc::new(model.params().clone()).default_values();
7314        let automatic = CpuBackend.prepare(&model);
7315        let interpreted =
7316            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7317        let execution = Execution::local(crate::ExecutionOptions {
7318            device: crate::Device::Cpu(crate::CpuOptions {
7319                jit: crate::JitPolicy::Disabled,
7320                ..crate::CpuOptions::default()
7321            }),
7322            ..crate::ExecutionOptions::default()
7323        })
7324        .unwrap();
7325        let configured = CpuBackend
7326            .prepare_for_execution(&model, &execution)
7327            .unwrap();
7328
7329        assert!(matches!(
7330            interpreted.scalar_executor,
7331            Some(ScalarExecutor::Interpreter(_))
7332        ));
7333        assert!(matches!(
7334            configured.scalar_executor,
7335            Some(ScalarExecutor::Interpreter(_))
7336        ));
7337        assert!(matches!(
7338            configured.gradient_executor,
7339            GradientExecutor::Interpreter(_)
7340        ));
7341        assert_eq!(
7342            automatic.evaluate(&params).unwrap(),
7343            interpreted.evaluate(&params).unwrap()
7344        );
7345
7346        let empty_model = CompiledModel::from_expr(&laddu_expr::Expr::from(1.0)).unwrap();
7347        let wrong_params = empty_model.params().default_values();
7348        assert!(matches!(
7349            automatic.evaluate(&wrong_params),
7350            Err(RuntimeError::Parameter(_))
7351        ));
7352    }
7353
7354    #[test]
7355    fn cpu_plan_executes_parameter_only_scalar_kernel_in_f32() {
7356        let x = laddu_expr::Expr::from(parameter!("x", initial: 16_777_216.0));
7357        let y = laddu_expr::Expr::from(parameter!("y", initial: 1.0));
7358        let model = CompiledModel::from_expr(&(x + y)).unwrap();
7359        let params = model.params().default_values();
7360        let execution = Execution::local(crate::ExecutionOptions {
7361            device: crate::Device::Cpu(crate::CpuOptions::default()),
7362            precision: Precision::F32,
7363            ..crate::ExecutionOptions::default()
7364        })
7365        .unwrap();
7366        let f32_plan = CpuBackend
7367            .prepare_for_execution(&model, &execution)
7368            .unwrap();
7369        let f64_plan = CpuBackend.prepare(&model);
7370
7371        assert_eq!(f32_plan.evaluate(&params).unwrap().re, 16_777_216.0);
7372        assert_eq!(f64_plan.evaluate(&params).unwrap().re, 16_777_217.0);
7373        let gradient = f32_plan.evaluate_with_gradient(&params).unwrap();
7374        assert_eq!(gradient.value().re, 16_777_216.0);
7375        assert_eq!(gradient.gradient(), &[Complex64::ONE, Complex64::ONE]);
7376    }
7377
7378    #[cfg(feature = "jit")]
7379    #[test]
7380    fn cpu_f32_auto_jit_matches_f32_interpreter_for_parameter_arithmetic() {
7381        let x = laddu_expr::Expr::from(parameter!("x", initial: 16_777_216.0));
7382        let y = laddu_expr::Expr::from(parameter!("y", initial: 1.0));
7383        let model = CompiledModel::from_expr(&(x + y)).unwrap();
7384        let params = model.params().default_values();
7385        let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7386        let GradientExecutor::Jit(kernel) = &automatic.gradient_executor else {
7387            unreachable!("f32_jit_and_interpreter already requires a gradient JIT")
7388        };
7389        assert_eq!(kernel.compiled_component_count(), 1);
7390
7391        assert_eq!(
7392            automatic.evaluate(&params).unwrap(),
7393            interpreted.evaluate(&params).unwrap()
7394        );
7395    }
7396
7397    #[cfg(feature = "jit")]
7398    #[test]
7399    fn cpu_f32_auto_jit_matches_f32_interpreter_for_unary_and_binary_ops() {
7400        let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
7401        let y = laddu_expr::Expr::from(parameter!("y", initial: -0.35));
7402        let z = complex(x.clone(), y.clone());
7403        let expression = x.clone().sqrt()
7404            + x.clone().log()
7405            + x.clone().powi(-2)
7406            + x.clone().sin()
7407            + x.clone().cos()
7408            + x.clone().exp()
7409            + z.clone().conj().real()
7410            + z.clone().imag()
7411            + z.norm_sqr()
7412            + atan2(y.clone(), x.clone())
7413            + complex(x.clone(), y.clone()) / complex(1.25, -0.5);
7414        let model = CompiledModel::from_expr(&expression).unwrap();
7415        let params = model.params().default_values();
7416        let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7417
7418        assert_complex_close(
7419            automatic.evaluate(&params).unwrap(),
7420            interpreted.evaluate(&params).unwrap(),
7421        );
7422    }
7423
7424    #[test]
7425    fn cpu_f32_reduces_event_scalar_arithmetic_with_f64_accumulation() {
7426        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.0));
7427        let model = CompiledModel::from_expr(&(event_scalar("x") + scale)).unwrap();
7428        let params = model.params().default_values();
7429        let execution = Execution::local(crate::ExecutionOptions {
7430            device: crate::Device::Cpu(crate::CpuOptions {
7431                threads: crate::ThreadPolicy::Serial,
7432                ..crate::CpuOptions::default()
7433            }),
7434            precision: Precision::F32,
7435            ..crate::ExecutionOptions::default()
7436        })
7437        .unwrap();
7438        let plan = CpuBackend
7439            .prepare_for_execution(&model, &execution)
7440            .unwrap();
7441        let dataset = Dataset::from_batch(
7442            EventBatch::from_events(
7443                Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7444                [OwnedEvent::new(vec![], vec![16_777_216.0])],
7445            )
7446            .unwrap(),
7447        );
7448        let prepared = plan.prepare_dataset(&execution, &dataset).unwrap();
7449
7450        assert_eq!(
7451            plan.reduce(
7452                &execution,
7453                &params,
7454                &prepared,
7455                ReductionPlan::weighted_real(),
7456            )
7457            .unwrap(),
7458            16_777_216.0
7459        );
7460    }
7461
7462    #[test]
7463    fn cpu_f32_evaluates_complex_linear_algebra() {
7464        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7465        let lhs = matrix([
7466            [scale.clone() + 2.0, complex(0.25, -0.5)],
7467            [0.5.into(), scale.clone() + 3.0],
7468        ]);
7469        let rhs = vector([1.0.into(), scale]);
7470        let model = CompiledModel::from_expr(&dot(
7471            vector([1.0.into(), complex(0.0, 1.0)]),
7472            solve(lhs, rhs),
7473        ))
7474        .unwrap();
7475        let params = model.params().default_values();
7476        let execution = Execution::local(crate::ExecutionOptions {
7477            device: crate::Device::Cpu(crate::CpuOptions::default()),
7478            precision: Precision::F32,
7479            ..crate::ExecutionOptions::default()
7480        })
7481        .unwrap();
7482        let f32_plan = CpuBackend
7483            .prepare_for_execution(&model, &execution)
7484            .unwrap();
7485        let f64_plan = CpuBackend.prepare(&model);
7486
7487        let actual = f32_plan.evaluate(&params).unwrap();
7488        let expected = f64_plan.evaluate(&params).unwrap();
7489        assert!((actual.re - expected.re).abs() < 1.0e-6);
7490        assert!((actual.im - expected.im).abs() < 1.0e-6);
7491    }
7492
7493    #[test]
7494    fn cpu_f32_evaluates_computed_event_cache_entries() {
7495        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 2.0));
7496        let model =
7497            CompiledModel::from_expr(&(event_scalar("x").sin() * scale + 16_777_216.0)).unwrap();
7498        let execution = Execution::local(crate::ExecutionOptions {
7499            device: crate::Device::Cpu(crate::CpuOptions::default()),
7500            precision: Precision::F32,
7501            ..crate::ExecutionOptions::default()
7502        })
7503        .unwrap();
7504        let plan = CpuBackend
7505            .prepare_for_execution(&model, &execution)
7506            .unwrap();
7507        let params = model.params().default_values();
7508        let dataset = Dataset::from_batch(
7509            EventBatch::from_events(
7510                Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7511                [OwnedEvent::new(vec![], vec![1.0])],
7512            )
7513            .unwrap(),
7514        );
7515        let prepared = plan.prepare_dataset(&execution, &dataset).unwrap();
7516
7517        let reduction = plan
7518            .reduce_with_gradient(
7519                &execution,
7520                &params,
7521                &prepared,
7522                ReductionPlan::weighted_real(),
7523            )
7524            .unwrap();
7525        assert_eq!(reduction.value(), 16_777_218.0);
7526        assert_eq!(reduction.gradient(), &[(1.0_f32.sin() as f64)]);
7527    }
7528
7529    #[test]
7530    fn cpu_f32_direct_event_gradient_matches_cached_event_gradient() {
7531        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.5));
7532        let offset = laddu_expr::Expr::from(parameter!("offset", initial: -0.25));
7533        let x = event_scalar("x");
7534        let expression =
7535            (x.clone().sin() * scale.clone() + offset.clone()).exp() + complex(scale, x).norm_sqr();
7536        let model = CompiledModel::from_expr(&expression).unwrap();
7537        let execution = Execution::local(crate::ExecutionOptions {
7538            device: crate::Device::Cpu(crate::CpuOptions::default()),
7539            precision: Precision::F32,
7540            ..crate::ExecutionOptions::default()
7541        })
7542        .unwrap();
7543        let plan = CpuBackend
7544            .prepare_for_execution(&model, &execution)
7545            .unwrap();
7546        let params = model.params().default_values();
7547        let event = HashMap::from([("x".to_owned(), 0.75)]);
7548        let batch = EventBatch::from_events(
7549            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7550            [OwnedEvent::new(vec![], vec![0.75])],
7551        )
7552        .unwrap();
7553        let cache = plan.cache_event_batch(&batch).unwrap();
7554
7555        let direct_value = plan.evaluate_with_event(&params, &event).unwrap();
7556        let direct = plan
7557            .evaluate_with_event_and_gradient(&params, &event)
7558            .unwrap();
7559        let cached_value = plan.evaluate_cache_row(&params, &cache, 0).unwrap();
7560        let cached = plan
7561            .evaluate_cache_row_with_gradient(&params, &cache, 0)
7562            .unwrap();
7563
7564        assert_eq!(direct_value, cached_value);
7565        assert_eq!(direct.value(), cached.value());
7566        assert_eq!(direct.gradient(), cached.gradient());
7567    }
7568
7569    #[cfg(feature = "jit")]
7570    #[test]
7571    fn cpu_f32_auto_jit_matches_f32_interpreter_for_cached_linear_algebra() {
7572        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7573        let matrix = matrix([
7574            [event_scalar("x") + 2.0, complex(0.25, -0.5)],
7575            [0.5.into(), scale.clone() + 3.0],
7576        ]);
7577        let rhs = vector([1.0.into(), scale]);
7578        let model = CompiledModel::from_expr(&dot(
7579            vector([1.0.into(), complex(0.0, 1.0)]),
7580            solve(matrix, rhs),
7581        ))
7582        .unwrap();
7583        let params = model.params().default_values();
7584        let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7585        let batch = EventBatch::from_events(
7586            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7587            [OwnedEvent::new(vec![], vec![0.75])],
7588        )
7589        .unwrap();
7590        let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
7591        let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
7592
7593        let actual = automatic.evaluate_cache(&params, &automatic_cache).unwrap();
7594        let expected = interpreted
7595            .evaluate_cache(&params, &interpreted_cache)
7596            .unwrap();
7597        assert_complex_slices_close(&actual, &expected);
7598    }
7599
7600    #[cfg(feature = "jit")]
7601    #[test]
7602    fn cpu_f32_auto_jit_matches_f32_interpreter_for_event_cache_ops() {
7603        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.75));
7604        let x = event_scalar("x");
7605        let y = event_scalar("y");
7606        let expression = ((x.clone() + scale.clone()).sin()
7607            + (y.clone() - 0.25).cos()
7608            + (x.clone() * y.clone()).exp()
7609            + atan2(y.clone(), x.clone() + 1.0))
7610            / complex(scale, -0.5);
7611        let model = CompiledModel::from_expr(&expression).unwrap();
7612        let params = model.params().default_values();
7613        let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7614        let batch = EventBatch::from_events(
7615            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], false).unwrap()),
7616            [
7617                OwnedEvent::new(vec![], vec![0.25, -0.5]),
7618                OwnedEvent::new(vec![], vec![0.75, 0.125]),
7619                OwnedEvent::new(vec![], vec![1.5, 0.5]),
7620            ],
7621        )
7622        .unwrap();
7623        let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
7624        let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
7625
7626        assert_complex_slices_close(
7627            &automatic.evaluate_cache(&params, &automatic_cache).unwrap(),
7628            &interpreted
7629                .evaluate_cache(&params, &interpreted_cache)
7630                .unwrap(),
7631        );
7632    }
7633
7634    #[cfg(feature = "jit")]
7635    #[test]
7636    fn cpu_f32_auto_jit_matches_f32_interpreter_for_reductions_and_gradients() {
7637        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7638        let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
7639        let x = event_scalar("x");
7640        let expression = (x.clone() * scale.clone() + offset.clone()).sin()
7641            + complex(scale, offset).norm_sqr()
7642            + 2.0;
7643        let model = CompiledModel::from_expr(&expression).unwrap();
7644        let params = model.params().default_values();
7645        let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7646        let auto_execution = f32_execution(JitPolicy::Auto);
7647        let interpreter_execution = f32_execution(JitPolicy::Disabled);
7648        let dataset = Dataset::from_batch(
7649            EventBatch::from_events(
7650                Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap()),
7651                [
7652                    OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7653                    OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7654                    OwnedEvent::weighted(vec![], vec![1.25], 2.0),
7655                ],
7656            )
7657            .unwrap(),
7658        );
7659        let automatic_data = automatic
7660            .prepare_dataset(&auto_execution, &dataset)
7661            .unwrap();
7662        let interpreted_data = interpreted
7663            .prepare_dataset(&interpreter_execution, &dataset)
7664            .unwrap();
7665
7666        let actual = automatic
7667            .reduce_with_gradient(
7668                &auto_execution,
7669                &params,
7670                &automatic_data,
7671                ReductionPlan::weighted_real(),
7672            )
7673            .unwrap();
7674        let expected = interpreted
7675            .reduce_with_gradient(
7676                &interpreter_execution,
7677                &params,
7678                &interpreted_data,
7679                ReductionPlan::weighted_real(),
7680            )
7681            .unwrap();
7682        assert!((actual.value() - expected.value()).abs() < 1.0e-6);
7683        assert_eq!(actual.gradient().len(), expected.gradient().len());
7684        for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
7685            assert!((actual - expected).abs() < 1.0e-6);
7686        }
7687    }
7688
7689    #[cfg(feature = "jit")]
7690    #[test]
7691    fn auto_jit_matches_interpreter_for_supported_real_arithmetic() {
7692        let x = laddu_expr::Expr::from(parameter!("x", initial: 2.0));
7693        let y = laddu_expr::Expr::from(parameter!("y", initial: -0.5));
7694        let expr = (x * 3.0 + y) / 2.0;
7695        let model = CompiledModel::from_expr(&expr).unwrap();
7696        let params = Arc::new(model.params().clone()).default_values();
7697        let automatic = CpuBackend.prepare(&model);
7698        let interpreted =
7699            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7700
7701        assert!(matches!(
7702            automatic.scalar_executor,
7703            Some(ScalarExecutor::Jit(_))
7704        ));
7705        assert!(matches!(
7706            automatic.gradient_executor,
7707            GradientExecutor::Jit(_)
7708        ));
7709        assert_eq!(
7710            automatic.evaluate(&params).unwrap(),
7711            interpreted.evaluate(&params).unwrap()
7712        );
7713        assert_eq!(
7714            automatic.evaluate_with_gradient(&params).unwrap(),
7715            interpreted.evaluate_with_gradient(&params).unwrap()
7716        );
7717    }
7718
7719    #[cfg(feature = "jit")]
7720    #[test]
7721    fn auto_jit_supports_complex_transcendentals() {
7722        let expr = laddu_expr::Expr::from(parameter!("x", initial: 2.0)).exp();
7723        let model = CompiledModel::from_expr(&expr).unwrap();
7724        let plan = CpuBackend.prepare(&model);
7725
7726        assert!(matches!(plan.scalar_executor, Some(ScalarExecutor::Jit(_))));
7727        let params = Arc::new(model.params().clone()).default_values();
7728        assert_eq!(plan.evaluate(&params).unwrap(), Complex64::from(2.0).exp());
7729    }
7730
7731    #[cfg(feature = "jit")]
7732    #[test]
7733    fn cpu_f64_auto_jit_matches_interpreter_for_unary_binary_ops_and_gradients() {
7734        let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
7735        let y = laddu_expr::Expr::from(parameter!("y", initial: -0.35));
7736        let z = complex(x.clone(), y.clone());
7737        let expression = x.clone().sqrt()
7738            + x.clone().log()
7739            + x.clone().powi(-2)
7740            + x.clone().sin()
7741            + x.clone().cos()
7742            + x.clone().exp()
7743            + z.clone().conj().real()
7744            + z.clone().imag()
7745            + z.norm_sqr()
7746            + atan2(y.clone(), x.clone())
7747            + complex(x.clone(), y.clone()) / complex(1.25, -0.5);
7748        let model = CompiledModel::from_expr(&expression).unwrap();
7749        let params = model.params().default_values();
7750        let (automatic, interpreted) = f64_jit_and_interpreter(&model);
7751
7752        assert_complex_close_f64(
7753            automatic.evaluate(&params).unwrap(),
7754            interpreted.evaluate(&params).unwrap(),
7755        );
7756        let actual_gradient = automatic.evaluate_with_gradient(&params).unwrap();
7757        let expected_gradient = interpreted.evaluate_with_gradient(&params).unwrap();
7758        assert_complex_close_f64(actual_gradient.value(), expected_gradient.value());
7759        assert_complex_slices_close_f64(actual_gradient.gradient(), expected_gradient.gradient());
7760    }
7761
7762    #[cfg(feature = "jit")]
7763    #[test]
7764    fn cpu_f64_auto_jit_matches_interpreter_for_event_cache_ops_and_gradients() {
7765        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.75));
7766        let x = event_scalar("x");
7767        let y = event_scalar("y");
7768        let expression = ((x.clone() + scale.clone()).sin()
7769            + (y.clone() - 0.25).cos()
7770            + (x.clone() * y.clone()).exp()
7771            + atan2(y.clone(), x.clone() + 1.0))
7772            / complex(scale, -0.5);
7773        let model = CompiledModel::from_expr(&expression).unwrap();
7774        let params = model.params().default_values();
7775        let (automatic, interpreted) = f64_jit_and_interpreter(&model);
7776        let batch = EventBatch::from_events(
7777            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], false).unwrap()),
7778            [
7779                OwnedEvent::new(vec![], vec![0.25, -0.5]),
7780                OwnedEvent::new(vec![], vec![0.75, 0.125]),
7781                OwnedEvent::new(vec![], vec![1.5, 0.5]),
7782            ],
7783        )
7784        .unwrap();
7785        let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
7786        let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
7787
7788        assert_complex_slices_close_f64(
7789            &automatic.evaluate_cache(&params, &automatic_cache).unwrap(),
7790            &interpreted
7791                .evaluate_cache(&params, &interpreted_cache)
7792                .unwrap(),
7793        );
7794        for (actual, expected) in automatic
7795            .evaluate_cache_with_gradient(&params, &automatic_cache)
7796            .unwrap()
7797            .iter()
7798            .zip(
7799                interpreted
7800                    .evaluate_cache_with_gradient(&params, &interpreted_cache)
7801                    .unwrap(),
7802            )
7803        {
7804            assert_complex_close_f64(actual.value(), expected.value());
7805            assert_complex_slices_close_f64(actual.gradient(), expected.gradient());
7806        }
7807    }
7808
7809    #[cfg(feature = "jit")]
7810    #[test]
7811    fn cpu_f64_auto_jit_matches_interpreter_for_reductions_and_gradients() {
7812        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7813        let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
7814        let x = event_scalar("x");
7815        let expression = (x.clone() * scale.clone() + offset.clone()).sin()
7816            + complex(scale, offset).norm_sqr()
7817            + 2.0;
7818        let model = CompiledModel::from_expr(&expression).unwrap();
7819        let params = model.params().default_values();
7820        let (automatic, interpreted) = f64_jit_and_interpreter(&model);
7821        assert!(matches!(
7822            automatic.gradient_executor,
7823            GradientExecutor::Jit(_)
7824        ));
7825        let execution = Execution::default();
7826        let interpreter_execution = Execution::local(crate::ExecutionOptions {
7827            device: crate::Device::Cpu(crate::CpuOptions {
7828                jit: JitPolicy::Disabled,
7829                ..crate::CpuOptions::default()
7830            }),
7831            ..crate::ExecutionOptions::default()
7832        })
7833        .unwrap();
7834        let dataset = Dataset::from_batch(
7835            EventBatch::from_events(
7836                Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap()),
7837                [
7838                    OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7839                    OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7840                    OwnedEvent::weighted(vec![], vec![1.25], 2.0),
7841                ],
7842            )
7843            .unwrap(),
7844        );
7845        let automatic_data = automatic.prepare_dataset(&execution, &dataset).unwrap();
7846        let interpreted_data = interpreted
7847            .prepare_dataset(&interpreter_execution, &dataset)
7848            .unwrap();
7849
7850        let actual = automatic
7851            .reduce_with_gradient(
7852                &execution,
7853                &params,
7854                &automatic_data,
7855                ReductionPlan::weighted_real(),
7856            )
7857            .unwrap();
7858        let expected = interpreted
7859            .reduce_with_gradient(
7860                &interpreter_execution,
7861                &params,
7862                &interpreted_data,
7863                ReductionPlan::weighted_real(),
7864            )
7865            .unwrap();
7866        assert!((actual.value() - expected.value()).abs() < 1.0e-10);
7867        assert_eq!(actual.gradient().len(), expected.gradient().len());
7868        for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
7869            assert!((actual - expected).abs() < 1.0e-10);
7870        }
7871    }
7872
7873    #[test]
7874    fn evaluates_p4_schema_components_and_atan2() {
7875        let expr = event_p4_component("ks1", P4Component::E)
7876            + event_p4_component("ks1", P4Component::Px)
7877            + atan2(
7878                event_p4_component("ks1", P4Component::Py),
7879                event_p4_component("ks1", P4Component::Px),
7880            );
7881        let model = CompiledModel::from_expr(&expr).unwrap();
7882        let params = Arc::new(model.params().clone()).default_values();
7883        let plan = CpuBackend.prepare(&model);
7884        let batch = EventBatch::from_events(
7885            Arc::new(Schema::new(["ks1"], std::iter::empty::<&str>(), false).unwrap()),
7886            [OwnedEvent::new(
7887                vec![RealVec4::new(10.0, 3.0, 4.0, 5.0)],
7888                vec![],
7889            )],
7890        )
7891        .unwrap();
7892
7893        assert_eq!(
7894            plan.evaluate_batch(&params, &batch).unwrap()[0],
7895            Complex64::from(13.0 + 4.0_f64.atan2(3.0))
7896        );
7897    }
7898
7899    #[test]
7900    fn batch_cache_evaluates_without_original_event_batch() {
7901        let expr = event_scalar("x").real().sin() * parameter!("scale", initial: 2.0);
7902        let model = CompiledModel::from_expr(&expr).unwrap();
7903        let layout = Arc::new(model.params().clone());
7904        let mut params = layout.default_values();
7905        let plan = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7906        let batch = EventBatch::from_events(
7907            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7908            [
7909                OwnedEvent::new(vec![], vec![0.5]),
7910                OwnedEvent::new(vec![], vec![1.0]),
7911            ],
7912        )
7913        .unwrap();
7914        let cache = plan.cache_event_batch(&batch).unwrap();
7915
7916        assert_eq!(cache.weights(), &[1.0, 1.0]);
7917        assert!(matches!(&cache.slots[0], CachedSlot::Real(values) if values.len() == 2));
7918        assert_eq!(cache.slots[0].resident_bytes(), 2 * size_of::<f64>());
7919        assert_eq!(
7920            plan.evaluate_cache(&params, &cache).unwrap(),
7921            vec![
7922                Complex64::from(2.0 * 0.5_f64.sin()),
7923                Complex64::from(2.0 * 1.0_f64.sin())
7924            ]
7925        );
7926
7927        let scale = layout
7928            .free_id(layout.id("scale").unwrap())
7929            .unwrap()
7930            .unwrap();
7931        params.set_free(scale, 3.0).unwrap();
7932        assert_eq!(
7933            plan.evaluate_cache(&params, &cache).unwrap(),
7934            vec![
7935                Complex64::from(3.0 * 0.5_f64.sin()),
7936                Complex64::from(3.0 * 1.0_f64.sin())
7937            ]
7938        );
7939    }
7940
7941    #[test]
7942    fn real_cache_slots_use_half_the_scalar_payload_of_complex_slots() {
7943        let real_model =
7944            CompiledModel::from_expr(&(parameter!("scale") * event_scalar("x").real().sin()))
7945                .unwrap();
7946        let x = event_scalar("x");
7947        let complex_model =
7948            CompiledModel::from_expr(&(parameter!("scale") * complex(x.clone().sin(), x.cos())))
7949                .unwrap();
7950        let batch = EventBatch::from_events(
7951            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7952            [
7953                OwnedEvent::new(vec![], vec![0.5]),
7954                OwnedEvent::new(vec![], vec![1.0]),
7955            ],
7956        )
7957        .unwrap();
7958        let real_cache = CpuBackend
7959            .prepare(&real_model)
7960            .cache_event_batch(&batch)
7961            .unwrap();
7962        let complex_cache = CpuBackend
7963            .prepare(&complex_model)
7964            .cache_event_batch(&batch)
7965            .unwrap();
7966
7967        assert!(matches!(&real_cache.slots[0], CachedSlot::Real(_)));
7968        assert!(matches!(&complex_cache.slots[0], CachedSlot::Complex(_)));
7969        assert_eq!(
7970            complex_cache.slots[0].resident_bytes(),
7971            2 * real_cache.slots[0].resident_bytes()
7972        );
7973    }
7974
7975    #[test]
7976    fn selected_event_only_solve_components_cache_inverse_rows() {
7977        let expression = solve(
7978            matrix([[event_scalar("x") + 2.0]]),
7979            vector([parameter!("rhs", initial: 3.0)]),
7980        )
7981        .component(0);
7982        let model = CompiledModel::from_expr(&expression).unwrap();
7983        let params = Arc::new(model.params().clone()).default_values();
7984        let plan = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7985        let scalar_plan = plan.scalar_interpreter_plan().unwrap();
7986        assert!(!scalar_plan.invariant_instructions.is_empty());
7987        assert!(!scalar_plan.event_instructions.is_empty());
7988        let batch = EventBatch::from_events(
7989            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7990            [
7991                OwnedEvent::new(vec![], vec![0.0]),
7992                OwnedEvent::new(vec![], vec![1.0]),
7993            ],
7994        )
7995        .unwrap();
7996        let cache = plan.cache_event_batch(&batch).unwrap();
7997
7998        assert!(cache.factor_slots.is_empty());
7999        assert_eq!(cache.solve_row_slots.len(), 1);
8000        assert_eq!(cache.solve_row_slots[0].values.len(), 2);
8001        assert!(cache.resident_bytes() > 0);
8002        let first = plan
8003            .evaluate_cache_row_with_gradient(&params, &cache, 0)
8004            .unwrap();
8005        let second = plan
8006            .evaluate_cache_row_with_gradient(&params, &cache, 1)
8007            .unwrap();
8008        assert_eq!(first.value(), Complex64::from(1.5));
8009        assert_eq!(first.gradient(), &[Complex64::from(0.5)]);
8010        assert_eq!(second.value(), Complex64::from(1.0));
8011        assert_eq!(second.gradient(), &[Complex64::from(1.0 / 3.0)]);
8012    }
8013
8014    #[test]
8015    fn cached_solve_component_matches_general_complex_nonsymmetric_solve() {
8016        let expression = solve(
8017            matrix([
8018                [event_scalar("x") + 2.0, Complex64::I.into()],
8019                [Complex64::new(2.0, -1.0).into(), 3.0.into()],
8020            ]),
8021            vector([
8022                parameter!("p", initial: 1.5),
8023                parameter!("q", initial: -0.25),
8024            ]),
8025        )
8026        .component(1);
8027        let model = CompiledModel::from_expr(&expression).unwrap();
8028        let params = Arc::new(model.params().clone()).default_values();
8029        let plan = CpuBackend.prepare(&model);
8030        assert!(plan.solve_components.iter().any(Option::is_some));
8031
8032        let event = HashMap::from([("x".to_owned(), 0.75)]);
8033        let direct = plan
8034            .evaluate_with_event_and_gradient(&params, &event)
8035            .unwrap();
8036        let batch = EventBatch::from_events(
8037            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8038            [OwnedEvent::new(vec![], vec![0.75])],
8039        )
8040        .unwrap();
8041        let cache = plan.cache_event_batch(&batch).unwrap();
8042        let cached = plan
8043            .evaluate_cache_row_with_gradient(&params, &cache, 0)
8044            .unwrap();
8045        let ir_gradient = gradient_interpreter::GradientInterpreter::new(
8046            plan.scalar_kernel.as_ref().unwrap(),
8047            model.params().free_params(),
8048        )
8049        .unwrap()
8050        .evaluate(&params, Some((&cache, 0)))
8051        .unwrap()
8052        .1;
8053
8054        assert!((cached.value() - direct.value()).norm() < 1.0e-12);
8055        for (cached, direct) in cached.gradient().iter().zip(direct.gradient()) {
8056            assert!((cached - direct).norm() < 1.0e-12);
8057        }
8058        for (actual, expected) in ir_gradient.iter().zip(cached.gradient()) {
8059            assert!((actual - expected).norm() < 1.0e-12);
8060        }
8061    }
8062
8063    #[test]
8064    fn reverse_cached_solve_component_matches_forward_nonsymmetric_solve() {
8065        let expression = solve(
8066            matrix([
8067                [event_scalar("x") + 2.0, Complex64::I.into()],
8068                [Complex64::new(2.0, -1.0).into(), 3.0.into()],
8069            ]),
8070            vector([
8071                parameter!("p", initial: 1.5),
8072                parameter!("q", initial: -0.25),
8073            ]),
8074        )
8075        .component(1);
8076        let model = CompiledModel::from_expr(&expression).unwrap();
8077        let params = Arc::new(model.params().clone()).default_values();
8078        let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8079        let reverse = CpuBackend
8080            .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
8081            .unwrap();
8082        assert!(reverse.solve_components.iter().any(Option::is_some));
8083
8084        let batch = EventBatch::from_events(
8085            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8086            [OwnedEvent::new(vec![], vec![0.75])],
8087        )
8088        .unwrap();
8089        let expected = forward
8090            .evaluate_cache_row_with_gradient(
8091                &params,
8092                &forward.cache_event_batch(&batch).unwrap(),
8093                0,
8094            )
8095            .unwrap();
8096        let actual = reverse
8097            .evaluate_cache_row_with_gradient(
8098                &params,
8099                &reverse.cache_event_batch(&batch).unwrap(),
8100                0,
8101            )
8102            .unwrap();
8103
8104        assert!((actual.value() - expected.value()).norm() < 1.0e-12);
8105        assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
8106    }
8107
8108    #[cfg(feature = "jit")]
8109    #[test]
8110    fn jit_gradient_reduction_handles_cached_solve_rows() {
8111        let expression = solve(
8112            matrix([
8113                [event_scalar("x") + 2.0, Complex64::I.into()],
8114                [Complex64::new(2.0, -1.0).into(), 3.0.into()],
8115            ]),
8116            vector([
8117                parameter!("p", initial: 1.5),
8118                parameter!("q", initial: -0.25),
8119            ]),
8120        )
8121        .component(1);
8122        let model = CompiledModel::from_expr(&expression).unwrap();
8123        let params = Arc::new(model.params().clone()).default_values();
8124        let automatic = CpuBackend.prepare(&model);
8125        let interpreted =
8126            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8127        assert!(matches!(
8128            automatic.gradient_executor,
8129            GradientExecutor::Jit(_)
8130        ));
8131
8132        let dataset = Dataset::from_batch(
8133            EventBatch::from_events(
8134                Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8135                [
8136                    OwnedEvent::new(vec![], vec![0.25]),
8137                    OwnedEvent::new(vec![], vec![0.75]),
8138                    OwnedEvent::new(vec![], vec![1.25]),
8139                ],
8140            )
8141            .unwrap(),
8142        );
8143        let execution = Execution::local(crate::ExecutionOptions {
8144            device: crate::Device::Cpu(crate::CpuOptions {
8145                threads: crate::ThreadPolicy::Fixed(2),
8146                ..crate::CpuOptions::default()
8147            }),
8148            ..crate::ExecutionOptions::default()
8149        })
8150        .unwrap();
8151        let automatic_data = automatic.prepare_dataset(&execution, &dataset).unwrap();
8152        let interpreted_data = interpreted.prepare_dataset(&execution, &dataset).unwrap();
8153        let actual = automatic
8154            .reduce_with_gradient(
8155                &execution,
8156                &params,
8157                &automatic_data,
8158                ReductionPlan::weighted_real(),
8159            )
8160            .unwrap();
8161        let expected = interpreted
8162            .reduce_with_gradient(
8163                &execution,
8164                &params,
8165                &interpreted_data,
8166                ReductionPlan::weighted_real(),
8167            )
8168            .unwrap();
8169
8170        assert!((actual.value() - expected.value()).abs() < 1.0e-12);
8171        for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
8172            assert!((actual - expected).abs() < 1.0e-12);
8173        }
8174    }
8175
8176    #[test]
8177    fn batch_cache_reports_missing_event_columns() {
8178        let expr = event_scalar("missing");
8179        let model = CompiledModel::from_expr(&expr).unwrap();
8180        let plan = CpuBackend.prepare(&model);
8181        let batch = EventBatch::from_events(
8182            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8183            [OwnedEvent::new(vec![], vec![0.5])],
8184        )
8185        .unwrap();
8186
8187        assert!(matches!(
8188            plan.cache_event_batch(&batch),
8189            Err(RuntimeError::MissingEventColumn(name)) if name == "missing"
8190        ));
8191    }
8192
8193    #[test]
8194    fn cached_dataset_preserves_transformed_batches_and_weights() {
8195        let expr = event_scalar("x") * parameter!("scale", initial: 2.0);
8196        let model = CompiledModel::from_expr(&expr).unwrap();
8197        let params = Arc::new(model.params().clone()).default_values();
8198        let plan = CpuBackend.prepare(&model);
8199        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
8200        let batch = EventBatch::from_events(
8201            schema,
8202            [
8203                OwnedEvent::weighted(vec![], vec![0.5], 2.0),
8204                OwnedEvent::weighted(vec![], vec![1.0], 3.0),
8205            ],
8206        )
8207        .unwrap();
8208        let dataset = Dataset::from_batch(batch).filter(|event| event.scalar(0) > 0.75);
8209        let cached = plan.cache_dataset(&dataset).unwrap();
8210
8211        assert_eq!(cached.len(), 1);
8212        assert_eq!(cached.batches()[0].weights(), &[3.0]);
8213        assert_eq!(cached.batches()[0].sum_weights(), 3.0);
8214        assert_eq!(
8215            plan.evaluate_cached_dataset(&params, &cached).unwrap(),
8216            vec![Complex64::from(2.0)]
8217        );
8218    }
8219
8220    #[test]
8221    fn cached_dataset_weighted_reductions_match_dataset_path() {
8222        let expr = event_scalar("x") * parameter!("scale", initial: 2.0);
8223        let model = CompiledModel::from_expr(&expr).unwrap();
8224        let params = Arc::new(model.params().clone()).default_values();
8225        let plan = CpuBackend.prepare(&model);
8226        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
8227        let first = EventBatch::from_events(
8228            Arc::clone(&schema),
8229            [
8230                OwnedEvent::weighted(vec![], vec![1.0], 2.0),
8231                OwnedEvent::weighted(vec![], vec![2.0], 3.0),
8232            ],
8233        )
8234        .unwrap();
8235        let second =
8236            EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![3.0], 4.0)])
8237                .unwrap();
8238        let dataset = Dataset::from_batches(vec![first, second]).unwrap();
8239        let cached = plan.cache_dataset(&dataset).unwrap();
8240
8241        let expected = dataset.weighted_sum(|event| 2.0 * event.scalar(0)).unwrap();
8242        assert_eq!(cached.sum_weights(), dataset.sum_weights().unwrap());
8243        assert_eq!(
8244            plan.weighted_sum_cached(&params, &cached, |value| value.re)
8245                .unwrap(),
8246            expected
8247        );
8248        assert_eq!(
8249            plan.weighted_complex_sum_cached(&params, &cached, |value| value * Complex64::I)
8250                .unwrap(),
8251            Complex64::I * expected
8252        );
8253        assert_eq!(
8254            plan.par_weighted_sum_cached(&params, &cached, |value| value.re)
8255                .unwrap(),
8256            expected
8257        );
8258        assert_eq!(
8259            plan.par_weighted_complex_sum_cached(&params, &cached, |value| value * Complex64::I)
8260                .unwrap(),
8261            Complex64::I * expected
8262        );
8263        let serial_gradient = plan
8264            .try_weighted_real_sum_with_gradient_cached(&params, &cached, |value| {
8265                Ok::<_, RuntimeError>((value.re.powi(2), 2.0 * value.re))
8266            })
8267            .unwrap();
8268        let parallel_gradient = plan
8269            .par_try_weighted_real_sum_with_gradient_cached(&params, &cached, |value| {
8270                Ok::<_, RuntimeError>((value.re.powi(2), 2.0 * value.re))
8271            })
8272            .unwrap();
8273        assert_eq!(serial_gradient, parallel_gradient);
8274    }
8275
8276    #[test]
8277    fn reduction_plans_match_across_storage_and_thread_policies() {
8278        let expr = event_scalar("x") * parameter!("scale", initial: 2.0) + 1.0;
8279        let model = CompiledModel::from_expr(&expr).unwrap();
8280        let params = Arc::new(model.params().clone()).default_values();
8281        let plan = CpuBackend.prepare(&model);
8282        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
8283        let first = EventBatch::from_events(
8284            Arc::clone(&schema),
8285            [
8286                OwnedEvent::weighted(vec![], vec![1.0], 2.0),
8287                OwnedEvent::weighted(vec![], vec![2.0], 3.0),
8288            ],
8289        )
8290        .unwrap();
8291        let second =
8292            EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![3.0], 4.0)])
8293                .unwrap();
8294        let resident = Dataset::from_batches(vec![first, second]).unwrap();
8295        let datasets = [resident.clone(), resident.streaming()];
8296        let executions = [
8297            Execution::local(crate::ExecutionOptions {
8298                device: crate::Device::Cpu(crate::CpuOptions {
8299                    threads: crate::ThreadPolicy::Serial,
8300                    ..crate::CpuOptions::default()
8301                }),
8302                ..crate::ExecutionOptions::default()
8303            })
8304            .unwrap(),
8305            Execution::local(crate::ExecutionOptions {
8306                device: crate::Device::Cpu(crate::CpuOptions {
8307                    threads: crate::ThreadPolicy::Fixed(2),
8308                    ..crate::CpuOptions::default()
8309                }),
8310                ..crate::ExecutionOptions::default()
8311            })
8312            .unwrap(),
8313        ];
8314        let expected_real = 49.0;
8315        let expected_log = 2.0 * 3.0_f64.ln() + 3.0 * 5.0_f64.ln() + 4.0 * 7.0_f64.ln();
8316        let expected_log_gradient = 2.0 / 3.0 + 6.0 / 5.0 + 12.0 / 7.0;
8317
8318        for execution in &executions {
8319            for dataset in &datasets {
8320                let prepared = plan.prepare_dataset(execution, dataset).unwrap();
8321                assert_eq!(
8322                    plan.reduce(
8323                        execution,
8324                        &params,
8325                        &prepared,
8326                        ReductionPlan::weighted_real(),
8327                    )
8328                    .unwrap(),
8329                    expected_real
8330                );
8331                assert_eq!(
8332                    plan.reduce(
8333                        execution,
8334                        &params,
8335                        &prepared,
8336                        ReductionPlan::weighted_positive_real(),
8337                    )
8338                    .unwrap(),
8339                    expected_real
8340                );
8341                let evaluation = plan
8342                    .reduce_with_gradient(
8343                        execution,
8344                        &params,
8345                        &prepared,
8346                        ReductionPlan::weighted_log_positive_real(),
8347                    )
8348                    .unwrap();
8349                assert!((evaluation.value() - expected_log).abs() < 1.0e-12);
8350                assert!((evaluation.gradient()[0] - expected_log_gradient).abs() < 1.0e-12);
8351            }
8352        }
8353    }
8354
8355    #[test]
8356    fn positive_reduction_reports_the_invalid_value() {
8357        let model = CompiledModel::from_expr(&event_scalar("x")).unwrap();
8358        let params = Arc::new(model.params().clone()).default_values();
8359        let plan = CpuBackend.prepare(&model);
8360        let dataset = Dataset::from_batch(
8361            EventBatch::from_events(
8362                Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8363                [OwnedEvent::new(vec![], vec![-2.0])],
8364            )
8365            .unwrap(),
8366        );
8367        let execution = Execution::default();
8368        let prepared = plan.prepare_dataset(&execution, &dataset).unwrap();
8369
8370        assert!(matches!(
8371            plan.reduce(
8372                &execution,
8373                &params,
8374                &prepared,
8375                ReductionPlan::weighted_log_positive_real(),
8376            ),
8377            Err(RuntimeError::Reduction(
8378                laddu_compile::ReductionError::NonPositiveValue {
8379                    transform: laddu_compile::ReductionTransform::LogPositiveReal,
8380                    value: -2.0,
8381                }
8382            ))
8383        ));
8384    }
8385
8386    #[test]
8387    fn evaluates_linear_algebra_nodes() {
8388        let a = matrix([[2.0, 0.0], [0.0, 4.0]]);
8389        let b = vector([8.0, 12.0]);
8390        let x = solve(a, b);
8391        let expr = dot(&x, vector([1.0, 1.0]));
8392        let model = CompiledModel::from_expr(&expr).unwrap();
8393        let params = Arc::new(model.params().clone()).default_values();
8394        let plan = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8395
8396        assert_eq!(plan.evaluate(&params).unwrap(), Complex64::from(7.0));
8397        assert_eq!(plan.constant_factors.len(), 1);
8398        assert!(plan.constant_factors[0].get().is_some());
8399    }
8400
8401    #[cfg(feature = "jit")]
8402    #[test]
8403    fn auto_jit_matches_interpreter_for_complex_linear_algebra() {
8404        let diagonal = laddu_expr::Expr::from(parameter!("diagonal", initial: 4.0));
8405        let matrix = matrix([
8406            [complex(2.0, 0.5), complex(0.25, -0.1)],
8407            [complex(-0.2, 0.3), diagonal],
8408        ]);
8409        let rhs = vector([complex(8.0, 1.0), complex(12.0, -0.5)]);
8410        let solution = solve(matrix, rhs);
8411        let expression = dot(solution, vector([complex(1.0, -0.2), complex(0.5, 0.3)])).exp();
8412        let model = CompiledModel::from_expr(&expression).unwrap();
8413        let params = Arc::new(model.params().clone()).default_values();
8414        let automatic = CpuBackend.prepare(&model);
8415        let interpreted =
8416            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8417
8418        assert!(matches!(
8419            automatic.scalar_executor,
8420            Some(ScalarExecutor::Jit(_))
8421        ));
8422        assert!(matches!(
8423            automatic.gradient_executor,
8424            GradientExecutor::Jit(_)
8425        ));
8426        let actual = automatic.evaluate(&params).unwrap();
8427        let expected = interpreted.evaluate(&params).unwrap();
8428        assert!(
8429            (actual - expected).norm() < 1.0e-12,
8430            "{actual} != {expected}"
8431        );
8432        let actual = automatic.evaluate_with_gradient(&params).unwrap();
8433        let expected = interpreted.evaluate_with_gradient(&params).unwrap();
8434        assert!((actual.value() - expected.value()).norm() < 1.0e-12);
8435        for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
8436            assert!((actual - expected).norm() < 1.0e-12);
8437        }
8438    }
8439
8440    #[cfg(feature = "jit")]
8441    #[test]
8442    fn auto_jit_gradients_support_parameter_dependent_solve() {
8443        let x = event_scalar("x");
8444        let coupling = laddu_expr::Expr::from(parameter!("coupling", initial: 0.2));
8445        let drive = laddu_expr::Expr::from(parameter!("drive", initial: -0.4));
8446        let matrix = matrix([
8447            [x.clone() + 2.0, complex(coupling.clone(), 0.1)],
8448            [complex(-0.3, coupling), 3.0.into()],
8449        ]);
8450        let expression = solve(
8451            matrix,
8452            vector([x.clone().sin() + drive, complex(x.cos(), 0.5)]),
8453        )
8454        .component(1)
8455        .norm_sqr();
8456        let model = CompiledModel::from_expr(&expression).unwrap();
8457        let params = Arc::new(model.params().clone()).default_values();
8458        let automatic = CpuBackend.prepare(&model);
8459        let interpreted =
8460            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8461        let batch = EventBatch::from_events(
8462            Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8463            [
8464                OwnedEvent::new(vec![], vec![0.25]),
8465                OwnedEvent::new(vec![], vec![0.75]),
8466                OwnedEvent::new(vec![], vec![1.25]),
8467            ],
8468        )
8469        .unwrap();
8470        let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
8471        let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
8472
8473        assert!(matches!(
8474            automatic.scalar_executor,
8475            Some(ScalarExecutor::Jit(_))
8476        ));
8477        assert!(matches!(
8478            automatic.gradient_executor,
8479            GradientExecutor::Jit(_)
8480        ));
8481        let actual = automatic.evaluate_cache(&params, &automatic_cache).unwrap();
8482        let expected = interpreted
8483            .evaluate_cache(&params, &interpreted_cache)
8484            .unwrap();
8485        for (actual, expected) in actual.iter().zip(expected) {
8486            assert!(
8487                (*actual - expected).norm() < 1.0e-12,
8488                "{actual} != {expected}"
8489            );
8490        }
8491        let actual = automatic
8492            .evaluate_cache_with_gradient(&params, &automatic_cache)
8493            .unwrap();
8494        let expected = interpreted
8495            .evaluate_cache_with_gradient(&params, &interpreted_cache)
8496            .unwrap();
8497        for (actual, expected) in actual.iter().zip(&expected) {
8498            assert!((actual.value() - expected.value()).norm() < 1.0e-12);
8499            for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
8500                assert!((actual - expected).norm() < 1.0e-12);
8501            }
8502        }
8503        let ir_interpreter = gradient_interpreter::GradientInterpreter::new(
8504            automatic.scalar_kernel.as_ref().unwrap(),
8505            model.params().free_params(),
8506        )
8507        .unwrap();
8508        for (row, expected) in expected.iter().enumerate() {
8509            let actual = ir_interpreter
8510                .evaluate(&params, Some((&automatic_cache, row)))
8511                .unwrap()
8512                .1;
8513            for (actual, expected) in actual.iter().zip(expected.gradient()) {
8514                assert!((actual - expected).norm() < 1.0e-12);
8515            }
8516        }
8517        for (row, actual) in actual.iter().enumerate() {
8518            for parameter in 0..params.layout().n_free() {
8519                let h = 1.0e-6;
8520                let id = params.layout().free_params()[parameter];
8521                let free_id = params.layout().free_id(id).unwrap().unwrap();
8522                let value = params.get(id).unwrap();
8523                let mut plus = params.clone();
8524                let mut minus = params.clone();
8525                plus.set_free(free_id, value + h).unwrap();
8526                minus.set_free(free_id, value - h).unwrap();
8527                let expected = (automatic.evaluate_cache(&plus, &automatic_cache).unwrap()[row]
8528                    - automatic.evaluate_cache(&minus, &automatic_cache).unwrap()[row])
8529                    / (2.0 * h);
8530                assert!((actual.gradient()[parameter] - expected).norm() < 1.0e-8);
8531            }
8532        }
8533    }
8534
8535    #[cfg(feature = "jit")]
8536    #[test]
8537    fn jit_and_interpreter_reject_singular_parameter_dependent_solve() {
8538        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.0));
8539        let expression = solve(
8540            matrix([[scale.clone(), 2.0.into()], [scale * 2.0, 4.0.into()]]),
8541            vector([1.0, 2.0]),
8542        )
8543        .component(0);
8544        let model = CompiledModel::from_expr(&expression).unwrap();
8545        let params = Arc::new(model.params().clone()).default_values();
8546        let automatic = CpuBackend.prepare(&model);
8547        let interpreted =
8548            CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8549
8550        assert!(matches!(
8551            automatic.gradient_executor,
8552            GradientExecutor::Jit(_)
8553        ));
8554        assert!(automatic.evaluate_with_gradient(&params).is_err());
8555        assert!(interpreted.evaluate_with_gradient(&params).is_err());
8556    }
8557
8558    #[test]
8559    fn optimized_and_unoptimized_plans_evaluate_the_same_expression() {
8560        let solved = solve(matrix([[2.0, 0.0], [0.0, 4.0]]), vector([8.0, 12.0]));
8561        let complex_offset = complex(
8562            parameter!("offset_re", initial: 1.5),
8563            parameter!("offset_im", initial: -0.5),
8564        );
8565        let polar_product = polar_complex(
8566            parameter!("mag1", initial: 2.0),
8567            parameter!("phase1", initial: 0.25),
8568        ) * polar_complex(
8569            parameter!("mag2", initial: 3.0),
8570            parameter!("phase2", initial: -0.5),
8571        );
8572        let expr = ((laddu_expr::event_scalar("mass") + 0.0) * 1.0
8573            + dot(solved, vector([1.0, 1.0]))
8574            + complex_offset.conj().real()
8575            + polar_product.real()
8576            + parameter!("unused", initial: 3.0) * 0.0)
8577            .norm_sqr();
8578        let no_optimization = CompileOptions::without_optimizations();
8579        let optimized = CompiledModel::from_expr(&expr).unwrap();
8580        let unoptimized = CompiledModel::from_expr_with_options(&expr, &no_optimization).unwrap();
8581        let optimized_params = Arc::new(optimized.params().clone()).default_values();
8582        let unoptimized_params = Arc::new(unoptimized.params().clone()).default_values();
8583        let event = HashMap::from([("mass".to_owned(), 2.0)]);
8584
8585        let optimized = CpuBackend
8586            .prepare(&optimized)
8587            .evaluate_with_event_and_gradient(&optimized_params, &event)
8588            .unwrap();
8589        let unoptimized = CpuBackend
8590            .prepare(&unoptimized)
8591            .evaluate_with_event_and_gradient(&unoptimized_params, &event)
8592            .unwrap();
8593        assert_eq!(optimized.value(), unoptimized.value());
8594        for (optimized, unoptimized) in optimized.gradient().iter().zip(unoptimized.gradient()) {
8595            assert!((optimized - unoptimized).norm() < 1.0e-12);
8596        }
8597    }
8598}