Skip to main content

sim_lib_compute_model/
ode.rs

1//! Resident tensor ODE execution over the canonical numeric/RK pipeline.
2
3use std::sync::Arc;
4
5use sim_kernel::{
6    Consistency, Cx, Error, EvalFabric, EvalMode, EvalReply, EvalRequest, Expr, Result, Symbol,
7};
8use sim_lib_numbers_tensor::{TensorExecutor, TensorSite, tensor_value_ref};
9
10use crate::model::{ModeledComputeProfile, ModeledComputeSnapshot, ModeledTensorExecutor};
11use crate::site::compute_model_site_symbol;
12
13/// ODE execution family requested by the resident adapter.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ResidentOdeKind {
16    /// Fixed-step RK methods.
17    Fixed,
18    /// Adaptive RK methods with scalar error decisions.
19    Adaptive,
20}
21
22/// Provider-side lowering classification for a tensor RHS.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ResidentRhsLowering {
25    /// RHS is a lowerable tensor expression.
26    TensorExpression,
27    /// RHS is host-only or effectful and must not be accepted by resident ODE.
28    NonLowerable,
29}
30
31/// Checked resident ODE plan.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct ResidentOdePlan {
34    kind: ResidentOdeKind,
35    state_shape: Vec<usize>,
36    dtype: Symbol,
37}
38
39impl ResidentOdePlan {
40    /// Builds a fixed-step resident ODE plan for tensor state.
41    pub fn fixed(state_shape: Vec<usize>, dtype: Symbol, rhs: ResidentRhsLowering) -> Result<Self> {
42        Self::new(ResidentOdeKind::Fixed, state_shape, dtype, rhs)
43    }
44
45    /// Builds an adaptive resident ODE plan for tensor state.
46    pub fn adaptive(
47        state_shape: Vec<usize>,
48        dtype: Symbol,
49        rhs: ResidentRhsLowering,
50    ) -> Result<Self> {
51        Self::new(ResidentOdeKind::Adaptive, state_shape, dtype, rhs)
52    }
53
54    fn new(
55        kind: ResidentOdeKind,
56        state_shape: Vec<usize>,
57        dtype: Symbol,
58        rhs: ResidentRhsLowering,
59    ) -> Result<Self> {
60        if state_shape.iter().product::<usize>() < 2 {
61            return Err(Error::Eval(
62                "resident ODE declines scalar or too-small tensor state".to_owned(),
63            ));
64        }
65        if rhs != ResidentRhsLowering::TensorExpression {
66            return Err(Error::Eval(
67                "resident ODE requires a lowerable tensor RHS expression".to_owned(),
68            ));
69        }
70        Ok(Self {
71            kind,
72            state_shape,
73            dtype,
74        })
75    }
76
77    /// Returns the execution family.
78    pub fn kind(&self) -> ResidentOdeKind {
79        self.kind
80    }
81
82    /// Returns the planned tensor state shape.
83    pub fn state_shape(&self) -> &[usize] {
84        &self.state_shape
85    }
86
87    /// Returns the planned tensor dtype.
88    pub fn dtype(&self) -> &Symbol {
89        &self.dtype
90    }
91}
92
93/// Result plus provider counters for a resident ODE execution.
94#[derive(Clone)]
95pub struct ResidentOdeExecution {
96    /// Eval reply produced by the canonical numeric pipeline.
97    pub reply: EvalReply,
98    /// Counter snapshot after final synchronization.
99    pub snapshot: ModeledComputeSnapshot,
100    /// Resident readbacks performed while executing the request.
101    pub readbacks: usize,
102    /// Accepted tensor submissions represented by the final flush.
103    pub final_flush_accepted: usize,
104}
105
106/// Executes checked tensor ODE plans through the modeled resident site.
107#[derive(Clone)]
108pub struct ResidentOdeExecutor {
109    executor: ModeledTensorExecutor,
110}
111
112impl ResidentOdeExecutor {
113    /// Builds an ODE executor that auto-flushes bounded tensor submission batches.
114    pub fn new(mut profile: ModeledComputeProfile) -> Self {
115        profile.auto_flush_batches = true;
116        Self {
117            executor: ModeledTensorExecutor::new(profile),
118        }
119    }
120
121    /// Returns the underlying modeled tensor executor.
122    pub fn tensor_executor(&self) -> &ModeledTensorExecutor {
123        &self.executor
124    }
125
126    /// Executes an ODE expression through the resident tensor site.
127    pub fn execute(
128        &self,
129        cx: &mut Cx,
130        plan: &ResidentOdePlan,
131        expr: Expr,
132    ) -> Result<ResidentOdeExecution> {
133        let before = self.executor.snapshot();
134        let site = TensorSite::new(
135            compute_model_site_symbol(),
136            Arc::new(self.executor.clone()) as Arc<dyn TensorExecutor>,
137            Vec::new(),
138        );
139        let request = eval_request(expr);
140        let reply = if plan.kind() == ResidentOdeKind::Fixed {
141            self.executor.begin_internal_materialization();
142            let reply = site.realize(cx, request);
143            self.executor.end_internal_materialization();
144            reply?
145        } else {
146            site.realize(cx, request)?
147        };
148        let value = if let Some(table) = reply.value.object().as_table_impl() {
149            table.get(cx, Symbol::new("value"))?
150        } else {
151            reply.value.clone()
152        };
153        let tensor = tensor_value_ref(&value).ok_or_else(|| {
154            Error::Eval("resident ODE result did not produce tensor state".to_owned())
155        })?;
156        if tensor.shape() != plan.state_shape() || tensor.dtype() != plan.dtype() {
157            return Err(Error::Eval(format!(
158                "resident ODE result shape/dtype {:?}/{} did not match plan {:?}/{}",
159                tensor.shape(),
160                tensor.dtype(),
161                plan.state_shape(),
162                plan.dtype()
163            )));
164        }
165        let final_flush_accepted = self.executor.flush().map_err(Error::from)?.accepted;
166        let snapshot = self.executor.snapshot();
167        Ok(ResidentOdeExecution {
168            reply,
169            readbacks: snapshot.readbacks.saturating_sub(before.readbacks),
170            snapshot,
171            final_flush_accepted,
172        })
173    }
174}
175
176impl Default for ResidentOdeExecutor {
177    fn default() -> Self {
178        Self::new(ModeledComputeProfile::default())
179    }
180}
181
182fn eval_request(expr: Expr) -> EvalRequest {
183    EvalRequest {
184        expr,
185        result_shape: None,
186        required_capabilities: Vec::new(),
187        deadline: None,
188        consistency: Consistency::LocalFirst,
189        mode: EvalMode::Eval,
190        answer_limit: None,
191        stream_buffer: None,
192        stream: false,
193        trace: false,
194    }
195}