sim_lib_numbers_tensor/implementation/
elementwise.rs1use 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
12pub fn add_op_symbol() -> Symbol {
14 Symbol::qualified("tensor", "op/add")
15}
16
17pub fn sub_op_symbol() -> Symbol {
19 Symbol::qualified("tensor", "op/sub")
20}
21
22pub fn mul_op_symbol() -> Symbol {
24 Symbol::qualified("tensor", "op/mul")
25}
26
27pub fn div_op_symbol() -> Symbol {
29 Symbol::qualified("tensor", "op/div")
30}
31
32pub fn rem_op_symbol() -> Symbol {
34 Symbol::qualified("tensor", "op/rem")
35}
36
37pub fn pow_op_symbol() -> Symbol {
39 Symbol::qualified("tensor", "op/pow")
40}
41
42pub 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
59pub 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
77pub 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}