Skip to main content

torsh_tensor/
custom_ops.rs

1//! Custom operation registration system for torsh-tensor
2//!
3//! This module provides a flexible system for registering custom operations that can be used
4//! with tensors, including automatic differentiation support. Users can define their own
5//! operations and integrate them seamlessly with the existing tensor API.
6
7use 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
15/// Trait for custom operation implementations
16///
17/// Custom operations must implement this trait to be registered and used with tensors.
18/// The trait provides both forward and backward operations for automatic differentiation.
19pub trait CustomOperation<T: TensorElement>: Send + Sync {
20    /// Get the name of this operation
21    fn name(&self) -> &str;
22
23    /// Get a description of what this operation does
24    fn description(&self) -> &str;
25
26    /// Execute the forward pass of the operation
27    ///
28    /// # Arguments
29    /// * `inputs` - Input tensors for the operation
30    /// * `params` - Optional parameters for the operation
31    ///
32    /// # Returns
33    /// The result tensor(s) from applying this operation
34    fn forward(&self, inputs: &[Tensor<T>], params: &OperationParams) -> Result<Vec<Tensor<T>>>;
35
36    /// Execute the backward pass of the operation (optional for non-differentiable ops)
37    ///
38    /// # Arguments
39    /// * `grad_outputs` - Gradients with respect to the outputs
40    /// * `inputs` - Original input tensors
41    /// * `outputs` - Original output tensors
42    /// * `params` - Operation parameters
43    ///
44    /// # Returns
45    /// Gradients with respect to the inputs
46    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        // Default implementation for non-differentiable operations
54        // Validate that we have gradient outputs matching expected count
55        let _ = grad_outputs.is_empty(); // Check if empty but continue
56
57        // Return None gradients for all inputs (non-differentiable by default)
58        Ok(vec![None; inputs.len()])
59    }
60
61    /// Validate that the inputs are compatible with this operation
62    ///
63    /// # Arguments
64    /// * `inputs` - Input tensors to validate
65    /// * `params` - Operation parameters
66    ///
67    /// # Returns
68    /// True if inputs are valid, false otherwise
69    fn validate_inputs(&self, inputs: &[Tensor<T>], _params: &OperationParams) -> Result<()> {
70        // Default implementation - basic validation
71        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        // Validate that all input tensors have data
78        for (idx, input) in inputs.iter().enumerate() {
79            let _ = (idx, input.shape.is_empty()); // Check shape validity
80        }
81
82        Ok(())
83    }
84
85    /// Get the expected output shapes given input shapes
86    ///
87    /// # Arguments
88    /// * `input_shapes` - Shapes of input tensors
89    /// * `params` - Operation parameters
90    ///
91    /// # Returns
92    /// Expected shapes of output tensors
93    fn output_shapes(
94        &self,
95        input_shapes: &[Vec<usize>],
96        params: &OperationParams,
97    ) -> Result<Vec<Vec<usize>>>;
98
99    /// Check if this operation supports automatic differentiation
100    fn supports_autograd(&self) -> bool {
101        true // Most operations should support autograd
102    }
103
104    /// Get the number of expected inputs
105    fn num_inputs(&self) -> usize;
106
107    /// Get the number of expected outputs
108    fn num_outputs(&self) -> usize;
109}
110
111/// Parameters that can be passed to custom operations
112#[derive(Debug, Clone)]
113pub struct OperationParams {
114    /// String parameters
115    pub strings: HashMap<String, String>,
116    /// Integer parameters
117    pub integers: HashMap<String, i64>,
118    /// Float parameters
119    pub floats: HashMap<String, f64>,
120    /// Boolean parameters
121    pub booleans: HashMap<String, bool>,
122    /// Vector parameters
123    pub vectors: HashMap<String, Vec<f64>>,
124    /// Shape parameters
125    pub shapes: HashMap<String, Vec<usize>>,
126}
127
128impl OperationParams {
129    /// Create a new empty parameter set
130    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    /// Add a string parameter
142    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    /// Add an integer parameter
148    pub fn with_int(mut self, key: &str, value: i64) -> Self {
149        self.integers.insert(key.to_string(), value);
150        self
151    }
152
153    /// Add a float parameter
154    pub fn with_float(mut self, key: &str, value: f64) -> Self {
155        self.floats.insert(key.to_string(), value);
156        self
157    }
158
159    /// Add a boolean parameter
160    pub fn with_bool(mut self, key: &str, value: bool) -> Self {
161        self.booleans.insert(key.to_string(), value);
162        self
163    }
164
165    /// Add a vector parameter
166    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    /// Add a shape parameter
172    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    /// Get a string parameter
178    pub fn get_string(&self, key: &str) -> Option<&String> {
179        self.strings.get(key)
180    }
181
182    /// Get an integer parameter
183    pub fn get_int(&self, key: &str) -> Option<i64> {
184        self.integers.get(key).copied()
185    }
186
187    /// Get a float parameter
188    pub fn get_float(&self, key: &str) -> Option<f64> {
189        self.floats.get(key).copied()
190    }
191
192    /// Get a boolean parameter
193    pub fn get_bool(&self, key: &str) -> Option<bool> {
194        self.booleans.get(key).copied()
195    }
196
197    /// Get a vector parameter
198    pub fn get_vector(&self, key: &str) -> Option<&Vec<f64>> {
199        self.vectors.get(key)
200    }
201
202    /// Get a shape parameter
203    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/// Metadata about a registered operation
215#[derive(Debug, Clone)]
216pub struct OperationMetadata {
217    /// Operation name
218    pub name: String,
219    /// Operation description
220    pub description: String,
221    /// Number of inputs
222    pub num_inputs: usize,
223    /// Number of outputs
224    pub num_outputs: usize,
225    /// Whether the operation supports autograd
226    pub supports_autograd: bool,
227    /// Data type this operation is registered for
228    pub data_type: TypeId,
229    /// Version of the operation
230    pub version: String,
231    /// Author/creator information
232    pub author: Option<String>,
233    /// Additional tags for categorization
234    pub tags: Vec<String>,
235}
236
237/// Registry for custom operations
238///
239/// This registry maintains a collection of custom operations that can be applied to tensors.
240/// Operations are stored per data type to ensure type safety.
241pub struct CustomOperationRegistry {
242    /// Operations stored by (TypeId, operation_name)
243    operations: RwLock<HashMap<(TypeId, String), Arc<dyn Any + Send + Sync>>>,
244    /// Metadata for registered operations
245    metadata: RwLock<HashMap<(TypeId, String), OperationMetadata>>,
246}
247
248impl CustomOperationRegistry {
249    /// Create a new operation registry
250    pub fn new() -> Self {
251        Self {
252            operations: RwLock::new(HashMap::new()),
253            metadata: RwLock::new(HashMap::new()),
254        }
255    }
256
257    /// Register a custom operation
258    ///
259    /// # Arguments
260    /// * `operation` - The operation implementation
261    /// * `version` - Version string for this operation
262    /// * `author` - Optional author information
263    /// * `tags` - Optional tags for categorization
264    ///
265    /// # Returns
266    /// Success or error if registration fails
267    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        // Create metadata
279        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        // Store the operation and metadata
292        {
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            // Store the operation as Arc<dyn CustomOperation<T>> wrapped in Arc<dyn Any>
304            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    /// Get a registered operation
314    ///
315    /// # Arguments
316    /// * `name` - Name of the operation to retrieve
317    ///
318    /// # Returns
319    /// Reference to the operation if found
320    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            // Downcast Arc<dyn Any> to Arc<dyn CustomOperation<T>>
330            arc_any
331                .downcast_ref::<Arc<dyn CustomOperation<T>>>()
332                .map(|arc_op| Arc::clone(arc_op))
333        })
334    }
335
336    /// Get metadata for a registered operation
337    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    /// List all registered operations for a given type
349    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    /// Remove a registered operation
360    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    /// Check if an operation is registered
379    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    /// Get total number of registered operations
388    pub fn count(&self) -> usize {
389        let ops = self.operations.read_or_recover();
390        ops.len()
391    }
392
393    /// Clear all registered operations
394    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
408/// Global custom operation registry
409static GLOBAL_REGISTRY: std::sync::LazyLock<CustomOperationRegistry> =
410    std::sync::LazyLock::new(CustomOperationRegistry::new);
411
412/// Get the global custom operation registry
413pub fn global_registry() -> &'static CustomOperationRegistry {
414    &GLOBAL_REGISTRY
415}
416
417/// Extension trait to add custom operation support to tensors
418pub trait TensorCustomOps<T: TensorElement> {
419    /// Apply a custom operation to this tensor
420    ///
421    /// # Arguments
422    /// * `op_name` - Name of the registered operation
423    /// * `other_inputs` - Additional input tensors (if any)
424    /// * `params` - Operation parameters
425    ///
426    /// # Returns
427    /// Result tensor(s) from the operation
428    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    /// Apply a custom operation using a specific registry
436    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        // Get the operation from the registry
463        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        // Prepare input tensors
471        let mut inputs = vec![self.clone()];
472        inputs.extend(other_inputs.iter().map(|&t| t.clone()));
473
474        // Validate inputs
475        operation.validate_inputs(&inputs, params)?;
476
477        // Check input count
478        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        // Execute the forward pass
488        let outputs = operation.forward(&inputs, params)?;
489
490        // Check output count
491        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
504// Example custom operations
505
506/// A simple element-wise scaling operation
507pub 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
574/// A tensor concatenation operation along a specified axis
575pub 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        // Use the existing cat operation from the tensor API
596        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        // Split the gradient back to match input sizes
612        let mut split_sizes = Vec::new();
613        for input in inputs {
614            split_sizes.push(input.shape().dims()[axis]);
615        }
616
617        // Use multiple slice operations instead of split_with_sizes
618        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        // Sum the sizes along the concatenation axis
652        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            // Check that all dimensions except the concat axis match
661            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        // Variable number of inputs, but we'll validate at runtime
679        2 // Minimum required
680    }
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        // Validate that all tensors have compatible shapes
706        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        // Register a scale operation
762        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        // Check registration
773        assert!(registry.is_registered::<f32>("scale"));
774        assert!(!registry.is_registered::<f32>("nonexistent"));
775
776        // Get metadata
777        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        // List operations
792        let ops = registry.list_operations::<f32>();
793        assert_eq!(ops, vec!["scale".to_string()]);
794
795        // Unregister
796        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        // Create test tensor
811        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        // Apply scale operation
816        let params = OperationParams::new().with_float("scale", 2.0);
817        let results = tensor
818            .apply_custom_op_with_registry(&registry, "scale", &[], &params)
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        // Create test tensors (1D to work with current cat implementation)
839        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        // Apply concat operation along axis 0
848        let params = OperationParams::new().with_int("axis", 0);
849        let results = tensor1
850            .apply_custom_op_with_registry(&registry, "concat", &[&tensor2], &params)
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]); // 2 + 2 = 4 elements
856        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        // Create tensors with incompatible dimensions (2D vs 1D should fail)
872        let data1 = vec![1.0f32, 2.0];
873        let tensor1 = Tensor::from_data(data1, vec![2], DeviceType::Cpu)
874            .expect("tensor creation should succeed"); // 1D tensor
875
876        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"); // 2D tensor
879
880        // This should fail validation due to different number of dimensions
881        let params = OperationParams::new().with_int("axis", 0);
882        let result =
883            tensor1.apply_custom_op_with_registry(&registry, "concat", &[&tensor2], &params);
884        assert!(result.is_err());
885    }
886
887    #[test]
888    fn test_output_shape_inference() {
889        let concat_op = ConcatOperation;
890
891        // Test shape inference for concat operation (1D tensors)
892        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            &params,
899        )
900        .expect("custom dtype operation should succeed");
901        assert_eq!(output_shapes, vec![vec![7]]); // 3 + 4 = 7 along axis 0
902    }
903
904    #[test]
905    fn test_error_cases() {
906        let registry = CustomOperationRegistry::new();
907
908        // Try to register duplicate operation
909        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        // Try to unregister non-existent operation
919        let result = registry.unregister::<f32>("nonexistent");
920        assert!(result.is_err());
921
922        // Try to apply non-existent operation
923        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(&registry, "nonexistent", &[], &params);
928        assert!(result.is_err());
929    }
930
931    #[test]
932    fn test_global_registry() {
933        let registry = global_registry();
934
935        // Register an operation in the global registry
936        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        // Use the operation via the tensor extension trait
942        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", &[], &params)
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        // Clean up
958        registry
959            .unregister::<f32>("scale")
960            .expect("unregister should succeed");
961    }
962}