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