Skip to main content

sim_lib_numbers_tensor/implementation/
elementwise.rs

1//! Element-wise tensor operation request construction and CPU semantics.
2
3use sim_kernel::{Cx, Error, Result, Symbol, Value};
4
5use crate::spec::bounded_element_count;
6
7use super::{
8    execution::{TensorExecError, TensorMeta, TensorOp, TensorRequest, execute_tensor_request},
9    value::Tensor,
10};
11
12/// Open operation symbol for element-wise tensor addition.
13pub fn add_op_symbol() -> Symbol {
14    Symbol::qualified("tensor", "op/add")
15}
16
17/// Open operation symbol for element-wise tensor subtraction.
18pub fn sub_op_symbol() -> Symbol {
19    Symbol::qualified("tensor", "op/sub")
20}
21
22/// Open operation symbol for element-wise tensor multiplication.
23pub fn mul_op_symbol() -> Symbol {
24    Symbol::qualified("tensor", "op/mul")
25}
26
27/// Open operation symbol for element-wise tensor division.
28pub fn div_op_symbol() -> Symbol {
29    Symbol::qualified("tensor", "op/div")
30}
31
32/// Open operation symbol for element-wise tensor remainder.
33pub fn rem_op_symbol() -> Symbol {
34    Symbol::qualified("tensor", "op/rem")
35}
36
37/// Open operation symbol for element-wise tensor exponentiation.
38pub fn pow_op_symbol() -> Symbol {
39    Symbol::qualified("tensor", "op/pow")
40}
41
42/// Open operation symbol for element-wise tensor negation.
43pub fn neg_op_symbol() -> Symbol {
44    Symbol::qualified("tensor", "op/neg")
45}
46
47pub(crate) fn tensor_elementwise_op_symbols() -> Vec<Symbol> {
48    vec![
49        add_op_symbol(),
50        sub_op_symbol(),
51        mul_op_symbol(),
52        div_op_symbol(),
53        rem_op_symbol(),
54        pow_op_symbol(),
55        neg_op_symbol(),
56    ]
57}
58
59/// Runs a binary tensor operation through the active executor, or CPU when no
60/// executor is bound in the environment.
61pub fn execute_tensor_binary_op(
62    cx: &mut Cx,
63    operator: Symbol,
64    left: &Tensor,
65    right: &Tensor,
66) -> Result<Tensor> {
67    let op_symbol = binary_tensor_op_symbol(&operator)
68        .ok_or_else(|| Error::Eval(format!("unsupported tensor binary operator {operator}")))?;
69    let output = binary_output_meta(cx, &operator, left, right)?;
70    let op = TensorOp::without_attributes(cx, op_symbol)?;
71    execute_tensor_request(
72        cx,
73        TensorRequest::new(op, vec![left.clone(), right.clone()], output),
74    )
75}
76
77/// Runs a unary tensor operation through the active executor, or CPU when no
78/// executor is bound in the environment.
79pub fn execute_tensor_unary_op(cx: &mut Cx, operator: Symbol, tensor: &Tensor) -> Result<Tensor> {
80    let op_symbol = unary_tensor_op_symbol(&operator)
81        .ok_or_else(|| Error::Eval(format!("unsupported tensor unary operator {operator}")))?;
82    let output = TensorMeta::new(tensor.shape().to_vec(), tensor.dtype().clone());
83    let op = TensorOp::without_attributes(cx, op_symbol)?;
84    execute_tensor_request(cx, TensorRequest::new(op, vec![tensor.clone()], output))
85}
86
87pub(crate) fn is_elementwise_binary_op(symbol: &Symbol) -> bool {
88    binary_math_operator(symbol).is_some()
89}
90
91pub(crate) fn is_elementwise_unary_op(symbol: &Symbol) -> bool {
92    unary_math_operator(symbol).is_some()
93}
94
95pub(crate) fn execute_elementwise_binary_request(
96    cx: &mut Cx,
97    request: &TensorRequest,
98) -> std::result::Result<Tensor, TensorExecError> {
99    let operator = binary_math_operator(&request.operation.symbol).ok_or_else(|| {
100        TensorExecError::unsupported(
101            request.operation.symbol.clone(),
102            "unknown element-wise binary operation",
103        )
104    })?;
105    let [left, right] = request.inputs.as_ref() else {
106        return Err(TensorExecError::invalid(
107            "element-wise binary operation expects exactly two tensor inputs",
108        ));
109    };
110    let shape = broadcast_shape(left.shape(), right.shape()).map_err(TensorExecError::from)?;
111    if shape != request.output.shape() {
112        return Err(TensorExecError::shape(format!(
113            "element-wise output shape {:?} did not match {:?}",
114            shape,
115            request.output.shape()
116        )));
117    }
118    bounded_element_count(&shape).map_err(TensorExecError::from)?;
119    let mut cells = Vec::with_capacity(bounded_element_count(&shape).unwrap_or(0));
120    for coord in Tensor::coordinates(&shape) {
121        let left_cell = select_cell(left, &coord, &shape).map_err(TensorExecError::from)?;
122        let right_cell = select_cell(right, &coord, &shape).map_err(TensorExecError::from)?;
123        cells.push(
124            cx.apply_value_number_binary_op(&operator, left_cell, right_cell)
125                .map_err(TensorExecError::from)?,
126        );
127    }
128    Tensor::new_checked(cx, shape, request.output.dtype().clone(), cells)
129        .map_err(TensorExecError::from)
130}
131
132pub(crate) fn execute_elementwise_unary_request(
133    cx: &mut Cx,
134    request: &TensorRequest,
135) -> std::result::Result<Tensor, TensorExecError> {
136    let operator = unary_math_operator(&request.operation.symbol).ok_or_else(|| {
137        TensorExecError::unsupported(
138            request.operation.symbol.clone(),
139            "unknown element-wise unary operation",
140        )
141    })?;
142    let [tensor] = request.inputs.as_ref() else {
143        return Err(TensorExecError::invalid(
144            "element-wise unary operation expects exactly one tensor input",
145        ));
146    };
147    if tensor.shape() != request.output.shape() {
148        return Err(TensorExecError::shape(format!(
149            "element-wise output shape {:?} did not match {:?}",
150            tensor.shape(),
151            request.output.shape()
152        )));
153    }
154    let source = tensor.cells().map_err(TensorExecError::from)?;
155    let mut cells = Vec::with_capacity(source.len());
156    for cell in source.iter().cloned() {
157        cells.push(
158            cx.apply_value_number_unary_op(&operator, cell)
159                .map_err(TensorExecError::from)?,
160        );
161    }
162    Tensor::new_checked(
163        cx,
164        tensor.shape().to_vec(),
165        request.output.dtype().clone(),
166        cells,
167    )
168    .map_err(TensorExecError::from)
169}
170
171fn binary_output_meta(
172    cx: &mut Cx,
173    operator: &Symbol,
174    left: &Tensor,
175    right: &Tensor,
176) -> Result<TensorMeta> {
177    let shape = broadcast_shape(left.shape(), right.shape())?;
178    let len = bounded_element_count(&shape)?;
179    let dtype = if len == 0 {
180        left.dtype().clone()
181    } else {
182        let coord = vec![0; shape.len()];
183        let left_cell = select_cell(left, &coord, &shape)?;
184        let right_cell = select_cell(right, &coord, &shape)?;
185        let sample = cx.apply_value_number_binary_op(operator, left_cell, right_cell)?;
186        scalar_domain(cx, &sample)?
187    };
188    Ok(TensorMeta::new(shape, dtype))
189}
190
191fn scalar_domain(cx: &mut Cx, value: &Value) -> Result<Symbol> {
192    let Some(number) = cx.number_value_ref(value.clone())? else {
193        return Err(Error::Eval(
194            "element-wise tensor operation produced a non-number cell".to_owned(),
195        ));
196    };
197    Ok(number.domain)
198}
199
200fn binary_tensor_op_symbol(operator: &Symbol) -> Option<Symbol> {
201    if *operator == Symbol::qualified("math", "add") {
202        Some(add_op_symbol())
203    } else if *operator == Symbol::qualified("math", "sub") {
204        Some(sub_op_symbol())
205    } else if *operator == Symbol::qualified("math", "mul") {
206        Some(mul_op_symbol())
207    } else if *operator == Symbol::qualified("math", "div") {
208        Some(div_op_symbol())
209    } else if *operator == Symbol::qualified("math", "rem") {
210        Some(rem_op_symbol())
211    } else if *operator == Symbol::qualified("math", "pow") {
212        Some(pow_op_symbol())
213    } else {
214        None
215    }
216}
217
218fn unary_tensor_op_symbol(operator: &Symbol) -> Option<Symbol> {
219    (*operator == Symbol::qualified("math", "neg")).then(neg_op_symbol)
220}
221
222fn binary_math_operator(op_symbol: &Symbol) -> Option<Symbol> {
223    if *op_symbol == add_op_symbol() {
224        Some(Symbol::qualified("math", "add"))
225    } else if *op_symbol == sub_op_symbol() {
226        Some(Symbol::qualified("math", "sub"))
227    } else if *op_symbol == mul_op_symbol() {
228        Some(Symbol::qualified("math", "mul"))
229    } else if *op_symbol == div_op_symbol() {
230        Some(Symbol::qualified("math", "div"))
231    } else if *op_symbol == rem_op_symbol() {
232        Some(Symbol::qualified("math", "rem"))
233    } else if *op_symbol == pow_op_symbol() {
234        Some(Symbol::qualified("math", "pow"))
235    } else {
236        None
237    }
238}
239
240fn unary_math_operator(op_symbol: &Symbol) -> Option<Symbol> {
241    (*op_symbol == neg_op_symbol()).then(|| Symbol::qualified("math", "neg"))
242}
243
244fn broadcast_shape(left: &[usize], right: &[usize]) -> Result<Vec<usize>> {
245    let rank = left.len().max(right.len());
246    let mut out = Vec::with_capacity(rank);
247    for axis in 0..rank {
248        let left_dim = *left
249            .get(left.len().wrapping_sub(rank - axis))
250            .unwrap_or(&1usize);
251        let right_dim = *right
252            .get(right.len().wrapping_sub(rank - axis))
253            .unwrap_or(&1usize);
254        if left_dim == right_dim {
255            out.push(left_dim);
256        } else if left_dim == 1 {
257            out.push(right_dim);
258        } else if right_dim == 1 {
259            out.push(left_dim);
260        } else {
261            return Err(Error::Eval(format!(
262                "cannot broadcast tensor shapes {left:?} and {right:?}"
263            )));
264        }
265    }
266    Ok(out)
267}
268
269fn select_cell(tensor: &Tensor, coord: &[usize], result_shape: &[usize]) -> Result<Value> {
270    let shape = tensor.shape();
271    let rank_gap = result_shape.len().saturating_sub(shape.len());
272    let mut local = Vec::with_capacity(shape.len());
273    for (axis, dim) in shape.iter().enumerate() {
274        let result_axis = axis + rank_gap;
275        let coord_value = coord
276            .get(result_axis)
277            .copied()
278            .ok_or_else(|| Error::Eval("tensor broadcast axis mismatch".to_owned()))?;
279        local.push(if *dim == 1 { 0 } else { coord_value });
280    }
281    let flat = Tensor::flat_offset(shape, &local)?;
282    tensor.cell(flat)
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn zero_extent_broadcast_does_not_expand_to_one() {
291        assert_eq!(broadcast_shape(&[0], &[1]).unwrap(), vec![0]);
292        assert_eq!(broadcast_shape(&[1, 0], &[3, 1]).unwrap(), vec![3, 0]);
293    }
294
295    #[test]
296    fn incompatible_shapes_fail_closed() {
297        let err = broadcast_shape(&[2, 3], &[2]).unwrap_err();
298        assert!(err.to_string().contains("cannot broadcast"));
299    }
300
301    #[test]
302    fn tensor_op_symbols_map_to_scalar_symbols() {
303        assert_eq!(
304            binary_math_operator(&add_op_symbol()),
305            Some(Symbol::qualified("math", "add"))
306        );
307        assert_eq!(
308            unary_math_operator(&neg_op_symbol()),
309            Some(Symbol::qualified("math", "neg"))
310        );
311    }
312}