1use 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#[derive(Clone, Debug, PartialEq)]
19pub enum PadMode {
20 Constant(f64),
22}
23
24#[derive(Clone, Debug, PartialEq)]
26pub enum CanonicalAttrs {
27 Range {
29 start: f64,
31 stop: f64,
33 step: f64,
35 inclusive: bool,
37 },
38 Space {
40 start: f64,
42 stop: f64,
44 count: usize,
46 endpoint: bool,
48 base: Option<f64>,
50 geometric: bool,
52 },
53 Eye {
55 rows: usize,
57 cols: usize,
59 diagonal: isize,
61 },
62 Full {
64 value: f64,
66 },
67 Axis {
69 axis: usize,
71 },
72 Pad {
74 widths: Arc<[(usize, usize)]>,
76 mode: PadMode,
78 },
79 Clip {
81 minimum: f64,
83 maximum: f64,
85 },
86 Diff {
88 axis: usize,
90 periods: usize,
92 },
93 Close {
95 relative: f64,
97 absolute: f64,
99 equal_nan: bool,
101 },
102 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
141pub 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
177pub 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::*;