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