Skip to main content

laddu_runtime/cpu/
cache.rs

1#[cfg(feature = "jit")]
2use std::marker::PhantomData;
3use std::{mem::size_of, sync::Arc};
4
5use laddu_compile::CachePlan;
6use laddu_data::{
7    data::{CacheStorage, Dataset, EventBatch},
8    io::ReadPlan,
9};
10use laddu_expr::{ExprId, ValueKind};
11use nalgebra::{DMatrix, DVector};
12use num::complex::Complex64;
13
14use super::layout::{FlatRows, matrix_at_optional};
15use super::{CpuPlan, DynamicLu, PreparedDatasetStats, RuntimeError, RuntimeResult, Value};
16use crate::MemoryLease;
17
18/// Raw cache payload metadata consumed by compiled JIT kernels.
19#[cfg(feature = "jit")]
20#[repr(C)]
21#[derive(Copy, Clone)]
22pub(crate) struct CacheDescriptor {
23    pub(crate) values: *const u8,
24    pub(crate) width: usize,
25}
26
27#[cfg(feature = "jit")]
28pub(crate) struct JitDescriptorSet<'a> {
29    pub(crate) values: Vec<CacheDescriptor>,
30    pub(crate) solve_rows: Vec<CacheDescriptor>,
31    pub(crate) _cache: PhantomData<&'a CpuBatchCache>,
32}
33
34/// Materialized event-dependent values for one batch.
35#[derive(Clone, Debug)]
36pub struct CpuBatchCache {
37    pub(super) len: usize,
38    pub(super) weights: Vec<f64>,
39    pub(super) sum_weights: f64,
40    pub(super) nodes: Vec<ExprId>,
41    pub(crate) slots: Vec<CachedSlot>,
42    pub(super) factor_nodes: Vec<ExprId>,
43    pub(super) factor_slots: Vec<CachedFactorSlot>,
44    pub(super) solve_row_keys: Vec<(ExprId, usize, usize)>,
45    pub(crate) solve_row_slots: Vec<CachedSolveRowSlot>,
46}
47
48impl CpuBatchCache {
49    pub(super) fn new(
50        cache_plan: &CachePlan,
51        factor_matrices: &[(ExprId, usize)],
52        solve_row_keys: &[(ExprId, usize, usize)],
53        len: usize,
54    ) -> RuntimeResult<Self> {
55        let slots = cache_plan
56            .entries()
57            .iter()
58            .map(|entry| CachedSlot::new(entry.value_kind(), len))
59            .collect::<RuntimeResult<Vec<_>>>()?;
60        let solve_row_slots = solve_row_keys
61            .iter()
62            .map(|(_, _, dimension)| CachedSolveRowSlot::new(*dimension, len))
63            .collect::<RuntimeResult<Vec<_>>>()?;
64        Ok(Self {
65            len,
66            weights: vec![1.0; len],
67            sum_weights: len as f64,
68            nodes: cache_plan
69                .entries()
70                .iter()
71                .map(|entry| entry.node())
72                .collect(),
73            slots,
74            factor_nodes: factor_matrices.iter().map(|(node, _)| *node).collect(),
75            factor_slots: factor_matrices
76                .iter()
77                .map(|(_, dimension)| CachedFactorSlot::new(*dimension))
78                .collect(),
79            solve_row_keys: solve_row_keys.to_vec(),
80            solve_row_slots,
81        })
82    }
83
84    /// Returns the number of cached events.
85    pub fn len(&self) -> usize {
86        self.len
87    }
88
89    /// Returns whether the cache contains no events.
90    pub fn is_empty(&self) -> bool {
91        self.len == 0
92    }
93
94    /// Returns per-event weights.
95    pub fn weights(&self) -> &[f64] {
96        &self.weights
97    }
98
99    /// Returns the sum of event weights.
100    pub fn sum_weights(&self) -> f64 {
101        self.sum_weights
102    }
103
104    /// Returns the raw cache payload metadata required by the JIT ABI.
105    ///
106    /// The returned pointers borrow this cache and are valid only while it is
107    /// immutably borrowed.  Keeping this projection here prevents the JIT
108    /// backend from depending on cache-slot representation details.
109    #[cfg(feature = "jit")]
110    #[cfg(feature = "jit")]
111    pub(crate) fn jit_descriptors(&self) -> JitDescriptorSet<'_> {
112        JitDescriptorSet {
113            values: self
114                .slots
115                .iter()
116                .map(|slot| CacheDescriptor {
117                    values: slot.values_ptr(),
118                    width: slot.width(),
119                })
120                .collect(),
121            solve_rows: self
122                .solve_row_slots
123                .iter()
124                .map(|slot| CacheDescriptor {
125                    values: slot.values.as_ptr().cast(),
126                    width: slot.dimension,
127                })
128                .collect(),
129            _cache: PhantomData,
130        }
131    }
132
133    /// Estimates heap memory retained by this cache, in bytes.
134    pub fn resident_bytes(&self) -> usize {
135        self.weights.capacity() * size_of::<f64>()
136            + self.nodes.capacity() * size_of::<ExprId>()
137            + self
138                .slots
139                .iter()
140                .map(CachedSlot::resident_bytes)
141                .sum::<usize>()
142            + self.factor_nodes.capacity() * size_of::<ExprId>()
143            + self
144                .factor_slots
145                .iter()
146                .map(CachedFactorSlot::resident_bytes)
147                .sum::<usize>()
148            + self.solve_row_keys.capacity() * size_of::<(ExprId, usize, usize)>()
149            + self
150                .solve_row_slots
151                .iter()
152                .map(CachedSolveRowSlot::resident_bytes)
153                .sum::<usize>()
154    }
155
156    pub(super) fn set_weights(&mut self, weights: Vec<f64>) {
157        self.sum_weights = weights.iter().sum();
158        self.weights = weights;
159    }
160
161    pub(super) fn push(&mut self, slot: usize, value: Value) -> RuntimeResult<()> {
162        let len = self.slots.len();
163        self.slots
164            .get_mut(slot)
165            .ok_or(RuntimeError::InvalidCache {
166                expected: len,
167                actual: slot + 1,
168            })?
169            .push(value)
170    }
171
172    pub(super) fn value(&self, slot: usize, row: usize) -> RuntimeResult<Value> {
173        if row >= self.len {
174            return Err(RuntimeError::InvalidShape {
175                index: row,
176                message: format!("cache row {row} out of bounds for len {}", self.len),
177            });
178        }
179        self.slots
180            .get(slot)
181            .ok_or(RuntimeError::InvalidCache {
182                expected: self.slots.len(),
183                actual: slot + 1,
184            })?
185            .value(row)
186    }
187
188    pub(super) fn scalar(&self, slot: usize, row: usize) -> RuntimeResult<Complex64> {
189        if row >= self.len {
190            return Err(RuntimeError::InvalidShape {
191                index: row,
192                message: format!("cache row {row} out of bounds for len {}", self.len),
193            });
194        }
195        self.slots
196            .get(slot)
197            .ok_or(RuntimeError::InvalidCache {
198                expected: self.slots.len(),
199                actual: slot + 1,
200            })?
201            .scalar(row)
202    }
203
204    pub(super) fn real_range(
205        &self,
206        slot: usize,
207        start: usize,
208        end: usize,
209    ) -> RuntimeResult<&[f64]> {
210        if start > end || end > self.len {
211            return Err(RuntimeError::InvalidShape {
212                index: start,
213                message: format!(
214                    "cache range {start}..{end} out of bounds for len {}",
215                    self.len
216                ),
217            });
218        }
219        self.slots
220            .get(slot)
221            .ok_or(RuntimeError::InvalidCache {
222                expected: self.slots.len(),
223                actual: slot + 1,
224            })?
225            .real_range(start, end)
226    }
227
228    pub(super) fn complex_range(
229        &self,
230        slot: usize,
231        start: usize,
232        end: usize,
233    ) -> RuntimeResult<&[Complex64]> {
234        if start > end || end > self.len {
235            return Err(RuntimeError::InvalidShape {
236                index: start,
237                message: format!(
238                    "cache range {start}..{end} out of bounds for len {}",
239                    self.len
240                ),
241            });
242        }
243        self.slots
244            .get(slot)
245            .ok_or(RuntimeError::InvalidCache {
246                expected: self.slots.len(),
247                actual: slot + 1,
248            })?
249            .complex_range(start, end)
250    }
251
252    pub(super) fn push_factor(&mut self, slot: usize, factor: DynamicLu) -> RuntimeResult<()> {
253        let len = self.factor_slots.len();
254        self.factor_slots
255            .get_mut(slot)
256            .ok_or(RuntimeError::InvalidCache {
257                expected: len,
258                actual: slot + 1,
259            })?
260            .push(factor)
261    }
262
263    pub(super) fn factor(&self, slot: usize, row: usize) -> RuntimeResult<&DynamicLu> {
264        self.factor_slots
265            .get(slot)
266            .ok_or(RuntimeError::InvalidCache {
267                expected: self.factor_slots.len(),
268                actual: slot + 1,
269            })?
270            .factor(row)
271    }
272
273    pub(super) fn push_solve_row(
274        &mut self,
275        slot: usize,
276        values: impl IntoIterator<Item = Complex64>,
277    ) -> RuntimeResult<()> {
278        let len = self.solve_row_slots.len();
279        self.solve_row_slots
280            .get_mut(slot)
281            .ok_or(RuntimeError::InvalidCache {
282                expected: len,
283                actual: slot + 1,
284            })?
285            .push(values)
286    }
287
288    pub(super) fn solve_row(&self, slot: usize, row: usize) -> RuntimeResult<&[Complex64]> {
289        self.solve_row_slots
290            .get(slot)
291            .ok_or(RuntimeError::InvalidCache {
292                expected: self.solve_row_slots.len(),
293                actual: slot + 1,
294            })?
295            .row(row)
296    }
297}
298
299impl CpuPlan {
300    /// Materializes the event-dependent cache for a batch.
301    ///
302    /// # Errors
303    ///
304    /// Returns [`RuntimeError`] when required columns are missing, expression
305    /// shapes are invalid, cache construction fails, or a matrix is singular.
306    ///
307    /// # Panics
308    ///
309    /// Panics if a node selected by the validated cache plan was not evaluated.
310    pub fn cache_event_batch(&self, batch: &EventBatch) -> RuntimeResult<CpuBatchCache> {
311        let event_columns = self.event_columns(batch.schema())?;
312        let mut cache = CpuBatchCache::new(
313            &self.cache_plan,
314            &self.factor_matrices,
315            &self.solve_row_keys,
316            batch.len(),
317        )?;
318        for row in 0..batch.len() {
319            let values = self.evaluate_cache_values_for_row(batch, row, &event_columns)?;
320            for (slot, entry) in self.cache_plan.entries().iter().enumerate() {
321                let value = values[entry.node().index()]
322                    .as_ref()
323                    .expect("cacheable node should have been evaluated")
324                    .clone();
325                cache.push(slot, value)?;
326            }
327            for plan in &self.solve_row_matrices {
328                let (rows, cols, values) = matrix_at_optional(&values, plan.matrix().index())?;
329                if rows != plan.dimension() || cols != plan.dimension() {
330                    return Err(RuntimeError::InvalidShape {
331                        index: plan.matrix().index(),
332                        message: format!(
333                            "specialized solve expected a {}x{} matrix, got {rows}x{cols}",
334                            plan.dimension(),
335                            plan.dimension()
336                        ),
337                    });
338                }
339                let transpose_factor = DMatrix::from_row_slice(rows, cols, values).transpose().lu();
340                for (slot, index) in plan.rows() {
341                    let mut basis = DVector::zeros(plan.dimension());
342                    basis[*index] = Complex64::ONE;
343                    let inverse_row = transpose_factor
344                        .solve(&basis)
345                        .ok_or(RuntimeError::SingularMatrix(plan.matrix().index()))?;
346                    cache.push_solve_row(*slot, inverse_row.iter().copied())?;
347                }
348            }
349            for (slot, (matrix, _)) in self.factor_matrices.iter().enumerate() {
350                let (rows, cols, values) = matrix_at_optional(&values, matrix.index())?;
351                cache.push_factor(slot, DMatrix::from_row_slice(rows, cols, values).lu())?;
352            }
353        }
354        cache.set_weights((0..batch.len()).map(|row| batch.weights_at(row)).collect());
355        Ok(cache)
356    }
357}
358
359/// A cached event batch and its associated weights.
360#[derive(Clone, Debug)]
361pub struct CpuCachedBatch {
362    pub(super) cache: CpuBatchCache,
363}
364
365impl CpuCachedBatch {
366    pub(crate) fn from_cache(cache: CpuBatchCache) -> Self {
367        Self { cache }
368    }
369
370    /// Returns the underlying materialized cache.
371    pub fn cache(&self) -> &CpuBatchCache {
372        &self.cache
373    }
374
375    /// Returns the number of events.
376    pub fn len(&self) -> usize {
377        self.cache.len()
378    }
379
380    /// Returns whether the batch contains no events.
381    pub fn is_empty(&self) -> bool {
382        self.cache.is_empty()
383    }
384
385    /// Returns per-event weights.
386    pub fn weights(&self) -> &[f64] {
387        self.cache.weights()
388    }
389
390    /// Returns the sum of event weights.
391    pub fn sum_weights(&self) -> f64 {
392        self.cache.sum_weights()
393    }
394
395    /// Estimates retained heap memory, in bytes.
396    pub fn resident_bytes(&self) -> usize {
397        self.cache.resident_bytes()
398    }
399}
400
401/// A dataset whose event-dependent model values are fully cached in memory.
402#[derive(Clone, Debug, Default)]
403pub struct CpuCachedDataset {
404    pub(super) batches: Vec<CpuCachedBatch>,
405    pub(super) sum_weights: f64,
406}
407
408impl PreparedDatasetStats {
409    pub(crate) fn new(
410        local_events: usize,
411        global_events: usize,
412        local_batches: usize,
413        sum_weights: f64,
414        resident_bytes: usize,
415        storage: CacheStorage,
416    ) -> Self {
417        Self {
418            local_events,
419            global_events,
420            local_batches,
421            sum_weights,
422            resident_bytes,
423            storage,
424        }
425    }
426
427    /// Returns the number of events assigned to this rank.
428    pub fn local_events(&self) -> usize {
429        self.local_events
430    }
431
432    /// Returns the total number of events across all ranks.
433    pub fn global_events(&self) -> usize {
434        self.global_events
435    }
436
437    /// Returns the number of batches assigned to this rank.
438    pub fn local_batches(&self) -> usize {
439        self.local_batches
440    }
441
442    /// Returns the total event-weight sum across all ranks.
443    pub fn sum_weights(&self) -> f64 {
444        self.sum_weights
445    }
446
447    /// Returns the number of bytes retained for prepared data on this rank.
448    pub fn resident_bytes(&self) -> usize {
449        self.resident_bytes
450    }
451
452    /// Returns the dataset's cache-storage policy.
453    pub fn storage(&self) -> CacheStorage {
454        self.storage
455    }
456}
457
458#[derive(Clone)]
459/// A dataset prepared according to its [`CacheStorage`] policy.
460///
461/// Resident datasets own all event-dependent cache values. Streaming datasets retain the source
462/// and read plan and rebuild transient batch caches on every reduction.
463pub enum CpuPreparedDataset {
464    /// A dataset whose event caches are resident in memory.
465    Resident {
466        /// Fully cached dataset.
467        dataset: Arc<CpuCachedDataset>,
468        /// Preparation statistics.
469        stats: PreparedDatasetStats,
470        /// Persistent host-memory reservation shared by clones.
471        memory_lease: MemoryLease,
472    },
473    /// A dataset whose event caches are rebuilt while streaming.
474    Streaming {
475        /// Source dataset.
476        dataset: Dataset,
477        /// Read plan used for each pass.
478        read_plan: ReadPlan,
479        /// Preparation statistics.
480        stats: PreparedDatasetStats,
481        /// Peak transient bytes reserved during each reduction.
482        transient_bytes: u64,
483    },
484}
485
486impl std::fmt::Debug for CpuPreparedDataset {
487    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        formatter
489            .debug_struct("CpuPreparedDataset")
490            .field("stats", self.stats())
491            .finish_non_exhaustive()
492    }
493}
494
495impl CpuPreparedDataset {
496    /// Returns statistics collected while preparing the dataset.
497    pub fn stats(&self) -> &PreparedDatasetStats {
498        match self {
499            Self::Resident { stats, .. } | Self::Streaming { stats, .. } => stats,
500        }
501    }
502}
503
504impl CpuCachedDataset {
505    pub(crate) fn from_parts(batches: Vec<CpuCachedBatch>, sum_weights: f64) -> Self {
506        Self {
507            batches,
508            sum_weights,
509        }
510    }
511
512    /// Returns the cached batches.
513    pub fn batches(&self) -> &[CpuCachedBatch] {
514        &self.batches
515    }
516
517    /// Returns the total number of cached events.
518    pub fn len(&self) -> usize {
519        self.batches.iter().map(CpuCachedBatch::len).sum()
520    }
521
522    /// Returns whether the dataset contains no events.
523    pub fn is_empty(&self) -> bool {
524        self.batches.iter().all(CpuCachedBatch::is_empty)
525    }
526
527    /// Returns the sum of all event weights.
528    pub fn sum_weights(&self) -> f64 {
529        self.sum_weights
530    }
531
532    /// Estimates retained heap memory, in bytes.
533    pub fn resident_bytes(&self) -> usize {
534        self.batches
535            .iter()
536            .map(CpuCachedBatch::resident_bytes)
537            .sum()
538    }
539}
540
541#[derive(Clone, Debug)]
542pub(super) struct CachedFactorSlot {
543    dimension: usize,
544    factors: Vec<DynamicLu>,
545}
546
547#[derive(Clone, Debug)]
548pub(crate) struct CachedSolveRowSlot {
549    #[cfg_attr(not(feature = "jit"), allow(dead_code))]
550    pub(crate) dimension: usize,
551    pub(crate) values: FlatRows<Complex64>,
552}
553
554impl CachedSolveRowSlot {
555    fn new(dimension: usize, events: usize) -> RuntimeResult<Self> {
556        Ok(Self {
557            dimension,
558            values: FlatRows::try_with_capacity(dimension, events)?,
559        })
560    }
561
562    fn push(&mut self, values: impl IntoIterator<Item = Complex64>) -> RuntimeResult<()> {
563        self.values.push_row(values)
564    }
565
566    fn row(&self, row: usize) -> RuntimeResult<&[Complex64]> {
567        self.values.row(row)
568    }
569
570    fn resident_bytes(&self) -> usize {
571        self.values.capacity() * size_of::<Complex64>()
572    }
573}
574
575impl CachedFactorSlot {
576    fn new(dimension: usize) -> Self {
577        Self {
578            dimension,
579            factors: Vec::new(),
580        }
581    }
582
583    fn push(&mut self, factor: DynamicLu) -> RuntimeResult<()> {
584        self.factors.push(factor);
585        Ok(())
586    }
587
588    fn factor(&self, row: usize) -> RuntimeResult<&DynamicLu> {
589        self.factors
590            .get(row)
591            .ok_or_else(|| RuntimeError::InvalidShape {
592                index: row,
593                message: format!(
594                    "factor row {row} out of bounds for len {}",
595                    self.factors.len()
596                ),
597            })
598    }
599
600    fn resident_bytes(&self) -> usize {
601        self.factors.capacity()
602            * (self.dimension * self.dimension * size_of::<Complex64>()
603                + self.dimension * size_of::<usize>())
604    }
605}
606
607#[derive(Clone, Debug, PartialEq)]
608pub(crate) enum CachedSlot {
609    Real(Vec<f64>),
610    Complex(Vec<Complex64>),
611    Vector {
612        len: usize,
613        values: FlatRows<Complex64>,
614    },
615    Matrix {
616        rows: usize,
617        cols: usize,
618        values: FlatRows<Complex64>,
619    },
620}
621
622impl CachedSlot {
623    #[cfg(feature = "jit")]
624    pub(crate) fn values_ptr(&self) -> *const u8 {
625        match self {
626            Self::Real(values) => values.as_ptr().cast(),
627            Self::Complex(values) => values.as_ptr().cast(),
628            Self::Vector { values, .. } | Self::Matrix { values, .. } => values.as_ptr().cast(),
629        }
630    }
631
632    #[cfg(feature = "jit")]
633    pub(crate) fn width(&self) -> usize {
634        match self {
635            Self::Real(_) | Self::Complex(_) => 1,
636            Self::Vector { values, .. } | Self::Matrix { values, .. } => values.width(),
637        }
638    }
639
640    fn new(kind: ValueKind, events: usize) -> RuntimeResult<Self> {
641        Ok(match kind {
642            ValueKind::Real => Self::Real(Vec::with_capacity(events)),
643            ValueKind::Complex => Self::Complex(Vec::with_capacity(events)),
644            ValueKind::Vector { len } => Self::Vector {
645                len,
646                values: FlatRows::try_with_capacity(len, events)?,
647            },
648            ValueKind::Matrix { rows, cols } => Self::Matrix {
649                rows,
650                cols,
651                values: FlatRows::try_with_capacity(
652                    rows.checked_mul(cols)
653                        .ok_or_else(|| RuntimeError::InvalidShape {
654                            index: rows,
655                            message: format!("matrix width overflowed for {rows}x{cols}"),
656                        })?,
657                    events,
658                )?,
659            },
660        })
661    }
662
663    pub(crate) fn resident_bytes(&self) -> usize {
664        match self {
665            Self::Real(values) => values.capacity() * size_of::<f64>(),
666            Self::Complex(values) => values.capacity() * size_of::<Complex64>(),
667            Self::Vector { values, .. } | Self::Matrix { values, .. } => {
668                values.capacity() * size_of::<Complex64>()
669            }
670        }
671    }
672
673    fn push(&mut self, value: Value) -> RuntimeResult<()> {
674        match (self, value) {
675            (Self::Real(values), Value::Scalar(value)) => {
676                values.push(value.re);
677                Ok(())
678            }
679            (Self::Complex(values), Value::Scalar(value)) => {
680                values.push(value);
681                Ok(())
682            }
683            (Self::Vector { len, values }, Value::Vector(value)) if *len == value.len() => {
684                values.push_row(value)
685            }
686            (
687                Self::Matrix { rows, cols, values },
688                Value::Matrix {
689                    rows: value_rows,
690                    cols: value_cols,
691                    values: value,
692                },
693            ) if *rows == value_rows && *cols == value_cols => values.push_row(value),
694            (_, value) => Err(RuntimeError::InvalidShape {
695                index: 0,
696                message: format!("cached value kind did not match slot: {}", value.kind()),
697            }),
698        }
699    }
700
701    pub(super) fn value(&self, row: usize) -> RuntimeResult<Value> {
702        match self {
703            Self::Real(values) => values
704                .get(row)
705                .copied()
706                .map(Complex64::from)
707                .map(Value::Scalar)
708                .ok_or_else(|| RuntimeError::InvalidShape {
709                    index: row,
710                    message: format!("cache row {row} out of bounds"),
711                }),
712            Self::Complex(values) => values.get(row).copied().map(Value::Scalar).ok_or_else(|| {
713                RuntimeError::InvalidShape {
714                    index: row,
715                    message: format!("cache row {row} out of bounds"),
716                }
717            }),
718            Self::Vector { values, .. } => {
719                values.row(row).map(|value| Value::Vector(value.to_vec()))
720            }
721            Self::Matrix { rows, cols, values } => values.row(row).map(|value| Value::Matrix {
722                rows: *rows,
723                cols: *cols,
724                values: value.to_vec(),
725            }),
726        }
727    }
728
729    fn scalar(&self, row: usize) -> RuntimeResult<Complex64> {
730        match self {
731            Self::Real(values) => values
732                .get(row)
733                .copied()
734                .map(Complex64::from)
735                .ok_or_else(|| RuntimeError::InvalidShape {
736                    index: row,
737                    message: format!("cache row {row} out of bounds"),
738                }),
739            Self::Complex(values) => {
740                values
741                    .get(row)
742                    .copied()
743                    .ok_or_else(|| RuntimeError::InvalidShape {
744                        index: row,
745                        message: format!("cache row {row} out of bounds"),
746                    })
747            }
748            Self::Vector { .. } | Self::Matrix { .. } => Err(RuntimeError::TypeMismatch {
749                index: row,
750                expected: "scalar",
751                actual: match self {
752                    Self::Vector { .. } => "vector",
753                    Self::Matrix { .. } => "matrix",
754                    Self::Real(_) | Self::Complex(_) => unreachable!(),
755                },
756            }),
757        }
758    }
759
760    fn real_range(&self, start: usize, end: usize) -> RuntimeResult<&[f64]> {
761        match self {
762            Self::Real(values) => {
763                values
764                    .get(start..end)
765                    .ok_or_else(|| RuntimeError::InvalidShape {
766                        index: start,
767                        message: format!("cache range {start}..{end} out of bounds"),
768                    })
769            }
770            Self::Complex(_) | Self::Vector { .. } | Self::Matrix { .. } => {
771                Err(RuntimeError::TypeMismatch {
772                    index: start,
773                    expected: "real scalar",
774                    actual: match self {
775                        Self::Complex(_) => "complex scalar",
776                        Self::Vector { .. } => "vector",
777                        Self::Matrix { .. } => "matrix",
778                        Self::Real(_) => unreachable!(),
779                    },
780                })
781            }
782        }
783    }
784
785    fn complex_range(&self, start: usize, end: usize) -> RuntimeResult<&[Complex64]> {
786        match self {
787            Self::Complex(values) => {
788                values
789                    .get(start..end)
790                    .ok_or_else(|| RuntimeError::InvalidShape {
791                        index: start,
792                        message: format!("cache range {start}..{end} out of bounds"),
793                    })
794            }
795            Self::Real(_) | Self::Vector { .. } | Self::Matrix { .. } => {
796                Err(RuntimeError::TypeMismatch {
797                    index: start,
798                    expected: "complex scalar",
799                    actual: match self {
800                        Self::Real(_) => "real scalar",
801                        Self::Vector { .. } => "vector",
802                        Self::Matrix { .. } => "matrix",
803                        Self::Complex(_) => unreachable!(),
804                    },
805                })
806            }
807        }
808    }
809}