Skip to main content

sim_lib_numbers_tensor/implementation/
canonical_ops.rs

1//! Canonical, executor-routed tensor vocabulary.
2//!
3//! Every non-tensor parameter is carried by [`CanonicalAttrs`] in the operation
4//! descriptor.  This keeps endpoint, axis, tolerance, padding, and empty-input
5//! policy visible to providers instead of inheriting ambient defaults.
6
7use std::{any::Any, sync::Arc};
8
9use sim_kernel::{Cx, Error, Expr, Object, Result, Symbol};
10
11use super::{
12    execution::{TensorExecError, TensorMeta, TensorOp, TensorRequest, execute_tensor_request},
13    execution_math_support::{numeric_f64, numeric_value, tensor_from_cells},
14    value::Tensor,
15};
16
17/// Explicit padding behavior. Only constant padding is presently canonical.
18#[derive(Clone, Debug, PartialEq)]
19pub enum PadMode {
20    /// Fill every padded cell with the supplied scalar.
21    Constant(f64),
22}
23
24/// Explicit parameters and edge policy carried by canonical operation requests.
25#[derive(Clone, Debug, PartialEq)]
26pub enum CanonicalAttrs {
27    /// Finite arithmetic progression and endpoint policy.
28    Range {
29        /// First value.
30        start: f64,
31        /// Endpoint bound.
32        stop: f64,
33        /// Nonzero increment.
34        step: f64,
35        /// Whether an exactly reached endpoint is included.
36        inclusive: bool,
37    },
38    /// Counted linear, logarithmic, or geometric space.
39    Space {
40        /// First value or exponent.
41        start: f64,
42        /// Last value or exponent.
43        stop: f64,
44        /// Requested output count; zero is valid.
45        count: usize,
46        /// Whether the last value is the exact endpoint.
47        endpoint: bool,
48        /// Optional logarithm base.
49        base: Option<f64>,
50        /// Whether interpolation is geometric.
51        geometric: bool,
52    },
53    /// Identity-grid dimensions and diagonal offset.
54    Eye {
55        /// Row count.
56        rows: usize,
57        /// Column count.
58        cols: usize,
59        /// Signed diagonal offset.
60        diagonal: isize,
61    },
62    /// Scalar repeated by `full`.
63    Full {
64        /// Repeated value.
65        value: f64,
66    },
67    /// Explicit row-major axis.
68    Axis {
69        /// Zero-based axis.
70        axis: usize,
71    },
72    /// Per-axis padding widths and mode.
73    Pad {
74        /// `(before, after)` widths for every axis.
75        widths: Arc<[(usize, usize)]>,
76        /// Explicit fill behavior.
77        mode: PadMode,
78    },
79    /// Inclusive clipping interval.
80    Clip {
81        /// Lower bound.
82        minimum: f64,
83        /// Upper bound.
84        maximum: f64,
85    },
86    /// Repeated finite difference parameters.
87    Diff {
88        /// Axis along which differences are taken.
89        axis: usize,
90        /// Number of difference passes.
91        periods: usize,
92    },
93    /// Separate closeness tolerances and NaN policy.
94    Close {
95        /// Relative tolerance.
96        relative: f64,
97        /// Absolute tolerance.
98        absolute: f64,
99        /// Whether paired NaNs compare close.
100        equal_nan: bool,
101    },
102    /// Operation has no scalar parameters.
103    None,
104}
105
106impl Object for CanonicalAttrs {
107    fn display(&self, _cx: &mut Cx) -> Result<String> {
108        Ok(format!("#<tensor-op-attributes {self:?}>"))
109    }
110    fn as_any(&self) -> &dyn Any {
111        self
112    }
113}
114impl sim_kernel::ObjectCompat for CanonicalAttrs {
115    fn class(&self, cx: &mut Cx) -> Result<sim_kernel::ClassRef> {
116        sim_lib_numbers_core::number_domain_class_stub(cx)
117    }
118    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
119        Err(Error::Eval(
120            "tensor operation attributes are request descriptors, not expressions".into(),
121        ))
122    }
123}
124
125macro_rules! symbols { ($($name:ident => $text:literal),+ $(,)?) => {$(
126    #[doc = concat!("Canonical `", $text, "` operation symbol.")]
127    pub fn $name() -> Symbol { Symbol::qualified("tensor", concat!("op/", $text)) }
128)+}; }
129symbols! {
130    arange_op_symbol=>"arange", linspace_op_symbol=>"linspace", logspace_op_symbol=>"logspace",
131    geomspace_op_symbol=>"geomspace", eye_op_symbol=>"eye", diag_op_symbol=>"diag",
132    full_op_symbol=>"full", outer_op_symbol=>"outer", concat_op_symbol=>"concat",
133    stack_op_symbol=>"stack", column_stack_op_symbol=>"column-stack", pad_op_symbol=>"pad",
134    argmax_op_symbol=>"argmax", argmin_op_symbol=>"argmin", where_op_symbol=>"where",
135    nonzero_op_symbol=>"nonzero", unique_op_symbol=>"unique", clip_op_symbol=>"clip",
136    diff_op_symbol=>"diff", cumsum_op_symbol=>"cumsum", maximum_op_symbol=>"maximum",
137    minimum_op_symbol=>"minimum", sign_op_symbol=>"sign", signbit_op_symbol=>"signbit",
138    isfinite_op_symbol=>"isfinite", isclose_op_symbol=>"isclose", allclose_op_symbol=>"allclose"
139}
140
141/// All canonical operation symbols advertised by a capable provider.
142pub fn canonical_tensor_op_symbols() -> Vec<Symbol> {
143    vec![
144        arange_op_symbol(),
145        linspace_op_symbol(),
146        logspace_op_symbol(),
147        geomspace_op_symbol(),
148        eye_op_symbol(),
149        diag_op_symbol(),
150        full_op_symbol(),
151        outer_op_symbol(),
152        concat_op_symbol(),
153        stack_op_symbol(),
154        column_stack_op_symbol(),
155        pad_op_symbol(),
156        argmax_op_symbol(),
157        argmin_op_symbol(),
158        where_op_symbol(),
159        nonzero_op_symbol(),
160        unique_op_symbol(),
161        clip_op_symbol(),
162        diff_op_symbol(),
163        cumsum_op_symbol(),
164        maximum_op_symbol(),
165        minimum_op_symbol(),
166        sign_op_symbol(),
167        signbit_op_symbol(),
168        isfinite_op_symbol(),
169        isclose_op_symbol(),
170        allclose_op_symbol(),
171    ]
172}
173pub(crate) fn is_canonical_tensor_op(symbol: &Symbol) -> bool {
174    canonical_tensor_op_symbols().contains(symbol)
175}
176
177/// Submits a canonical operation. Providers that decline are handled by the
178/// existing executor fallback policy.
179pub fn execute_canonical_tensor_op(
180    cx: &mut Cx,
181    symbol: Symbol,
182    inputs: Vec<Tensor>,
183    output: TensorMeta,
184    attrs: CanonicalAttrs,
185) -> Result<Tensor> {
186    let attributes = cx.factory().opaque(Arc::new(attrs))?;
187    execute_tensor_request(
188        cx,
189        TensorRequest::new(TensorOp::new(symbol, attributes), inputs, output),
190    )
191}
192
193fn attrs(request: &TensorRequest) -> std::result::Result<&CanonicalAttrs, TensorExecError> {
194    request
195        .operation
196        .attributes
197        .object()
198        .downcast_ref::<CanonicalAttrs>()
199        .ok_or_else(|| {
200            TensorExecError::invalid("canonical tensor operation requires explicit CanonicalAttrs")
201        })
202}
203fn cells(cx: &mut Cx, tensor: &Tensor) -> std::result::Result<Vec<f64>, TensorExecError> {
204    tensor
205        .cells()
206        .map_err(TensorExecError::from)?
207        .iter()
208        .map(|v| numeric_f64(cx, v))
209        .collect()
210}
211fn output(
212    cx: &mut Cx,
213    request: &TensorRequest,
214    values: Vec<f64>,
215) -> std::result::Result<Tensor, TensorExecError> {
216    let vals = values
217        .into_iter()
218        .map(|v| numeric_value(cx, request.output.dtype(), v))
219        .collect::<std::result::Result<Vec<_>, _>>()?;
220    tensor_from_cells(
221        cx,
222        request.output.shape().to_vec(),
223        request.output.dtype().clone(),
224        vals,
225    )
226}
227fn unary(request: &TensorRequest) -> std::result::Result<&Tensor, TensorExecError> {
228    request
229        .inputs
230        .first()
231        .filter(|_| request.inputs.len() == 1)
232        .ok_or_else(|| TensorExecError::invalid("operation expects one tensor input"))
233}
234fn pair(request: &TensorRequest) -> std::result::Result<(&Tensor, &Tensor), TensorExecError> {
235    match request.inputs.as_ref() {
236        [a, b] => Ok((a, b)),
237        _ => Err(TensorExecError::invalid(
238            "operation expects two tensor inputs",
239        )),
240    }
241}
242fn strides(shape: &[usize]) -> Vec<usize> {
243    (0..shape.len())
244        .map(|i| shape[i + 1..].iter().product())
245        .collect()
246}
247
248pub(crate) fn execute_canonical_request(
249    cx: &mut Cx,
250    request: &TensorRequest,
251) -> std::result::Result<Tensor, TensorExecError> {
252    let op = &request.operation.symbol;
253    if *op == arange_op_symbol() {
254        let CanonicalAttrs::Range {
255            start,
256            stop,
257            step,
258            inclusive,
259        } = *attrs(request)?
260        else {
261            return Err(TensorExecError::invalid("arange requires Range attributes"));
262        };
263        if !start.is_finite() || !stop.is_finite() || !step.is_finite() || step == 0.0 {
264            return Err(TensorExecError::invalid(
265                "arange requires finite bounds and a nonzero finite step",
266            ));
267        }
268        let mut out = Vec::new();
269        let mut v = start;
270        let forward = step > 0.0;
271        while if forward {
272            v < stop || (inclusive && v <= stop)
273        } else {
274            v > stop || (inclusive && v >= stop)
275        } {
276            out.push(v);
277            v += step;
278            if out.len() > request.output.shape().iter().product() {
279                return Err(TensorExecError::invalid("arange output count overflow"));
280            }
281        }
282        return output(cx, request, out);
283    }
284    if [
285        linspace_op_symbol(),
286        logspace_op_symbol(),
287        geomspace_op_symbol(),
288    ]
289    .contains(op)
290    {
291        let CanonicalAttrs::Space {
292            start,
293            stop,
294            count,
295            endpoint,
296            base,
297            geometric,
298        } = *attrs(request)?
299        else {
300            return Err(TensorExecError::invalid(
301                "space operation requires Space attributes",
302            ));
303        };
304        if !start.is_finite()
305            || !stop.is_finite()
306            || base.is_some_and(|b| !b.is_finite() || b <= 0.0)
307        {
308            return Err(TensorExecError::invalid(
309                "space operation requires finite inputs and a positive finite base",
310            ));
311        }
312        if count == 0 {
313            return output(cx, request, Vec::new());
314        }
315        let denom = if endpoint && count > 1 {
316            count - 1
317        } else {
318            count
319        };
320        let vals = (0..count)
321            .map(|i| {
322                let t = if denom == 0 {
323                    0.0
324                } else {
325                    i as f64 / denom as f64
326                };
327                let v = if geometric {
328                    if start == 0.0 || stop == 0.0 || start.signum() != stop.signum() {
329                        f64::NAN
330                    } else {
331                        start.signum()
332                            * (start.abs().ln() + t * (stop.abs().ln() - start.abs().ln())).exp()
333                    }
334                } else {
335                    start + t * (stop - start)
336                };
337                base.map_or(v, |b| b.powf(v))
338            })
339            .collect();
340        return output(cx, request, vals);
341    }
342    if *op == full_op_symbol() {
343        let CanonicalAttrs::Full { value } = *attrs(request)? else {
344            return Err(TensorExecError::invalid("full requires Full attributes"));
345        };
346        return output(
347            cx,
348            request,
349            vec![value; request.output.shape().iter().product()],
350        );
351    }
352    if *op == eye_op_symbol() {
353        let CanonicalAttrs::Eye {
354            rows,
355            cols,
356            diagonal,
357        } = *attrs(request)?
358        else {
359            return Err(TensorExecError::invalid("eye requires Eye attributes"));
360        };
361        let mut v = vec![
362            0.0;
363            rows.checked_mul(cols)
364                .ok_or_else(|| TensorExecError::invalid("eye shape overflow"))?
365        ];
366        for r in 0..rows {
367            let c = r as isize + diagonal;
368            if c >= 0 && (c as usize) < cols {
369                v[r * cols + c as usize] = 1.0
370            }
371        }
372        return output(cx, request, v);
373    }
374    if *op == outer_op_symbol() {
375        let (a, b) = pair(request)?;
376        let av = cells(cx, a)?;
377        let bv = cells(cx, b)?;
378        return output(
379            cx,
380            request,
381            av.iter()
382                .flat_map(|x| bv.iter().map(move |y| x * y))
383                .collect(),
384        );
385    }
386    if *op == diag_op_symbol() {
387        let t = unary(request)?;
388        let v = cells(cx, t)?;
389        if t.shape().len() == 1 {
390            let n = v.len();
391            let mut o = vec![0.0; n * n];
392            for i in 0..n {
393                o[i * n + i] = v[i]
394            }
395            return output(cx, request, o);
396        }
397        if let [r, c] = t.shape() {
398            return output(
399                cx,
400                request,
401                (0..(*r).min(*c)).map(|i| v[i * c + i]).collect(),
402            );
403        }
404        return Err(TensorExecError::invalid("diag expects rank one or two"));
405    }
406    if *op == concat_op_symbol() || *op == stack_op_symbol() || *op == column_stack_op_symbol() {
407        return concat(cx, request);
408    }
409    if *op == pad_op_symbol() {
410        return pad(cx, request);
411    }
412    if *op == argmax_op_symbol() || *op == argmin_op_symbol() {
413        let v = cells(cx, unary(request)?)?;
414        if v.is_empty() {
415            return Err(TensorExecError::invalid("argmin/argmax reject empty input"));
416        }
417        let max = *op == argmax_op_symbol();
418        let mut best = 0;
419        for i in 1..v.len() {
420            if v[best].is_nan()
421                || (!v[i].is_nan() && ((max && v[i] > v[best]) || (!max && v[i] < v[best])))
422            {
423                best = i
424            }
425        }
426        return output(cx, request, vec![best as f64]);
427    }
428    if *op == nonzero_op_symbol() {
429        let v = cells(cx, unary(request)?)?;
430        return output(
431            cx,
432            request,
433            v.iter()
434                .enumerate()
435                .filter(|(_, x)| **x != 0.0)
436                .map(|(i, _)| i as f64)
437                .collect(),
438        );
439    }
440    if *op == unique_op_symbol() {
441        let mut out = Vec::new();
442        for v in cells(cx, unary(request)?)? {
443            if !out.iter().any(|x: &f64| x.to_bits() == v.to_bits()) {
444                out.push(v)
445            }
446        }
447        return output(cx, request, out);
448    }
449    if *op == clip_op_symbol() {
450        let CanonicalAttrs::Clip { minimum, maximum } = *attrs(request)? else {
451            return Err(TensorExecError::invalid("clip requires Clip attributes"));
452        };
453        if minimum > maximum {
454            return Err(TensorExecError::invalid("clip minimum exceeds maximum"));
455        }
456        let values = cells(cx, unary(request)?)?
457            .into_iter()
458            .map(|v| v.max(minimum).min(maximum))
459            .collect();
460        return output(cx, request, values);
461    }
462    if *op == diff_op_symbol() {
463        return diff(cx, request);
464    }
465    if *op == cumsum_op_symbol() {
466        let input = cells(cx, unary(request)?)?;
467        let values = super::reduction::cumsum_f64(&input, super::reduction::SumMode::Naive);
468        return output(cx, request, values);
469    }
470    if [
471        maximum_op_symbol(),
472        minimum_op_symbol(),
473        isclose_op_symbol(),
474    ]
475    .contains(op)
476    {
477        let (a, b) = pair(request)?;
478        if a.shape() != b.shape() || a.dtype() != b.dtype() {
479            return Err(TensorExecError::invalid(
480                "elementwise canonical operations require identical shape and dtype",
481            ));
482        }
483        let av = cells(cx, a)?;
484        let bv = cells(cx, b)?;
485        if *op == isclose_op_symbol() {
486            let CanonicalAttrs::Close {
487                relative,
488                absolute,
489                equal_nan,
490            } = *attrs(request)?
491            else {
492                return Err(TensorExecError::invalid(
493                    "isclose requires Close attributes",
494                ));
495            };
496            valid_tolerances(relative, absolute)?;
497            return output(
498                cx,
499                request,
500                av.into_iter()
501                    .zip(bv)
502                    .map(|(a, b)| {
503                        ((a == b)
504                            || (equal_nan && a.is_nan() && b.is_nan())
505                            || ((a - b).abs() <= absolute + relative * b.abs()))
506                            as u8 as f64
507                    })
508                    .collect(),
509            );
510        }
511        return output(
512            cx,
513            request,
514            av.into_iter()
515                .zip(bv)
516                .map(|(a, b)| {
517                    if *op == maximum_op_symbol() {
518                        a.max(b)
519                    } else {
520                        a.min(b)
521                    }
522                })
523                .collect(),
524        );
525    }
526    if [sign_op_symbol(), signbit_op_symbol(), isfinite_op_symbol()].contains(op) {
527        let vals = cells(cx, unary(request)?)?
528            .into_iter()
529            .map(|v| {
530                if *op == sign_op_symbol() {
531                    v.signum()
532                } else if *op == signbit_op_symbol() {
533                    v.is_sign_negative() as u8 as f64
534                } else {
535                    v.is_finite() as u8 as f64
536                }
537            })
538            .collect();
539        return output(cx, request, vals);
540    }
541    if *op == allclose_op_symbol() {
542        let (a, b) = pair(request)?;
543        if a.shape() != b.shape() || a.dtype() != b.dtype() {
544            return Err(TensorExecError::invalid(
545                "allclose requires identical shape and dtype",
546            ));
547        }
548        let CanonicalAttrs::Close {
549            relative,
550            absolute,
551            equal_nan,
552        } = *attrs(request)?
553        else {
554            return Err(TensorExecError::invalid(
555                "allclose requires Close attributes",
556            ));
557        };
558        valid_tolerances(relative, absolute)?;
559        let yes = cells(cx, a)?.into_iter().zip(cells(cx, b)?).all(|(a, b)| {
560            (a == b)
561                || (equal_nan && a.is_nan() && b.is_nan())
562                || (a - b).abs() <= absolute + relative * b.abs()
563        });
564        return output(cx, request, vec![yes as u8 as f64]);
565    }
566    if *op == where_op_symbol() {
567        if let [condition, yes, no] = request.inputs.as_ref() {
568            if condition.shape() != yes.shape()
569                || yes.shape() != no.shape()
570                || yes.dtype() != no.dtype()
571            {
572                return Err(TensorExecError::invalid(
573                    "where requires identical shapes and matching branch dtypes",
574                ));
575            }
576            let c = cells(cx, condition)?;
577            let y = cells(cx, yes)?;
578            let n = cells(cx, no)?;
579            return output(
580                cx,
581                request,
582                c.into_iter()
583                    .enumerate()
584                    .map(|(i, v)| if v != 0.0 { y[i] } else { n[i] })
585                    .collect(),
586            );
587        }
588        return Err(TensorExecError::invalid(
589            "where expects condition, yes, and no tensors",
590        ));
591    }
592    Err(TensorExecError::unsupported(
593        op.clone(),
594        "unknown canonical tensor operation",
595    ))
596}
597
598fn valid_tolerances(r: f64, a: f64) -> std::result::Result<(), TensorExecError> {
599    if r < 0.0 || a < 0.0 || !r.is_finite() || !a.is_finite() {
600        Err(TensorExecError::invalid(
601            "closeness tolerances must be finite and non-negative",
602        ))
603    } else {
604        Ok(())
605    }
606}
607mod array;
608
609use array::*;