1use crate::{core_ops::Tensor, TensorElement};
8use scirs2_core::numeric::FromPrimitive;
9use std::any::{Any, TypeId};
10use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12use torsh_core::error::{Result, TorshError};
13use torsh_core::sync::RwLockExt;
14
15pub trait CustomOperation<T: TensorElement>: Send + Sync {
20 fn name(&self) -> &str;
22
23 fn description(&self) -> &str;
25
26 fn forward(&self, inputs: &[Tensor<T>], params: &OperationParams) -> Result<Vec<Tensor<T>>>;
35
36 fn backward(
47 &self,
48 grad_outputs: &[Tensor<T>],
49 inputs: &[Tensor<T>],
50 _outputs: &[Tensor<T>],
51 _params: &OperationParams,
52 ) -> Result<Vec<Option<Tensor<T>>>> {
53 let _ = grad_outputs.is_empty(); Ok(vec![None; inputs.len()])
59 }
60
61 fn validate_inputs(&self, inputs: &[Tensor<T>], _params: &OperationParams) -> Result<()> {
70 if inputs.is_empty() {
72 return Err(torsh_core::error::TorshError::InvalidShape(
73 "Operation requires at least one input tensor".to_string(),
74 ));
75 }
76
77 for (idx, input) in inputs.iter().enumerate() {
79 let _ = (idx, input.shape.is_empty()); }
81
82 Ok(())
83 }
84
85 fn output_shapes(
94 &self,
95 input_shapes: &[Vec<usize>],
96 params: &OperationParams,
97 ) -> Result<Vec<Vec<usize>>>;
98
99 fn supports_autograd(&self) -> bool {
101 true }
103
104 fn num_inputs(&self) -> usize;
106
107 fn num_outputs(&self) -> usize;
109}
110
111#[derive(Debug, Clone)]
113pub struct OperationParams {
114 pub strings: HashMap<String, String>,
116 pub integers: HashMap<String, i64>,
118 pub floats: HashMap<String, f64>,
120 pub booleans: HashMap<String, bool>,
122 pub vectors: HashMap<String, Vec<f64>>,
124 pub shapes: HashMap<String, Vec<usize>>,
126}
127
128impl OperationParams {
129 pub fn new() -> Self {
131 Self {
132 strings: HashMap::new(),
133 integers: HashMap::new(),
134 floats: HashMap::new(),
135 booleans: HashMap::new(),
136 vectors: HashMap::new(),
137 shapes: HashMap::new(),
138 }
139 }
140
141 pub fn with_string(mut self, key: &str, value: &str) -> Self {
143 self.strings.insert(key.to_string(), value.to_string());
144 self
145 }
146
147 pub fn with_int(mut self, key: &str, value: i64) -> Self {
149 self.integers.insert(key.to_string(), value);
150 self
151 }
152
153 pub fn with_float(mut self, key: &str, value: f64) -> Self {
155 self.floats.insert(key.to_string(), value);
156 self
157 }
158
159 pub fn with_bool(mut self, key: &str, value: bool) -> Self {
161 self.booleans.insert(key.to_string(), value);
162 self
163 }
164
165 pub fn with_vector(mut self, key: &str, value: Vec<f64>) -> Self {
167 self.vectors.insert(key.to_string(), value);
168 self
169 }
170
171 pub fn with_shape(mut self, key: &str, value: Vec<usize>) -> Self {
173 self.shapes.insert(key.to_string(), value);
174 self
175 }
176
177 pub fn get_string(&self, key: &str) -> Option<&String> {
179 self.strings.get(key)
180 }
181
182 pub fn get_int(&self, key: &str) -> Option<i64> {
184 self.integers.get(key).copied()
185 }
186
187 pub fn get_float(&self, key: &str) -> Option<f64> {
189 self.floats.get(key).copied()
190 }
191
192 pub fn get_bool(&self, key: &str) -> Option<bool> {
194 self.booleans.get(key).copied()
195 }
196
197 pub fn get_vector(&self, key: &str) -> Option<&Vec<f64>> {
199 self.vectors.get(key)
200 }
201
202 pub fn get_shape(&self, key: &str) -> Option<&Vec<usize>> {
204 self.shapes.get(key)
205 }
206}
207
208impl Default for OperationParams {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214#[derive(Debug, Clone)]
216pub struct OperationMetadata {
217 pub name: String,
219 pub description: String,
221 pub num_inputs: usize,
223 pub num_outputs: usize,
225 pub supports_autograd: bool,
227 pub data_type: TypeId,
229 pub version: String,
231 pub author: Option<String>,
233 pub tags: Vec<String>,
235}
236
237pub struct CustomOperationRegistry {
242 operations: RwLock<HashMap<(TypeId, String), Arc<dyn Any + Send + Sync>>>,
244 metadata: RwLock<HashMap<(TypeId, String), OperationMetadata>>,
246}
247
248impl CustomOperationRegistry {
249 pub fn new() -> Self {
251 Self {
252 operations: RwLock::new(HashMap::new()),
253 metadata: RwLock::new(HashMap::new()),
254 }
255 }
256
257 pub fn register<T: TensorElement + 'static>(
268 &self,
269 operation: Box<dyn CustomOperation<T>>,
270 version: &str,
271 author: Option<String>,
272 tags: Vec<String>,
273 ) -> Result<()> {
274 let type_id = TypeId::of::<T>();
275 let name = operation.name().to_string();
276 let key = (type_id, name.clone());
277
278 let metadata = OperationMetadata {
280 name: name.clone(),
281 description: operation.description().to_string(),
282 num_inputs: operation.num_inputs(),
283 num_outputs: operation.num_outputs(),
284 supports_autograd: operation.supports_autograd(),
285 data_type: type_id,
286 version: version.to_string(),
287 author,
288 tags,
289 };
290
291 {
293 let mut ops = self.operations.write_or_recover();
294 let mut meta = self.metadata.write_or_recover();
295
296 if ops.contains_key(&key) {
297 return Err(TorshError::InvalidArgument(format!(
298 "Operation '{}' for type {:?} is already registered",
299 name, type_id
300 )));
301 }
302
303 let arc_op: Arc<dyn CustomOperation<T>> = Arc::from(operation);
305 let boxed_any: Arc<dyn Any + Send + Sync> = Arc::new(arc_op);
306 ops.insert(key.clone(), boxed_any);
307 meta.insert(key, metadata);
308 }
309
310 Ok(())
311 }
312
313 pub fn get<T: TensorElement + 'static>(
321 &self,
322 name: &str,
323 ) -> Option<Arc<dyn CustomOperation<T>>> {
324 let type_id = TypeId::of::<T>();
325 let key = (type_id, name.to_string());
326
327 let ops = self.operations.read_or_recover();
328 ops.get(&key).and_then(|arc_any| {
329 arc_any
331 .downcast_ref::<Arc<dyn CustomOperation<T>>>()
332 .map(|arc_op| Arc::clone(arc_op))
333 })
334 }
335
336 pub fn get_metadata<T: TensorElement + 'static>(
338 &self,
339 name: &str,
340 ) -> Option<OperationMetadata> {
341 let type_id = TypeId::of::<T>();
342 let key = (type_id, name.to_string());
343
344 let meta = self.metadata.read_or_recover();
345 meta.get(&key).cloned()
346 }
347
348 pub fn list_operations<T: TensorElement + 'static>(&self) -> Vec<String> {
350 let type_id = TypeId::of::<T>();
351 let meta = self.metadata.read_or_recover();
352
353 meta.keys()
354 .filter(|(tid, _)| *tid == type_id)
355 .map(|(_, name)| name.clone())
356 .collect()
357 }
358
359 pub fn unregister<T: TensorElement + 'static>(&self, name: &str) -> Result<()> {
361 let type_id = TypeId::of::<T>();
362 let key = (type_id, name.to_string());
363
364 let mut ops = self.operations.write_or_recover();
365 let mut meta = self.metadata.write_or_recover();
366
367 if ops.remove(&key).is_none() {
368 return Err(TorshError::InvalidArgument(format!(
369 "Operation '{}' for type {:?} is not registered",
370 name, type_id
371 )));
372 }
373
374 meta.remove(&key);
375 Ok(())
376 }
377
378 pub fn is_registered<T: TensorElement + 'static>(&self, name: &str) -> bool {
380 let type_id = TypeId::of::<T>();
381 let key = (type_id, name.to_string());
382
383 let ops = self.operations.read_or_recover();
384 ops.contains_key(&key)
385 }
386
387 pub fn count(&self) -> usize {
389 let ops = self.operations.read_or_recover();
390 ops.len()
391 }
392
393 pub fn clear(&self) {
395 let mut ops = self.operations.write_or_recover();
396 let mut meta = self.metadata.write_or_recover();
397 ops.clear();
398 meta.clear();
399 }
400}
401
402impl Default for CustomOperationRegistry {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408static GLOBAL_REGISTRY: std::sync::LazyLock<CustomOperationRegistry> =
410 std::sync::LazyLock::new(CustomOperationRegistry::new);
411
412pub fn global_registry() -> &'static CustomOperationRegistry {
414 &GLOBAL_REGISTRY
415}
416
417pub trait TensorCustomOps<T: TensorElement> {
419 fn apply_custom_op(
429 &self,
430 op_name: &str,
431 other_inputs: &[&Tensor<T>],
432 params: &OperationParams,
433 ) -> Result<Vec<Tensor<T>>>;
434
435 fn apply_custom_op_with_registry(
437 &self,
438 registry: &CustomOperationRegistry,
439 op_name: &str,
440 other_inputs: &[&Tensor<T>],
441 params: &OperationParams,
442 ) -> Result<Vec<Tensor<T>>>;
443}
444
445impl<T: TensorElement + 'static> TensorCustomOps<T> for Tensor<T> {
446 fn apply_custom_op(
447 &self,
448 op_name: &str,
449 other_inputs: &[&Tensor<T>],
450 params: &OperationParams,
451 ) -> Result<Vec<Tensor<T>>> {
452 self.apply_custom_op_with_registry(global_registry(), op_name, other_inputs, params)
453 }
454
455 fn apply_custom_op_with_registry(
456 &self,
457 registry: &CustomOperationRegistry,
458 op_name: &str,
459 other_inputs: &[&Tensor<T>],
460 params: &OperationParams,
461 ) -> Result<Vec<Tensor<T>>> {
462 let operation = registry.get::<T>(op_name).ok_or_else(|| {
464 TorshError::InvalidArgument(format!(
465 "Custom operation '{}' not found for type",
466 op_name
467 ))
468 })?;
469
470 let mut inputs = vec![self.clone()];
472 inputs.extend(other_inputs.iter().map(|&t| t.clone()));
473
474 operation.validate_inputs(&inputs, params)?;
476
477 if inputs.len() != operation.num_inputs() {
479 return Err(TorshError::InvalidArgument(format!(
480 "Operation '{}' expects {} inputs, got {}",
481 op_name,
482 operation.num_inputs(),
483 inputs.len()
484 )));
485 }
486
487 let outputs = operation.forward(&inputs, params)?;
489
490 if outputs.len() != operation.num_outputs() {
492 return Err(TorshError::InvalidArgument(format!(
493 "Operation '{}' produced {} outputs, expected {}",
494 op_name,
495 outputs.len(),
496 operation.num_outputs()
497 )));
498 }
499
500 Ok(outputs)
501 }
502}
503
504pub struct ScaleOperation;
508
509impl<T: TensorElement + Copy + std::ops::Mul<Output = T> + num_traits::FromPrimitive>
510 CustomOperation<T> for ScaleOperation
511{
512 fn name(&self) -> &str {
513 "scale"
514 }
515
516 fn description(&self) -> &str {
517 "Scales tensor elements by a constant factor"
518 }
519
520 fn forward(&self, inputs: &[Tensor<T>], params: &OperationParams) -> Result<Vec<Tensor<T>>> {
521 if inputs.len() != 1 {
522 return Err(TorshError::InvalidArgument(
523 "Scale operation requires exactly 1 input".to_string(),
524 ));
525 }
526
527 let scale = params.get_float("scale").unwrap_or(1.0);
528 let scale_val = <T as FromPrimitive>::from_f64(scale).ok_or_else(|| {
529 TorshError::InvalidArgument("Cannot convert scale factor to tensor type".to_string())
530 })?;
531
532 let result = inputs[0].mul_scalar(scale_val)?;
533 Ok(vec![result])
534 }
535
536 fn backward(
537 &self,
538 grad_outputs: &[Tensor<T>],
539 _inputs: &[Tensor<T>],
540 _outputs: &[Tensor<T>],
541 params: &OperationParams,
542 ) -> Result<Vec<Option<Tensor<T>>>> {
543 let scale = params.get_float("scale").unwrap_or(1.0);
544 let scale_val = <T as FromPrimitive>::from_f64(scale).ok_or_else(|| {
545 TorshError::InvalidArgument("Cannot convert scale factor to tensor type".to_string())
546 })?;
547
548 let grad_input = grad_outputs[0].mul_scalar(scale_val)?;
549 Ok(vec![Some(grad_input)])
550 }
551
552 fn output_shapes(
553 &self,
554 input_shapes: &[Vec<usize>],
555 _params: &OperationParams,
556 ) -> Result<Vec<Vec<usize>>> {
557 if input_shapes.len() != 1 {
558 return Err(TorshError::InvalidArgument(
559 "Scale operation requires exactly 1 input".to_string(),
560 ));
561 }
562 Ok(vec![input_shapes[0].clone()])
563 }
564
565 fn num_inputs(&self) -> usize {
566 1
567 }
568
569 fn num_outputs(&self) -> usize {
570 1
571 }
572}
573
574pub struct ConcatOperation;
576
577impl<T: TensorElement + Copy> CustomOperation<T> for ConcatOperation {
578 fn name(&self) -> &str {
579 "concat"
580 }
581
582 fn description(&self) -> &str {
583 "Concatenates tensors along a specified axis"
584 }
585
586 fn forward(&self, inputs: &[Tensor<T>], params: &OperationParams) -> Result<Vec<Tensor<T>>> {
587 if inputs.len() < 2 {
588 return Err(TorshError::InvalidArgument(
589 "Concat operation requires at least 2 inputs".to_string(),
590 ));
591 }
592
593 let axis = params.get_int("axis").unwrap_or(0) as usize;
594
595 let input_refs: Vec<&Tensor<T>> = inputs.iter().collect();
597 let result = Tensor::cat(&input_refs, axis as i32)?;
598 Ok(vec![result])
599 }
600
601 fn backward(
602 &self,
603 grad_outputs: &[Tensor<T>],
604 inputs: &[Tensor<T>],
605 _outputs: &[Tensor<T>],
606 params: &OperationParams,
607 ) -> Result<Vec<Option<Tensor<T>>>> {
608 let axis = params.get_int("axis").unwrap_or(0) as usize;
609 let grad_output = &grad_outputs[0];
610
611 let mut split_sizes = Vec::new();
613 for input in inputs {
614 split_sizes.push(input.shape().dims()[axis]);
615 }
616
617 let mut grad_inputs = Vec::new();
619 let mut start = 0;
620 for &size in &split_sizes {
621 let end = start + size;
622 let slice = grad_output.slice_tensor(axis, start, end)?;
623 grad_inputs.push(Some(slice));
624 start = end;
625 }
626 Ok(grad_inputs)
627 }
628
629 fn output_shapes(
630 &self,
631 input_shapes: &[Vec<usize>],
632 params: &OperationParams,
633 ) -> Result<Vec<Vec<usize>>> {
634 if input_shapes.len() < 2 {
635 return Err(TorshError::InvalidArgument(
636 "Concat operation requires at least 2 inputs".to_string(),
637 ));
638 }
639
640 let axis = params.get_int("axis").unwrap_or(0) as usize;
641 let mut output_shape = input_shapes[0].clone();
642
643 if axis >= output_shape.len() {
644 return Err(TorshError::InvalidArgument(format!(
645 "Concat axis {} out of bounds for {} dimensions",
646 axis,
647 output_shape.len()
648 )));
649 }
650
651 let mut total_size = output_shape[axis];
653 for shape in &input_shapes[1..] {
654 if shape.len() != output_shape.len() {
655 return Err(TorshError::InvalidArgument(
656 "All tensors must have the same number of dimensions".to_string(),
657 ));
658 }
659
660 for (i, (&dim1, &dim2)) in output_shape.iter().zip(shape.iter()).enumerate() {
662 if i != axis && dim1 != dim2 {
663 return Err(TorshError::InvalidArgument(format!(
664 "Dimension {} mismatch: {} vs {}",
665 i, dim1, dim2
666 )));
667 }
668 }
669
670 total_size += shape[axis];
671 }
672
673 output_shape[axis] = total_size;
674 Ok(vec![output_shape])
675 }
676
677 fn num_inputs(&self) -> usize {
678 2 }
681
682 fn num_outputs(&self) -> usize {
683 1
684 }
685
686 fn validate_inputs(&self, inputs: &[Tensor<T>], params: &OperationParams) -> Result<()> {
687 if inputs.len() < 2 {
688 return Err(TorshError::InvalidArgument(
689 "Concat operation requires at least 2 inputs".to_string(),
690 ));
691 }
692
693 let axis = params.get_int("axis").unwrap_or(0) as usize;
694 let first_tensor_shape = inputs[0].shape();
695 let first_shape = first_tensor_shape.dims();
696
697 if axis >= first_shape.len() {
698 return Err(TorshError::InvalidArgument(format!(
699 "Concat axis {} out of bounds for {} dimensions",
700 axis,
701 first_shape.len()
702 )));
703 }
704
705 for (i, tensor) in inputs.iter().enumerate().skip(1) {
707 let tensor_shape = tensor.shape();
708 let shape = tensor_shape.dims();
709 if shape.len() != first_shape.len() {
710 return Err(TorshError::InvalidArgument(format!(
711 "Tensor {} has {} dimensions, expected {}",
712 i,
713 shape.len(),
714 first_shape.len()
715 )));
716 }
717
718 for (dim_idx, (&dim1, &dim2)) in first_shape.iter().zip(shape.iter()).enumerate() {
719 if dim_idx != axis && dim1 != dim2 {
720 return Err(TorshError::InvalidArgument(format!(
721 "Tensor {} dimension {} mismatch: {} vs {}",
722 i, dim_idx, dim1, dim2
723 )));
724 }
725 }
726 }
727
728 Ok(())
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use torsh_core::device::DeviceType;
736
737 #[test]
738 fn test_operation_params() {
739 let params = OperationParams::new()
740 .with_string("mode", "linear")
741 .with_int("axis", 1)
742 .with_float("scale", 2.5)
743 .with_bool("inplace", false)
744 .with_vector("weights", vec![1.0, 2.0, 3.0])
745 .with_shape("target_shape", vec![10, 20]);
746
747 assert_eq!(params.get_string("mode"), Some(&"linear".to_string()));
748 assert_eq!(params.get_int("axis"), Some(1));
749 assert_eq!(params.get_float("scale"), Some(2.5));
750 assert_eq!(params.get_bool("inplace"), Some(false));
751 assert_eq!(params.get_vector("weights"), Some(&vec![1.0, 2.0, 3.0]));
752 assert_eq!(params.get_shape("target_shape"), Some(&vec![10, 20]));
753
754 assert_eq!(params.get_string("nonexistent"), None);
755 }
756
757 #[test]
758 fn test_registry_operations() {
759 let registry = CustomOperationRegistry::new();
760
761 let scale_op = Box::new(ScaleOperation);
763 registry
764 .register::<f32>(
765 scale_op,
766 "1.0.0",
767 Some("Test".to_string()),
768 vec!["math".to_string()],
769 )
770 .expect("registration should succeed");
771
772 assert!(registry.is_registered::<f32>("scale"));
774 assert!(!registry.is_registered::<f32>("nonexistent"));
775
776 let metadata = registry
778 .get_metadata::<f32>("scale")
779 .expect("metadata retrieval should succeed");
780 assert_eq!(metadata.name, "scale");
781 assert_eq!(
782 metadata.description,
783 "Scales tensor elements by a constant factor"
784 );
785 assert_eq!(metadata.num_inputs, 1);
786 assert_eq!(metadata.num_outputs, 1);
787 assert_eq!(metadata.version, "1.0.0");
788 assert_eq!(metadata.author, Some("Test".to_string()));
789 assert_eq!(metadata.tags, vec!["math".to_string()]);
790
791 let ops = registry.list_operations::<f32>();
793 assert_eq!(ops, vec!["scale".to_string()]);
794
795 registry
797 .unregister::<f32>("scale")
798 .expect("unregister should succeed");
799 assert!(!registry.is_registered::<f32>("scale"));
800 }
801
802 #[test]
803 fn test_scale_operation() {
804 let registry = CustomOperationRegistry::new();
805 let scale_op = Box::new(ScaleOperation);
806 registry
807 .register::<f32>(scale_op, "1.0.0", None, vec![])
808 .expect("unregister should succeed");
809
810 let data = vec![1.0f32, 2.0, 3.0, 4.0];
812 let tensor = Tensor::from_data(data, vec![2, 2], DeviceType::Cpu)
813 .expect("tensor creation should succeed");
814
815 let params = OperationParams::new().with_float("scale", 2.0);
817 let results = tensor
818 .apply_custom_op_with_registry(®istry, "scale", &[], ¶ms)
819 .expect("tensor creation should succeed");
820
821 assert_eq!(results.len(), 1);
822 let result = &results[0];
823 let expected_data = vec![2.0f32, 4.0, 6.0, 8.0];
824 assert_eq!(
825 result.data().expect("data retrieval should succeed"),
826 expected_data
827 );
828 }
829
830 #[test]
831 fn test_concat_operation() {
832 let registry = CustomOperationRegistry::new();
833 let concat_op = Box::new(ConcatOperation);
834 registry
835 .register::<f32>(concat_op, "1.0.0", None, vec![])
836 .expect("registration should succeed");
837
838 let data1 = vec![1.0f32, 2.0];
840 let tensor1 = Tensor::from_data(data1, vec![2], DeviceType::Cpu)
841 .expect("tensor creation should succeed");
842
843 let data2 = vec![3.0f32, 4.0];
844 let tensor2 = Tensor::from_data(data2, vec![2], DeviceType::Cpu)
845 .expect("tensor creation should succeed");
846
847 let params = OperationParams::new().with_int("axis", 0);
849 let results = tensor1
850 .apply_custom_op_with_registry(®istry, "concat", &[&tensor2], ¶ms)
851 .expect("tensor creation should succeed");
852
853 assert_eq!(results.len(), 1);
854 let result = &results[0];
855 assert_eq!(result.shape().dims(), &[4]); let expected_data = vec![1.0f32, 2.0, 3.0, 4.0];
857 assert_eq!(
858 result.data().expect("data retrieval should succeed"),
859 expected_data
860 );
861 }
862
863 #[test]
864 fn test_operation_validation() {
865 let registry = CustomOperationRegistry::new();
866 let concat_op = Box::new(ConcatOperation);
867 registry
868 .register::<f32>(concat_op, "1.0.0", None, vec![])
869 .expect("registration should succeed");
870
871 let data1 = vec![1.0f32, 2.0];
873 let tensor1 = Tensor::from_data(data1, vec![2], DeviceType::Cpu)
874 .expect("tensor creation should succeed"); let data2 = vec![3.0f32, 4.0, 5.0, 6.0];
877 let tensor2 = Tensor::from_data(data2, vec![2, 2], DeviceType::Cpu)
878 .expect("tensor creation should succeed"); let params = OperationParams::new().with_int("axis", 0);
882 let result =
883 tensor1.apply_custom_op_with_registry(®istry, "concat", &[&tensor2], ¶ms);
884 assert!(result.is_err());
885 }
886
887 #[test]
888 fn test_output_shape_inference() {
889 let concat_op = ConcatOperation;
890
891 let input_shapes = vec![vec![3], vec![4]];
893 let params = OperationParams::new().with_int("axis", 0);
894
895 let output_shapes = <ConcatOperation as CustomOperation<f32>>::output_shapes(
896 &concat_op,
897 &input_shapes,
898 ¶ms,
899 )
900 .expect("custom dtype operation should succeed");
901 assert_eq!(output_shapes, vec![vec![7]]); }
903
904 #[test]
905 fn test_error_cases() {
906 let registry = CustomOperationRegistry::new();
907
908 let scale_op1 = Box::new(ScaleOperation);
910 let scale_op2 = Box::new(ScaleOperation);
911
912 registry
913 .register::<f32>(scale_op1, "1.0.0", None, vec![])
914 .expect("registration should succeed");
915 let result = registry.register::<f32>(scale_op2, "1.0.0", None, vec![]);
916 assert!(result.is_err());
917
918 let result = registry.unregister::<f32>("nonexistent");
920 assert!(result.is_err());
921
922 let data = vec![1.0f32, 2.0];
924 let tensor = Tensor::from_data(data, vec![1, 2], DeviceType::Cpu)
925 .expect("tensor creation should succeed");
926 let params = OperationParams::new();
927 let result = tensor.apply_custom_op_with_registry(®istry, "nonexistent", &[], ¶ms);
928 assert!(result.is_err());
929 }
930
931 #[test]
932 fn test_global_registry() {
933 let registry = global_registry();
934
935 let scale_op = Box::new(ScaleOperation);
937 registry
938 .register::<f32>(scale_op, "1.0.0", None, vec![])
939 .expect("registration should succeed");
940
941 let data = vec![1.0f32, 2.0, 3.0];
943 let tensor = Tensor::from_data(data, vec![3], DeviceType::Cpu)
944 .expect("tensor creation should succeed");
945 let params = OperationParams::new().with_float("scale", 3.0);
946
947 let results = tensor
948 .apply_custom_op("scale", &[], ¶ms)
949 .expect("custom_op should succeed");
950 assert_eq!(results.len(), 1);
951 let expected_data = vec![3.0f32, 6.0, 9.0];
952 assert_eq!(
953 results[0].data().expect("data retrieval should succeed"),
954 expected_data
955 );
956
957 registry
959 .unregister::<f32>("scale")
960 .expect("unregister should succeed");
961 }
962}