Skip to main content

teeny_core/graph/
mod.rs

1/*
2 * Copyright (c) 2026 Teenygrad.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use alloc::{collections::BTreeMap, rc::Rc, string::String, sync::Arc, vec, vec::Vec};
18use core::{any::Any, cell::RefCell};
19
20use crate::{
21    dtype::{Dtype, Float, RankedTensor, Tensor},
22    nn::{
23        Layer,
24        activation::{
25            elu::{Celu, Elu, Selu},
26            gelu::{Gelu, Mish},
27            hard::{Hardshrink, Hardsigmoid, Hardswish, Hardtanh, Relu6},
28            misc::{LeakyRelu, Softplus, Softshrink, Softsign, Threshold},
29            relu::Relu,
30            sigmoid::{Logsigmoid, Sigmoid, Silu},
31            softmax::Softmax,
32            tanh::{Tanh, Tanhshrink},
33        },
34        batchnorm::{BatchNorm1d, BatchNorm2d, BatchNorm3d},
35        conv1d::Conv1d,
36        conv2d::Conv2d,
37        conv3d::Conv3d,
38        flatten::Flatten,
39        groupnorm::GroupNorm,
40        instancenorm::{InstanceNorm1d, InstanceNorm2d, InstanceNorm3d},
41        layernorm::LayerNorm,
42        linear::Linear,
43        pad::{
44            CircularPad1d, CircularPad2d, CircularPad3d, ConstantPad1d, ConstantPad2d,
45            ConstantPad3d, ReflectionPad1d, ReflectionPad2d, ReflectionPad3d, ReplicationPad1d,
46            ReplicationPad2d, ReplicationPad3d,
47        },
48        pool::{
49            AvgPool1d, AvgPool2d, AvgPool3d, LpPool1d, LpPool2d, LpPool3d, MaxPool1d, MaxPool2d,
50            MaxPool3d,
51        },
52        rmsnorm::RmsNorm,
53    },
54};
55
56/// Graph-to-FXGraph lowering.
57pub mod compiler;
58
59// ---------------------------------------------------------------------------
60// Shape — dynamic tensor shape used throughout the graph IR
61// ---------------------------------------------------------------------------
62
63/// A dynamic shape vector. Each element is either a known size (`Some(n)`) or a
64/// dynamic/unknown dimension (`None`), e.g. a batch axis whose size is determined
65/// at runtime.
66pub type Shape = Vec<Option<usize>>;
67
68// ---------------------------------------------------------------------------
69// Runtime dtype tag — used in the graph since D is erased at the node level
70// ---------------------------------------------------------------------------
71
72/// Runtime dtype tag: the graph-level (type-erased) representation of a tensor's dtype,
73/// mirroring `dtype::Dtype`'s implementors.
74#[derive(Copy, Clone, Debug, PartialEq, Eq)]
75pub enum DtypeRepr {
76    /// `bool`.
77    Bool,
78    /// Signed 8-bit integer.
79    I8,
80    /// Signed 16-bit integer.
81    I16,
82    /// Signed 32-bit integer.
83    I32,
84    /// Signed 64-bit integer.
85    I64,
86    /// Unsigned 8-bit integer.
87    U8,
88    /// Unsigned 16-bit integer.
89    U16,
90    /// Unsigned 32-bit integer.
91    U32,
92    /// Unsigned 64-bit integer.
93    U64,
94    /// 16-bit float.
95    F16,
96    /// `bfloat16`.
97    BF16,
98    /// 32-bit float.
99    F32,
100    /// 64-bit float.
101    F64,
102}
103
104// ---------------------------------------------------------------------------
105// Graph IR
106// ---------------------------------------------------------------------------
107
108/// Trait implemented by user-defined ops.
109pub trait CustomOp: Any + Send + Sync {
110    /// Identifier used in error messages and debug output.
111    fn name(&self) -> &str;
112
113    /// Compute the output shape given the shapes of all input tensors in order.
114    fn infer_output_shape(&self, input_shapes: &[&Shape]) -> Shape;
115
116    /// Expose `self` as `&dyn Any` so the custom lowering can downcast to the
117    /// concrete op type.  Implement as `fn as_any(&self) -> &dyn Any { self }`.
118    fn as_any(&self) -> &dyn Any;
119
120    /// Return kernel lowering info so `TritonLowering` can compile this op
121    /// without a project-specific middleware.  Return `None` to keep the
122    /// existing middleware / error behaviour.
123    ///
124    /// Tuple layout: `(name, kernel_source, entry_point_name, runtime_op)`.
125    /// `entry_point_name` is the PTX symbol name, conventionally `"{name}_entry_point"`.
126    fn lower(&self) -> Option<(String, String, String, Arc<dyn crate::model::RuntimeOp>)> {
127        None
128    }
129
130    /// Return the backward kernel source for this op (used in training mode).
131    /// Return an empty string if this op has no backward pass.
132    fn lower_backward_source(&self) -> String {
133        String::new()
134    }
135}
136
137/// Wrapper around `Arc<dyn CustomOp>` that implements `Debug` for [`Op`].
138#[derive(Clone)]
139pub struct CustomData(pub Arc<dyn CustomOp>);
140
141impl CustomData {
142    /// Wraps a [`CustomOp`] implementation.
143    pub fn new<T: CustomOp>(op: T) -> Self {
144        Self(Arc::new(op))
145    }
146
147    /// The wrapped op's name.
148    pub fn name(&self) -> &str {
149        self.0.name()
150    }
151
152    /// The wrapped op's inferred output shape.
153    pub fn infer_output_shape(&self, input_shapes: &[&Shape]) -> Shape {
154        self.0.infer_output_shape(input_shapes)
155    }
156
157    /// Downcast to a concrete op type via [`CustomOp::as_any`].
158    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
159        self.0.as_any().downcast_ref::<T>()
160    }
161}
162
163impl core::fmt::Debug for CustomData {
164    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
165        write!(f, "Custom({})", self.0.name())
166    }
167}
168
169/// A single computational-graph operation. Each variant corresponds to one `nn` layer or
170/// primitive op; variant fields are that op's configuration (mirroring the corresponding
171/// `nn::*` layer struct's fields).
172#[derive(Debug, Clone)]
173pub enum Op {
174    /// Model input placeholder.
175    Input,
176
177    // --- Linear / MLP ---
178    /// Fully-connected layer (see `nn::linear::Linear`).
179    Linear {
180        /// Size of the last input dimension.
181        in_features: usize,
182        /// Size of the last output dimension.
183        out_features: usize,
184        /// Whether a learned bias is added.
185        has_bias: bool,
186    },
187    /// Flattens all spatial dimensions into a single feature vector (see `nn::flatten::Flatten`).
188    Flatten,
189
190    // --- Normalisation ---
191    /// 1-D batch normalization (see `nn::batchnorm::BatchNorm1d`).
192    BatchNorm1d {
193        /// Number of channels/features.
194        num_features: usize,
195        /// Numerical stability constant.
196        eps: f64,
197        /// Running-stats exponential moving average weight.
198        momentum: f64,
199        /// Whether to learn per-channel scale/shift parameters.
200        affine: bool,
201        /// Whether to maintain running mean/variance across batches.
202        track_running_stats: bool,
203    },
204    /// 2-D batch normalization (see `nn::batchnorm::BatchNorm2d`).
205    BatchNorm2d {
206        /// Number of channels/features.
207        num_features: usize,
208        /// Numerical stability constant.
209        eps: f64,
210        /// Running-stats exponential moving average weight.
211        momentum: f64,
212        /// Whether to learn per-channel scale/shift parameters.
213        affine: bool,
214        /// Whether to maintain running mean/variance across batches.
215        track_running_stats: bool,
216    },
217    /// 3-D batch normalization (see `nn::batchnorm::BatchNorm3d`).
218    BatchNorm3d {
219        /// Number of channels/features.
220        num_features: usize,
221        /// Numerical stability constant.
222        eps: f64,
223        /// Running-stats exponential moving average weight.
224        momentum: f64,
225        /// Whether to learn per-channel scale/shift parameters.
226        affine: bool,
227        /// Whether to maintain running mean/variance across batches.
228        track_running_stats: bool,
229    },
230    /// Layer normalization (see `nn::layernorm::LayerNorm`).
231    LayerNorm {
232        /// Shape of the trailing axes to normalize over.
233        normalized_shape: alloc::vec::Vec<usize>,
234        /// Numerical stability constant.
235        eps: f64,
236        /// Whether to learn per-channel scale/shift parameters.
237        affine: bool,
238    },
239    /// RMS normalization (see `nn::rmsnorm::RmsNorm`).
240    RmsNorm {
241        /// Shape of the trailing axes to normalize over.
242        normalized_shape: alloc::vec::Vec<usize>,
243        /// Numerical stability constant.
244        eps: f64,
245        /// Whether to learn per-channel scale/shift parameters.
246        affine: bool,
247    },
248    /// Group normalization (see `nn::groupnorm::GroupNorm`).
249    GroupNorm {
250        /// Number of groups.
251        num_groups: usize,
252        /// Number of channels.
253        num_channels: usize,
254        /// Numerical stability constant.
255        eps: f64,
256        /// Whether to learn per-channel scale/shift parameters.
257        affine: bool,
258    },
259    /// 1-D instance normalization (see `nn::instancenorm::InstanceNorm1d`).
260    InstanceNorm1d {
261        /// Number of channels/features.
262        num_features: usize,
263        /// Numerical stability constant.
264        eps: f64,
265        /// Running-stats exponential moving average weight.
266        momentum: f64,
267        /// Whether to learn per-channel scale/shift parameters.
268        affine: bool,
269        /// Whether to maintain running mean/variance across batches.
270        track_running_stats: bool,
271    },
272    /// 2-D instance normalization (see `nn::instancenorm::InstanceNorm2d`).
273    InstanceNorm2d {
274        /// Number of channels/features.
275        num_features: usize,
276        /// Numerical stability constant.
277        eps: f64,
278        /// Running-stats exponential moving average weight.
279        momentum: f64,
280        /// Whether to learn per-channel scale/shift parameters.
281        affine: bool,
282        /// Whether to maintain running mean/variance across batches.
283        track_running_stats: bool,
284    },
285    /// 3-D instance normalization (see `nn::instancenorm::InstanceNorm3d`).
286    InstanceNorm3d {
287        /// Number of channels/features.
288        num_features: usize,
289        /// Numerical stability constant.
290        eps: f64,
291        /// Running-stats exponential moving average weight.
292        momentum: f64,
293        /// Whether to learn per-channel scale/shift parameters.
294        affine: bool,
295        /// Whether to maintain running mean/variance across batches.
296        track_running_stats: bool,
297    },
298
299    // --- Convolution ---
300    /// 1-D convolution (see `nn::conv1d::Conv1d`).
301    Conv1d {
302        /// Number of input channels.
303        in_channels: usize,
304        /// Number of output channels.
305        out_channels: usize,
306        /// Convolution/pooling kernel length.
307        kernel_l: usize,
308        /// Stride between kernel applications.
309        stride: usize,
310        /// Zero-padding applied to the input.
311        padding: usize,
312        /// Whether a learned bias is added.
313        has_bias: bool,
314    },
315    /// 2-D convolution (see `nn::conv2d::Conv2d`).
316    Conv2d {
317        /// Number of input channels.
318        in_channels: usize,
319        /// Number of output channels.
320        out_channels: usize,
321        /// Convolution/pooling kernel height.
322        kernel_h: usize,
323        /// Convolution/pooling kernel width.
324        kernel_w: usize,
325        /// Vertical stride.
326        stride_h: usize,
327        /// Horizontal stride.
328        stride_w: usize,
329        /// Vertical zero-padding.
330        padding_h: usize,
331        /// Horizontal zero-padding.
332        padding_w: usize,
333        /// Number of blocked/grouped connections (1 = standard).
334        groups: usize,
335        /// Whether a learned bias is added.
336        has_bias: bool,
337    },
338    /// 3-D convolution (see `nn::conv3d::Conv3d`).
339    Conv3d {
340        /// Number of input channels.
341        in_channels: usize,
342        /// Number of output channels.
343        out_channels: usize,
344        /// Convolution/pooling kernel depth.
345        kernel_d: usize,
346        /// Convolution/pooling kernel height.
347        kernel_h: usize,
348        /// Convolution/pooling kernel width.
349        kernel_w: usize,
350        /// Stride along the depth dimension.
351        stride_d: usize,
352        /// Vertical stride.
353        stride_h: usize,
354        /// Horizontal stride.
355        stride_w: usize,
356        /// Zero-padding along the depth dimension.
357        padding_d: usize,
358        /// Vertical zero-padding.
359        padding_h: usize,
360        /// Horizontal zero-padding.
361        padding_w: usize,
362        /// Whether a learned bias is added.
363        has_bias: bool,
364    },
365
366    /// Fused Conv2d + BatchNorm2d (inference-only) + SiLU forward.
367    ///
368    /// The BN parameters (scale, shift) are stored as precomputed affine
369    /// constants — not raw mean/var/gamma/beta.  Produced by `Graph::optimise()`
370    /// when it detects the pattern `Conv2d(no bias) → BatchNorm2d → Silu`.
371    ///
372    /// `bn_eps` is carried forward only for reference; the BN affine constants
373    /// are passed at runtime via the `bn_scale` and `bn_shift` parameters.
374    Conv2dBnSilu {
375        /// Number of input channels.
376        in_channels: usize,
377        /// Number of output channels.
378        out_channels: usize,
379        /// Convolution/pooling kernel height.
380        kernel_h: usize,
381        /// Convolution/pooling kernel width.
382        kernel_w: usize,
383        /// Vertical stride.
384        stride_h: usize,
385        /// Horizontal stride.
386        stride_w: usize,
387        /// Vertical zero-padding.
388        padding_h: usize,
389        /// Horizontal zero-padding.
390        padding_w: usize,
391        /// Number of blocked/grouped connections (1 = standard).
392        groups: usize,
393        /// The fused BatchNorm's numerical stability constant (kept for reference only; the
394        /// affine constants are passed at runtime via `bn_scale`/`bn_shift`).
395        bn_eps: f64,
396    },
397
398    // --- Pooling ---
399    /// 1-D average pooling (see `nn::pool::AvgPool1d`).
400    AvgPool1d {
401        /// Convolution/pooling kernel length.
402        kernel_l: usize,
403        /// Stride between kernel applications.
404        stride: usize,
405    },
406    /// 2-D average pooling (see `nn::pool::AvgPool2d`).
407    AvgPool2d {
408        /// Convolution/pooling kernel height.
409        kernel_h: usize,
410        /// Convolution/pooling kernel width.
411        kernel_w: usize,
412        /// Vertical stride.
413        stride_h: usize,
414        /// Horizontal stride.
415        stride_w: usize,
416    },
417    /// 3-D average pooling (see `nn::pool::AvgPool3d`).
418    AvgPool3d {
419        /// Convolution/pooling kernel depth.
420        kernel_d: usize,
421        /// Convolution/pooling kernel height.
422        kernel_h: usize,
423        /// Convolution/pooling kernel width.
424        kernel_w: usize,
425        /// Stride along the depth dimension.
426        stride_d: usize,
427        /// Vertical stride.
428        stride_h: usize,
429        /// Horizontal stride.
430        stride_w: usize,
431    },
432    /// 1-D max pooling (see `nn::pool::MaxPool1d`).
433    MaxPool1d {
434        /// Convolution/pooling kernel length.
435        kernel_l: usize,
436        /// Stride between kernel applications.
437        stride: usize,
438    },
439    /// 2-D max pooling (see `nn::pool::MaxPool2d`).
440    MaxPool2d {
441        /// Convolution/pooling kernel height.
442        kernel_h: usize,
443        /// Convolution/pooling kernel width.
444        kernel_w: usize,
445        /// Vertical stride.
446        stride_h: usize,
447        /// Horizontal stride.
448        stride_w: usize,
449        /// Vertical padding.
450        pad_h: usize,
451        /// Horizontal padding.
452        pad_w: usize,
453    },
454    /// 3-D max pooling (see `nn::pool::MaxPool3d`).
455    MaxPool3d {
456        /// Convolution/pooling kernel depth.
457        kernel_d: usize,
458        /// Convolution/pooling kernel height.
459        kernel_h: usize,
460        /// Convolution/pooling kernel width.
461        kernel_w: usize,
462        /// Stride along the depth dimension.
463        stride_d: usize,
464        /// Vertical stride.
465        stride_h: usize,
466        /// Horizontal stride.
467        stride_w: usize,
468    },
469    /// 1-D power-average (Lp) pooling (see `nn::pool::LpPool1d`).
470    LpPool1d {
471        /// Convolution/pooling kernel length.
472        kernel_l: usize,
473        /// Stride between kernel applications.
474        stride: usize,
475        /// The `p` in the p-norm.
476        p: f64,
477    },
478    /// 2-D power-average (Lp) pooling (see `nn::pool::LpPool2d`).
479    LpPool2d {
480        /// Convolution/pooling kernel height.
481        kernel_h: usize,
482        /// Convolution/pooling kernel width.
483        kernel_w: usize,
484        /// Vertical stride.
485        stride_h: usize,
486        /// Horizontal stride.
487        stride_w: usize,
488        /// The `p` in the p-norm.
489        p: f64,
490    },
491    /// 3-D power-average (Lp) pooling (see `nn::pool::LpPool3d`).
492    LpPool3d {
493        /// Convolution/pooling kernel depth.
494        kernel_d: usize,
495        /// Convolution/pooling kernel height.
496        kernel_h: usize,
497        /// Convolution/pooling kernel width.
498        kernel_w: usize,
499        /// Stride along the depth dimension.
500        stride_d: usize,
501        /// Vertical stride.
502        stride_h: usize,
503        /// Horizontal stride.
504        stride_w: usize,
505        /// The `p` in the p-norm.
506        p: f64,
507    },
508
509    // --- Upsample ---
510    /// Nearest-neighbour 2-D upsampling.
511    /// Output shape: `[N, C, H * scale_h, W * scale_w]`.
512    UpsampleNearest2d {
513        /// Vertical upsampling scale factor.
514        scale_h: usize,
515        /// Horizontal upsampling scale factor.
516        scale_w: usize,
517    },
518
519    // --- Padding ---
520    /// 1-D constant padding (see `nn::pad::ConstantPad1d`).
521    ConstantPad1d {
522        /// Left padding.
523        pad_left: usize,
524        /// Right padding.
525        pad_right: usize,
526        /// The constant fill value.
527        value: f64,
528    },
529    /// 2-D constant padding (see `nn::pad::ConstantPad2d`).
530    ConstantPad2d {
531        /// Left padding.
532        pad_l: usize,
533        /// Right padding.
534        pad_r: usize,
535        /// Top padding.
536        pad_t: usize,
537        /// Bottom padding.
538        pad_b: usize,
539        /// The constant fill value.
540        value: f64,
541    },
542    /// 3-D constant padding (see `nn::pad::ConstantPad3d`).
543    ConstantPad3d {
544        /// Padding before the depth dimension.
545        pad_d1: usize,
546        /// Padding after the depth dimension.
547        pad_d2: usize,
548        /// Padding before the height dimension.
549        pad_h1: usize,
550        /// Padding after the height dimension.
551        pad_h2: usize,
552        /// Padding before the width dimension.
553        pad_w1: usize,
554        /// Padding after the width dimension.
555        pad_w2: usize,
556        /// The constant fill value.
557        value: f64,
558    },
559    /// 1-D reflection padding (see `nn::pad::ReflectionPad1d`).
560    ReflectionPad1d {
561        /// Left padding.
562        pad_left: usize,
563        /// Right padding.
564        pad_right: usize,
565    },
566    /// 2-D reflection padding (see `nn::pad::ReflectionPad2d`).
567    ReflectionPad2d {
568        /// Left padding.
569        pad_l: usize,
570        /// Right padding.
571        pad_r: usize,
572        /// Top padding.
573        pad_t: usize,
574        /// Bottom padding.
575        pad_b: usize,
576    },
577    /// 3-D reflection padding (see `nn::pad::ReflectionPad3d`).
578    ReflectionPad3d {
579        /// Padding before the depth dimension.
580        pad_d1: usize,
581        /// Padding after the depth dimension.
582        pad_d2: usize,
583        /// Padding before the height dimension.
584        pad_h1: usize,
585        /// Padding after the height dimension.
586        pad_h2: usize,
587        /// Padding before the width dimension.
588        pad_w1: usize,
589        /// Padding after the width dimension.
590        pad_w2: usize,
591    },
592    /// 1-D replication padding (see `nn::pad::ReplicationPad1d`).
593    ReplicationPad1d {
594        /// Left padding.
595        pad_left: usize,
596        /// Right padding.
597        pad_right: usize,
598    },
599    /// 2-D replication padding (see `nn::pad::ReplicationPad2d`).
600    ReplicationPad2d {
601        /// Left padding.
602        pad_l: usize,
603        /// Right padding.
604        pad_r: usize,
605        /// Top padding.
606        pad_t: usize,
607        /// Bottom padding.
608        pad_b: usize,
609    },
610    /// 3-D replication padding (see `nn::pad::ReplicationPad3d`).
611    ReplicationPad3d {
612        /// Padding before the depth dimension.
613        pad_d1: usize,
614        /// Padding after the depth dimension.
615        pad_d2: usize,
616        /// Padding before the height dimension.
617        pad_h1: usize,
618        /// Padding after the height dimension.
619        pad_h2: usize,
620        /// Padding before the width dimension.
621        pad_w1: usize,
622        /// Padding after the width dimension.
623        pad_w2: usize,
624    },
625    /// 1-D circular padding (see `nn::pad::CircularPad1d`).
626    CircularPad1d {
627        /// Left padding.
628        pad_left: usize,
629        /// Right padding.
630        pad_right: usize,
631    },
632    /// 2-D circular padding (see `nn::pad::CircularPad2d`).
633    CircularPad2d {
634        /// Left padding.
635        pad_l: usize,
636        /// Right padding.
637        pad_r: usize,
638        /// Top padding.
639        pad_t: usize,
640        /// Bottom padding.
641        pad_b: usize,
642    },
643    /// 3-D circular padding (see `nn::pad::CircularPad3d`).
644    CircularPad3d {
645        /// Padding before the depth dimension.
646        pad_d1: usize,
647        /// Padding after the depth dimension.
648        pad_d2: usize,
649        /// Padding before the height dimension.
650        pad_h1: usize,
651        /// Padding after the height dimension.
652        pad_h2: usize,
653        /// Padding before the width dimension.
654        pad_w1: usize,
655        /// Padding after the width dimension.
656        pad_w2: usize,
657    },
658
659    // --- Activation ---
660    /// ReLU activation (see `nn::activation::relu::Relu`).
661    Relu,
662    /// ELU activation (see `nn::activation::elu::Elu`).
663    Elu {
664        /// The `alpha` parameter.
665        alpha: f64,
666    },
667    /// SELU activation (see `nn::activation::elu::Selu`).
668    Selu,
669    /// CELU activation (see `nn::activation::elu::Celu`).
670    Celu {
671        /// The `alpha` parameter.
672        alpha: f64,
673    },
674    /// GELU activation (see `nn::activation::gelu::Gelu`).
675    Gelu,
676    /// Mish activation (see `nn::activation::gelu::Mish`).
677    Mish,
678    /// Hardtanh activation (see `nn::activation::hard::Hardtanh`).
679    Hardtanh {
680        /// The lower clamp bound.
681        min_val: f64,
682        /// The upper clamp bound.
683        max_val: f64,
684    },
685    /// ReLU6 activation (see `nn::activation::hard::Relu6`).
686    Relu6,
687    /// Hard-sigmoid activation (see `nn::activation::hard::Hardsigmoid`).
688    Hardsigmoid,
689    /// Hard-swish activation (see `nn::activation::hard::Hardswish`).
690    Hardswish,
691    /// Hardshrink activation (see `nn::activation::hard::Hardshrink`).
692    Hardshrink {
693        /// The shrinkage threshold.
694        lambda: f64,
695    },
696    /// Leaky ReLU activation (see `nn::activation::misc::LeakyRelu`).
697    LeakyRelu {
698        /// The slope applied to negative inputs.
699        negative_slope: f64,
700    },
701    /// Threshold activation (see `nn::activation::misc::Threshold`).
702    Threshold {
703        /// The threshold value.
704        threshold: f64,
705        /// The constant fill value.
706        value: f64,
707    },
708    /// Softsign activation (see `nn::activation::misc::Softsign`).
709    Softsign,
710    /// Softshrink activation (see `nn::activation::misc::Softshrink`).
711    Softshrink {
712        /// The shrinkage threshold.
713        lambda: f64,
714    },
715    /// Softplus activation (see `nn::activation::misc::Softplus`).
716    Softplus {
717        /// The `beta` parameter.
718        beta: f64,
719        /// The threshold value.
720        threshold: f64,
721    },
722    /// Sigmoid activation (see `nn::activation::sigmoid::Sigmoid`).
723    Sigmoid,
724    /// SiLU/Swish activation (see `nn::activation::sigmoid::Silu`).
725    Silu,
726    /// Log-sigmoid activation (see `nn::activation::sigmoid::Logsigmoid`).
727    Logsigmoid,
728    /// Tanh activation (see `nn::activation::tanh::Tanh`).
729    Tanh,
730    /// Tanhshrink activation (see `nn::activation::tanh::Tanhshrink`).
731    Tanhshrink,
732    /// Softmax activation (see `nn::activation::softmax::Softmax`).
733    Softmax {
734        /// The dimension to operate along.
735        dim: usize,
736    },
737
738    // --- Attention ---
739    /// Multi-head self-attention with Flash Attention 2 and position encoding.
740    /// Represents the full `Attention.forward()` in PSABlock:
741    ///   qkv conv → FA2 → pe depthwise conv → proj conv → residual add.
742    /// Input/output shape: `[N, c, H, W]`.
743    Attention {
744        /// Number of channels.
745        c: usize,
746        /// Number of attention heads.
747        num_heads: usize,
748        /// Per-head key/query dimension.
749        key_dim: usize,
750    },
751
752    // --- Tensor structural ops ---
753    /// Element-wise addition of two tensors with identical shapes.
754    Add,
755    /// Extract one contiguous channel slice from a 4-D NCHW tensor.
756    /// Output shape: `[N, chunk_c, H, W]`.
757    ChannelChunk {
758        /// Total number of channels across all inputs/outputs.
759        c_total: usize,
760        /// Number of channels in this chunk.
761        chunk_c: usize,
762        /// Channel offset of this chunk within the total.
763        chunk_offset: usize,
764    },
765    /// Concatenate N 4-D NCHW tensors along the channel dimension.
766    /// Output shape: `[N, c_total, H, W]`.
767    ChannelCat {
768        /// Total number of channels across all inputs/outputs.
769        c_total: usize,
770    },
771    /// Adds a (C,) bias vector to a (B, C, H, W) feature map — NC layout (N=B*H*W).
772    /// Output shape equals input shape.
773    ChannelBiasAdd {
774        /// Number of channels.
775        c: usize,
776    },
777
778    /// User-defined op.  Shape and dtype must be provided via [`Graph::add_node`]
779    /// or [`SymTensor::record_custom`] — the base system cannot infer them.
780    Custom {
781        /// The wrapped user-defined op.
782        data: CustomData,
783    },
784
785    // -----------------------------------------------------------------------
786    // ONNX-sourced ops — added to let the ONNX loader build a complete graph.
787    // Triton/CPU lowering is not yet implemented for these variants.
788    // -----------------------------------------------------------------------
789
790    // --- Element-wise unary math ---
791    /// Element-wise absolute value (ONNX `Abs`).
792    Abs,
793    /// Element-wise negation (ONNX `Neg`).
794    Neg,
795    /// Element-wise ceiling (ONNX `Ceil`).
796    Ceil,
797    /// Element-wise floor (ONNX `Floor`).
798    Floor,
799    /// Element-wise round-to-nearest-even (ONNX `Round`).
800    Round,
801    /// Element-wise square root (ONNX `Sqrt`).
802    Sqrt,
803    /// Element-wise reciprocal, `1/x` (ONNX `Reciprocal`).
804    Reciprocal,
805    /// Element-wise natural exponential (ONNX `Exp`).
806    Exp,
807    /// Element-wise natural logarithm (ONNX `Log`).
808    Log,
809    /// Element-wise error function (ONNX `Erf`).
810    Erf,
811    /// Element-wise sign (ONNX `Sign`).
812    Sign,
813    /// Element-wise NaN test (ONNX `IsNaN`).
814    IsNaN,
815    /// Element-wise infinity test (ONNX `IsInf`).
816    IsInf {
817        /// Whether to treat negative infinity as infinite.
818        detect_negative: bool,
819        /// Whether to treat positive infinity as infinite.
820        detect_positive: bool,
821    },
822    /// Element-wise logical NOT (ONNX `Not`).
823    Not,
824    /// Element-wise bitwise NOT (ONNX `BitwiseNot`).
825    BitwiseNot,
826    /// Element-wise sine (ONNX `Sin`).
827    Sin,
828    /// Element-wise cosine (ONNX `Cos`).
829    Cos,
830    /// Element-wise tangent (ONNX `Tan`).
831    Tan,
832    /// Element-wise arcsine (ONNX `Asin`).
833    Asin,
834    /// Element-wise arccosine (ONNX `Acos`).
835    Acos,
836    /// Element-wise arctangent (ONNX `Atan`).
837    Atan,
838    /// Element-wise hyperbolic sine (ONNX `Sinh`).
839    Sinh,
840    /// Element-wise hyperbolic cosine (ONNX `Cosh`).
841    Cosh,
842    /// Element-wise inverse hyperbolic sine (ONNX `Asinh`).
843    Asinh,
844    /// Element-wise inverse hyperbolic cosine (ONNX `Acosh`).
845    Acosh,
846    /// Element-wise inverse hyperbolic tangent (ONNX `Atanh`).
847    Atanh,
848
849    // --- Element-wise binary / variadic ---
850    /// Element-wise multiplication (ONNX `Mul`).
851    Mul,
852    /// Element-wise subtraction (ONNX `Sub`).
853    Sub,
854    /// Element-wise division (ONNX `Div`).
855    Div,
856    /// Element-wise exponentiation (ONNX `Pow`).
857    Pow,
858    /// Element-wise modulo (ONNX `Mod`).
859    Mod {
860        /// Whether to use C-style (`fmod`) semantics instead of Python-style modulo.
861        fmod: bool,
862    },
863    /// Element-wise minimum across inputs (ONNX `Min`).
864    ElemMin,
865    /// Element-wise maximum across inputs (ONNX `Max`).
866    ElemMax,
867    /// Element-wise mean across inputs (ONNX `Mean`).
868    ElemMean,
869    /// Element-wise sum across inputs (ONNX `Sum`).
870    ElemSum,
871    /// Element-wise equality (ONNX `Equal`).
872    Equal,
873    /// Element-wise greater-than (ONNX `Greater`).
874    Greater,
875    /// Element-wise greater-than-or-equal (ONNX `GreaterOrEqual`).
876    GreaterOrEqual,
877    /// Element-wise less-than (ONNX `Less`).
878    Less,
879    /// Element-wise less-than-or-equal (ONNX `LessOrEqual`).
880    LessOrEqual,
881    /// Element-wise logical AND (ONNX `And`).
882    And,
883    /// Element-wise logical OR (ONNX `Or`).
884    Or,
885    /// Element-wise logical XOR (ONNX `Xor`).
886    Xor,
887    /// Element-wise bitwise AND (ONNX `BitwiseAnd`).
888    BitwiseAnd,
889    /// Element-wise bitwise OR (ONNX `BitwiseOr`).
890    BitwiseOr,
891    /// Element-wise bitwise XOR (ONNX `BitwiseXor`).
892    BitwiseXor,
893    /// Element-wise bit shift (ONNX `BitShift`).
894    BitShift {
895        /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
896        direction: alloc::string::String,
897    },
898
899    // --- Tensor structural ---
900    /// Reshapes a tensor without changing its data (ONNX `Reshape`).
901    Reshape,
902    /// Permutes a tensor's dimensions (ONNX `Transpose`).
903    Transpose {
904        /// The output permutation of input dimensions.
905        perm: alloc::vec::Vec<usize>,
906    },
907    /// Removes size-1 dimensions (ONNX `Squeeze`).
908    Squeeze {
909        /// The axes to operate along.
910        axes: alloc::vec::Vec<i64>,
911    },
912    /// Inserts size-1 dimensions (ONNX `Unsqueeze`).
913    Unsqueeze {
914        /// The axes to operate along.
915        axes: alloc::vec::Vec<i64>,
916    },
917    /// Concatenates tensors along an axis (ONNX `Concat`).
918    Concat {
919        /// The axis to operate along.
920        axis: i64,
921    },
922    /// Splits a tensor into multiple outputs along an axis (ONNX `Split`).
923    Split {
924        /// The axis to operate along.
925        axis: i64,
926        /// Number of outputs to split into.
927        num_outputs: usize,
928    },
929    /// Extracts a slice of a tensor (ONNX `Slice`).
930    Slice,
931    /// Gathers slices along an axis using an index tensor (ONNX `Gather`).
932    Gather {
933        /// The axis to operate along.
934        axis: i64,
935    },
936    /// Gathers individual elements along an axis (ONNX `GatherElements`).
937    GatherElements {
938        /// The axis to operate along.
939        axis: i64,
940    },
941    /// Gathers slices using N-D indices (ONNX `GatherND`).
942    GatherND {
943        /// Number of leading batch dimensions.
944        batch_dims: i64,
945    },
946    /// Scatters individual elements along an axis (ONNX `ScatterElements`).
947    ScatterElements {
948        /// The axis to operate along.
949        axis: i64,
950    },
951    /// Scatters slices using N-D indices (ONNX `ScatterND`).
952    ScatterND,
953    /// Tiles a tensor by repeating it (ONNX `Tile`).
954    Tile,
955    /// Broadcasts a tensor to a larger shape (ONNX `Expand`).
956    Expand,
957    /// Returns a tensor's shape as a 1-D tensor (ONNX `Shape`).
958    ShapeOf {
959        /// Start index.
960        start: i64,
961        /// End index.
962        end: i64,
963    },
964    /// Returns the total number of elements (ONNX `Size`).
965    SizeOf,
966    /// Passes the input through unchanged (ONNX `Identity`).
967    Identity,
968    /// Casts a tensor to another dtype (ONNX `Cast`).
969    Cast {
970        /// Target dtype.
971        to: DtypeRepr,
972    },
973    /// Casts a tensor to match another tensor's dtype (ONNX `CastLike`).
974    CastLike,
975    /// Element-wise conditional selection (ONNX `Where`).
976    Where,
977    /// Selects slices along an axis using a boolean mask (ONNX `Compress`).
978    Compress {
979        /// The axis to operate along.
980        axis: i64,
981    },
982    /// Generates a range of values (ONNX `Range`).
983    Range,
984    /// Constant tensor (value embedded in the ONNX model).
985    Constant {
986        /// The dtype to use.
987        dtype: DtypeRepr,
988        /// The tensor shape.
989        shape: Shape,
990    },
991    /// Creates a constant-filled tensor of a given shape (ONNX `ConstantOfShape`).
992    ConstantOfShape {
993        /// The dtype to use.
994        dtype: DtypeRepr,
995    },
996    /// Extracts the upper or lower triangular part of a matrix (ONNX `Trilu`).
997    Trilu {
998        /// Whether to keep the upper (vs. lower) triangular part.
999        upper: bool,
1000    },
1001    /// Reinterprets a tensor's bits as another dtype without conversion.
1002    BitCast {
1003        /// Target dtype.
1004        to: DtypeRepr,
1005    },
1006    /// Generic padding (ONNX `Pad`).
1007    Pad {
1008        /// The mode string selecting op-specific behavior.
1009        mode: alloc::string::String,
1010    },
1011    /// Reverses variable-length sequences along an axis (ONNX `ReverseSequence`).
1012    ReverseSequence {
1013        /// The batch axis.
1014        batch_axis: i64,
1015        /// The time axis.
1016        time_axis: i64,
1017    },
1018    /// Returns the indices of non-zero elements (ONNX `NonZero`).
1019    NonZero,
1020    /// Scatters values along an axis (deprecated ONNX `Scatter`, superseded by `ScatterElements`).
1021    Scatter {
1022        /// The axis to operate along.
1023        axis: i64,
1024    },
1025    /// Scatters an entire tensor into another at given indices.
1026    TensorScatter,
1027
1028    // --- Matrix ---
1029    /// General matrix multiply: `alpha * A @ B + beta * C` (ONNX `Gemm`).
1030    Gemm {
1031        /// The `alpha` parameter.
1032        alpha: f64,
1033        /// The `beta` parameter.
1034        beta: f64,
1035        /// Whether to transpose the first matrix operand.
1036        trans_a: bool,
1037        /// Whether to transpose the second matrix operand.
1038        trans_b: bool,
1039    },
1040    /// Matrix multiplication (ONNX `MatMul`).
1041    MatMul,
1042    /// Integer matrix multiplication (ONNX `MatMulInteger`).
1043    MatMulInteger,
1044    /// Einstein-summation contraction (ONNX `Einsum`).
1045    Einsum {
1046        /// The Einstein-summation equation string.
1047        equation: alloc::string::String,
1048    },
1049    /// Matrix determinant (ONNX `Det`).
1050    Det,
1051    /// Quantized linear matrix multiplication (ONNX `QLinearMatMul`).
1052    QLinearMatMul,
1053
1054    // --- Convolution extras ---
1055    /// Transposed (deconvolution) 2-D convolution (ONNX `ConvTranspose`).
1056    ConvTranspose {
1057        /// Number of input channels.
1058        in_channels: usize,
1059        /// Number of output channels.
1060        out_channels: usize,
1061        /// Convolution/pooling kernel height.
1062        kernel_h: usize,
1063        /// Convolution/pooling kernel width.
1064        kernel_w: usize,
1065        /// Vertical stride.
1066        stride_h: usize,
1067        /// Horizontal stride.
1068        stride_w: usize,
1069        /// Vertical zero-padding.
1070        padding_h: usize,
1071        /// Horizontal zero-padding.
1072        padding_w: usize,
1073        /// Additional vertical padding added to the output (transposed convolution).
1074        output_padding_h: usize,
1075        /// Additional horizontal padding added to the output (transposed convolution).
1076        output_padding_w: usize,
1077        /// Number of blocked/grouped connections (1 = standard).
1078        groups: usize,
1079        /// Whether a learned bias is added.
1080        has_bias: bool,
1081    },
1082    /// Integer convolution (ONNX `ConvInteger`).
1083    ConvInteger {
1084        /// Number of blocked/grouped connections (1 = standard).
1085        groups: usize,
1086    },
1087    /// Deformable convolution (ONNX `DeformConv`).
1088    DeformConv {
1089        /// Number of blocked/grouped connections (1 = standard).
1090        group: usize,
1091        /// Number of groups for deformable-convolution offset channels.
1092        offset_group: usize,
1093    },
1094    /// Quantized linear convolution (ONNX `QLinearConv`).
1095    QLinearConv {
1096        /// Number of blocked/grouped connections (1 = standard).
1097        groups: usize,
1098    },
1099    /// Combines sliding local blocks into a large tensor (ONNX `Col2Im`, inverse of im2col).
1100    Col2Im {
1101        /// Convolution/pooling kernel height.
1102        kernel_h: usize,
1103        /// Convolution/pooling kernel width.
1104        kernel_w: usize,
1105    },
1106    /// Stateful causal 1-D convolution, carrying a sliding-window state between calls (ONNX
1107    /// `CausalConvWithState`).
1108    CausalConvWithState {
1109        /// Name of the activation function applied after the convolution (empty = none).
1110        activation: alloc::string::String,
1111    },
1112
1113    // --- Reductions ---
1114    /// Sum reduction along axes (ONNX `ReduceSum`).
1115    ReduceSum {
1116        /// Whether to retain reduced dimensions with length 1.
1117        keepdims: bool,
1118        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1119        noop_with_empty_axes: bool,
1120    },
1121    /// Mean reduction along axes (ONNX `ReduceMean`).
1122    ReduceMean {
1123        /// Whether to retain reduced dimensions with length 1.
1124        keepdims: bool,
1125        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1126        noop_with_empty_axes: bool,
1127    },
1128    /// Max reduction along axes (ONNX `ReduceMax`).
1129    ReduceMax {
1130        /// Whether to retain reduced dimensions with length 1.
1131        keepdims: bool,
1132        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1133        noop_with_empty_axes: bool,
1134    },
1135    /// Min reduction along axes (ONNX `ReduceMin`).
1136    ReduceMin {
1137        /// Whether to retain reduced dimensions with length 1.
1138        keepdims: bool,
1139        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1140        noop_with_empty_axes: bool,
1141    },
1142    /// Product reduction along axes (ONNX `ReduceProd`).
1143    ReduceProd {
1144        /// Whether to retain reduced dimensions with length 1.
1145        keepdims: bool,
1146        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1147        noop_with_empty_axes: bool,
1148    },
1149    /// L1-norm reduction along axes (ONNX `ReduceL1`).
1150    ReduceL1 {
1151        /// Whether to retain reduced dimensions with length 1.
1152        keepdims: bool,
1153        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1154        noop_with_empty_axes: bool,
1155    },
1156    /// L2-norm reduction along axes (ONNX `ReduceL2`).
1157    ReduceL2 {
1158        /// Whether to retain reduced dimensions with length 1.
1159        keepdims: bool,
1160        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1161        noop_with_empty_axes: bool,
1162    },
1163    /// Log-sum reduction along axes (ONNX `ReduceLogSum`).
1164    ReduceLogSum {
1165        /// Whether to retain reduced dimensions with length 1.
1166        keepdims: bool,
1167        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1168        noop_with_empty_axes: bool,
1169    },
1170    /// Log-sum-exp reduction along axes (ONNX `ReduceLogSumExp`).
1171    ReduceLogSumExp {
1172        /// Whether to retain reduced dimensions with length 1.
1173        keepdims: bool,
1174        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1175        noop_with_empty_axes: bool,
1176    },
1177    /// Sum-of-squares reduction along axes (ONNX `ReduceSumSquare`).
1178    ReduceSumSquare {
1179        /// Whether to retain reduced dimensions with length 1.
1180        keepdims: bool,
1181        /// Whether an empty `axes` list means "no-op" instead of "reduce all".
1182        noop_with_empty_axes: bool,
1183    },
1184    /// Cumulative sum along an axis (ONNX `CumSum`).
1185    CumSum {
1186        /// Whether to exclude the current element from the cumulative result.
1187        exclusive: bool,
1188        /// Whether to accumulate in reverse order.
1189        reverse: bool,
1190    },
1191    /// Cumulative product along an axis (ONNX `CumProd`).
1192    CumProd {
1193        /// Whether to exclude the current element from the cumulative result.
1194        exclusive: bool,
1195        /// Whether to accumulate in reverse order.
1196        reverse: bool,
1197    },
1198    /// Index of the maximum along an axis (ONNX `ArgMax`).
1199    ArgMax {
1200        /// The axis to operate along.
1201        axis: i64,
1202        /// Whether to retain reduced dimensions with length 1.
1203        keepdims: bool,
1204        /// Whether ties select the last (rather than first) matching index.
1205        select_last_index: bool,
1206    },
1207    /// Index of the minimum along an axis (ONNX `ArgMin`).
1208    ArgMin {
1209        /// The axis to operate along.
1210        axis: i64,
1211        /// Whether to retain reduced dimensions with length 1.
1212        keepdims: bool,
1213        /// Whether ties select the last (rather than first) matching index.
1214        select_last_index: bool,
1215    },
1216    /// Average-pools over the entire spatial extent (ONNX `GlobalAveragePool`).
1217    GlobalAvgPool,
1218    /// Max-pools over the entire spatial extent (ONNX `GlobalMaxPool`).
1219    GlobalMaxPool,
1220    /// Lp-norm normalization along an axis (ONNX `LpNormalization`).
1221    LpNormalization {
1222        /// The axis to operate along.
1223        axis: i64,
1224        /// The `p` in the p-norm.
1225        p: i64,
1226    },
1227    /// Mean/variance normalization along axes (ONNX `MeanVarianceNormalization`).
1228    MeanVarianceNormalization {
1229        /// The axes to operate along.
1230        axes: alloc::vec::Vec<i64>,
1231    },
1232
1233    // --- Additional activations ---
1234    /// Log-softmax along an axis (ONNX `LogSoftmax`).
1235    LogSoftmax {
1236        /// The axis to operate along.
1237        axis: i64,
1238    },
1239    /// One-hot of the argmax along an axis (ONNX `Hardmax`).
1240    Hardmax {
1241        /// The axis to operate along.
1242        axis: i64,
1243    },
1244    /// Parametric ReLU, with a learned per-channel slope (ONNX `PRelu`).
1245    PRelu,
1246    /// ReLU that zeroes values at or below `alpha` (ONNX `ThresholdedRelu`).
1247    ThresholdedRelu {
1248        /// The `alpha` parameter.
1249        alpha: f64,
1250    },
1251    /// Shrinks values toward zero by `lambd`, with a `bias` offset (ONNX `Shrink`).
1252    Shrink {
1253        /// The shrinkage threshold.
1254        lambd: f64,
1255        /// A bias/offset value.
1256        bias: f64,
1257    },
1258    /// Clamps values to a `[min, max]` range (ONNX `Clip`).
1259    Clip,
1260    /// Swish/SiLU activation (ONNX `Swish`).
1261    Swish,
1262    /// Multi-head attention (ONNX `MultiHeadAttention`).
1263    MultiHeadAttention {
1264        /// Number of query attention heads.
1265        q_num_heads: usize,
1266        /// Number of key/value attention heads.
1267        kv_num_heads: usize,
1268    },
1269    /// Attention with a user-defined score-modification function (ONNX `FlexAttention`). The
1270    /// score-modification subgraph itself is not captured (consistent with `Loop`/`If`/`Scan`
1271    /// not capturing their subgraph bodies).
1272    FlexAttention {
1273        /// Attention score scale factor, if explicitly specified.
1274        scale: f64,
1275    },
1276    /// Linear-complexity attention (e.g. gated delta rule variants), optionally carrying state
1277    /// between calls (ONNX `LinearAttention`).
1278    LinearAttention {
1279        /// Number of query attention heads.
1280        q_num_heads: usize,
1281        /// Number of key/value attention heads.
1282        kv_num_heads: usize,
1283        /// Name of the state-update rule (e.g. a gated-delta variant).
1284        update_rule: alloc::string::String,
1285        /// Attention score scale factor, if explicitly specified.
1286        scale: f64,
1287    },
1288
1289    // --- Normalisation (generic) ---
1290    /// Local response normalization (ONNX `LRN`).
1291    LRN {
1292        /// The `alpha` parameter.
1293        alpha: f64,
1294        /// The `beta` parameter.
1295        beta: f64,
1296        /// A bias/offset value.
1297        bias: f64,
1298        /// Window/kernel size.
1299        size: usize,
1300    },
1301
1302    // --- Recurrent ---
1303    /// Long short-term memory recurrent layer (ONNX `LSTM`).
1304    Lstm {
1305        /// Size of the hidden state.
1306        hidden_size: usize,
1307        /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
1308        direction: alloc::string::String,
1309        /// Whether to run the recurrence in both directions.
1310        bidirectional: bool,
1311    },
1312    /// Gated recurrent unit layer (ONNX `GRU`).
1313    Gru {
1314        /// Size of the hidden state.
1315        hidden_size: usize,
1316        /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
1317        direction: alloc::string::String,
1318        /// Whether to run the recurrence in both directions.
1319        bidirectional: bool,
1320    },
1321    /// Simple recurrent layer (ONNX `RNN`).
1322    Rnn {
1323        /// Size of the hidden state.
1324        hidden_size: usize,
1325        /// Direction of iteration/shift (e.g. `"forward"`, `"reverse"`, `"bidirectional"`).
1326        direction: alloc::string::String,
1327        /// Whether to run the recurrence in both directions.
1328        bidirectional: bool,
1329    },
1330
1331    // --- Resize / spatial ---
1332    /// Resizes a tensor (interpolation) (ONNX `Resize`).
1333    Resize {
1334        /// The mode string selecting op-specific behavior.
1335        mode: alloc::string::String,
1336        /// How resized coordinates map back to the input (ONNX `Resize` mode string).
1337        coordinate_transformation_mode: alloc::string::String,
1338        /// Whether to apply an anti-aliasing filter when downsampling.
1339        antialias: bool,
1340    },
1341    /// Samples a tensor at grid-specified locations (ONNX `GridSample`).
1342    GridSample {
1343        /// The mode string selecting op-specific behavior.
1344        mode: alloc::string::String,
1345        /// How out-of-bounds sample coordinates are handled.
1346        padding_mode: alloc::string::String,
1347        /// Whether corner pixels are aligned (vs. edge-aligned) when sampling/resizing.
1348        align_corners: bool,
1349    },
1350    /// Rearranges spatial blocks into depth/channels (ONNX `SpaceToDepth`).
1351    SpaceToDepth {
1352        /// Block size for the space/depth rearrangement.
1353        blocksize: usize,
1354    },
1355    /// Rearranges depth/channels into spatial blocks (ONNX `DepthToSpace`).
1356    DepthToSpace {
1357        /// Block size for the space/depth rearrangement.
1358        blocksize: usize,
1359        /// The mode string selecting op-specific behavior.
1360        mode: alloc::string::String,
1361    },
1362    /// Region-of-interest pooling with bilinear alignment (ONNX `RoiAlign`).
1363    RoiAlign {
1364        /// Output region height.
1365        output_h: usize,
1366        /// Output region width.
1367        output_w: usize,
1368        /// Number of sampling points per output bin (0 = adaptive).
1369        sampling_ratio: i64,
1370        /// Scale factor mapping ROI coordinates to the input feature map.
1371        spatial_scale: f64,
1372    },
1373    /// Generates a 2-D/3-D sampling grid from an affine matrix (ONNX `AffineGrid`).
1374    AffineGrid {
1375        /// Whether corner pixels are aligned (vs. edge-aligned) when sampling/resizing.
1376        align_corners: bool,
1377    },
1378    /// Inverse of max pooling, using stored indices (ONNX `MaxUnpool`).
1379    MaxUnpool {
1380        /// Convolution/pooling kernel height.
1381        kernel_h: usize,
1382        /// Convolution/pooling kernel width.
1383        kernel_w: usize,
1384        /// Vertical stride.
1385        stride_h: usize,
1386        /// Horizontal stride.
1387        stride_w: usize,
1388    },
1389    /// Crops or pads a tensor to a target shape, centered (ONNX `CenterCropPad`).
1390    CenterCropPad {
1391        /// The axes to operate along.
1392        axes: alloc::vec::Vec<i64>,
1393    },
1394    /// Filters overlapping boxes by score (ONNX `NonMaxSuppression`).
1395    NonMaxSuppression {
1396        /// Whether boxes are given as `(center_x, center_y, width, height)` instead of corners.
1397        center_point_box: bool,
1398    },
1399
1400    // --- Misc ---
1401    /// Returns the top-K values/indices along an axis (ONNX `TopK`).
1402    TopK {
1403        /// The axis to operate along.
1404        axis: i64,
1405        /// Whether to return the largest (vs. smallest) K values.
1406        largest: bool,
1407        /// Whether outputs are sorted.
1408        sorted: bool,
1409    },
1410    /// Returns unique elements (ONNX `Unique`).
1411    Unique {
1412        /// Whether outputs are sorted.
1413        sorted: bool,
1414    },
1415    /// Dropout regularization (ONNX `Dropout`).
1416    Dropout {
1417        /// Whether dropout is active (vs. a no-op at inference).
1418        training_mode: bool,
1419    },
1420    /// Creates an identity-like 2-D tensor (ONNX `EyeLike`).
1421    EyeLike {
1422        /// The dtype to use.
1423        dtype: Option<DtypeRepr>,
1424        /// Diagonal offset.
1425        k: i64,
1426    },
1427    /// One-hot encodes indices along an axis (ONNX `OneHot`).
1428    OneHot {
1429        /// The axis to operate along.
1430        axis: i64,
1431    },
1432    /// Samples from a Bernoulli distribution using input probabilities (ONNX `Bernoulli`).
1433    Bernoulli {
1434        /// The dtype to use.
1435        dtype: Option<DtypeRepr>,
1436    },
1437    /// Samples uniform random values with another tensor's shape (ONNX `RandomUniformLike`).
1438    RandomUniformLike {
1439        /// The dtype to use.
1440        dtype: Option<DtypeRepr>,
1441        /// Upper bound of the sampling range.
1442        high: f64,
1443        /// Lower bound of the sampling range.
1444        low: f64,
1445    },
1446    /// Rotary position embedding (ONNX `RotaryEmbedding`).
1447    RotaryEmbedding,
1448
1449    // --- Quantisation ---
1450    /// Linear quantization to a lower-precision dtype (ONNX `QuantizeLinear`).
1451    QuantizeLinear {
1452        /// The axis to operate along.
1453        axis: i64,
1454        /// Whether to saturate (clamp) out-of-range values instead of wrapping.
1455        saturate: bool,
1456    },
1457    /// Linear dequantization back to a floating-point dtype (ONNX `DequantizeLinear`).
1458    DequantizeLinear {
1459        /// The axis to operate along.
1460        axis: i64,
1461    },
1462    /// Dynamically computes quantization parameters and quantizes (ONNX `DynamicQuantizeLinear`).
1463    DynamicQuantizeLinear,
1464
1465    // --- Signal ---
1466    /// Discrete Fourier transform (ONNX `DFT`).
1467    Dft {
1468        /// Whether to compute the inverse transform.
1469        inverse: bool,
1470        /// Whether to return only the non-redundant half of the spectrum.
1471        onesided: bool,
1472    },
1473    /// Short-time Fourier transform (ONNX `STFT`).
1474    Stft,
1475    /// Generates a mel-scale filterbank matrix (ONNX `MelWeightMatrix`).
1476    MelWeightMatrix,
1477    /// Generates a Hann window (ONNX `HannWindow`).
1478    HannWindow {
1479        /// Whether the window is periodic (vs. symmetric).
1480        periodic: bool,
1481    },
1482    /// Generates a Blackman window (ONNX `BlackmanWindow`).
1483    BlackmanWindow {
1484        /// Whether the window is periodic (vs. symmetric).
1485        periodic: bool,
1486    },
1487    /// Generates a Hamming window (ONNX `HammingWindow`).
1488    HammingWindow {
1489        /// Whether the window is periodic (vs. symmetric).
1490        periodic: bool,
1491    },
1492
1493    // --- Loss ---
1494    /// Negative log-likelihood loss (ONNX `NegativeLogLikelihoodLoss`).
1495    NegativeLogLikelihoodLoss {
1496        /// The reduction mode applied to the per-element loss (e.g. `"mean"`, `"sum"`, `"none"`).
1497        reduction: alloc::string::String,
1498    },
1499    /// Softmax + cross-entropy loss (ONNX `SoftmaxCrossEntropyLoss`).
1500    SoftmaxCrossEntropyLoss {
1501        /// The reduction mode applied to the per-element loss (e.g. `"mean"`, `"sum"`, `"none"`).
1502        reduction: alloc::string::String,
1503    },
1504
1505    // --- Sequences ---
1506    /// Indexes into a sequence (ONNX `SequenceAt`).
1507    SequenceAt,
1508    /// Constructs a sequence from tensors (ONNX `SequenceConstruct`).
1509    SequenceConstruct,
1510    /// Constructs an empty sequence (ONNX `SequenceEmpty`).
1511    SequenceEmpty,
1512    /// Removes an element from a sequence (ONNX `SequenceErase`).
1513    SequenceErase,
1514    /// Inserts an element into a sequence (ONNX `SequenceInsert`).
1515    SequenceInsert,
1516    /// Returns a sequence's length (ONNX `SequenceLength`).
1517    SequenceLength,
1518    /// Applies a subgraph to each element of a sequence (ONNX `SequenceMap`).
1519    SequenceMap,
1520    /// Splits a tensor into a sequence along an axis (ONNX `SplitToSequence`).
1521    SplitToSequence {
1522        /// The axis to operate along.
1523        axis: i64,
1524        /// Whether to retain reduced dimensions with length 1.
1525        keepdims: bool,
1526    },
1527    /// Concatenates a sequence's elements into one tensor (ONNX `ConcatFromSequence`).
1528    ConcatFromSequence {
1529        /// The axis to operate along.
1530        axis: i64,
1531        /// Whether to insert a new axis for the concatenation dimension.
1532        new_axis: bool,
1533    },
1534    /// Extracts the value from an optional (ONNX `OptionalGetElement`).
1535    OptionalGetElement,
1536    /// Tests whether an optional has a value (ONNX `OptionalHasElement`).
1537    OptionalHasElement,
1538
1539    // --- Control flow ---
1540    /// Generic looping construct over a subgraph (ONNX `Loop`).
1541    Loop,
1542    /// Applies a subgraph iteratively over input sequences (ONNX `Scan`).
1543    Scan {
1544        /// Number of inputs treated as scanned sequences.
1545        num_scan_inputs: i64,
1546    },
1547    /// Conditional branch over subgraphs (ONNX `If`).
1548    If,
1549
1550    // --- Optimiser ops ---
1551    /// Adagrad optimizer step (ONNX `Adagrad`).
1552    Adagrad,
1553    /// Adam optimizer step (ONNX `Adam`).
1554    Adam,
1555    /// Momentum optimizer step (ONNX `Momentum`).
1556    Momentum,
1557    /// Computes gradients of a subgraph (ONNX `Gradient`).
1558    Gradient,
1559
1560    // --- String / NLP ---
1561    /// Normalizes strings (case folding, stop-word removal) (ONNX `StringNormalizer`).
1562    StringNormalizer,
1563    /// Tests strings against a regex (ONNX `RegexFullMatch`).
1564    RegexFullMatch {
1565        /// The regular expression pattern.
1566        pattern: alloc::string::String,
1567    },
1568    /// Concatenates strings element-wise (ONNX `StringConcat`).
1569    StringConcat,
1570    /// Splits strings on a delimiter (ONNX `StringSplit`).
1571    StringSplit,
1572    /// Computes TF-IDF n-gram features (ONNX `TfIdfVectorizer`).
1573    TfIdfVectorizer,
1574    /// Maps categorical labels to/from encoded values (ONNX `LabelEncoder`).
1575    LabelEncoder,
1576
1577    // --- Other ML ---
1578    /// Selects elements from a tensor by index (ONNX-ML `ArrayFeatureExtractor`).
1579    ArrayFeatureExtractor,
1580    /// Binarizes values against a threshold (ONNX-ML `Binarizer`).
1581    Binarizer {
1582        /// The threshold value.
1583        threshold: f64,
1584    },
1585    /// Decision-tree ensemble inference (ONNX-ML `TreeEnsemble`).
1586    TreeEnsemble,
1587    /// Decodes an encoded image (e.g. PNG/JPEG) into a tensor (ONNX `ImageDecoder`).
1588    ImageDecoder,
1589}
1590
1591/// One node in a [`Graph`]: an [`Op`] plus its producer indices, dtype, and output shape.
1592#[derive(Debug, Clone)]
1593pub struct GraphNode {
1594    /// This node's operation.
1595    pub op: Op,
1596    /// Indices of producer nodes in `Graph::nodes`; empty for `Input`.
1597    pub inputs: Vec<usize>,
1598    /// This node's output dtype.
1599    pub dtype: DtypeRepr,
1600    /// Output shape of this node. `None` in a slot means a dynamic/unknown
1601    /// dimension (e.g. the batch axis).
1602    pub shape: Shape,
1603}
1604
1605/// The traced computational graph: a list of [`GraphNode`]s plus optional node names.
1606#[derive(Debug, Default, Clone)]
1607pub struct Graph {
1608    /// The graph's nodes, in the order they were recorded.
1609    pub nodes: Vec<GraphNode>,
1610    /// Node index → dotted name captured from [`crate::name_scope`] at recording time.
1611    pub names: BTreeMap<usize, String>,
1612}
1613
1614impl Graph {
1615    /// Creates an empty graph.
1616    pub fn new() -> Self {
1617        Self::default()
1618    }
1619
1620    /// Appends a new node to the graph, returning its index.
1621    pub fn add_node(
1622        &mut self,
1623        op: Op,
1624        inputs: Vec<usize>,
1625        dtype: DtypeRepr,
1626        shape: Shape,
1627    ) -> usize {
1628        let id = self.nodes.len();
1629        self.nodes.push(GraphNode {
1630            op,
1631            inputs,
1632            dtype,
1633            shape,
1634        });
1635        #[cfg(feature = "std")]
1636        if let Some(name) = crate::name_scope::current_scope() {
1637            self.names.insert(id, name);
1638        }
1639        id
1640    }
1641
1642    /// Returns node indices in topological order (producers before consumers)
1643    /// using Kahn's algorithm. Panics if the graph contains a cycle.
1644    pub fn topological_sort(&self) -> Vec<usize> {
1645        let n = self.nodes.len();
1646        let mut in_degree = vec![0usize; n];
1647        let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
1648
1649        for (id, node) in self.nodes.iter().enumerate() {
1650            for &input in &node.inputs {
1651                in_degree[id] += 1;
1652                dependents[input].push(id);
1653            }
1654        }
1655
1656        let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
1657        let mut order = Vec::with_capacity(n);
1658
1659        while let Some(id) = queue.pop() {
1660            order.push(id);
1661            for &dep in &dependents[id] {
1662                in_degree[dep] -= 1;
1663                if in_degree[dep] == 0 {
1664                    queue.push(dep);
1665                }
1666            }
1667        }
1668
1669        assert_eq!(order.len(), n, "graph contains a cycle");
1670        order
1671    }
1672
1673    /// Rewrite the graph by fusing compatible op sequences into single fused ops.
1674    ///
1675    /// Currently recognises: `Conv2d(no bias) → BatchNorm2d → Silu`
1676    /// and replaces the triple with a single `Conv2dBnSilu` node.
1677    ///
1678    /// Nodes that are absorbed into a fused node are removed from the output
1679    /// graph; remaining node indices are renumbered contiguously.
1680    pub fn optimise(&self) -> Graph {
1681        let n = self.nodes.len();
1682
1683        // Build per-node consumer counts so we can detect single-use nodes.
1684        let mut n_consumers = vec![0usize; n];
1685        for node in &self.nodes {
1686            for &inp in &node.inputs {
1687                n_consumers[inp] += 1;
1688            }
1689        }
1690
1691        // dead[i] — node i is absorbed into a downstream fused node.
1692        let mut dead = vec![false; n];
1693        // node_override[i] — replacement (op, inputs) for node i.
1694        let mut node_override: Vec<Option<(Op, Vec<usize>)>> = vec![None; n];
1695
1696        // `silu_idx` indexes multiple parallel collections below (self.nodes, n_consumers,
1697        // node_override), not just one -- an iterator/enumerate() rewrite wouldn't be clearer.
1698        #[allow(clippy::needless_range_loop)]
1699        for silu_idx in 0..n {
1700            if !matches!(self.nodes[silu_idx].op, Op::Silu) {
1701                continue;
1702            }
1703            if self.nodes[silu_idx].inputs.len() != 1 {
1704                continue;
1705            }
1706
1707            let bn_idx = self.nodes[silu_idx].inputs[0];
1708            if !matches!(self.nodes[bn_idx].op, Op::BatchNorm2d { .. }) {
1709                continue;
1710            }
1711            if n_consumers[bn_idx] != 1 {
1712                continue;
1713            }
1714            if self.nodes[bn_idx].inputs.len() != 1 {
1715                continue;
1716            }
1717
1718            let conv_idx = self.nodes[bn_idx].inputs[0];
1719            if !matches!(
1720                self.nodes[conv_idx].op,
1721                Op::Conv2d {
1722                    has_bias: false,
1723                    ..
1724                }
1725            ) {
1726                continue;
1727            }
1728            if n_consumers[conv_idx] != 1 {
1729                continue;
1730            }
1731
1732            let (
1733                in_channels,
1734                out_channels,
1735                kernel_h,
1736                kernel_w,
1737                stride_h,
1738                stride_w,
1739                padding_h,
1740                padding_w,
1741                groups,
1742            ) = if let Op::Conv2d {
1743                in_channels,
1744                out_channels,
1745                kernel_h,
1746                kernel_w,
1747                stride_h,
1748                stride_w,
1749                padding_h,
1750                padding_w,
1751                groups,
1752                ..
1753            } = self.nodes[conv_idx].op
1754            {
1755                (
1756                    in_channels,
1757                    out_channels,
1758                    kernel_h,
1759                    kernel_w,
1760                    stride_h,
1761                    stride_w,
1762                    padding_h,
1763                    padding_w,
1764                    groups,
1765                )
1766            } else {
1767                unreachable!()
1768            };
1769
1770            let bn_eps = if let Op::BatchNorm2d { eps, .. } = self.nodes[bn_idx].op {
1771                eps
1772            } else {
1773                unreachable!()
1774            };
1775
1776            dead[conv_idx] = true;
1777            dead[bn_idx] = true;
1778            node_override[silu_idx] = Some((
1779                Op::Conv2dBnSilu {
1780                    in_channels,
1781                    out_channels,
1782                    kernel_h,
1783                    kernel_w,
1784                    stride_h,
1785                    stride_w,
1786                    padding_h,
1787                    padding_w,
1788                    groups,
1789                    bn_eps,
1790                },
1791                self.nodes[conv_idx].inputs.clone(),
1792            ));
1793        }
1794
1795        // Build old → new index mapping (dead nodes are skipped).
1796        let mut old_to_new = vec![0usize; n];
1797        let mut new_count = 0usize;
1798        for i in 0..n {
1799            if !dead[i] {
1800                old_to_new[i] = new_count;
1801                new_count += 1;
1802            }
1803        }
1804
1805        // Emit new graph in topological order (original ordering is already valid).
1806        let mut new_graph = Graph::new();
1807        for old_idx in 0..n {
1808            if dead[old_idx] {
1809                continue;
1810            }
1811            let node = &self.nodes[old_idx];
1812            let (op, inputs) =
1813                if let Some((fused_op, fused_inputs)) = node_override[old_idx].clone() {
1814                    let mapped = fused_inputs.iter().map(|&i| old_to_new[i]).collect();
1815                    (fused_op, mapped)
1816                } else {
1817                    let mapped = node.inputs.iter().map(|&i| old_to_new[i]).collect();
1818                    (node.op.clone(), mapped)
1819                };
1820            let new_idx = new_graph.nodes.len();
1821            new_graph.nodes.push(GraphNode {
1822                op,
1823                inputs,
1824                dtype: node.dtype,
1825                shape: node.shape.clone(),
1826            });
1827            if let Some(name) = self.names.get(&old_idx) {
1828                new_graph.names.insert(new_idx, name.clone());
1829            }
1830        }
1831
1832        new_graph
1833    }
1834}
1835
1836// ---------------------------------------------------------------------------
1837// Shape inference — computes the output shape for each Op given an input shape
1838// ---------------------------------------------------------------------------
1839
1840fn infer_output_shape(op: &Op, inputs: &[&Shape]) -> Shape {
1841    // Constant has no tensor inputs — its shape is embedded in the op itself.
1842    if let Op::Constant { shape, .. } = op {
1843        return shape.clone();
1844    }
1845    // Zero-input ops that produce no tensor output.
1846    if matches!(op, Op::SequenceEmpty | Op::OptionalHasElement) {
1847        return vec![];
1848    }
1849    let input = inputs[0];
1850    match op {
1851        Op::Input => input.clone(),
1852
1853        // Element-wise / shape-preserving — output shape = input shape
1854        Op::Relu
1855        | Op::Elu { .. }
1856        | Op::Selu
1857        | Op::Celu { .. }
1858        | Op::Gelu
1859        | Op::Mish
1860        | Op::Hardtanh { .. }
1861        | Op::Relu6
1862        | Op::Hardsigmoid
1863        | Op::Hardswish
1864        | Op::Hardshrink { .. }
1865        | Op::LeakyRelu { .. }
1866        | Op::Threshold { .. }
1867        | Op::Softsign
1868        | Op::Softshrink { .. }
1869        | Op::Softplus { .. }
1870        | Op::Sigmoid
1871        | Op::Silu
1872        | Op::Logsigmoid
1873        | Op::Tanh
1874        | Op::Tanhshrink
1875        | Op::Softmax { .. }
1876        | Op::BatchNorm1d { .. }
1877        | Op::BatchNorm2d { .. }
1878        | Op::BatchNorm3d { .. }
1879        | Op::LayerNorm { .. }
1880        | Op::RmsNorm { .. }
1881        | Op::GroupNorm { .. }
1882        | Op::InstanceNorm1d { .. }
1883        | Op::InstanceNorm2d { .. }
1884        | Op::InstanceNorm3d { .. } => input.clone(),
1885
1886        Op::Linear { out_features, .. } => {
1887            // [..., in_features] → [..., out_features]
1888            let mut out = input[..input.len() - 1].to_vec();
1889            out.push(Some(*out_features));
1890            out
1891        }
1892
1893        Op::Flatten => {
1894            // [N, C, H, W, ...] → [N, C*H*W*...]
1895            let rest = &input[1..];
1896            let flat: Option<usize> = rest
1897                .iter()
1898                .try_fold(1usize, |acc, dim| dim.map(|d| acc * d));
1899            vec![input[0], flat]
1900        }
1901
1902        // --- Convolution ---
1903        Op::Conv1d {
1904            out_channels,
1905            kernel_l,
1906            stride,
1907            padding,
1908            ..
1909        } => {
1910            // [N, C_in, L] → [N, C_out, L_out]
1911            let l_out = input[2].map(|l| (l + 2 * padding - kernel_l) / stride + 1);
1912            vec![input[0], Some(*out_channels), l_out]
1913        }
1914
1915        Op::Conv2d {
1916            out_channels,
1917            kernel_h,
1918            kernel_w,
1919            stride_h,
1920            stride_w,
1921            padding_h,
1922            padding_w,
1923            ..
1924        }
1925        | Op::Conv2dBnSilu {
1926            out_channels,
1927            kernel_h,
1928            kernel_w,
1929            stride_h,
1930            stride_w,
1931            padding_h,
1932            padding_w,
1933            ..
1934        } => {
1935            // [N, C_in, H, W] → [N, C_out, H_out, W_out]
1936            let h_out = input[2].map(|h| (h + 2 * padding_h - kernel_h) / stride_h + 1);
1937            let w_out = input[3].map(|w| (w + 2 * padding_w - kernel_w) / stride_w + 1);
1938            vec![input[0], Some(*out_channels), h_out, w_out]
1939        }
1940
1941        Op::Conv3d {
1942            out_channels,
1943            kernel_d,
1944            kernel_h,
1945            kernel_w,
1946            stride_d,
1947            stride_h,
1948            stride_w,
1949            padding_d,
1950            padding_h,
1951            padding_w,
1952            ..
1953        } => {
1954            // [N, C_in, D, H, W] → [N, C_out, D_out, H_out, W_out]
1955            let d_out = input[2].map(|d| (d + 2 * padding_d - kernel_d) / stride_d + 1);
1956            let h_out = input[3].map(|h| (h + 2 * padding_h - kernel_h) / stride_h + 1);
1957            let w_out = input[4].map(|w| (w + 2 * padding_w - kernel_w) / stride_w + 1);
1958            vec![input[0], Some(*out_channels), d_out, h_out, w_out]
1959        }
1960
1961        // --- Pooling ---
1962        Op::AvgPool1d { kernel_l, stride } | Op::MaxPool1d { kernel_l, stride } => {
1963            let l_out = input[2].map(|l| (l - kernel_l) / stride + 1);
1964            vec![input[0], input[1], l_out]
1965        }
1966
1967        Op::LpPool1d {
1968            kernel_l, stride, ..
1969        } => {
1970            let l_out = input[2].map(|l| (l - kernel_l) / stride + 1);
1971            vec![input[0], input[1], l_out]
1972        }
1973
1974        Op::AvgPool2d {
1975            kernel_h,
1976            kernel_w,
1977            stride_h,
1978            stride_w,
1979        } => {
1980            let h_out = input[2].map(|h| (h - kernel_h) / stride_h + 1);
1981            let w_out = input[3].map(|w| (w - kernel_w) / stride_w + 1);
1982            vec![input[0], input[1], h_out, w_out]
1983        }
1984
1985        Op::MaxPool2d {
1986            kernel_h,
1987            kernel_w,
1988            stride_h,
1989            stride_w,
1990            pad_h,
1991            pad_w,
1992        } => {
1993            let h_out = input[2].map(|h| (h + 2 * pad_h - kernel_h) / stride_h + 1);
1994            let w_out = input[3].map(|w| (w + 2 * pad_w - kernel_w) / stride_w + 1);
1995            vec![input[0], input[1], h_out, w_out]
1996        }
1997
1998        Op::LpPool2d {
1999            kernel_h,
2000            kernel_w,
2001            stride_h,
2002            stride_w,
2003            ..
2004        } => {
2005            let h_out = input[2].map(|h| (h - kernel_h) / stride_h + 1);
2006            let w_out = input[3].map(|w| (w - kernel_w) / stride_w + 1);
2007            vec![input[0], input[1], h_out, w_out]
2008        }
2009
2010        Op::AvgPool3d {
2011            kernel_d,
2012            kernel_h,
2013            kernel_w,
2014            stride_d,
2015            stride_h,
2016            stride_w,
2017        }
2018        | Op::MaxPool3d {
2019            kernel_d,
2020            kernel_h,
2021            kernel_w,
2022            stride_d,
2023            stride_h,
2024            stride_w,
2025        } => {
2026            let d_out = input[2].map(|d| (d - kernel_d) / stride_d + 1);
2027            let h_out = input[3].map(|h| (h - kernel_h) / stride_h + 1);
2028            let w_out = input[4].map(|w| (w - kernel_w) / stride_w + 1);
2029            vec![input[0], input[1], d_out, h_out, w_out]
2030        }
2031
2032        Op::LpPool3d {
2033            kernel_d,
2034            kernel_h,
2035            kernel_w,
2036            stride_d,
2037            stride_h,
2038            stride_w,
2039            ..
2040        } => {
2041            let d_out = input[2].map(|d| (d - kernel_d) / stride_d + 1);
2042            let h_out = input[3].map(|h| (h - kernel_h) / stride_h + 1);
2043            let w_out = input[4].map(|w| (w - kernel_w) / stride_w + 1);
2044            vec![input[0], input[1], d_out, h_out, w_out]
2045        }
2046
2047        // --- Upsample ---
2048        Op::UpsampleNearest2d { scale_h, scale_w } => {
2049            // [N, C, H, W] → [N, C, H * scale_h, W * scale_w]
2050            let h_out = input[2].map(|h| h * scale_h);
2051            let w_out = input[3].map(|w| w * scale_w);
2052            vec![input[0], input[1], h_out, w_out]
2053        }
2054
2055        // --- Padding ---
2056        Op::ConstantPad1d {
2057            pad_left,
2058            pad_right,
2059            ..
2060        }
2061        | Op::ReflectionPad1d {
2062            pad_left,
2063            pad_right,
2064        }
2065        | Op::ReplicationPad1d {
2066            pad_left,
2067            pad_right,
2068        }
2069        | Op::CircularPad1d {
2070            pad_left,
2071            pad_right,
2072        } => {
2073            // [N, C, L] → [N, C, L + pad_left + pad_right]
2074            let l_out = input[2].map(|l| l + pad_left + pad_right);
2075            vec![input[0], input[1], l_out]
2076        }
2077
2078        Op::ConstantPad2d {
2079            pad_l,
2080            pad_r,
2081            pad_t,
2082            pad_b,
2083            ..
2084        }
2085        | Op::ReflectionPad2d {
2086            pad_l,
2087            pad_r,
2088            pad_t,
2089            pad_b,
2090        }
2091        | Op::ReplicationPad2d {
2092            pad_l,
2093            pad_r,
2094            pad_t,
2095            pad_b,
2096        }
2097        | Op::CircularPad2d {
2098            pad_l,
2099            pad_r,
2100            pad_t,
2101            pad_b,
2102        } => {
2103            // [N, C, H, W] → [N, C, H + pad_t + pad_b, W + pad_l + pad_r]
2104            let h_out = input[2].map(|h| h + pad_t + pad_b);
2105            let w_out = input[3].map(|w| w + pad_l + pad_r);
2106            vec![input[0], input[1], h_out, w_out]
2107        }
2108
2109        Op::ConstantPad3d {
2110            pad_d1,
2111            pad_d2,
2112            pad_h1,
2113            pad_h2,
2114            pad_w1,
2115            pad_w2,
2116            ..
2117        }
2118        | Op::ReflectionPad3d {
2119            pad_d1,
2120            pad_d2,
2121            pad_h1,
2122            pad_h2,
2123            pad_w1,
2124            pad_w2,
2125        }
2126        | Op::ReplicationPad3d {
2127            pad_d1,
2128            pad_d2,
2129            pad_h1,
2130            pad_h2,
2131            pad_w1,
2132            pad_w2,
2133        }
2134        | Op::CircularPad3d {
2135            pad_d1,
2136            pad_d2,
2137            pad_h1,
2138            pad_h2,
2139            pad_w1,
2140            pad_w2,
2141        } => {
2142            // [N, C, D, H, W] → padded on each spatial dim
2143            let d_out = input[2].map(|d| d + pad_d1 + pad_d2);
2144            let h_out = input[3].map(|h| h + pad_h1 + pad_h2);
2145            let w_out = input[4].map(|w| w + pad_w1 + pad_w2);
2146            vec![input[0], input[1], d_out, h_out, w_out]
2147        }
2148
2149        Op::Attention { .. } => input.clone(),
2150
2151        Op::Add => input.clone(),
2152
2153        Op::ChannelChunk { chunk_c, .. } => {
2154            // [N, c_total, H, W] → [N, chunk_c, H, W]
2155            vec![input[0], Some(*chunk_c), input[2], input[3]]
2156        }
2157
2158        Op::ChannelCat { c_total } => {
2159            // multi-input; c_total encodes the output channel count
2160            vec![input[0], Some(*c_total), input[2], input[3]]
2161        }
2162
2163        Op::ChannelBiasAdd { .. } => input.to_vec(),
2164
2165        Op::Custom { data } => data.infer_output_shape(inputs),
2166
2167        // -------------------------------------------------------------------
2168        // ONNX-sourced ops — shape inference below.
2169        // For ops whose output shape equals the primary input shape (element-
2170        // wise, identity-like, or shape-tracked via ONNX value_info), we just
2171        // clone the input shape.  Ops with genuinely different output shapes
2172        // have explicit arms.
2173        // -------------------------------------------------------------------
2174
2175        // Unary element-wise — output shape = input shape
2176        Op::Abs
2177        | Op::Neg
2178        | Op::Ceil
2179        | Op::Floor
2180        | Op::Round
2181        | Op::Sqrt
2182        | Op::Reciprocal
2183        | Op::Exp
2184        | Op::Log
2185        | Op::Erf
2186        | Op::Sign
2187        | Op::IsNaN
2188        | Op::IsInf { .. }
2189        | Op::Not
2190        | Op::BitwiseNot
2191        | Op::Sin
2192        | Op::Cos
2193        | Op::Tan
2194        | Op::Asin
2195        | Op::Acos
2196        | Op::Atan
2197        | Op::Sinh
2198        | Op::Cosh
2199        | Op::Asinh
2200        | Op::Acosh
2201        | Op::Atanh
2202        | Op::PRelu
2203        | Op::ThresholdedRelu { .. }
2204        | Op::Shrink { .. }
2205        | Op::Clip
2206        | Op::Swish
2207        | Op::LogSoftmax { .. }
2208        | Op::Hardmax { .. }
2209        | Op::Dropout { .. }
2210        | Op::Identity
2211        | Op::LRN { .. }
2212        | Op::MeanVarianceNormalization { .. }
2213        | Op::LpNormalization { .. }
2214        | Op::Pad { .. }
2215        | Op::ReverseSequence { .. }
2216        | Op::Trilu { .. }
2217        | Op::CumSum { .. }
2218        | Op::CumProd { .. }
2219        | Op::QuantizeLinear { .. }
2220        | Op::DequantizeLinear { .. }
2221        | Op::DynamicQuantizeLinear
2222        | Op::Bernoulli { .. }
2223        | Op::RandomUniformLike { .. }
2224        | Op::EyeLike { .. }
2225        | Op::RotaryEmbedding
2226        | Op::MultiHeadAttention { .. }
2227        | Op::FlexAttention { .. }
2228        | Op::LinearAttention { .. }
2229        | Op::CausalConvWithState { .. } => input.clone(),
2230
2231        // Binary / variadic element-wise — approximate as first-input shape
2232        Op::Mul
2233        | Op::Sub
2234        | Op::Div
2235        | Op::Pow
2236        | Op::Mod { .. }
2237        | Op::ElemMin
2238        | Op::ElemMax
2239        | Op::ElemMean
2240        | Op::ElemSum
2241        | Op::Equal
2242        | Op::Greater
2243        | Op::GreaterOrEqual
2244        | Op::Less
2245        | Op::LessOrEqual
2246        | Op::And
2247        | Op::Or
2248        | Op::Xor
2249        | Op::BitwiseAnd
2250        | Op::BitwiseOr
2251        | Op::BitwiseXor
2252        | Op::BitShift { .. }
2253        | Op::Cast { .. }
2254        | Op::CastLike
2255        | Op::BitCast { .. }
2256        | Op::Where => input.clone(),
2257
2258        // Structural ops where output shape = input shape or is unknown at
2259        // static inference time (ONNX value_info carries the true shape).
2260        Op::Reshape
2261        | Op::Squeeze { .. }
2262        | Op::Unsqueeze { .. }
2263        | Op::Slice
2264        | Op::Gather { .. }
2265        | Op::GatherElements { .. }
2266        | Op::GatherND { .. }
2267        | Op::ScatterElements { .. }
2268        | Op::ScatterND
2269        | Op::Tile
2270        | Op::Expand
2271        | Op::Compress { .. }
2272        | Op::Range
2273        | Op::ConstantOfShape { .. }
2274        | Op::NonZero
2275        | Op::Scatter { .. }
2276        | Op::TensorScatter
2277        | Op::Resize { .. }
2278        | Op::GridSample { .. }
2279        | Op::AffineGrid { .. }
2280        | Op::CenterCropPad { .. } => input.clone(),
2281
2282        Op::Transpose { perm } => {
2283            if perm.is_empty() {
2284                input.iter().rev().cloned().collect()
2285            } else {
2286                perm.iter()
2287                    .map(|&i| input.get(i).copied().unwrap_or(None))
2288                    .collect()
2289            }
2290        }
2291
2292        Op::Concat { axis } => {
2293            let rank = input.len();
2294            if rank == 0 {
2295                return input.clone();
2296            }
2297            let ax = axis.rem_euclid(rank as i64) as usize;
2298            let mut out = input.clone();
2299            // Sum the concatenated axis across all inputs.
2300            out[ax] = inputs
2301                .iter()
2302                .try_fold(0usize, |acc, s| {
2303                    s.get(ax).copied().unwrap_or(None).map(|d| acc + d)
2304                })
2305                .map(Some)
2306                .unwrap_or(None);
2307            out
2308        }
2309
2310        Op::Split { axis, num_outputs } => {
2311            let rank = input.len();
2312            if rank == 0 {
2313                return input.clone();
2314            }
2315            let ax = axis.rem_euclid(rank as i64) as usize;
2316            let mut out = input.clone();
2317            out[ax] = input[ax].map(|d| d / num_outputs.max(&1));
2318            out
2319        }
2320
2321        Op::ShapeOf { start, end } => {
2322            let rank = input.len() as i64;
2323            let s = start.rem_euclid(rank.max(1));
2324            let e = end.rem_euclid(rank.max(1));
2325            vec![Some((e - s).max(0) as usize)]
2326        }
2327
2328        Op::SizeOf => vec![Some(1)],
2329
2330        Op::Gemm {
2331            trans_a, trans_b, ..
2332        } => {
2333            let m = if *trans_a {
2334                input.get(1)
2335            } else {
2336                input.first()
2337            }
2338            .copied()
2339            .unwrap_or(None);
2340            let n = if inputs.len() >= 2 {
2341                let b = inputs[1];
2342                if *trans_b { b.first() } else { b.get(1) }
2343                    .copied()
2344                    .unwrap_or(None)
2345            } else {
2346                None
2347            };
2348            vec![m, n]
2349        }
2350
2351        Op::MatMul | Op::MatMulInteger | Op::QLinearMatMul => {
2352            if inputs.len() >= 2 && !input.is_empty() {
2353                let other = inputs[1];
2354                let mut out = input[..input.len() - 1].to_vec();
2355                out.push(other.last().copied().unwrap_or(None));
2356                out
2357            } else {
2358                input.clone()
2359            }
2360        }
2361
2362        Op::Einsum { .. }
2363        | Op::Det
2364        | Op::Col2Im { .. }
2365        | Op::ConvInteger { .. }
2366        | Op::DeformConv { .. }
2367        | Op::QLinearConv { .. } => input.clone(),
2368
2369        Op::ConvTranspose {
2370            out_channels,
2371            kernel_h,
2372            kernel_w,
2373            stride_h,
2374            stride_w,
2375            padding_h,
2376            padding_w,
2377            output_padding_h,
2378            output_padding_w,
2379            ..
2380        } => {
2381            let h_out =
2382                input[2].map(|h| (h - 1) * stride_h - 2 * padding_h + kernel_h + output_padding_h);
2383            let w_out =
2384                input[3].map(|w| (w - 1) * stride_w - 2 * padding_w + kernel_w + output_padding_w);
2385            vec![input[0], Some(*out_channels), h_out, w_out]
2386        }
2387
2388        Op::ReduceSum { keepdims, .. }
2389        | Op::ReduceMean { keepdims, .. }
2390        | Op::ReduceMax { keepdims, .. }
2391        | Op::ReduceMin { keepdims, .. }
2392        | Op::ReduceProd { keepdims, .. }
2393        | Op::ReduceL1 { keepdims, .. }
2394        | Op::ReduceL2 { keepdims, .. }
2395        | Op::ReduceLogSum { keepdims, .. }
2396        | Op::ReduceLogSumExp { keepdims, .. }
2397        | Op::ReduceSumSquare { keepdims, .. } => {
2398            // Without axis info at static-inference time, approximate:
2399            // keepdims=true → same rank, keepdims=false → reduce all → scalar.
2400            if *keepdims {
2401                input.clone()
2402            } else {
2403                vec![Some(1)]
2404            }
2405        }
2406
2407        Op::ArgMax { axis, keepdims, .. } | Op::ArgMin { axis, keepdims, .. } => {
2408            if input.is_empty() {
2409                return vec![];
2410            }
2411            let ax = axis.rem_euclid(input.len() as i64) as usize;
2412            if *keepdims {
2413                let mut out = input.clone();
2414                out[ax] = Some(1);
2415                out
2416            } else {
2417                let mut out = input.clone();
2418                out.remove(ax);
2419                out
2420            }
2421        }
2422
2423        Op::GlobalAvgPool | Op::GlobalMaxPool => {
2424            let mut out = input[..2.min(input.len())].to_vec();
2425            for _ in 2..input.len() {
2426                out.push(Some(1));
2427            }
2428            out
2429        }
2430
2431        Op::Lstm {
2432            hidden_size,
2433            bidirectional,
2434            ..
2435        }
2436        | Op::Gru {
2437            hidden_size,
2438            bidirectional,
2439            ..
2440        }
2441        | Op::Rnn {
2442            hidden_size,
2443            bidirectional,
2444            ..
2445        } => {
2446            let num_dirs: usize = if *bidirectional { 2 } else { 1 };
2447            // [seq_len, num_directions, batch, hidden_size] (approximate)
2448            vec![
2449                input.first().copied().unwrap_or(None),
2450                Some(num_dirs),
2451                input.get(1).copied().unwrap_or(None),
2452                Some(*hidden_size),
2453            ]
2454        }
2455
2456        Op::SpaceToDepth { blocksize } => {
2457            let c_out = input[1].map(|c| c * blocksize * blocksize);
2458            let h_out = input[2].map(|h| h / blocksize);
2459            let w_out = input[3].map(|w| w / blocksize);
2460            vec![input[0], c_out, h_out, w_out]
2461        }
2462
2463        Op::DepthToSpace { blocksize, .. } => {
2464            let c_out = input[1].map(|c| c / (blocksize * blocksize));
2465            let h_out = input[2].map(|h| h * blocksize);
2466            let w_out = input[3].map(|w| w * blocksize);
2467            vec![input[0], c_out, h_out, w_out]
2468        }
2469
2470        Op::RoiAlign {
2471            output_h, output_w, ..
2472        } => {
2473            vec![input[0], input[1], Some(*output_h), Some(*output_w)]
2474        }
2475
2476        Op::MaxUnpool {
2477            kernel_h,
2478            kernel_w,
2479            stride_h,
2480            stride_w,
2481        } => {
2482            let h_out = input[2].map(|h| (h - 1) * stride_h + kernel_h);
2483            let w_out = input[3].map(|w| (w - 1) * stride_w + kernel_w);
2484            vec![input[0], input[1], h_out, w_out]
2485        }
2486
2487        Op::NonMaxSuppression { .. } => vec![None, Some(3)],
2488
2489        Op::TopK { axis, .. } => {
2490            // Second input is k (runtime). Return input shape as approximation.
2491            let _ = axis;
2492            input.clone()
2493        }
2494
2495        Op::Unique { .. } => input.clone(),
2496        Op::OneHot { .. } => input.clone(),
2497
2498        Op::NegativeLogLikelihoodLoss { .. } | Op::SoftmaxCrossEntropyLoss { .. } => {
2499            vec![Some(1)]
2500        }
2501
2502        Op::Dft { onesided, .. } => {
2503            // DFT last dim: full=N, onesided=N/2+1. Approximate.
2504            if *onesided && input.len() >= 2 {
2505                let mut out = input.clone();
2506                *out.last_mut().unwrap() = None;
2507                out
2508            } else {
2509                input.clone()
2510            }
2511        }
2512
2513        Op::Stft
2514        | Op::MelWeightMatrix
2515        | Op::HannWindow { .. }
2516        | Op::BlackmanWindow { .. }
2517        | Op::HammingWindow { .. } => input.clone(),
2518
2519        Op::SequenceAt
2520        | Op::SequenceConstruct
2521        | Op::SequenceErase
2522        | Op::SequenceInsert
2523        | Op::SequenceLength
2524        | Op::SequenceMap
2525        | Op::SplitToSequence { .. }
2526        | Op::ConcatFromSequence { .. }
2527        | Op::OptionalGetElement
2528        | Op::Loop
2529        | Op::Scan { .. }
2530        | Op::If
2531        | Op::Adagrad
2532        | Op::Adam
2533        | Op::Momentum
2534        | Op::Gradient
2535        | Op::StringNormalizer
2536        | Op::RegexFullMatch { .. }
2537        | Op::StringConcat
2538        | Op::StringSplit
2539        | Op::TfIdfVectorizer
2540        | Op::LabelEncoder
2541        | Op::ArrayFeatureExtractor
2542        | Op::Binarizer { .. }
2543        | Op::TreeEnsemble
2544        | Op::ImageDecoder => input.clone(),
2545
2546        // Handled by early returns above the match; arms required for exhaustiveness.
2547        Op::Constant { shape, .. } => shape.clone(),
2548        Op::SequenceEmpty | Op::OptionalHasElement => vec![],
2549    }
2550}
2551
2552// ---------------------------------------------------------------------------
2553// SymTensor — a tensor that writes to the graph on every operation
2554// ---------------------------------------------------------------------------
2555
2556/// A symbolic tensor handle. Every layer operation on a `SymTensor` records
2557/// itself in the shared `Graph` and returns a new `SymTensor` pointing to
2558/// the new node. Cloning is cheap — it shares the graph via `Rc`.
2559#[derive(Clone)]
2560pub struct SymTensor {
2561    /// This tensor's node index in `graph`.
2562    pub node_id: usize,
2563    /// The shared graph this tensor's operations record into.
2564    pub graph: Rc<RefCell<Graph>>,
2565    /// This tensor's dtype.
2566    pub dtype: DtypeRepr,
2567    /// Output shape of this tensor. `None` in a slot means a dynamic/unknown
2568    /// dimension (e.g. the batch axis).
2569    pub shape: Shape,
2570}
2571
2572// SymTensor satisfies Tensor<D, RANK> for any D and RANK — shape is tracked
2573// dynamically at runtime; the compile-time SHAPE constant is zeroed (unused).
2574impl<D: Dtype, const RANK: usize> RankedTensor<D, RANK> for SymTensor {
2575    const SHAPE: [usize; RANK] = [0; RANK];
2576}
2577impl<D: Dtype, const RANK: usize> Tensor<D, RANK> for SymTensor {}
2578
2579impl SymTensor {
2580    /// Create an input placeholder, returning both the tensor and the shared
2581    /// graph handle. Keep the graph handle to inspect the result after tracing.
2582    ///
2583    /// Use `None` for dynamic dimensions (e.g. the batch axis):
2584    /// ```ignore
2585    /// SymTensor::input(DtypeRepr::F32, vec![None, Some(784)])
2586    /// ```
2587    pub fn input(dtype: DtypeRepr, shape: Shape) -> (Self, Rc<RefCell<Graph>>) {
2588        let graph = Rc::new(RefCell::new(Graph::new()));
2589        let node_id = graph
2590            .borrow_mut()
2591            .add_node(Op::Input, vec![], dtype, shape.clone());
2592        let tensor = Self {
2593            node_id,
2594            graph: graph.clone(),
2595            dtype,
2596            shape,
2597        };
2598        (tensor, graph)
2599    }
2600
2601    /// Number of dimensions of this tensor.
2602    pub fn rank(&self) -> usize {
2603        self.shape.len()
2604    }
2605
2606    fn record(&self, op: Op) -> Self {
2607        let output_shape = infer_output_shape(&op, &[&self.shape]);
2608        self.record_with_shape(op, output_shape)
2609    }
2610
2611    fn record_with_shape(&self, op: Op, shape: Shape) -> Self {
2612        let node_id =
2613            self.graph
2614                .borrow_mut()
2615                .add_node(op, vec![self.node_id], self.dtype, shape.clone());
2616        Self {
2617            node_id,
2618            graph: self.graph.clone(),
2619            dtype: self.dtype,
2620            shape,
2621        }
2622    }
2623
2624    /// Record a custom op whose output shape is determined by [`CustomOp::infer_output_shape`].
2625    ///
2626    /// `self` is the primary (first) input.  Pass additional inputs via
2627    /// `other_inputs`.  Pass `dtype` to override the output element type;
2628    /// defaults to the primary input's dtype.
2629    pub fn record_custom(
2630        &self,
2631        data: CustomData,
2632        other_inputs: &[&SymTensor],
2633        dtype: Option<DtypeRepr>,
2634    ) -> Self {
2635        let mut shapes: Vec<&Shape> = vec![&self.shape];
2636        shapes.extend(other_inputs.iter().map(|t| &t.shape));
2637        let output_shape = data.infer_output_shape(&shapes);
2638
2639        let mut input_ids: Vec<usize> = vec![self.node_id];
2640        input_ids.extend(other_inputs.iter().map(|t| t.node_id));
2641
2642        let out_dtype = dtype.unwrap_or(self.dtype);
2643        let node_id = self.graph.borrow_mut().add_node(
2644            Op::Custom { data },
2645            input_ids,
2646            out_dtype,
2647            output_shape.clone(),
2648        );
2649        Self {
2650            node_id,
2651            graph: self.graph.clone(),
2652            dtype: out_dtype,
2653            shape: output_shape,
2654        }
2655    }
2656}
2657
2658// ---------------------------------------------------------------------------
2659// Layer<SymTensor> impls — record op instead of computing
2660// ---------------------------------------------------------------------------
2661
2662// --- Linear / MLP ---
2663
2664impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Linear<D, SymTensor, SymTensor, RANK> {
2665    type Output = SymTensor;
2666    fn call(&self, input: SymTensor) -> SymTensor {
2667        input.record(Op::Linear {
2668            in_features: self.in_features,
2669            out_features: self.out_features,
2670            has_bias: self.has_bias,
2671        })
2672    }
2673}
2674
2675impl<D: Dtype> Layer<SymTensor> for Flatten<D, SymTensor, SymTensor> {
2676    type Output = SymTensor;
2677    fn call(&self, input: SymTensor) -> SymTensor {
2678        input.record(Op::Flatten)
2679    }
2680}
2681
2682// --- Normalisation ---
2683
2684impl<D: Dtype, const RANK: usize> Layer<SymTensor> for BatchNorm1d<D, SymTensor, SymTensor, RANK> {
2685    type Output = SymTensor;
2686    fn call(&self, input: SymTensor) -> SymTensor {
2687        input.record(Op::BatchNorm1d {
2688            num_features: self.num_features,
2689            eps: self.eps,
2690            momentum: self.momentum,
2691            affine: self.affine,
2692            track_running_stats: self.track_running_stats,
2693        })
2694    }
2695}
2696
2697impl<D: Dtype, const RANK: usize> Layer<SymTensor> for BatchNorm2d<D, SymTensor, SymTensor, RANK> {
2698    type Output = SymTensor;
2699    fn call(&self, input: SymTensor) -> SymTensor {
2700        input.record(Op::BatchNorm2d {
2701            num_features: self.num_features,
2702            eps: self.eps,
2703            momentum: self.momentum,
2704            affine: self.affine,
2705            track_running_stats: self.track_running_stats,
2706        })
2707    }
2708}
2709
2710impl<D: Dtype, const RANK: usize> Layer<SymTensor> for BatchNorm3d<D, SymTensor, SymTensor, RANK> {
2711    type Output = SymTensor;
2712    fn call(&self, input: SymTensor) -> SymTensor {
2713        input.record(Op::BatchNorm3d {
2714            num_features: self.num_features,
2715            eps: self.eps,
2716            momentum: self.momentum,
2717            affine: self.affine,
2718            track_running_stats: self.track_running_stats,
2719        })
2720    }
2721}
2722
2723impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LayerNorm<D, SymTensor, SymTensor, RANK> {
2724    type Output = SymTensor;
2725    fn call(&self, input: SymTensor) -> SymTensor {
2726        input.record(Op::LayerNorm {
2727            normalized_shape: self.normalized_shape.clone(),
2728            eps: self.eps,
2729            affine: self.affine,
2730        })
2731    }
2732}
2733
2734impl<D: Dtype, const RANK: usize> Layer<SymTensor> for RmsNorm<D, SymTensor, SymTensor, RANK> {
2735    type Output = SymTensor;
2736    fn call(&self, input: SymTensor) -> SymTensor {
2737        input.record(Op::RmsNorm {
2738            normalized_shape: self.normalized_shape.clone(),
2739            eps: self.eps,
2740            affine: self.affine,
2741        })
2742    }
2743}
2744
2745impl<D: Dtype, const RANK: usize> Layer<SymTensor> for GroupNorm<D, SymTensor, SymTensor, RANK> {
2746    type Output = SymTensor;
2747    fn call(&self, input: SymTensor) -> SymTensor {
2748        input.record(Op::GroupNorm {
2749            num_groups: self.num_groups,
2750            num_channels: self.num_channels,
2751            eps: self.eps,
2752            affine: self.affine,
2753        })
2754    }
2755}
2756
2757impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2758    for InstanceNorm1d<D, SymTensor, SymTensor, RANK>
2759{
2760    type Output = SymTensor;
2761    fn call(&self, input: SymTensor) -> SymTensor {
2762        input.record(Op::InstanceNorm1d {
2763            num_features: self.num_features,
2764            eps: self.eps,
2765            momentum: self.momentum,
2766            affine: self.affine,
2767            track_running_stats: self.track_running_stats,
2768        })
2769    }
2770}
2771
2772impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2773    for InstanceNorm2d<D, SymTensor, SymTensor, RANK>
2774{
2775    type Output = SymTensor;
2776    fn call(&self, input: SymTensor) -> SymTensor {
2777        input.record(Op::InstanceNorm2d {
2778            num_features: self.num_features,
2779            eps: self.eps,
2780            momentum: self.momentum,
2781            affine: self.affine,
2782            track_running_stats: self.track_running_stats,
2783        })
2784    }
2785}
2786
2787impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2788    for InstanceNorm3d<D, SymTensor, SymTensor, RANK>
2789{
2790    type Output = SymTensor;
2791    fn call(&self, input: SymTensor) -> SymTensor {
2792        input.record(Op::InstanceNorm3d {
2793            num_features: self.num_features,
2794            eps: self.eps,
2795            momentum: self.momentum,
2796            affine: self.affine,
2797            track_running_stats: self.track_running_stats,
2798        })
2799    }
2800}
2801
2802// --- Convolution ---
2803
2804impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Conv1d<D, SymTensor, SymTensor, RANK> {
2805    type Output = SymTensor;
2806    fn call(&self, input: SymTensor) -> SymTensor {
2807        input.record(Op::Conv1d {
2808            in_channels: self.in_channels,
2809            out_channels: self.out_channels,
2810            kernel_l: self.kernel_l,
2811            stride: self.stride,
2812            padding: self.padding,
2813            has_bias: self.has_bias,
2814        })
2815    }
2816}
2817
2818impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Conv2d<D, SymTensor, SymTensor, RANK> {
2819    type Output = SymTensor;
2820    fn call(&self, input: SymTensor) -> SymTensor {
2821        input.record(Op::Conv2d {
2822            in_channels: self.in_channels,
2823            out_channels: self.out_channels,
2824            kernel_h: self.kernel_h,
2825            kernel_w: self.kernel_w,
2826            stride_h: self.stride_h,
2827            stride_w: self.stride_w,
2828            padding_h: self.padding_h,
2829            padding_w: self.padding_w,
2830            groups: self.groups,
2831            has_bias: self.has_bias,
2832        })
2833    }
2834}
2835
2836impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Conv3d<D, SymTensor, SymTensor, RANK> {
2837    type Output = SymTensor;
2838    fn call(&self, input: SymTensor) -> SymTensor {
2839        input.record(Op::Conv3d {
2840            in_channels: self.in_channels,
2841            out_channels: self.out_channels,
2842            kernel_d: self.kernel_d,
2843            kernel_h: self.kernel_h,
2844            kernel_w: self.kernel_w,
2845            stride_d: self.stride_d,
2846            stride_h: self.stride_h,
2847            stride_w: self.stride_w,
2848            padding_d: self.padding_d,
2849            padding_h: self.padding_h,
2850            padding_w: self.padding_w,
2851            has_bias: self.has_bias,
2852        })
2853    }
2854}
2855
2856// --- Pooling ---
2857
2858impl<D: Dtype, const RANK: usize> Layer<SymTensor> for AvgPool1d<D, SymTensor, SymTensor, RANK> {
2859    type Output = SymTensor;
2860    fn call(&self, input: SymTensor) -> SymTensor {
2861        input.record(Op::AvgPool1d {
2862            kernel_l: self.kernel_l,
2863            stride: self.stride,
2864        })
2865    }
2866}
2867
2868impl<D: Dtype, const RANK: usize> Layer<SymTensor> for AvgPool2d<D, SymTensor, SymTensor, RANK> {
2869    type Output = SymTensor;
2870    fn call(&self, input: SymTensor) -> SymTensor {
2871        input.record(Op::AvgPool2d {
2872            kernel_h: self.kernel_h,
2873            kernel_w: self.kernel_w,
2874            stride_h: self.stride_h,
2875            stride_w: self.stride_w,
2876        })
2877    }
2878}
2879
2880impl<D: Dtype, const RANK: usize> Layer<SymTensor> for AvgPool3d<D, SymTensor, SymTensor, RANK> {
2881    type Output = SymTensor;
2882    fn call(&self, input: SymTensor) -> SymTensor {
2883        input.record(Op::AvgPool3d {
2884            kernel_d: self.kernel_d,
2885            kernel_h: self.kernel_h,
2886            kernel_w: self.kernel_w,
2887            stride_d: self.stride_d,
2888            stride_h: self.stride_h,
2889            stride_w: self.stride_w,
2890        })
2891    }
2892}
2893
2894impl<D: Dtype, const RANK: usize> Layer<SymTensor> for MaxPool1d<D, SymTensor, SymTensor, RANK> {
2895    type Output = SymTensor;
2896    fn call(&self, input: SymTensor) -> SymTensor {
2897        input.record(Op::MaxPool1d {
2898            kernel_l: self.kernel_l,
2899            stride: self.stride,
2900        })
2901    }
2902}
2903
2904impl<D: Dtype, const RANK: usize> Layer<SymTensor> for MaxPool2d<D, SymTensor, SymTensor, RANK> {
2905    type Output = SymTensor;
2906    fn call(&self, input: SymTensor) -> SymTensor {
2907        input.record(Op::MaxPool2d {
2908            kernel_h: self.kernel_h,
2909            kernel_w: self.kernel_w,
2910            stride_h: self.stride_h,
2911            stride_w: self.stride_w,
2912            pad_h: self.padding_h,
2913            pad_w: self.padding_w,
2914        })
2915    }
2916}
2917
2918impl<D: Dtype, const RANK: usize> Layer<SymTensor> for MaxPool3d<D, SymTensor, SymTensor, RANK> {
2919    type Output = SymTensor;
2920    fn call(&self, input: SymTensor) -> SymTensor {
2921        input.record(Op::MaxPool3d {
2922            kernel_d: self.kernel_d,
2923            kernel_h: self.kernel_h,
2924            kernel_w: self.kernel_w,
2925            stride_d: self.stride_d,
2926            stride_h: self.stride_h,
2927            stride_w: self.stride_w,
2928        })
2929    }
2930}
2931
2932impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LpPool1d<D, SymTensor, SymTensor, RANK> {
2933    type Output = SymTensor;
2934    fn call(&self, input: SymTensor) -> SymTensor {
2935        input.record(Op::LpPool1d {
2936            kernel_l: self.kernel_l,
2937            stride: self.stride,
2938            p: self.p,
2939        })
2940    }
2941}
2942
2943impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LpPool2d<D, SymTensor, SymTensor, RANK> {
2944    type Output = SymTensor;
2945    fn call(&self, input: SymTensor) -> SymTensor {
2946        input.record(Op::LpPool2d {
2947            kernel_h: self.kernel_h,
2948            kernel_w: self.kernel_w,
2949            stride_h: self.stride_h,
2950            stride_w: self.stride_w,
2951            p: self.p,
2952        })
2953    }
2954}
2955
2956impl<D: Dtype, const RANK: usize> Layer<SymTensor> for LpPool3d<D, SymTensor, SymTensor, RANK> {
2957    type Output = SymTensor;
2958    fn call(&self, input: SymTensor) -> SymTensor {
2959        input.record(Op::LpPool3d {
2960            kernel_d: self.kernel_d,
2961            kernel_h: self.kernel_h,
2962            kernel_w: self.kernel_w,
2963            stride_d: self.stride_d,
2964            stride_h: self.stride_h,
2965            stride_w: self.stride_w,
2966            p: self.p,
2967        })
2968    }
2969}
2970
2971// --- Padding ---
2972
2973impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2974    for ConstantPad1d<D, SymTensor, SymTensor, RANK>
2975{
2976    type Output = SymTensor;
2977    fn call(&self, input: SymTensor) -> SymTensor {
2978        input.record(Op::ConstantPad1d {
2979            pad_left: self.pad_left,
2980            pad_right: self.pad_right,
2981            value: self.value,
2982        })
2983    }
2984}
2985
2986impl<D: Dtype, const RANK: usize> Layer<SymTensor>
2987    for ConstantPad2d<D, SymTensor, SymTensor, RANK>
2988{
2989    type Output = SymTensor;
2990    fn call(&self, input: SymTensor) -> SymTensor {
2991        input.record(Op::ConstantPad2d {
2992            pad_l: self.pad_l,
2993            pad_r: self.pad_r,
2994            pad_t: self.pad_t,
2995            pad_b: self.pad_b,
2996            value: self.value,
2997        })
2998    }
2999}
3000
3001impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3002    for ConstantPad3d<D, SymTensor, SymTensor, RANK>
3003{
3004    type Output = SymTensor;
3005    fn call(&self, input: SymTensor) -> SymTensor {
3006        input.record(Op::ConstantPad3d {
3007            pad_d1: self.pad_d1,
3008            pad_d2: self.pad_d2,
3009            pad_h1: self.pad_h1,
3010            pad_h2: self.pad_h2,
3011            pad_w1: self.pad_w1,
3012            pad_w2: self.pad_w2,
3013            value: self.value,
3014        })
3015    }
3016}
3017
3018impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3019    for ReflectionPad1d<D, SymTensor, SymTensor, RANK>
3020{
3021    type Output = SymTensor;
3022    fn call(&self, input: SymTensor) -> SymTensor {
3023        input.record(Op::ReflectionPad1d {
3024            pad_left: self.pad_left,
3025            pad_right: self.pad_right,
3026        })
3027    }
3028}
3029
3030impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3031    for ReflectionPad2d<D, SymTensor, SymTensor, RANK>
3032{
3033    type Output = SymTensor;
3034    fn call(&self, input: SymTensor) -> SymTensor {
3035        input.record(Op::ReflectionPad2d {
3036            pad_l: self.pad_l,
3037            pad_r: self.pad_r,
3038            pad_t: self.pad_t,
3039            pad_b: self.pad_b,
3040        })
3041    }
3042}
3043
3044impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3045    for ReflectionPad3d<D, SymTensor, SymTensor, RANK>
3046{
3047    type Output = SymTensor;
3048    fn call(&self, input: SymTensor) -> SymTensor {
3049        input.record(Op::ReflectionPad3d {
3050            pad_d1: self.pad_d1,
3051            pad_d2: self.pad_d2,
3052            pad_h1: self.pad_h1,
3053            pad_h2: self.pad_h2,
3054            pad_w1: self.pad_w1,
3055            pad_w2: self.pad_w2,
3056        })
3057    }
3058}
3059
3060impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3061    for ReplicationPad1d<D, SymTensor, SymTensor, RANK>
3062{
3063    type Output = SymTensor;
3064    fn call(&self, input: SymTensor) -> SymTensor {
3065        input.record(Op::ReplicationPad1d {
3066            pad_left: self.pad_left,
3067            pad_right: self.pad_right,
3068        })
3069    }
3070}
3071
3072impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3073    for ReplicationPad2d<D, SymTensor, SymTensor, RANK>
3074{
3075    type Output = SymTensor;
3076    fn call(&self, input: SymTensor) -> SymTensor {
3077        input.record(Op::ReplicationPad2d {
3078            pad_l: self.pad_l,
3079            pad_r: self.pad_r,
3080            pad_t: self.pad_t,
3081            pad_b: self.pad_b,
3082        })
3083    }
3084}
3085
3086impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3087    for ReplicationPad3d<D, SymTensor, SymTensor, RANK>
3088{
3089    type Output = SymTensor;
3090    fn call(&self, input: SymTensor) -> SymTensor {
3091        input.record(Op::ReplicationPad3d {
3092            pad_d1: self.pad_d1,
3093            pad_d2: self.pad_d2,
3094            pad_h1: self.pad_h1,
3095            pad_h2: self.pad_h2,
3096            pad_w1: self.pad_w1,
3097            pad_w2: self.pad_w2,
3098        })
3099    }
3100}
3101
3102impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3103    for CircularPad1d<D, SymTensor, SymTensor, RANK>
3104{
3105    type Output = SymTensor;
3106    fn call(&self, input: SymTensor) -> SymTensor {
3107        input.record(Op::CircularPad1d {
3108            pad_left: self.pad_left,
3109            pad_right: self.pad_right,
3110        })
3111    }
3112}
3113
3114impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3115    for CircularPad2d<D, SymTensor, SymTensor, RANK>
3116{
3117    type Output = SymTensor;
3118    fn call(&self, input: SymTensor) -> SymTensor {
3119        input.record(Op::CircularPad2d {
3120            pad_l: self.pad_l,
3121            pad_r: self.pad_r,
3122            pad_t: self.pad_t,
3123            pad_b: self.pad_b,
3124        })
3125    }
3126}
3127
3128impl<D: Dtype, const RANK: usize> Layer<SymTensor>
3129    for CircularPad3d<D, SymTensor, SymTensor, RANK>
3130{
3131    type Output = SymTensor;
3132    fn call(&self, input: SymTensor) -> SymTensor {
3133        input.record(Op::CircularPad3d {
3134            pad_d1: self.pad_d1,
3135            pad_d2: self.pad_d2,
3136            pad_h1: self.pad_h1,
3137            pad_h2: self.pad_h2,
3138            pad_w1: self.pad_w1,
3139            pad_w2: self.pad_w2,
3140        })
3141    }
3142}
3143
3144// --- Activation ---
3145
3146impl<D: Dtype, const RANK: usize> Layer<SymTensor> for Relu<D, SymTensor, RANK> {
3147    type Output = SymTensor;
3148    fn call(&self, input: SymTensor) -> SymTensor {
3149        input.record(Op::Relu)
3150    }
3151}
3152
3153impl<D: Float, const RANK: usize> Layer<SymTensor> for Elu<D, SymTensor, RANK> {
3154    type Output = SymTensor;
3155    fn call(&self, input: SymTensor) -> SymTensor {
3156        input.record(Op::Elu { alpha: self.alpha })
3157    }
3158}
3159
3160impl<D: Float, const RANK: usize> Layer<SymTensor> for Selu<D, SymTensor, RANK> {
3161    type Output = SymTensor;
3162    fn call(&self, input: SymTensor) -> SymTensor {
3163        input.record(Op::Selu)
3164    }
3165}
3166
3167impl<D: Float, const RANK: usize> Layer<SymTensor> for Celu<D, SymTensor, RANK> {
3168    type Output = SymTensor;
3169    fn call(&self, input: SymTensor) -> SymTensor {
3170        input.record(Op::Celu { alpha: self.alpha })
3171    }
3172}
3173
3174impl<D: Float, const RANK: usize> Layer<SymTensor> for Gelu<D, SymTensor, RANK> {
3175    type Output = SymTensor;
3176    fn call(&self, input: SymTensor) -> SymTensor {
3177        input.record(Op::Gelu)
3178    }
3179}
3180
3181impl<D: Float, const RANK: usize> Layer<SymTensor> for Mish<D, SymTensor, RANK> {
3182    type Output = SymTensor;
3183    fn call(&self, input: SymTensor) -> SymTensor {
3184        input.record(Op::Mish)
3185    }
3186}
3187
3188impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardtanh<D, SymTensor, RANK> {
3189    type Output = SymTensor;
3190    fn call(&self, input: SymTensor) -> SymTensor {
3191        input.record(Op::Hardtanh {
3192            min_val: self.min_val,
3193            max_val: self.max_val,
3194        })
3195    }
3196}
3197
3198impl<D: Float, const RANK: usize> Layer<SymTensor> for Relu6<D, SymTensor, RANK> {
3199    type Output = SymTensor;
3200    fn call(&self, input: SymTensor) -> SymTensor {
3201        input.record(Op::Relu6)
3202    }
3203}
3204
3205impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardsigmoid<D, SymTensor, RANK> {
3206    type Output = SymTensor;
3207    fn call(&self, input: SymTensor) -> SymTensor {
3208        input.record(Op::Hardsigmoid)
3209    }
3210}
3211
3212impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardswish<D, SymTensor, RANK> {
3213    type Output = SymTensor;
3214    fn call(&self, input: SymTensor) -> SymTensor {
3215        input.record(Op::Hardswish)
3216    }
3217}
3218
3219impl<D: Float, const RANK: usize> Layer<SymTensor> for Hardshrink<D, SymTensor, RANK> {
3220    type Output = SymTensor;
3221    fn call(&self, input: SymTensor) -> SymTensor {
3222        input.record(Op::Hardshrink {
3223            lambda: self.lambda,
3224        })
3225    }
3226}
3227
3228impl<D: Float, const RANK: usize> Layer<SymTensor> for LeakyRelu<D, SymTensor, RANK> {
3229    type Output = SymTensor;
3230    fn call(&self, input: SymTensor) -> SymTensor {
3231        input.record(Op::LeakyRelu {
3232            negative_slope: self.negative_slope,
3233        })
3234    }
3235}
3236
3237impl<D: Float, const RANK: usize> Layer<SymTensor> for Threshold<D, SymTensor, RANK> {
3238    type Output = SymTensor;
3239    fn call(&self, input: SymTensor) -> SymTensor {
3240        input.record(Op::Threshold {
3241            threshold: self.threshold,
3242            value: self.value,
3243        })
3244    }
3245}
3246
3247impl<D: Float, const RANK: usize> Layer<SymTensor> for Softsign<D, SymTensor, RANK> {
3248    type Output = SymTensor;
3249    fn call(&self, input: SymTensor) -> SymTensor {
3250        input.record(Op::Softsign)
3251    }
3252}
3253
3254impl<D: Float, const RANK: usize> Layer<SymTensor> for Softshrink<D, SymTensor, RANK> {
3255    type Output = SymTensor;
3256    fn call(&self, input: SymTensor) -> SymTensor {
3257        input.record(Op::Softshrink {
3258            lambda: self.lambda,
3259        })
3260    }
3261}
3262
3263impl<D: Float, const RANK: usize> Layer<SymTensor> for Softplus<D, SymTensor, RANK> {
3264    type Output = SymTensor;
3265    fn call(&self, input: SymTensor) -> SymTensor {
3266        input.record(Op::Softplus {
3267            beta: self.beta,
3268            threshold: self.threshold,
3269        })
3270    }
3271}
3272
3273impl<D: Float, const RANK: usize> Layer<SymTensor> for Sigmoid<D, SymTensor, RANK> {
3274    type Output = SymTensor;
3275    fn call(&self, input: SymTensor) -> SymTensor {
3276        input.record(Op::Sigmoid)
3277    }
3278}
3279
3280impl<D: Float, const RANK: usize> Layer<SymTensor> for Silu<D, SymTensor, RANK> {
3281    type Output = SymTensor;
3282    fn call(&self, input: SymTensor) -> SymTensor {
3283        input.record(Op::Silu)
3284    }
3285}
3286
3287impl<D: Float, const RANK: usize> Layer<SymTensor> for Logsigmoid<D, SymTensor, RANK> {
3288    type Output = SymTensor;
3289    fn call(&self, input: SymTensor) -> SymTensor {
3290        input.record(Op::Logsigmoid)
3291    }
3292}
3293
3294impl<D: Float, const RANK: usize> Layer<SymTensor> for Tanh<D, SymTensor, RANK> {
3295    type Output = SymTensor;
3296    fn call(&self, input: SymTensor) -> SymTensor {
3297        input.record(Op::Tanh)
3298    }
3299}
3300
3301impl<D: Float, const RANK: usize> Layer<SymTensor> for Tanhshrink<D, SymTensor, RANK> {
3302    type Output = SymTensor;
3303    fn call(&self, input: SymTensor) -> SymTensor {
3304        input.record(Op::Tanhshrink)
3305    }
3306}
3307
3308impl<D: Float, const RANK: usize> Layer<SymTensor> for Softmax<D, SymTensor, RANK> {
3309    type Output = SymTensor;
3310    fn call(&self, input: SymTensor) -> SymTensor {
3311        input.record(Op::Softmax { dim: self.dim })
3312    }
3313}
3314
3315// ---------------------------------------------------------------------------
3316// Tests
3317// ---------------------------------------------------------------------------
3318
3319#[cfg(test)]
3320mod tests {
3321    use super::*;
3322    use crate::{
3323        nn::{
3324            activation::{relu::Relu, softmax::Softmax},
3325            conv2d::Conv2d,
3326            linear::Linear,
3327        },
3328        sequential,
3329    };
3330
3331    #[test]
3332    fn test_sequential_graph_extraction() {
3333        let (input, graph) = SymTensor::input(DtypeRepr::F32, vec![None, Some(784)]);
3334
3335        let model = sequential![
3336            Linear::<f32, SymTensor, SymTensor, 2>::new(784, 128, true),
3337            Relu::<f32, SymTensor, 2>::new(),
3338            Linear::<f32, SymTensor, SymTensor, 2>::new(128, 10, true),
3339            Softmax::<f32, SymTensor, 2>::new(1)
3340        ];
3341
3342        let _out = Layer::call(&model, input);
3343
3344        let g = graph.borrow();
3345        assert_eq!(g.nodes.len(), 5);
3346        assert!(matches!(g.nodes[0].op, Op::Input));
3347        assert_eq!(g.nodes[0].shape, vec![None, Some(784)]);
3348
3349        assert!(matches!(
3350            g.nodes[1].op,
3351            Op::Linear {
3352                in_features: 784,
3353                out_features: 128,
3354                ..
3355            }
3356        ));
3357        assert_eq!(g.nodes[1].shape, vec![None, Some(128)]);
3358
3359        assert!(matches!(g.nodes[2].op, Op::Relu));
3360        assert_eq!(g.nodes[2].shape, vec![None, Some(128)]);
3361
3362        assert!(matches!(
3363            g.nodes[3].op,
3364            Op::Linear {
3365                in_features: 128,
3366                out_features: 10,
3367                ..
3368            }
3369        ));
3370        assert_eq!(g.nodes[3].shape, vec![None, Some(10)]);
3371
3372        assert!(matches!(g.nodes[4].op, Op::Softmax { dim: 1 }));
3373        assert_eq!(g.nodes[4].shape, vec![None, Some(10)]);
3374    }
3375
3376    #[test]
3377    fn test_topological_sort_linear_chain() {
3378        let (input, graph) = SymTensor::input(DtypeRepr::F32, vec![None, Some(784)]);
3379
3380        let model = sequential![
3381            Linear::<f32, SymTensor, SymTensor, 2>::new(784, 128, true),
3382            Relu::<f32, SymTensor, 2>::new(),
3383            Linear::<f32, SymTensor, SymTensor, 2>::new(128, 10, true),
3384            Softmax::<f32, SymTensor, 2>::new(1)
3385        ];
3386
3387        let _out = Layer::call(&model, input);
3388
3389        let g = graph.borrow();
3390        let order = g.topological_sort();
3391        assert_eq!(order.len(), g.nodes.len());
3392        for (pos, &id) in order.iter().enumerate() {
3393            for &input_id in &g.nodes[id].inputs {
3394                let input_pos = order.iter().position(|&x| x == input_id).unwrap();
3395                assert!(
3396                    input_pos < pos,
3397                    "producer {input_id} must come before consumer {id}"
3398                );
3399            }
3400        }
3401    }
3402
3403    #[test]
3404    fn test_residual_graph_extraction() {
3405        let (input, graph) = SymTensor::input(DtypeRepr::F32, vec![None, Some(64)]);
3406
3407        let main = Linear::<f32, SymTensor, SymTensor, 2>::new(64, 64, true).call(input.clone());
3408        let main = Relu::<f32, SymTensor, 2>::new().call(main);
3409        let skip = Linear::<f32, SymTensor, SymTensor, 2>::new(64, 64, false).call(input);
3410
3411        assert!(Rc::ptr_eq(&main.graph, &skip.graph));
3412
3413        let g = graph.borrow();
3414        assert_eq!(g.nodes.len(), 4);
3415        assert_eq!(g.nodes[1].inputs, vec![0]);
3416        assert_eq!(g.nodes[3].inputs, vec![0]);
3417    }
3418
3419    #[test]
3420    fn test_conv2d_graph_extraction() {
3421        let (input, graph) =
3422            SymTensor::input(DtypeRepr::F32, vec![None, Some(3), Some(32), Some(32)]);
3423
3424        let conv = Conv2d::<f32, SymTensor, SymTensor, 4>::new(3, 64, (3, 3), (1, 1), (1, 1), true);
3425        let _out = Layer::call(&conv, input);
3426
3427        let g = graph.borrow();
3428        assert_eq!(g.nodes.len(), 2);
3429        assert!(matches!(
3430            g.nodes[1].op,
3431            Op::Conv2d {
3432                in_channels: 3,
3433                out_channels: 64,
3434                kernel_h: 3,
3435                kernel_w: 3,
3436                stride_h: 1,
3437                stride_w: 1,
3438                padding_h: 1,
3439                padding_w: 1,
3440                has_bias: true,
3441                ..
3442            }
3443        ));
3444        assert_eq!(g.nodes[1].shape, vec![None, Some(64), Some(32), Some(32)]);
3445    }
3446
3447    #[test]
3448    fn test_lenet5_shapes() {
3449        let (input, graph) =
3450            SymTensor::input(DtypeRepr::F32, vec![None, Some(1), Some(28), Some(28)]);
3451
3452        use crate::{
3453            nn::{flatten::Flatten, pool::AvgPool2d},
3454            sequential,
3455        };
3456
3457        let model = sequential![
3458            Conv2d::<f32, SymTensor, SymTensor, 4>::new(1, 6, (5, 5), (1, 1), (2, 2), true),
3459            Relu::<f32, SymTensor, 4>::new(),
3460            AvgPool2d::<f32, SymTensor, SymTensor, 4>::new((2, 2), (2, 2)),
3461            Conv2d::<f32, SymTensor, SymTensor, 4>::new(6, 16, (5, 5), (1, 1), (0, 0), true),
3462            Relu::<f32, SymTensor, 4>::new(),
3463            AvgPool2d::<f32, SymTensor, SymTensor, 4>::new((2, 2), (2, 2)),
3464            Flatten::<f32, SymTensor, SymTensor>::new(),
3465            Linear::<f32, SymTensor, SymTensor, 2>::new(400, 120, true),
3466            Relu::<f32, SymTensor, 2>::new(),
3467            Linear::<f32, SymTensor, SymTensor, 2>::new(120, 84, true),
3468            Relu::<f32, SymTensor, 2>::new(),
3469            Linear::<f32, SymTensor, SymTensor, 2>::new(84, 10, true),
3470            Softmax::<f32, SymTensor, 2>::new(1)
3471        ];
3472
3473        let _out = Layer::call(&model, input);
3474
3475        let g = graph.borrow();
3476        assert_eq!(g.nodes.len(), 14);
3477        assert_eq!(g.nodes[0].shape, vec![None, Some(1), Some(28), Some(28)]);
3478        assert_eq!(g.nodes[1].shape, vec![None, Some(6), Some(28), Some(28)]);
3479        assert_eq!(g.nodes[2].shape, vec![None, Some(6), Some(28), Some(28)]);
3480        assert_eq!(g.nodes[3].shape, vec![None, Some(6), Some(14), Some(14)]);
3481        assert_eq!(g.nodes[4].shape, vec![None, Some(16), Some(10), Some(10)]);
3482        assert_eq!(g.nodes[5].shape, vec![None, Some(16), Some(10), Some(10)]);
3483        assert_eq!(g.nodes[6].shape, vec![None, Some(16), Some(5), Some(5)]);
3484        assert_eq!(g.nodes[7].shape, vec![None, Some(400)]);
3485        assert_eq!(g.nodes[8].shape, vec![None, Some(120)]);
3486        assert_eq!(g.nodes[9].shape, vec![None, Some(120)]);
3487        assert_eq!(g.nodes[10].shape, vec![None, Some(84)]);
3488        assert_eq!(g.nodes[11].shape, vec![None, Some(84)]);
3489        assert_eq!(g.nodes[12].shape, vec![None, Some(10)]);
3490        assert_eq!(g.nodes[13].shape, vec![None, Some(10)]);
3491    }
3492}