Skip to main content

earthsci_ast/simulate_array/range/
mod.rs

1//! Range-restricted and operator-restricted right-hand sides (API_SPEC §5.8,
2//! RFC U3): [`RhsProgram`] and the programs it restricts to.
3//!
4//! * [`RhsProgram::select`] keeps the summands of each `D(x)` whose provenance
5//!   (esm-libraries-spec §4.7.5) is selected and writes only the selected
6//!   variables; the states it reads but does not write become a read-only
7//!   `aux` input. It is a rewrite of the flattened system, recompiled.
8//! * [`RhsProgram::halo`] reports how far each owned cell reaches into each
9//!   state along each decomposed or face index set.
10//! * [`RhsProgram::tile`] restricts a `native` program to an owned box that
11//!   reads a storage box (RFC U3 §4): a re-lowering of the tape over the need
12//!   boxes, with global index space, so boundary regions apply exactly where
13//!   the global domain has a boundary.
14//! * [`RhsProgram::lanes`] evaluates a pointwise or column operator over a run-time
15//!   list of cells.
16//!
17//! The contract is bitwise: a tile's owned rows, and every lane, equal the
18//! same program evaluated over the whole domain by the same compiler.
19
20mod analysis;
21mod forcing;
22mod lanes;
23mod model;
24mod select;
25mod tile;
26mod walk;
27
28pub use forcing::{ForcingSet, ForcingSlot, ParamSlot, RhsError, SlotKind, StateBlock};
29pub use lanes::{LaneInputs, LaneProgram};
30use model::ProgramModel;
31pub use tile::TileProgram;
32
33use super::tape::{CallInputs, ForcingView, TapeCtx, TapeProgram, run_tape_inputs};
34use super::{ArrayCompiled, RhsCall, RhsScratch, RhsStats, evaluate_rhs_with_scratch};
35use crate::flatten::{FlattenedSystem, TermOrigin};
36use crate::problem::{Compiler, EsmProblem, ProgramSource};
37use crate::simulate::SimulateError;
38use ndarray::{ArrayD, IxDyn, ShapeBuilder};
39use std::cell::{Cell, RefCell};
40use std::collections::{BTreeMap, HashMap, HashSet};
41use std::marker::PhantomData;
42use std::ops::RangeInclusive;
43use std::sync::Arc;
44use std::sync::atomic::{AtomicU64, Ordering};
45use walk::{AxisSpec, VarInfo};
46
47/// Which terms and which variables an operator keeps (API_SPEC §5.8,
48/// `select`). `None` keeps every term, or writes every state.
49#[derive(Debug, Clone, Default, PartialEq)]
50pub struct RhsSelection {
51    /// The variables the operator writes, `W_k`, by flattened name.
52    pub writes: Option<Vec<String>>,
53    /// The provenance tags whose summands are kept, matched by equality
54    /// against each summand's origins (esm-libraries-spec §4.7.5).
55    pub terms: Option<Vec<TermOrigin>>,
56}
57
58/// One decomposed horizontal index set: its name and its period, when the
59/// axis is periodic.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct AxisCut {
62    pub index_set: String,
63    pub period: Option<i64>,
64}
65
66/// The horizontal decomposition a tile cuts (RFC U3 §1): the decomposed index
67/// sets and the face sets that stagger off them, `(face set, cell set)`.
68/// Every other index set is whole on every tile.
69#[derive(Debug, Clone, Default, PartialEq, Eq)]
70pub struct Decomposition {
71    pub axes: Vec<AxisCut>,
72    pub faces: Vec<(String, String)>,
73}
74
75/// One tile's owned box and storage box, one inclusive range per decomposed
76/// index set, in global 1-based indices; a periodic storage range is
77/// unwrapped (it may run below 1 or past `N`).
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct TileBox {
80    pub owned: Vec<(String, RangeInclusive<i64>)>,
81    pub storage: Vec<(String, RangeInclusive<i64>)>,
82}
83
84/// The locality a lane program evaluates over (RFC U3 §6), with the index
85/// sets one lane's `cells` column names, in order.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum LaneKind {
88    /// One lane per cell: `sets` are every spatial index set of the output.
89    Cells { sets: Vec<String> },
90    /// One lane per column: `sets` are the horizontal index sets; every other
91    /// axis is whole in each lane.
92    Columns { sets: Vec<String> },
93}
94
95impl LaneKind {
96    /// The index sets a lane's `cells` column names.
97    pub fn sets(&self) -> &[String] {
98        match self {
99            LaneKind::Cells { sets } | LaneKind::Columns { sets } => sets,
100        }
101    }
102}
103
104/// [`RhsProgram::halo`]'s report (RFC U3 §3): for each state the program
105/// reads and each decomposed or face index set on its shape, keyed
106/// `(variable, index set)`, how far the owned output reaches into it: `lo`
107/// cells below the owned box, `hi` above it.
108#[derive(Debug, Clone, Default, PartialEq, Eq)]
109pub struct HaloReport {
110    pub reach: BTreeMap<(String, String), (i64, i64)>,
111}
112
113impl HaloReport {
114    /// The `(lo, hi)` reach into `variable` along `index_set`, if reported.
115    pub fn get(&self, variable: &str, index_set: &str) -> Option<(i64, i64)> {
116        self.reach
117            .get(&(variable.to_string(), index_set.to_string()))
118            .copied()
119    }
120}
121
122/// Which input a variable's block lives in.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub(crate) enum TileInput {
125    /// `u`: the variables the program writes.
126    U,
127    /// `aux`: the states it reads but does not write.
128    Aux,
129}
130
131/// One variable's block in a program's inputs (RFC U3 §5): a column-major
132/// block over its storage box, first index fastest.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub(crate) struct Block {
135    pub(crate) name: String,
136    pub(crate) input: TileInput,
137    /// Offset of the block in its input.
138    pub(crate) base: usize,
139    /// The index-set name of each axis (empty when the declaration names
140    /// none).
141    pub(crate) index_sets: Vec<String>,
142    /// Inclusive storage range per axis, in global indices.
143    pub(crate) storage: Vec<(i64, i64)>,
144    /// Column-major strides per axis.
145    pub(crate) strides: Vec<usize>,
146    /// Inclusive owned range per axis: the rows a call writes (for a `u`
147    /// variable).
148    pub(crate) owned: Vec<(i64, i64)>,
149}
150
151/// A program's input layout as the program addresses it: `u` blocks, then
152/// `aux` blocks, each in flat-layout order.
153#[derive(Debug, Clone, Default, PartialEq, Eq)]
154pub(crate) struct Blocks {
155    pub(crate) vars: Vec<Block>,
156    pub(crate) u_len: usize,
157    pub(crate) aux_len: usize,
158}
159
160/// One variable's block in a tile's (or a whole-domain program's) inputs
161/// (API_SPEC §5.8, "Tile layout"): a column-major block over its storage box,
162/// first index fastest.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct TileBlock {
165    pub name: String,
166    /// The index-set name of each axis, in shape order (empty when the
167    /// declaration names none).
168    pub sets: Vec<String>,
169    /// Inclusive storage range per axis, in global indices.
170    pub storage: Vec<(i64, i64)>,
171    /// Inclusive owned range per axis: the rows a call writes. Empty for an
172    /// `aux` block.
173    pub owned: Vec<(i64, i64)>,
174    /// 0-based offset of the block in its buffer.
175    pub base: usize,
176    /// Column-major strides per axis.
177    pub strides: Vec<usize>,
178}
179
180impl TileBlock {
181    /// The buffer slot of the global multi-index `g`, or `None` outside the
182    /// storage box.
183    pub fn slot(&self, g: &[i64]) -> Option<usize> {
184        let mut off = self.base;
185        for (d, &x) in g.iter().enumerate() {
186            let (lo, hi) = *self.storage.get(d)?;
187            if x < lo || x > hi {
188                return None;
189            }
190            off += (x - lo) as usize * self.strides[d];
191        }
192        Some(off)
193    }
194
195    /// The number of elements in the block.
196    pub fn len(&self) -> usize {
197        self.storage
198            .iter()
199            .map(|(lo, hi)| (hi - lo + 1).max(0) as usize)
200            .product()
201    }
202
203    /// Whether the block holds no element.
204    pub fn is_empty(&self) -> bool {
205        self.len() == 0
206    }
207}
208
209/// A program's `u` and `aux` layout (API_SPEC §5.8): the written variables'
210/// blocks, then the read-only ones, each list in flat-layout order.
211#[derive(Debug, Clone, Default, PartialEq, Eq)]
212pub struct TileLayout {
213    pub u: Vec<TileBlock>,
214    pub aux: Vec<TileBlock>,
215    pub u_len: usize,
216    pub aux_len: usize,
217}
218
219impl TileLayout {
220    /// The block of `name`, in `u` or `aux`.
221    pub fn block(&self, name: &str) -> Option<&TileBlock> {
222        self.u.iter().chain(&self.aux).find(|b| b.name == name)
223    }
224}
225
226/// One variable's block in a lane program's inputs (API_SPEC §5.8, "Lanes"):
227/// its index sets outside the lane sets, their extents, and its offset in
228/// units of lanes. Element `e` (1-based, column-major over `sets`) of lane `l`
229/// is at slot `(offset + e - 1)·L + l` (`l` 0-based in Rust).
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct LaneBlock {
232    pub name: String,
233    pub sets: Vec<String>,
234    pub extents: Vec<usize>,
235    pub offset: usize,
236}
237
238/// A lane program's `u` and `aux` layout, with the lane sets a column of
239/// `cells` indexes and the capacity `L`.
240#[derive(Debug, Clone, Default, PartialEq, Eq)]
241pub struct LaneLayout {
242    pub u: Vec<LaneBlock>,
243    pub aux: Vec<LaneBlock>,
244    pub sets: Vec<String>,
245    pub capacity: usize,
246    pub u_len: usize,
247    pub aux_len: usize,
248}
249
250impl LaneLayout {
251    /// The block of `name`, in `u` or `aux`.
252    pub fn block(&self, name: &str) -> Option<&LaneBlock> {
253        self.u.iter().chain(&self.aux).find(|b| b.name == name)
254    }
255}
256
257impl Blocks {
258    /// The block of `name`.
259    pub(crate) fn var(&self, name: &str) -> Option<&Block> {
260        self.vars.iter().find(|v| v.name == name)
261    }
262
263    /// The public record of this layout.
264    pub(crate) fn public(&self) -> TileLayout {
265        let block = |v: &Block| TileBlock {
266            name: v.name.clone(),
267            sets: v.index_sets.clone(),
268            storage: v.storage.clone(),
269            owned: if v.input == TileInput::U {
270                v.owned.clone()
271            } else {
272                Vec::new()
273            },
274            base: v.base,
275            strides: v.strides.clone(),
276        };
277        TileLayout {
278            u: self
279                .vars
280                .iter()
281                .filter(|v| v.input == TileInput::U)
282                .map(block)
283                .collect(),
284            aux: self
285                .vars
286                .iter()
287                .filter(|v| v.input == TileInput::Aux)
288                .map(block)
289                .collect(),
290            u_len: self.u_len,
291            aux_len: self.aux_len,
292        }
293    }
294
295    /// The public record of a lane layout: each block's first axis is the
296    /// lane axis of `capacity` lanes.
297    pub(crate) fn lane_public(&self, sets: &[String], capacity: usize) -> LaneLayout {
298        let block = |v: &Block| LaneBlock {
299            name: v.name.clone(),
300            sets: v.index_sets[1..].to_vec(),
301            extents: v.storage[1..]
302                .iter()
303                .map(|(lo, hi)| (hi - lo + 1).max(0) as usize)
304                .collect(),
305            offset: v.base / capacity.max(1),
306        };
307        LaneLayout {
308            u: self
309                .vars
310                .iter()
311                .filter(|v| v.input == TileInput::U)
312                .map(block)
313                .collect(),
314            aux: self
315                .vars
316                .iter()
317                .filter(|v| v.input == TileInput::Aux)
318                .map(block)
319                .collect(),
320            sets: sets.to_vec(),
321            capacity,
322            u_len: self.u_len,
323            aux_len: self.aux_len,
324        }
325    }
326
327    /// Build a layout over the given per-variable boxes, `u` variables first.
328    fn build(
329        entries: Vec<(
330            String,
331            TileInput,
332            Vec<String>,
333            Vec<(i64, i64)>,
334            Vec<(i64, i64)>,
335        )>,
336    ) -> Blocks {
337        let mut vars = Vec::with_capacity(entries.len());
338        let mut u_len = 0usize;
339        let mut aux_len = 0usize;
340        for (name, input, index_sets, storage, owned) in entries {
341            let mut strides = Vec::with_capacity(storage.len());
342            let mut acc = 1usize;
343            for &(lo, hi) in &storage {
344                strides.push(acc);
345                acc *= (hi - lo + 1).max(0) as usize;
346            }
347            let len = if storage.is_empty() { 1 } else { acc };
348            let base = match input {
349                TileInput::U => {
350                    u_len += len;
351                    u_len - len
352                }
353                TileInput::Aux => {
354                    aux_len += len;
355                    aux_len - len
356                }
357            };
358            vars.push(Block {
359                name,
360                input,
361                base,
362                index_sets,
363                storage,
364                strides,
365                owned,
366            });
367        }
368        Blocks {
369            vars,
370            u_len,
371            aux_len,
372        }
373    }
374}
375
376/// The per-call buffers of one program (API_SPEC §5.8): reused across calls,
377/// so a warm call allocates nothing. Made by the program that runs it, and
378/// usable only with that program (or a clone of it), one call at a time. It
379/// may move between threads (`Send`); it is not `Sync`.
380pub struct RhsWorkspace {
381    owner: u64,
382    state: Vec<f64>,
383    dy: Vec<f64>,
384    exec: WsExec,
385    _not_sync: PhantomData<Cell<()>>,
386}
387
388enum WsExec {
389    Tape(Box<TapeCtx>),
390    Interp(Box<InterpWs>),
391    Lanes {
392        main: Box<TapeCtx>,
393        pre: Box<TapeCtx>,
394    },
395}
396
397/// The interpreter's per-call state: its scratch, and the forcing buffer it
398/// reads by name, rebuilt from a forcing set only when the set's epoch moves.
399struct InterpWs {
400    scratch: RhsScratch,
401    forcing: RefCell<HashMap<String, ArrayD<f64>>>,
402    epoch: u64,
403}
404
405impl std::fmt::Debug for RhsWorkspace {
406    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        f.debug_struct("RhsWorkspace")
408            .field("state_len", &self.state.len())
409            .finish()
410    }
411}
412
413/// A process-unique identity for a program, carried by every clone of it
414/// and recorded by the workspaces it makes.
415fn next_program_id() -> u64 {
416    static NEXT: AtomicU64 = AtomicU64::new(1);
417    NEXT.fetch_add(1, Ordering::Relaxed)
418}
419
420fn range_error(code: &'static str, message: String) -> SimulateError {
421    SimulateError::RangeRestriction { code, message }
422}
423
424fn build_error(message: String) -> SimulateError {
425    SimulateError::Compile(crate::compile_error::CompileError::build_err(message))
426}
427
428/// `compiler_unavailable` for `call` on a program not built with `native`.
429fn native_only(compiler: Compiler, call: &str) -> SimulateError {
430    SimulateError::CompilerUnavailable {
431        compiler: compiler.as_str(),
432        details: format!(
433            "`{call}` restricts the native tape, and this program was built with `{}`; build it \
434             with `compiler = native`",
435            compiler.as_str()
436        ),
437    }
438}
439
440/// A compiled right-hand side, possibly restricted to one operator's terms
441/// and writes (API_SPEC §5.8). Made by [`rhs_program`].
442///
443/// Built once and immutable: `Send + Sync`, and cheap to clone (every clone
444/// shares one program). Evaluate it concurrently with one [`RhsWorkspace`]
445/// per thread; a call never starts a thread of its own.
446#[derive(Clone)]
447pub struct RhsProgram {
448    inner: Arc<ProgramInner>,
449}
450
451struct ProgramInner {
452    id: u64,
453    /// The flattened system the program was compiled from (`None` for the
454    /// solve's own program, which is never restricted).
455    flat: Option<Arc<FlattenedSystem>>,
456    model: Arc<ProgramModel>,
457    compiler: Compiler,
458    runtime_array_parameters: bool,
459    /// The native tape, built at construction (`None` under `interpreter`).
460    tape: Option<Arc<TapeProgram>>,
461    /// The forcing slot each of the tape's forcing loads reads.
462    slot_of: Vec<u32>,
463    writes: Vec<String>,
464    aux: Vec<String>,
465    blocks: Blocks,
466    layout: TileLayout,
467    state_layout: Vec<StateBlock>,
468    params: Vec<ParamSlot>,
469    slots: Arc<[ForcingSlot]>,
470    /// The Problem's forcing when the program was built.
471    default_set: ForcingSet,
472    /// Per variable of the compiled state vector, in its order: the input it
473    /// is read from and the block offsets `(input base, state offset, len)`.
474    scatter: Vec<(TileInput, usize, usize, usize)>,
475    /// Whether `u` is the compiled state vector itself (every state written,
476    /// each at its own offset), so a call reads `u` and writes `du` in place.
477    identity: bool,
478    declared: Arc<HashSet<String>>,
479}
480
481impl ProgramInner {
482    fn flat(&self) -> &FlattenedSystem {
483        self.flat
484            .as_deref()
485            .expect("a program built by `rhs_program` keeps its flattened system")
486    }
487}
488
489impl std::fmt::Debug for RhsProgram {
490    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491        f.debug_struct("RhsProgram")
492            .field("compiler", &self.inner.compiler)
493            .field("writes", &self.inner.writes)
494            .field("aux", &self.inner.aux)
495            .finish()
496    }
497}
498
499/// The right-hand side `prob` compiled, as a program [`RhsProgram::select`],
500/// [`RhsProgram::halo`], [`RhsProgram::tile`] and [`RhsProgram::lanes`]
501/// accept (API_SPEC §5.8). Its names are the flattened (dot-namespaced)
502/// names on every route; it writes every state.
503///
504/// `native` and `interpreter` problems are accepted; any other compiler is
505/// `compiler_unavailable`. Under `native` a rule the tape cannot express is
506/// `compiler_refused_rule`.
507pub fn rhs_program(prob: &EsmProblem) -> Result<RhsProgram, SimulateError> {
508    let compiler = prob.compiler;
509    if !matches!(compiler, Compiler::Native | Compiler::Interpreter) {
510        return Err(SimulateError::CompilerUnavailable {
511            compiler: compiler.as_str(),
512            details: "`rhs_program` serves the `native` and `interpreter` compilers".to_string(),
513        });
514    }
515    let Some(source) = prob.program_source.as_ref() else {
516        return Err(build_error(
517            "`rhs_program`: this problem has no right-hand side to restrict".to_string(),
518        ));
519    };
520    let _precision = prob.precision.enter();
521    let flat = match &**source {
522        ProgramSource::Flattened(f) => (**f).clone(),
523        ProgramSource::File(file) => {
524            let annotated =
525                crate::precision_infer::annotated(file).map_err(SimulateError::Compile)?;
526            let file = annotated.as_ref().unwrap_or(file);
527            crate::flatten::flatten(file)
528                .map_err(|e| SimulateError::Compile(crate::compile_error::CompileError::from(e)))?
529        }
530    };
531    let forcing = match &*prob.backend {
532        crate::problem::Backend::Array(c) => problem_forcing(c, &flat),
533        crate::problem::Backend::Static(_) => HashMap::new(),
534    };
535    RhsProgram::compile(
536        Arc::new(flat),
537        compiler,
538        None,
539        ForcingSource::Problem(&forcing),
540        prob.runtime_array_parameters,
541    )
542}
543
544/// The Problem's forcing buffer, keyed by the flattened names: an entry whose
545/// name the flattened system does not declare is matched to the one
546/// flattened parameter it is the bare name of.
547fn problem_forcing(c: &ArrayCompiled, flat: &FlattenedSystem) -> HashMap<String, ArrayD<f64>> {
548    c.forcing
549        .borrow()
550        .iter()
551        .filter_map(|(k, v)| {
552            if flat.parameters.contains_key(k) {
553                return Some((k.clone(), v.clone()));
554            }
555            let suffix = format!(".{k}");
556            let mut m = flat.parameters.keys().filter(|n| n.ends_with(&suffix));
557            match (m.next(), m.next()) {
558                (Some(n), None) => Some((n.clone(), v.clone())),
559                _ => None,
560            }
561        })
562        .collect()
563}
564
565/// Where a program's forcing slots and their default buffers come from.
566enum ForcingSource<'a> {
567    /// The Problem's forcing buffer, by flattened name.
568    Problem(&'a HashMap<String, ArrayD<f64>>),
569    /// The program a selection is made from: the same slots and buffers.
570    Parent(&'a ProgramInner),
571}
572
573/// `names` in flat-layout order (API_SPEC §5.8): 0-d states by sorted name,
574/// then arrays by sorted name.
575fn flat_layout_order(names: &mut [String], model: &ProgramModel) {
576    names.sort_by(|a, b| {
577        let rank = |n: &String| !model.var_shapes[n].shape.is_empty();
578        (rank(a), a).cmp(&(rank(b), b))
579    });
580}
581
582/// A row-major array's elements in column-major order (first index fastest).
583fn column_major(a: &ArrayD<f64>) -> Arc<[f64]> {
584    a.view().reversed_axes().iter().copied().collect()
585}
586
587/// The forcing slots of `model`: every name its forcing buffer serves that
588/// has a shape, in byte order of the name, each with its default buffer (the
589/// Problem's data, else the declared default, else `NaN`: a slot nothing
590/// fills has no value until the caller's set gives it one).
591fn forcing_slots(
592    model: &ProgramModel,
593    problem: &HashMap<String, ArrayD<f64>>,
594    array_params: &HashSet<String>,
595) -> (Vec<ForcingSlot>, Vec<Arc<[f64]>>) {
596    let mut names: Vec<&String> = model.forcing_decls.keys().collect();
597    names.sort();
598    let mut slots = Vec::new();
599    let mut defaults = Vec::new();
600    for name in names {
601        let Some(shape) = model.forcing_decls[name].clone() else {
602            continue;
603        };
604        let len = shape.iter().product::<usize>();
605        let default = match problem.get(name) {
606            Some(a) => Some(column_major(a)),
607            None => model
608                .forcing_defaults
609                .get(name)
610                .cloned()
611                .flatten()
612                .and_then(|(s, v)| ArrayD::from_shape_vec(IxDyn(&s), v).ok())
613                .map(|a| column_major(&a)),
614        }
615        .filter(|b| b.len() == len)
616        .unwrap_or_else(|| vec![f64::NAN; len].into());
617        slots.push(ForcingSlot {
618            name: name.clone(),
619            shape,
620            kind: if array_params.contains(name) {
621                SlotKind::ArrayParameter
622            } else {
623                SlotKind::Forcing
624            },
625        });
626        defaults.push(default);
627    }
628    (slots, defaults)
629}
630
631/// The slot each forcing load of `tape` reads, refusing a load of a name no
632/// slot serves or at a shape the slot does not have.
633fn slot_map(tape: &TapeProgram, slots: &[ForcingSlot]) -> Result<Vec<u32>, SimulateError> {
634    tape.forcings
635        .iter()
636        .map(|fr| {
637            let k = slots
638                .iter()
639                .position(|s| s.name == fr.name)
640                .ok_or_else(|| {
641                    build_error(format!(
642                        "`rhs_program`: the program reads forcing `{}`, which no forcing slot serves",
643                        fr.name
644                    ))
645                })?;
646            if slots[k].shape[..] != fr.shape[..] {
647                return Err(build_error(format!(
648                    "`rhs_program`: the program reads forcing `{}` as {:?}, its slot is {:?}",
649                    fr.name, &fr.shape[..], slots[k].shape
650                )));
651            }
652            Ok(k as u32)
653        })
654        .collect()
655}
656
657impl RhsProgram {
658    /// Compile `flat` under `compiler`. `writes` is the selection's `W_k`
659    /// (`None`: every state).
660    fn compile(
661        flat: Arc<FlattenedSystem>,
662        compiler: Compiler,
663        writes: Option<&[String]>,
664        forcing: ForcingSource,
665        runtime_array_parameters: bool,
666    ) -> Result<RhsProgram, SimulateError> {
667        let mut compiled = ArrayCompiled::from_flattened_with(&flat, runtime_array_parameters)?;
668        compiled.runtime_mode = match compiler {
669            Compiler::Interpreter => super::RuntimeMode::Interpreter,
670            _ => super::RuntimeMode::Native,
671        };
672        // Every forcing read compiles against the shape the data has.
673        let shapes: Vec<(String, Vec<usize>)> = match &forcing {
674            ForcingSource::Problem(m) => m
675                .iter()
676                .map(|(k, a)| (k.clone(), a.shape().to_vec()))
677                .collect(),
678            ForcingSource::Parent(p) => p
679                .slots
680                .iter()
681                .map(|s| (s.name.clone(), s.shape.clone()))
682                .collect(),
683        };
684        for (name, shape) in shapes {
685            if let Some(decl) = compiled.forcing_decls.get_mut(&name) {
686                *decl = Some(shape);
687            }
688        }
689        let tape = match compiler {
690            Compiler::Native => {
691                let discrete: HashSet<String> = compiled.forcing_decls.keys().cloned().collect();
692                Some(compiled.strict_tape(&discrete)?)
693            }
694            _ => None,
695        };
696        let model = ProgramModel::from_compiled(compiled);
697        let array_params: HashSet<String> = if runtime_array_parameters {
698            model
699                .forcing_decls
700                .keys()
701                .filter(|n| {
702                    flat.parameters.get(*n).is_some_and(|v| {
703                        v.update.is_none() && v.shape.as_ref().is_some_and(|s| !s.is_empty())
704                    })
705                })
706                .cloned()
707                .collect()
708        } else {
709            HashSet::new()
710        };
711        let default_set = match forcing {
712            ForcingSource::Problem(m) => {
713                let (s, d) = forcing_slots(&model, m, &array_params);
714                ForcingSet::for_slots(s.into(), d).expect("each default holds its slot's shape")
715            }
716            ForcingSource::Parent(p) => p.default_set.clone(),
717        };
718        let slots = default_set.slots_arc();
719        let slot_of = match &tape {
720            Some(t) => slot_map(t, &slots)?,
721            None => Vec::new(),
722        };
723        let states: Vec<String> = model.var_shapes.keys().cloned().collect();
724        let mut writes: Vec<String> = match writes {
725            None => states.clone(),
726            Some(w) => states.iter().filter(|s| w.contains(s)).cloned().collect(),
727        };
728        let mut aux: Vec<String> = states
729            .iter()
730            .filter(|s| !writes.contains(s))
731            .cloned()
732            .collect();
733        flat_layout_order(&mut writes, &model);
734        flat_layout_order(&mut aux, &model);
735        let set_names = state_set_names(&flat, &model);
736        let mut entries = Vec::new();
737        for (input, names) in [(TileInput::U, &writes), (TileInput::Aux, &aux)] {
738            for name in names {
739                let vs = &model.var_shapes[name];
740                let bx: Vec<(i64, i64)> = vs
741                    .shape
742                    .iter()
743                    .zip(&vs.origin)
744                    .map(|(&n, &o)| (o, o + n as i64 - 1))
745                    .collect();
746                entries.push((name.clone(), input, set_names[name].clone(), bx.clone(), bx));
747            }
748        }
749        let blocks = Blocks::build(entries);
750        let layout = blocks.public();
751        let scatter: Vec<(TileInput, usize, usize, usize)> = model
752            .var_shapes
753            .iter()
754            .map(|(name, vs)| {
755                let v = blocks.var(name).expect("every state is in the layout");
756                let len = vs.shape.iter().product::<usize>().max(1);
757                (v.input, v.base, vs.flat_offset, len)
758            })
759            .collect();
760        let identity = scatter
761            .iter()
762            .all(|&(input, base, off, _)| input == TileInput::U && base == off);
763        let state_layout = layout
764            .u
765            .iter()
766            .map(|b| StateBlock {
767                name: b.name.clone(),
768                flat_offset: b.base,
769                shape: b
770                    .storage
771                    .iter()
772                    .map(|(lo, hi)| (hi - lo + 1).max(0) as usize)
773                    .collect(),
774            })
775            .collect();
776        let params = model
777            .param_names
778            .iter()
779            .zip(&model.param_defaults)
780            .map(|(name, default)| ParamSlot {
781                name: name.clone(),
782                default: *default,
783            })
784            .collect();
785        let declared = Arc::new(model.declared_names.clone());
786        Ok(RhsProgram {
787            inner: Arc::new(ProgramInner {
788                id: next_program_id(),
789                flat: Some(flat),
790                model: Arc::new(model),
791                compiler,
792                runtime_array_parameters,
793                tape,
794                slot_of,
795                writes,
796                aux,
797                blocks,
798                layout,
799                state_layout,
800                params,
801                slots,
802                default_set,
803                scatter,
804                identity,
805                declared,
806            }),
807        })
808    }
809
810    pub(super) fn slots_arc(&self) -> Arc<[ForcingSlot]> {
811        Arc::clone(&self.inner.slots)
812    }
813
814    /// The compiler this program was built with.
815    pub fn compiler(&self) -> Compiler {
816        self.inner.compiler
817    }
818
819    /// The variables the program writes (`u`), in flat-layout order.
820    pub fn writes(&self) -> &[String] {
821        &self.inner.writes
822    }
823
824    /// The states the program reads but does not write (`aux`), in
825    /// flat-layout order.
826    pub fn aux(&self) -> &[String] {
827        &self.inner.aux
828    }
829
830    /// The whole-domain layout of `u` and `aux`.
831    pub fn layout(&self) -> &TileLayout {
832        &self.inner.layout
833    }
834
835    /// The length of `u` (and of `du`).
836    pub fn n_states(&self) -> usize {
837        self.inner.blocks.u_len
838    }
839
840    /// `u`'s blocks in flat order, each column-major (first index fastest):
841    /// the `u` half of [`Self::layout`].
842    pub fn state_layout(&self) -> &[StateBlock] {
843        &self.inner.state_layout
844    }
845
846    /// The parameters `p` holds, in its order: the order the program's tape
847    /// reads them in (sorted flattened names).
848    pub fn parameters(&self) -> &[ParamSlot] {
849        &self.inner.params
850    }
851
852    /// The parameter vector with `overrides` applied over the declared
853    /// defaults. Strict: a key naming no parameter, or a parameter with
854    /// neither an override nor a default, is an error naming it.
855    pub fn parameter_vector(
856        &self,
857        overrides: &HashMap<String, f64>,
858    ) -> Result<Vec<f64>, SimulateError> {
859        self.inner.model.parameter_vector(overrides)
860    }
861
862    /// The buffers a [`ForcingSet`] for this program holds, in its order.
863    pub fn forcing_slots(&self) -> &[ForcingSlot] {
864        &self.inner.slots
865    }
866
867    /// The Problem's forcing when the program was built: provider data, a
868    /// caller's array, or the declared default, and for an array-parameter
869    /// slot the parameter's value. Every call returns the same set (the same
870    /// buffers and epoch). A slot none of these fills holds `NaN`.
871    pub fn default_forcing(&self) -> ForcingSet {
872        self.inner.default_set.clone()
873    }
874
875    /// The declared default of every state cell, as `(u, aux)` in this
876    /// program's whole-domain layout (`NaN` where none is declared).
877    pub fn default_state(&self) -> (Vec<f64>, Vec<f64>) {
878        let inner = &*self.inner;
879        let mut u = vec![f64::NAN; inner.blocks.u_len];
880        let mut aux = vec![f64::NAN; inner.blocks.aux_len];
881        for ((name, vs), default) in inner
882            .model
883            .var_shapes
884            .iter()
885            .zip(&inner.model.state_defaults)
886        {
887            let v = inner
888                .blocks
889                .var(name)
890                .expect("every state is in the layout");
891            let dst = match v.input {
892                TileInput::U => &mut u,
893                TileInput::Aux => &mut aux,
894            };
895            let len = vs.shape.iter().product::<usize>().max(1);
896            match default {
897                super::StateDefault::Scalar(Some(x)) => dst[v.base..v.base + len].fill(*x),
898                super::StateDefault::Scalar(None) => {}
899                super::StateDefault::Field(rm) => {
900                    // Row-major data into the column-major block.
901                    let shape = &vs.shape;
902                    for (k, &x) in rm.iter().enumerate().take(len) {
903                        let mut rem = k;
904                        let mut off = 0usize;
905                        let mut stride_cm = vec![1usize; shape.len()];
906                        for d in 1..shape.len() {
907                            stride_cm[d] = stride_cm[d - 1] * shape[d - 1];
908                        }
909                        for d in (0..shape.len()).rev() {
910                            let i = rem % shape[d];
911                            rem /= shape[d];
912                            off += i * stride_cm[d];
913                        }
914                        dst[v.base + off] = x;
915                    }
916                }
917            }
918        }
919        (u, aux)
920    }
921
922    /// The flattened system this program was compiled from.
923    pub fn flattened(&self) -> &FlattenedSystem {
924        self.inner.flat()
925    }
926
927    /// The program for one operator: the summands of each `D(x)` whose
928    /// provenance is in `sel.terms`, writing only `sel.writes` (API_SPEC
929    /// §5.8). `range_selection_writes_incomplete` when `writes` omits a
930    /// variable a kept summand writes. It takes the same forcing sets.
931    pub fn select(&self, sel: &RhsSelection) -> Result<RhsProgram, SimulateError> {
932        let (flat, writes) = select::select(self.inner.flat(), sel)?;
933        let _precision = self.inner.model.precision.enter();
934        RhsProgram::compile(
935            Arc::new(flat),
936            self.inner.compiler,
937            Some(&writes),
938            ForcingSource::Parent(&self.inner),
939            self.inner.runtime_array_parameters,
940        )
941    }
942
943    /// How far the owned output reaches into each state along each decomposed
944    /// or face index set of `dec` (RFC U3 §3). `native` only.
945    pub fn halo(&self, dec: &Decomposition) -> Result<HaloReport, SimulateError> {
946        if self.inner.compiler != Compiler::Native {
947            return Err(native_only(self.inner.compiler, "halo"));
948        }
949        analysis::halo(self, dec)
950    }
951
952    /// This program restricted to one tile of `dec` (RFC U3 §4). `native` only.
953    pub fn tile(&self, dec: &Decomposition, bx: &TileBox) -> Result<TileProgram, SimulateError> {
954        if self.inner.compiler != Compiler::Native {
955            return Err(native_only(self.inner.compiler, "tile"));
956        }
957        tile::build(self, dec, bx)
958    }
959
960    /// This program over a run-time list of at most `capacity` cells or
961    /// columns (RFC U3 §6). `native` only.
962    pub fn lanes(&self, kind: LaneKind, capacity: usize) -> Result<LaneProgram, SimulateError> {
963        if self.inner.compiler != Compiler::Native {
964            return Err(native_only(self.inner.compiler, "lanes"));
965        }
966        lanes::build(self, &kind, capacity)
967    }
968
969    /// Buffers for [`Self::eval_into`].
970    pub fn new_workspace(&self) -> RhsWorkspace {
971        let inner = &*self.inner;
972        let exec = match &inner.tape {
973            Some(t) => {
974                let mut ctx = TapeCtx::new(Arc::clone(t));
975                ctx.set_serial();
976                WsExec::Tape(Box::new(ctx))
977            }
978            None => {
979                let mut s = RhsScratch::new(&inner.model.var_shapes);
980                s.set_const_arrays(Arc::clone(&inner.model.const_scope));
981                WsExec::Interp(Box::new(InterpWs {
982                    scratch: s,
983                    forcing: RefCell::new(HashMap::new()),
984                    epoch: 0,
985                }))
986            }
987        };
988        let n = if inner.identity {
989            0
990        } else {
991            inner.model.n_states
992        };
993        RhsWorkspace {
994            owner: inner.id,
995            state: vec![0.0; n],
996            dy: vec![0.0; n],
997            exec,
998            _not_sync: PhantomData,
999        }
1000    }
1001
1002    /// Evaluate the program over the whole domain: `du = f(u, aux, p, t)` in
1003    /// [`Self::layout`], reading `forcing`. Every element of `du` is written
1004    /// (zero for a state with no `D` equation). This is the reference every
1005    /// tile and lane of the program is bitwise against.
1006    ///
1007    /// The result depends only on the program and the arguments' values:
1008    /// not on the thread, the workspace's history, or any other call.
1009    #[allow(clippy::too_many_arguments)]
1010    pub fn eval_into(
1011        &self,
1012        u: &[f64],
1013        aux: &[f64],
1014        p: &[f64],
1015        t: f64,
1016        forcing: &ForcingSet,
1017        du: &mut [f64],
1018        ws: &mut RhsWorkspace,
1019    ) -> Result<(), RhsError> {
1020        let inner = &*self.inner;
1021        check_lengths(&inner.blocks, u, aux, du, p, inner.params.len())?;
1022        check_owner(ws, inner.id)?;
1023        check_forcing(forcing, &inner.slots)?;
1024        let _precision = inner.model.precision.enter();
1025        let scope = super::FaultScope::enter();
1026        self.run(u, aux, p, t, Forcing::Set(forcing), du, ws);
1027        match scope.finish() {
1028            Some(msg) => {
1029                if let WsExec::Tape(ctx) = &mut ws.exec {
1030                    ctx.invalidate_sections();
1031                }
1032                Err(forcing::fault(msg))
1033            }
1034            None => Ok(()),
1035        }
1036    }
1037
1038    /// The program the solve integrates (`native`): `c`'s own state vector
1039    /// as `u` (so a call reads `y` and writes `dy` in place) and the solve's
1040    /// tape, reading the forcing buffer the solve refreshes.
1041    #[cfg(feature = "solve")]
1042    pub(in crate::simulate_array) fn for_solve(
1043        c: &ArrayCompiled,
1044        tape: Arc<TapeProgram>,
1045    ) -> RhsProgram {
1046        let entries = c
1047            .var_shapes
1048            .iter()
1049            .map(|(name, vs)| {
1050                let bx: Vec<(i64, i64)> = vs
1051                    .shape
1052                    .iter()
1053                    .zip(&vs.origin)
1054                    .map(|(&n, &o)| (o, o + n as i64 - 1))
1055                    .collect();
1056                (name.clone(), TileInput::U, Vec::new(), bx.clone(), bx)
1057            })
1058            .collect();
1059        let blocks = Blocks::build(entries);
1060        let layout = blocks.public();
1061        let model = ProgramModel {
1062            var_shapes: c.var_shapes.clone(),
1063            param_names: c.param_names.clone(),
1064            n_states: c.n_states,
1065            precision: c.precision.clone(),
1066            ..ProgramModel::default()
1067        };
1068        RhsProgram {
1069            inner: Arc::new(ProgramInner {
1070                id: next_program_id(),
1071                flat: None,
1072                compiler: Compiler::Native,
1073                runtime_array_parameters: false,
1074                tape: Some(tape),
1075                slot_of: Vec::new(),
1076                writes: c.var_shapes.keys().cloned().collect(),
1077                aux: Vec::new(),
1078                state_layout: Vec::new(),
1079                params: Vec::new(),
1080                slots: Arc::from(Vec::new()),
1081                default_set: ForcingSet::for_slots(Arc::from(Vec::new()), Vec::new())
1082                    .expect("an empty set"),
1083                scatter: Vec::new(),
1084                identity: true,
1085                declared: Arc::new(c.declared_names.clone()),
1086                blocks,
1087                layout,
1088                model: Arc::new(model),
1089            }),
1090        }
1091    }
1092
1093    /// A workspace for [`Self::eval_solve`]: the executor may split a call
1094    /// across the process pool, as the solve's right-hand side always has.
1095    #[cfg(feature = "solve")]
1096    pub(in crate::simulate_array) fn solve_workspace(&self) -> RhsWorkspace {
1097        let tape = self
1098            .inner
1099            .tape
1100            .as_ref()
1101            .expect("the solve's program is taped");
1102        RhsWorkspace {
1103            owner: self.inner.id,
1104            state: Vec::new(),
1105            dy: Vec::new(),
1106            exec: WsExec::Tape(Box::new(TapeCtx::new(Arc::clone(tape)))),
1107            _not_sync: PhantomData,
1108        }
1109    }
1110
1111    /// The solve's right-hand side: [`Self::eval_into`]'s evaluation, reading
1112    /// the forcing buffer by name. A fault stays latched on the thread for
1113    /// the solve to report after the run (the integrator's right-hand side
1114    /// has no error channel), and the SEGMENT section re-runs when the solve
1115    /// says the buffer was refreshed ([`RhsWorkspace::forcing_refreshed`]).
1116    #[cfg(feature = "solve")]
1117    pub(in crate::simulate_array) fn eval_solve(
1118        &self,
1119        u: &[f64],
1120        p: &[f64],
1121        t: f64,
1122        forcing: &HashMap<String, ArrayD<f64>>,
1123        du: &mut [f64],
1124        ws: &mut RhsWorkspace,
1125    ) {
1126        let _precision = self.inner.model.precision.enter();
1127        self.run(u, &[], p, t, Forcing::Buffer(forcing), du, ws);
1128    }
1129
1130    /// One evaluation: `u`/`aux` into the state the program addresses, the
1131    /// program run, the written rows out to `du`.
1132    #[allow(clippy::too_many_arguments)]
1133    fn run(
1134        &self,
1135        u: &[f64],
1136        aux: &[f64],
1137        p: &[f64],
1138        t: f64,
1139        forcing: Forcing,
1140        du: &mut [f64],
1141        ws: &mut RhsWorkspace,
1142    ) {
1143        let inner = &*self.inner;
1144        let RhsWorkspace {
1145            state, dy, exec, ..
1146        } = ws;
1147        let (state, dy): (&[f64], &mut [f64]) = if inner.identity {
1148            (u, &mut *du)
1149        } else {
1150            for &(input, base, off, len) in &inner.scatter {
1151                let src = match input {
1152                    TileInput::U => u,
1153                    TileInput::Aux => aux,
1154                };
1155                state[off..off + len].copy_from_slice(&src[base..base + len]);
1156            }
1157            (state.as_slice(), dy.as_mut_slice())
1158        };
1159        match exec {
1160            WsExec::Tape(ctx) => {
1161                let (view, epoch) = match forcing {
1162                    Forcing::Set(set) => (
1163                        ForcingView::Slots {
1164                            bufs: set.bufs(),
1165                            slot_of: &inner.slot_of,
1166                        },
1167                        Some(set.epoch()),
1168                    ),
1169                    Forcing::Buffer(map) => (ForcingView::Map(map), None),
1170                };
1171                let inputs = CallInputs {
1172                    state,
1173                    params: p,
1174                    forcing: view,
1175                    forcing_epoch: epoch,
1176                    t,
1177                    declared: &inner.declared,
1178                };
1179                run_tape_inputs(ctx, &inputs, dy, true);
1180            }
1181            WsExec::Lanes { .. } => unreachable!("a whole-domain workspace runs one program"),
1182            WsExec::Interp(iw) => {
1183                let Forcing::Set(set) = forcing else {
1184                    unreachable!("the solve's program is taped");
1185                };
1186                if iw.epoch != set.epoch() {
1187                    fill_forcing_map(&mut iw.forcing.borrow_mut(), set);
1188                    iw.epoch = set.epoch();
1189                }
1190                let m = &*inner.model;
1191                evaluate_rhs_with_scratch(
1192                    &RhsCall {
1193                        rhs_rules: &m.rhs_rules,
1194                        observed_rules: &m.observed_rules,
1195                        rule_set: m.rule_set,
1196                        var_shapes: &m.var_shapes,
1197                        param_names: &m.param_names,
1198                        state,
1199                        params: p,
1200                        forcing: &iw.forcing,
1201                        t,
1202                        declared: &m.declared_names,
1203                    },
1204                    dy,
1205                    true,
1206                    &mut RhsStats::default(),
1207                    &mut iw.scratch,
1208                );
1209            }
1210        }
1211        if !inner.identity {
1212            for &(input, base, off, len) in &inner.scatter {
1213                if input == TileInput::U {
1214                    du[base..base + len].copy_from_slice(&ws.dy[off..off + len]);
1215                }
1216            }
1217        }
1218    }
1219}
1220
1221/// What a call's forcing loads read.
1222#[derive(Clone, Copy)]
1223enum Forcing<'a> {
1224    /// A forcing set (every public call).
1225    Set(&'a ForcingSet),
1226    /// The solve's forcing buffer, by name.
1227    #[cfg_attr(not(feature = "solve"), allow(dead_code))]
1228    Buffer(&'a HashMap<String, ArrayD<f64>>),
1229}
1230
1231impl RhsWorkspace {
1232    /// The solve refreshed its forcing buffer: the next call re-runs the
1233    /// SEGMENT section.
1234    #[cfg(feature = "solve")]
1235    pub(in crate::simulate_array) fn forcing_refreshed(&mut self) {
1236        if let WsExec::Tape(ctx) = &mut self.exec {
1237            ctx.bump_forcing_epoch();
1238        }
1239    }
1240}
1241
1242/// The by-name forcing buffer the per-cell evaluator reads, from `set`:
1243/// row-major arrays, overwritten in place when one of that shape is there.
1244fn fill_forcing_map(map: &mut HashMap<String, ArrayD<f64>>, set: &ForcingSet) {
1245    for (k, slot) in set.slots().iter().enumerate() {
1246        let src = ndarray::ArrayViewD::from_shape(IxDyn(&slot.shape).f(), set.buffer(k))
1247            .expect("a forcing set's buffer holds its slot's shape");
1248        match map.get_mut(&slot.name) {
1249            Some(a) if a.shape() == &slot.shape[..] => a.assign(&src),
1250            _ => {
1251                map.insert(slot.name.clone(), src.as_standard_layout().into_owned());
1252            }
1253        }
1254    }
1255}
1256
1257fn check_forcing(set: &ForcingSet, slots: &Arc<[ForcingSlot]>) -> Result<(), RhsError> {
1258    if !set.fits(slots) {
1259        return Err(set.mismatch(slots));
1260    }
1261    Ok(())
1262}
1263
1264fn check_owner(ws: &RhsWorkspace, id: u64) -> Result<(), RhsError> {
1265    if ws.owner != id {
1266        return Err(RhsError::ForeignProgram { what: "workspace" });
1267    }
1268    Ok(())
1269}
1270
1271/// The fault a tape call latched, as an error, forgetting the sections the
1272/// call primed (they hold the fault's values).
1273fn tape_fault(scope: super::FaultScope, ctx: &mut TapeCtx) -> Result<(), RhsError> {
1274    match scope.finish() {
1275        Some(msg) => {
1276            ctx.invalidate_sections();
1277            Err(forcing::fault(msg))
1278        }
1279        None => Ok(()),
1280    }
1281}
1282
1283fn check_lengths(
1284    layout: &Blocks,
1285    u: &[f64],
1286    aux: &[f64],
1287    du: &[f64],
1288    p: &[f64],
1289    n_params: usize,
1290) -> Result<(), RhsError> {
1291    if u.len() != layout.u_len {
1292        return Err(forcing::shape("u", layout.u_len, u.len()));
1293    }
1294    if du.len() != layout.u_len {
1295        return Err(forcing::shape("du", layout.u_len, du.len()));
1296    }
1297    if aux.len() != layout.aux_len {
1298        return Err(forcing::shape("aux", layout.aux_len, aux.len()));
1299    }
1300    if p.len() != n_params {
1301        return Err(forcing::shape("p", n_params, p.len()));
1302    }
1303    Ok(())
1304}
1305
1306/// The index-set name of each axis of each compiled state, from its
1307/// flattened declaration (empty when it names none at the compiled rank).
1308fn state_set_names(
1309    flat: &FlattenedSystem,
1310    compiled: &ProgramModel,
1311) -> HashMap<String, Vec<String>> {
1312    let sizes = index_set_sizes(flat);
1313    compiled
1314        .var_shapes
1315        .iter()
1316        .map(|(name, vs)| {
1317            let declared = flat.state_variables.get(name).and_then(|v| v.shape.clone());
1318            let extents: Vec<i64> = vs.shape.iter().map(|&n| n as i64).collect();
1319            (name.clone(), axis_set_names(declared, &extents, &sizes))
1320        })
1321        .collect()
1322}
1323
1324/// The index-set name of each axis of extents `extents`: the declared shape
1325/// when it names one per axis, else, axis by axis, the one interval index set
1326/// of that size when exactly one has it (a state the pointwise lift promoted
1327/// carries no declared shape), else none.
1328fn axis_set_names(
1329    declared: Option<Vec<String>>,
1330    extents: &[i64],
1331    sizes: &HashMap<String, i64>,
1332) -> Vec<String> {
1333    if let Some(d) = declared.filter(|d| d.len() == extents.len()) {
1334        return d;
1335    }
1336    let names: Vec<Option<String>> = extents
1337        .iter()
1338        .map(|&n| {
1339            let mut c = sizes
1340                .iter()
1341                .filter(|(_, m)| **m == n)
1342                .map(|(k, _)| k.clone());
1343            match (c.next(), c.next()) {
1344                (Some(k), None) => Some(k),
1345                _ => None,
1346            }
1347        })
1348        .collect();
1349    if names.iter().all(Option::is_some) {
1350        names.into_iter().flatten().collect()
1351    } else {
1352        Vec::new()
1353    }
1354}
1355
1356/// The size of each index set the flattened system declares (an interval
1357/// set's `size`).
1358fn index_set_sizes(flat: &FlattenedSystem) -> HashMap<String, i64> {
1359    flat.index_sets
1360        .iter()
1361        .filter_map(|(k, v)| v.size.map(|n| (k.clone(), n)))
1362        .collect()
1363}
1364
1365/// How one axis named `set` (of global extent `lo..=hi`) sits in `dec`.
1366fn axis_spec(dec: &Decomposition, set: Option<&str>, sizes: &HashMap<String, i64>) -> AxisSpec {
1367    let Some(set) = set else {
1368        return AxisSpec::Whole { set: None };
1369    };
1370    if let Some(cut) = dec.axes.iter().find(|a| a.index_set == set) {
1371        let n = sizes.get(set).copied().unwrap_or(0);
1372        return AxisSpec::Cut {
1373            set: set.to_string(),
1374            cell: set.to_string(),
1375            hi: n,
1376            period: cut.period,
1377            face: false,
1378        };
1379    }
1380    if let Some((_, cell)) = dec.faces.iter().find(|(f, _)| f == set)
1381        && let Some(cut) = dec.axes.iter().find(|a| &a.index_set == cell)
1382    {
1383        let n = sizes.get(cell.as_str()).copied().unwrap_or(0);
1384        return AxisSpec::Cut {
1385            set: set.to_string(),
1386            cell: cell.clone(),
1387            hi: n + 1,
1388            period: cut.period,
1389            face: true,
1390        };
1391    }
1392    AxisSpec::Whole {
1393        set: Some(set.to_string()),
1394    }
1395}
1396
1397/// The walker's view of every state and every array observed of `prog` under
1398/// `dec`.
1399fn var_infos(prog: &RhsProgram, dec: &Decomposition) -> HashMap<String, VarInfo> {
1400    let prog = &*prog.inner;
1401    let flat = prog.flat();
1402    let sizes = index_set_sizes(flat);
1403    let set_names = state_set_names(flat, &prog.model);
1404    let mut out = HashMap::new();
1405    for (name, vs) in &prog.model.var_shapes {
1406        let sets = &set_names[name];
1407        let axes = (0..vs.shape.len())
1408            .map(|d| axis_spec(dec, sets.get(d).map(String::as_str), &sizes))
1409            .collect();
1410        let extent = vs
1411            .shape
1412            .iter()
1413            .zip(&vs.origin)
1414            .map(|(&n, &o)| (o, o + n as i64 - 1))
1415            .collect();
1416        out.insert(name.clone(), VarInfo { axes, extent });
1417    }
1418    for rule in &prog.model.observed_rules {
1419        let (var, ranges) = match rule {
1420            super::AlgebraicRule::ArrayLoop {
1421                var, output_ranges, ..
1422            }
1423            | super::AlgebraicRule::Recurrence {
1424                var, output_ranges, ..
1425            } => (var, output_ranges.clone()),
1426            super::AlgebraicRule::Scalar {
1427                var,
1428                declared_shape,
1429                ..
1430            } => (
1431                var,
1432                declared_shape
1433                    .as_ref()
1434                    .map(|s| s.iter().map(|&n| (1, n as i64)).collect())
1435                    .unwrap_or_default(),
1436            ),
1437        };
1438        let declared = flat
1439            .observed_variables
1440            .get(var)
1441            .or_else(|| flat.state_variables.get(var))
1442            .and_then(|v| v.shape.clone());
1443        let extents: Vec<i64> = ranges.iter().map(|r| r.1).collect();
1444        let sets = axis_set_names(declared, &extents, &sizes);
1445        let axes = (0..ranges.len())
1446            .map(|d| axis_spec(dec, sets.get(d).map(String::as_str), &sizes))
1447            .collect();
1448        let extent = ranges.iter().map(|&(_, hi)| (1, hi)).collect();
1449        out.insert(var.clone(), VarInfo { axes, extent });
1450    }
1451    out
1452}