runmat_runtime/builtins/math/reduction/
min.rs

1//! MATLAB-compatible `min` builtin with GPU-aware semantics for RunMat.
2
3use std::cmp::Ordering;
4use std::collections::BTreeSet;
5
6use runmat_accelerate_api::{GpuTensorHandle, ReduceDimResult};
7use runmat_builtins::{ComplexTensor, Tensor, Value};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::broadcast::BroadcastPlan;
11use crate::builtins::common::random_args::{complex_tensor_into_value, keyword_of};
12use crate::builtins::common::{gpu_helpers, tensor};
13#[cfg(feature = "doc_export")]
14use crate::register_builtin_doc_text;
15use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
16
17use crate::builtins::common::spec::{
18    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, FusionError,
19    FusionExprContext, FusionKernelTemplate, GpuOpKind, ProviderHook, ReductionNaN,
20    ResidencyPolicy, ScalarType, ShapeRequirements,
21};
22
23#[cfg(feature = "doc_export")]
24pub const DOC_MD: &str = r#"---
25title: "min"
26category: "math/reduction"
27keywords: ["min", "minimum", "reduction", "comparisonmethod", "omitnan", "gpu"]
28summary: "Return the minimum elements of scalars, vectors, matrices, or N-D tensors with MATLAB-compatible options."
29references: []
30gpu_support:
31  elementwise: false
32  reduction: true
33  precisions: ["f32", "f64"]
34  broadcasting: "matlab"
35  notes: "Uses provider reduce_min_dim / reduce_min when available. Fallback gathers data to the host for omitnan, custom comparison modes, or complex inputs."
36fusion:
37  elementwise: false
38  reduction: true
39  max_inputs: 1
40  constants: "inline"
41requires_feature: null
42tested:
43  unit: "builtins::math::reduction::min::tests"
44  integration: "builtins::math::reduction::min::tests::min_gpu_dim1_matches_cpu"
45---
46
47# What does the `min` function do in MATLAB / RunMat?
48`min` finds the smallest values in its input while preserving MATLAB semantics for reductions, elementwise comparisons, NaN handling, complex magnitude comparisons, and linear indexing.
49
50## How does the `min` function behave in MATLAB / RunMat?
51- `min(X)` on an `m Ă— n` array reduces along the first non-singleton dimension, returning a row vector of column-wise minima and the corresponding indices (when requested).
52- `min(X, [], dim)` reduces along the specified dimension; `min(X, [], vecdim)` reduces along each dimension listed in `vecdim`.
53- `min(X, [], 'all')` collapses every element into a scalar and returns the linear index when two outputs are requested.
54- `min(X, [], 'linear')` is equivalent to `'all'` but guarantees that the matching index is linear over `X(:)`.
55- `min(X, [], ..., 'omitnan')` ignores `NaN` values inside each slice. If every element in a slice is `NaN`, the result for that slice is `NaN` and the index is `NaN`.
56- `min(X, [], ..., 'includenan')` (default) propagates `NaN` whenever a slice contains any `NaN` element, returning the index of the first `NaN`.
57- `min(A, B)` performs elementwise comparison using MATLAB's implicit expansion rules. The second output indicates whether the minimum came from `A` (index `1`) or `B` (index `2`).
58- Complex inputs follow MATLAB ordering: `'ComparisonMethod','auto'` (default) compares magnitudes and breaks ties using phase angles, while `'real'` compares real components first. `'abs'` is an explicit alias for magnitude ordering on real and complex inputs.
59
60## `min` Function GPU Execution Behaviour
61When RunMat Accelerate is active, tensors that already reside on the GPU stay on the device whenever the provider exposes `reduce_min_dim` (for dimension reductions) or `reduce_min` (for whole-array reductions). Requests that require `omitnan`, custom comparison modes, `'linear'` indices, or complex arithmetic gather the data to the host, compute the MATLAB-compatible result, and return the output on the host. Elementwise `min(A, B)` currently executes on the host; the planner rematerializes tensors on the GPU when follow-on fused kernels make it profitable.
62
63## Examples of using the `min` function in MATLAB / RunMat
64
65### Finding column-wise minima of a matrix
66```matlab
67A = [3 1 5; 4 2 6];
68[m, idx] = min(A);
69```
70Expected output:
71```matlab
72m   = [3 1 5];
73idx = [1 2 1];
74```
75
76### Reducing along the second dimension
77```matlab
78A = [3 1 5; 4 2 6];
79[m, idx] = min(A, [], 2);
80```
81Expected output:
82```matlab
83m   = [1; 2];
84idx = [2; 2];
85```
86
87### Collapsing all elements with linear indices
88```matlab
89A = reshape(1:12, [3 4]);
90[m, idx] = min(A, [], 'all');
91```
92Expected output:
93```matlab
94m   = 1;
95idx = 1;  % linear index into A(:)
96```
97
98### Ignoring NaN values during reduction
99```matlab
100values = [NaN 4 2; 3 NaN 1];
101[m, idx] = min(values, [], 1, 'omitnan');
102```
103Expected output:
104```matlab
105m   = [3 4 1];
106idx = [2 1 2];
107```
108
109### Elementwise minimum with broadcasting
110```matlab
111A = [1 4 7];
112B = [2; 3; 5];
113[C, origin] = min(A, B);
114```
115Expected output:
116```matlab
117C =
118     1     3     5
119origin =
120     1     2     2
121```
122
123### Comparing complex values by magnitude
124```matlab
125Z = [1+2i, 2+1i, -2+2i];
126M = min(Z);                         % magnitude ordering
127R = min(Z, [], 'ComparisonMethod', 'real');
128```
129Expected output:
130```matlab
131M = 2.0000 + 1.0000i
132R = 1.0000 + 2.0000i
133```
134
135## GPU residency in RunMat (Do I need `gpuArray`?)
136You typically do **not** need to call `gpuArray` manually. The fusion planner keeps tensors on the GPU between compatible kernels. When a reduction is supported by the active provider, the minimum values and indices stay on device. If a provider lacks the necessary hook, RunMat gathers data to the host, computes the result, and returns host tensors—subsequent fused GPU kernels can re-upload data when profitable.
137
138## FAQ
139
140### Can I request the linear index of the global minimum?
141Yes. Use either `min(X, [], 'all')` or `min(X, [], 'linear')`. Both return a scalar minimum and the linear index into `X(:)` when you request two outputs.
142
143### Does `min` support `'ComparisonMethod'` for real and complex arrays?
144Absolutely. `'auto'` or `'abs'` compare magnitudes; `'real'` compares the real component first. The returned values always match MATLAB, including tie-breaking rules.
145
146### What happens when all elements are `NaN` and `'omitnan'` is requested?
147The value result is `NaN` and the index is `NaN`, matching MATLAB's behavior for empty slices.
148
149### Can I mix elementwise comparisons with dimension reductions?
150No. `min(A, B)` performs elementwise comparisons only. Use `min(A, [], dim)` when you want reductions along specific dimensions.
151
152### Do GPU reductions support `'omitnan'` or custom comparison methods?
153Not yet. Those requests fall back to the host implementation, which still honors MATLAB semantics. The output remains a host tensor in that case.
154
155### Are logical and integer inputs supported?
156Yes. Logical arrays are promoted to double precision, and integer inputs are converted to double before comparison, matching MATLAB's numeric tower.
157
158## See Also
159[max](./max), [sum](./sum), [mean](./mean), [gpuArray](../../acceleration/gpu/gpuArray), [gather](../../acceleration/gpu/gather)
160
161## Source & Feedback
162- The full source code for the implementation of the `min` function is available at: [`crates/runmat-runtime/src/builtins/math/reduction/min.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/math/reduction/min.rs)
163- Found a bug or behavioral difference? Please [open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with details and a minimal repro.
164"#;
165
166pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
167    name: "min",
168    op_kind: GpuOpKind::Reduction,
169    supported_precisions: &[ScalarType::F32, ScalarType::F64],
170    broadcast: BroadcastSemantics::Matlab,
171    provider_hooks: &[
172        ProviderHook::Reduction {
173            name: "reduce_min_dim",
174        },
175        ProviderHook::Reduction {
176            name: "reduce_min",
177        },
178    ],
179    constant_strategy: ConstantStrategy::InlineLiteral,
180    residency: ResidencyPolicy::NewHandle,
181    nan_mode: ReductionNaN::Include,
182    two_pass_threshold: Some(256),
183    workgroup_size: Some(256),
184    accepts_nan_mode: false,
185    notes:
186        "Providers should implement reduce_min_dim / reduce_min. Requests that require omitnan, comparisonmethod overrides, or complex inputs fall back to the host implementation.",
187};
188
189register_builtin_gpu_spec!(GPU_SPEC);
190
191pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
192    name: "min",
193    shape: ShapeRequirements::BroadcastCompatible,
194    constant_strategy: ConstantStrategy::InlineLiteral,
195    elementwise: None,
196    reduction: Some(FusionKernelTemplate {
197        scalar_precisions: &[ScalarType::F32, ScalarType::F64],
198        wgsl_body: |ctx: &FusionExprContext| {
199            let input = ctx
200                .inputs
201                .first()
202                .ok_or(FusionError::MissingInput(0))?;
203            Ok(format!(
204                "accumulator = min(accumulator, {input});"
205            ))
206        },
207    }),
208    emits_nan: true,
209    notes: "Fusion planner emits canonical reduction kernels; providers may substitute custom WGSL via reduce_min_dim hooks.",
210};
211
212register_builtin_fusion_spec!(FUSION_SPEC);
213
214#[cfg(feature = "doc_export")]
215register_builtin_doc_text!("min", DOC_MD);
216
217/// Evaluation artifact returned by `min` that carries both values and indices.
218#[derive(Debug, Clone)]
219pub struct MinEvaluation {
220    values: Value,
221    indices: Value,
222}
223
224impl MinEvaluation {
225    /// Consume the evaluation and return only the minimum values (single-output call).
226    pub fn into_value(self) -> Value {
227        self.values
228    }
229
230    /// Consume the evaluation and return both minima and indices.
231    pub fn into_pair(self) -> (Value, Value) {
232        (self.values, self.indices)
233    }
234
235    /// Peek at the indices without consuming.
236    pub fn indices_value(&self) -> Value {
237        self.indices.clone()
238    }
239}
240
241#[runtime_builtin(
242    name = "min",
243    category = "math/reduction",
244    summary = "Return the minimum elements of scalars, vectors, matrices, or N-D tensors.",
245    keywords = "min,minimum,reduction,gpu,comparisonmethod,omitnan",
246    accel = "reduction"
247)]
248fn min_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
249    evaluate(value, &rest).map(|eval| eval.into_value())
250}
251
252/// Evaluate the builtin once and expose both outputs (value + indices).
253pub fn evaluate(value: Value, rest: &[Value]) -> Result<MinEvaluation, String> {
254    match parse_call(rest)? {
255        ParsedCall::Elementwise(args) => elementwise_min(value, args),
256        ParsedCall::Reduction(args) => reduction_min(value, args),
257    }
258}
259
260#[derive(Debug, Clone)]
261enum ParsedCall {
262    Reduction(ReductionArgs),
263    Elementwise(ElementwiseArgs),
264}
265
266#[derive(Debug, Clone)]
267struct ReductionArgs {
268    selection: DimSelection,
269    nan_mode: ReductionNaN,
270    comparison: ComparisonMethod,
271    linear_index: bool,
272}
273
274impl Default for ReductionArgs {
275    fn default() -> Self {
276        Self {
277            selection: DimSelection::Auto,
278            nan_mode: ReductionNaN::Include,
279            comparison: ComparisonMethod::Auto,
280            linear_index: false,
281        }
282    }
283}
284
285#[derive(Debug, Clone)]
286enum DimSelection {
287    Auto,
288    Dim(usize),
289    Vec(Vec<usize>),
290    All,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294enum ComparisonMethod {
295    Auto,
296    Real,
297    Abs,
298}
299
300#[derive(Debug, Clone)]
301struct ElementwiseArgs {
302    other: Value,
303    comparison: ComparisonMethod,
304}
305
306fn parse_call(rest: &[Value]) -> Result<ParsedCall, String> {
307    if rest.is_empty() {
308        return Ok(ParsedCall::Reduction(ReductionArgs::default()));
309    }
310
311    let first = &rest[0];
312    if !is_empty_placeholder(first) {
313        let comparison = parse_elementwise_options(&rest[1..])?;
314        return Ok(ParsedCall::Elementwise(ElementwiseArgs {
315            other: first.clone(),
316            comparison,
317        }));
318    }
319
320    let mut args = ReductionArgs::default();
321    parse_reduction_options(&mut args, &rest[1..])?;
322    Ok(ParsedCall::Reduction(args))
323}
324
325fn is_empty_placeholder(value: &Value) -> bool {
326    match value {
327        Value::Tensor(t) => t.data.is_empty(),
328        Value::LogicalArray(l) => l.data.is_empty(),
329        Value::StringArray(sa) => sa.data.is_empty(),
330        Value::CharArray(ca) => ca.data.is_empty(),
331        Value::Cell(cell) => cell.data.is_empty(),
332        Value::String(s) => s.is_empty(),
333        _ => false,
334    }
335}
336
337fn parse_reduction_options(args: &mut ReductionArgs, rest: &[Value]) -> Result<(), String> {
338    let mut idx = 0usize;
339    let mut selection_set = !matches!(args.selection, DimSelection::Auto);
340    let mut comparison_set = matches!(args.comparison, ComparisonMethod::Auto);
341    while idx < rest.len() {
342        if let Some(keyword) = keyword_of(&rest[idx]) {
343            match keyword.as_str() {
344                "omitnan" => {
345                    args.nan_mode = ReductionNaN::Omit;
346                    idx += 1;
347                    continue;
348                }
349                "includenan" => {
350                    args.nan_mode = ReductionNaN::Include;
351                    idx += 1;
352                    continue;
353                }
354                "all" => {
355                    if selection_set {
356                        return Err(
357                            "min: 'all' cannot be combined with an explicit dimension".to_string()
358                        );
359                    }
360                    args.selection = DimSelection::All;
361                    selection_set = true;
362                    idx += 1;
363                    continue;
364                }
365                "linear" => {
366                    if selection_set {
367                        return Err(
368                            "min: 'linear' cannot be combined with an explicit dimension"
369                                .to_string(),
370                        );
371                    }
372                    args.selection = DimSelection::All;
373                    args.linear_index = true;
374                    selection_set = true;
375                    idx += 1;
376                    continue;
377                }
378                "comparisonmethod" => {
379                    let Some(value) = rest.get(idx + 1) else {
380                        return Err("min: expected a value after 'ComparisonMethod'".to_string());
381                    };
382                    args.comparison = parse_comparison_method(value)?;
383                    comparison_set = true;
384                    idx += 2;
385                    continue;
386                }
387                _ => {}
388            }
389        }
390
391        if !selection_set {
392            if let Some(selection) = parse_dimension_value(&rest[idx])? {
393                args.selection = selection;
394                selection_set = true;
395                idx += 1;
396                continue;
397            }
398        }
399
400        return Err(format!("min: unrecognised argument {:?}", rest[idx]));
401    }
402
403    if !comparison_set {
404        args.comparison = ComparisonMethod::Auto;
405    }
406
407    Ok(())
408}
409
410fn parse_elementwise_options(rest: &[Value]) -> Result<ComparisonMethod, String> {
411    let mut comparison = ComparisonMethod::Auto;
412    let mut comparison_set = false;
413    let mut idx = 0usize;
414    while idx < rest.len() {
415        if let Some(keyword) = keyword_of(&rest[idx]) {
416            match keyword.as_str() {
417                "comparisonmethod" => {
418                    let Some(value) = rest.get(idx + 1) else {
419                        return Err("min: expected a value after 'ComparisonMethod'".to_string());
420                    };
421                    comparison = parse_comparison_method(value)?;
422                    comparison_set = true;
423                    idx += 2;
424                    continue;
425                }
426                "omitnan" | "includenan" | "all" | "linear" => {
427                    return Err(format!(
428                        "min: '{}' is only supported for reduction calls",
429                        keyword
430                    ));
431                }
432                _ => {}
433            }
434        }
435        return Err(format!("min: unrecognised argument {:?}", rest[idx]));
436    }
437    if !comparison_set {
438        comparison = ComparisonMethod::Auto;
439    }
440    Ok(comparison)
441}
442
443fn parse_comparison_method(value: &Value) -> Result<ComparisonMethod, String> {
444    let Some(keyword) = keyword_of(value) else {
445        return Err("min: 'ComparisonMethod' expects a string value".to_string());
446    };
447    match keyword.as_str() {
448        "auto" => Ok(ComparisonMethod::Auto),
449        "abs" | "magnitude" => Ok(ComparisonMethod::Abs),
450        "real" => Ok(ComparisonMethod::Real),
451        other => Err(format!("min: unsupported ComparisonMethod '{other}'")),
452    }
453}
454
455fn parse_dimension_value(value: &Value) -> Result<Option<DimSelection>, String> {
456    match value {
457        Value::Int(i) => {
458            let raw = i.to_i64();
459            if raw < 1 {
460                return Err("min: dimension must be >= 1".to_string());
461            }
462            Ok(Some(DimSelection::Dim(raw as usize)))
463        }
464        Value::Num(n) => {
465            if !n.is_finite() {
466                return Err("min: dimension must be finite".to_string());
467            }
468            let rounded = n.round();
469            if (rounded - n).abs() > f64::EPSILON {
470                return Err("min: dimension must be integral".to_string());
471            }
472            if rounded < 1.0 {
473                return Err("min: dimension must be >= 1".to_string());
474            }
475            Ok(Some(DimSelection::Dim(rounded as usize)))
476        }
477        Value::Tensor(t) => parse_dimension_tensor(t),
478        Value::LogicalArray(logical) => {
479            let tensor = tensor::logical_to_tensor(logical)?;
480            parse_dimension_tensor(&tensor)
481        }
482        Value::GpuTensor(_) => Err(
483            "min: dimension arguments must reside on the host (they cannot be gpuArray values)"
484                .to_string(),
485        ),
486        _ => Ok(None),
487    }
488}
489
490fn parse_dimension_tensor(tensor: &Tensor) -> Result<Option<DimSelection>, String> {
491    if tensor.data.is_empty() {
492        return Ok(Some(DimSelection::Auto));
493    }
494    if tensor.rows() != 1 && tensor.cols() != 1 && tensor.shape.len() != 1 {
495        return Err("min: dimension vector must be a row or column vector".to_string());
496    }
497    let mut dims = Vec::with_capacity(tensor.data.len());
498    for &value in &tensor.data {
499        if !value.is_finite() {
500            return Err("min: dimension entries must be finite".to_string());
501        }
502        let rounded = value.round();
503        if (rounded - value).abs() > f64::EPSILON {
504            return Err("min: dimension entries must be integers".to_string());
505        }
506        if rounded < 1.0 {
507            return Err("min: dimension indices must be >= 1".to_string());
508        }
509        dims.push(rounded as usize);
510    }
511    if dims.is_empty() {
512        Ok(Some(DimSelection::Auto))
513    } else {
514        // MATLAB treats duplicate entries gracefully; remove duplicates while preserving order.
515        let mut seen = BTreeSet::new();
516        let mut uniq = Vec::with_capacity(dims.len());
517        for dim in dims {
518            if seen.insert(dim) {
519                uniq.push(dim);
520            }
521        }
522        Ok(Some(DimSelection::Vec(uniq)))
523    }
524}
525
526fn reduction_min(value: Value, args: ReductionArgs) -> Result<MinEvaluation, String> {
527    match value {
528        Value::GpuTensor(handle) => {
529            if let Some(eval) = reduction_min_gpu(handle.clone(), &args)? {
530                return Ok(eval);
531            }
532            // Fall back to host if GPU path is unavailable.
533            let tensor = gpu_helpers::gather_tensor(&handle)?;
534            reduction_min_host(Value::Tensor(tensor), &args)
535        }
536        other => reduction_min_host(other, &args),
537    }
538}
539
540fn reduction_min_gpu(
541    handle: GpuTensorHandle,
542    args: &ReductionArgs,
543) -> Result<Option<MinEvaluation>, String> {
544    #[cfg(all(test, feature = "wgpu"))]
545    {
546        if handle.device_id != 0 {
547            let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
548                runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
549            );
550        }
551    }
552    if args.nan_mode == ReductionNaN::Omit {
553        return Ok(None);
554    }
555    if args.comparison != ComparisonMethod::Auto {
556        return Ok(None);
557    }
558    if args.linear_index {
559        return Ok(None);
560    }
561    let provider = match runmat_accelerate_api::provider() {
562        Some(p) => p,
563        None => return Ok(None),
564    };
565    let target_dim = match args.selection {
566        DimSelection::Auto => default_dimension_from_shape(&handle.shape),
567        DimSelection::Dim(dim) => dim,
568        DimSelection::Vec(ref dims) if dims.len() == 1 => dims[0],
569        DimSelection::All => {
570            if handle.shape.len() <= 1 {
571                1
572            } else {
573                return Ok(None);
574            }
575        }
576        _ => return Ok(None),
577    };
578    if target_dim == 0 {
579        return Ok(None);
580    }
581    // MATLAB dimensions are 1-based; `reduce_min_dim` expects zero-based.
582    let zero_based = target_dim.saturating_sub(1);
583    if zero_based >= handle.shape.len() {
584        return Ok(None);
585    }
586    match provider.reduce_min_dim(&handle, zero_based) {
587        Ok(ReduceDimResult { values, indices }) => Ok(Some(MinEvaluation {
588            values: Value::GpuTensor(values),
589            indices: Value::GpuTensor(indices),
590        })),
591        Err(_) => Ok(None),
592    }
593}
594
595fn reduction_min_host(value: Value, args: &ReductionArgs) -> Result<MinEvaluation, String> {
596    match materialize_for_min("min", value)? {
597        InputData::Real(tensor) => reduce_real_tensor(tensor, args),
598        InputData::Complex(tensor) => reduce_complex_tensor(tensor, args),
599    }
600}
601
602enum InputData {
603    Real(Tensor),
604    Complex(ComplexTensor),
605}
606
607fn materialize_for_min(name: &str, value: Value) -> Result<InputData, String> {
608    match value {
609        Value::Tensor(t) => Ok(InputData::Real(t)),
610        Value::LogicalArray(logical) => {
611            let tensor = tensor::logical_to_tensor(&logical)?;
612            Ok(InputData::Real(tensor))
613        }
614        Value::Num(n) => {
615            let tensor = Tensor::new(vec![n], vec![1, 1]).map_err(|e| format!("{name}: {e}"))?;
616            Ok(InputData::Real(tensor))
617        }
618        Value::Int(i) => {
619            let tensor =
620                Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(|e| format!("{name}: {e}"))?;
621            Ok(InputData::Real(tensor))
622        }
623        Value::Bool(b) => {
624            let tensor = Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
625                .map_err(|e| format!("{name}: {e}"))?;
626            Ok(InputData::Real(tensor))
627        }
628        Value::Complex(re, im) => {
629            let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
630                .map_err(|e| format!("{name}: {e}"))?;
631            Ok(InputData::Complex(tensor))
632        }
633        Value::ComplexTensor(ct) => Ok(InputData::Complex(ct)),
634        Value::String(_) | Value::StringArray(_) | Value::CharArray(_) | Value::Cell(_) => Err(
635            format!("{name}: expected numeric or logical input, received non-numeric value"),
636        ),
637        Value::GpuTensor(_) => Err(format!(
638            "{name}: internal error – GPU tensors must be gathered before host execution"
639        )),
640        Value::Object(_) | Value::HandleObject(_) | Value::Struct(_) | Value::Listener(_) => {
641            Err(format!("{name}: unsupported input type"))
642        }
643        Value::FunctionHandle(_)
644        | Value::Closure(_)
645        | Value::ClassRef(_)
646        | Value::MException(_) => Err(format!("{name}: unsupported input type")),
647    }
648}
649
650fn reduce_real_tensor(tensor: Tensor, args: &ReductionArgs) -> Result<MinEvaluation, String> {
651    let shape = tensor.shape.clone();
652    if tensor.data.is_empty() {
653        let output_shape = resolve_output_shape(&shape, &args.selection, &[])?;
654        let values =
655            Tensor::new(Vec::new(), output_shape.clone()).map_err(|e| format!("min: {e}"))?;
656        let indices = Tensor::new(Vec::new(), output_shape).map_err(|e| format!("min: {e}"))?;
657        return Ok(MinEvaluation {
658            values: tensor::tensor_into_value(values),
659            indices: tensor::tensor_into_value(indices),
660        });
661    }
662    let resolved = resolve_reduction_dims(&shape, &args.selection)?;
663    let output_shape = resolved.output_shape.clone();
664    let output_len = tensor::element_count(&output_shape);
665
666    if output_len == 0 {
667        let values =
668            Tensor::new(Vec::new(), output_shape.clone()).map_err(|e| format!("min: {e}"))?;
669        let indices = Tensor::new(Vec::new(), output_shape).map_err(|e| format!("min: {e}"))?;
670        return Ok(MinEvaluation {
671            values: tensor::tensor_into_value(values),
672            indices: tensor::tensor_into_value(indices),
673        });
674    }
675
676    let strides = compute_strides(&shape);
677    let output_strides = compute_strides(&output_shape);
678    let dims_mask = resolved.dims_mask.clone();
679    let reduce_strides = resolved.reduce_strides.clone();
680
681    let mut best = vec![BestReal::new(); output_len];
682    let mut coords = vec![0usize; shape.len()];
683    for &value in &tensor.data {
684        let out_idx = map_output_index(&coords, &output_strides, &dims_mask);
685        let reduce_idx = map_reduce_index(
686            &coords,
687            &resolved.reduced_dims,
688            &reduce_strides,
689            resolved.reduce_all,
690        );
691        let full_idx = map_linear_index(&coords, &strides);
692
693        update_best_real(
694            &mut best[out_idx],
695            value,
696            reduce_idx,
697            full_idx,
698            args.nan_mode,
699            args.comparison,
700        );
701        increment_coords(&mut coords, &shape);
702    }
703
704    let mut values = vec![0.0f64; output_len];
705    let mut indices = vec![0.0f64; output_len];
706
707    for (i, entry) in best.iter().enumerate() {
708        if entry.nan_fixed {
709            values[i] = f64::NAN;
710            indices[i] = if args.linear_index || resolved.reduce_all {
711                (entry.full_index + 1) as f64
712            } else if resolved.reduced_dims.is_empty() {
713                1.0
714            } else {
715                (entry.reduce_index + 1) as f64
716            };
717            continue;
718        }
719        if !entry.has_value {
720            values[i] = f64::NAN;
721            indices[i] = f64::NAN;
722            continue;
723        }
724        values[i] = entry.value;
725        indices[i] = if args.linear_index || resolved.reduce_all {
726            (entry.full_index + 1) as f64
727        } else if resolved.reduced_dims.is_empty() {
728            1.0
729        } else {
730            (entry.reduce_index + 1) as f64
731        };
732    }
733
734    let value_tensor =
735        Tensor::new(values, output_shape.clone()).map_err(|e| format!("min: {e}"))?;
736    let index_tensor = Tensor::new(indices, output_shape).map_err(|e| format!("min: {e}"))?;
737
738    Ok(MinEvaluation {
739        values: tensor::tensor_into_value(value_tensor),
740        indices: tensor::tensor_into_value(index_tensor),
741    })
742}
743
744fn reduce_complex_tensor(
745    tensor: ComplexTensor,
746    args: &ReductionArgs,
747) -> Result<MinEvaluation, String> {
748    let shape = tensor.shape.clone();
749    if tensor.data.is_empty() {
750        let output_shape = resolve_output_shape(&shape, &args.selection, &[])?;
751        let values = ComplexTensor::new(Vec::new(), output_shape.clone())
752            .map_err(|e| format!("min: {e}"))?;
753        let indices = Tensor::new(Vec::new(), output_shape).map_err(|e| format!("min: {e}"))?;
754        return Ok(MinEvaluation {
755            values: complex_tensor_into_value(values),
756            indices: tensor::tensor_into_value(indices),
757        });
758    }
759
760    let resolved = resolve_reduction_dims(&shape, &args.selection)?;
761    let output_shape = resolved.output_shape.clone();
762    let output_len = tensor::element_count(&output_shape);
763
764    if output_len == 0 {
765        let values = ComplexTensor::new(Vec::new(), output_shape.clone())
766            .map_err(|e| format!("min: {e}"))?;
767        let indices = Tensor::new(Vec::new(), output_shape).map_err(|e| format!("min: {e}"))?;
768        return Ok(MinEvaluation {
769            values: complex_tensor_into_value(values),
770            indices: tensor::tensor_into_value(indices),
771        });
772    }
773
774    let strides = compute_strides(&shape);
775    let output_strides = compute_strides(&output_shape);
776    let dims_mask = resolved.dims_mask.clone();
777    let reduce_strides = resolved.reduce_strides.clone();
778
779    let mut best = vec![BestComplex::new(); output_len];
780    let mut coords = vec![0usize; shape.len()];
781
782    for &(re, im) in &tensor.data {
783        let out_idx = map_output_index(&coords, &output_strides, &dims_mask);
784        let reduce_idx = map_reduce_index(
785            &coords,
786            &resolved.reduced_dims,
787            &reduce_strides,
788            resolved.reduce_all,
789        );
790        let full_idx = map_linear_index(&coords, &strides);
791        update_best_complex(
792            &mut best[out_idx],
793            (re, im),
794            reduce_idx,
795            full_idx,
796            args.nan_mode,
797            args.comparison,
798        );
799        increment_coords(&mut coords, &shape);
800    }
801
802    let mut values = vec![(0.0f64, 0.0f64); output_len];
803    let mut indices = vec![0.0f64; output_len];
804
805    for (i, entry) in best.iter().enumerate() {
806        if entry.nan_fixed {
807            values[i] = (f64::NAN, f64::NAN);
808            indices[i] = if args.linear_index || resolved.reduce_all {
809                (entry.full_index + 1) as f64
810            } else if resolved.reduced_dims.is_empty() {
811                1.0
812            } else {
813                (entry.reduce_index + 1) as f64
814            };
815            continue;
816        }
817        if !entry.has_value {
818            values[i] = (f64::NAN, f64::NAN);
819            indices[i] = f64::NAN;
820            continue;
821        }
822        values[i] = entry.value;
823        indices[i] = if args.linear_index || resolved.reduce_all {
824            (entry.full_index + 1) as f64
825        } else if resolved.reduced_dims.is_empty() {
826            1.0
827        } else {
828            (entry.reduce_index + 1) as f64
829        };
830    }
831
832    let value_tensor =
833        ComplexTensor::new(values, output_shape.clone()).map_err(|e| format!("min: {e}"))?;
834    let index_tensor = Tensor::new(indices, output_shape).map_err(|e| format!("min: {e}"))?;
835    Ok(MinEvaluation {
836        values: complex_tensor_into_value(value_tensor),
837        indices: tensor::tensor_into_value(index_tensor),
838    })
839}
840
841#[derive(Debug, Clone)]
842struct BestReal {
843    value: f64,
844    reduce_index: usize,
845    full_index: usize,
846    has_value: bool,
847    nan_fixed: bool,
848}
849
850impl BestReal {
851    fn new() -> Self {
852        Self {
853            value: 0.0,
854            reduce_index: 0,
855            full_index: 0,
856            has_value: false,
857            nan_fixed: false,
858        }
859    }
860}
861
862#[derive(Debug, Clone)]
863struct BestComplex {
864    value: (f64, f64),
865    reduce_index: usize,
866    full_index: usize,
867    has_value: bool,
868    nan_fixed: bool,
869}
870
871impl BestComplex {
872    fn new() -> Self {
873        Self {
874            value: (0.0, 0.0),
875            reduce_index: 0,
876            full_index: 0,
877            has_value: false,
878            nan_fixed: false,
879        }
880    }
881}
882
883fn resolve_output_shape(
884    shape: &[usize],
885    selection: &DimSelection,
886    reduced_dims: &[usize],
887) -> Result<Vec<usize>, String> {
888    if shape.is_empty() {
889        return Ok(Vec::new());
890    }
891    let mut output = shape.to_vec();
892    match selection {
893        DimSelection::All => {
894            output.fill(1);
895        }
896        _ => {
897            for &dim in reduced_dims {
898                if dim < output.len() {
899                    output[dim] = 1;
900                }
901            }
902        }
903    }
904    Ok(output)
905}
906
907struct ResolvedDims {
908    output_shape: Vec<usize>,
909    reduced_dims: Vec<usize>,
910    reduce_all: bool,
911    dims_mask: Vec<bool>,
912    reduce_strides: Vec<usize>,
913}
914
915fn resolve_reduction_dims(
916    shape: &[usize],
917    selection: &DimSelection,
918) -> Result<ResolvedDims, String> {
919    if shape.is_empty() {
920        return Ok(ResolvedDims {
921            output_shape: Vec::new(),
922            reduced_dims: Vec::new(),
923            reduce_all: true,
924            dims_mask: Vec::new(),
925            reduce_strides: Vec::new(),
926        });
927    }
928
929    let mut reduced_dims = match selection {
930        DimSelection::Auto => {
931            let mut dim = None;
932            for (index, &len) in shape.iter().enumerate() {
933                if len > 1 {
934                    dim = Some(index);
935                    break;
936                }
937            }
938            vec![dim.unwrap_or(0)]
939        }
940        DimSelection::Dim(dim) => {
941            if *dim == 0 {
942                return Err("min: dimension must be >= 1".to_string());
943            }
944            let index = dim.saturating_sub(1);
945            if index >= shape.len() {
946                Vec::new()
947            } else {
948                vec![index]
949            }
950        }
951        DimSelection::Vec(dims) => {
952            if dims.is_empty() {
953                Vec::new()
954            } else {
955                dims.iter()
956                    .filter_map(|dim| {
957                        if *dim == 0 {
958                            None
959                        } else {
960                            let idx = dim - 1;
961                            if idx < shape.len() {
962                                Some(idx)
963                            } else {
964                                None
965                            }
966                        }
967                    })
968                    .collect()
969            }
970        }
971        DimSelection::All => (0..shape.len()).collect(),
972    };
973
974    reduced_dims.sort_unstable();
975    reduced_dims.dedup();
976
977    let reduce_all = !reduced_dims.is_empty()
978        && reduced_dims.len() == shape.len()
979        && reduced_dims.iter().enumerate().all(|(i, &d)| i == d);
980
981    let output_shape = resolve_output_shape(shape, selection, &reduced_dims)?;
982    let mut dims_mask = vec![false; shape.len()];
983    for &dim in &reduced_dims {
984        if dim < dims_mask.len() {
985            dims_mask[dim] = true;
986        }
987    }
988    let reduce_strides = compute_subspace_strides(shape, &reduced_dims);
989
990    Ok(ResolvedDims {
991        output_shape,
992        reduced_dims,
993        reduce_all,
994        dims_mask,
995        reduce_strides,
996    })
997}
998
999fn compute_strides(shape: &[usize]) -> Vec<usize> {
1000    let mut strides = Vec::with_capacity(shape.len());
1001    let mut stride = 1usize;
1002    for &len in shape {
1003        strides.push(stride);
1004        stride = stride.saturating_mul(len.max(1));
1005    }
1006    strides
1007}
1008
1009fn compute_subspace_strides(shape: &[usize], dims: &[usize]) -> Vec<usize> {
1010    if dims.is_empty() {
1011        return Vec::new();
1012    }
1013    let mut strides = Vec::with_capacity(dims.len());
1014    let mut accum = 1usize;
1015    for &dim in dims {
1016        let len = shape.get(dim).copied().unwrap_or(1).max(1);
1017        strides.push(accum);
1018        accum = accum.saturating_mul(len);
1019    }
1020    strides
1021}
1022
1023fn map_output_index(coords: &[usize], output_strides: &[usize], dims_mask: &[bool]) -> usize {
1024    if coords.is_empty() {
1025        return 0;
1026    }
1027    let mut index = 0usize;
1028    for (dim, stride) in output_strides.iter().enumerate() {
1029        let coord = if *dims_mask.get(dim).unwrap_or(&false) {
1030            0
1031        } else {
1032            coords[dim]
1033        };
1034        index = index.saturating_add(coord.saturating_mul(*stride));
1035    }
1036    index
1037}
1038
1039fn map_reduce_index(
1040    coords: &[usize],
1041    reduced_dims: &[usize],
1042    reduce_strides: &[usize],
1043    reduce_all: bool,
1044) -> usize {
1045    if reduced_dims.is_empty() {
1046        return 0;
1047    }
1048    if reduce_all {
1049        // When all dimensions are reduced, the full index is used separately.
1050        return 0;
1051    }
1052    let mut index = 0usize;
1053    for (pos, &dim) in reduced_dims.iter().enumerate() {
1054        if let Some(coord) = coords.get(dim) {
1055            if let Some(stride) = reduce_strides.get(pos) {
1056                index = index.saturating_add(coord.saturating_mul(*stride));
1057            }
1058        }
1059    }
1060    index
1061}
1062
1063fn map_linear_index(coords: &[usize], strides: &[usize]) -> usize {
1064    coords
1065        .iter()
1066        .zip(strides.iter())
1067        .fold(0usize, |acc, (&coord, &stride)| {
1068            acc.saturating_add(coord.saturating_mul(stride))
1069        })
1070}
1071
1072fn increment_coords(coords: &mut [usize], shape: &[usize]) {
1073    for dim in 0..coords.len() {
1074        if shape[dim] == 0 {
1075            continue;
1076        }
1077        coords[dim] += 1;
1078        if coords[dim] < shape[dim] {
1079            break;
1080        }
1081        coords[dim] = 0;
1082    }
1083}
1084
1085fn update_best_real(
1086    best: &mut BestReal,
1087    value: f64,
1088    reduce_index: usize,
1089    full_index: usize,
1090    nan_mode: ReductionNaN,
1091    comparison: ComparisonMethod,
1092) {
1093    if value.is_nan() {
1094        match nan_mode {
1095            ReductionNaN::Include => {
1096                if !best.nan_fixed {
1097                    best.value = f64::NAN;
1098                    best.reduce_index = reduce_index;
1099                    best.full_index = full_index;
1100                    best.has_value = true;
1101                    best.nan_fixed = true;
1102                }
1103            }
1104            ReductionNaN::Omit => {}
1105        }
1106        return;
1107    }
1108    if best.nan_fixed {
1109        return;
1110    }
1111
1112    if !best.has_value {
1113        best.value = value;
1114        best.reduce_index = reduce_index;
1115        best.full_index = full_index;
1116        best.has_value = true;
1117        return;
1118    }
1119
1120    if should_replace_real(best.value, value, comparison) {
1121        best.value = value;
1122        best.reduce_index = reduce_index;
1123        best.full_index = full_index;
1124    }
1125}
1126
1127fn update_best_complex(
1128    best: &mut BestComplex,
1129    value: (f64, f64),
1130    reduce_index: usize,
1131    full_index: usize,
1132    nan_mode: ReductionNaN,
1133    comparison: ComparisonMethod,
1134) {
1135    if value.0.is_nan() || value.1.is_nan() {
1136        match nan_mode {
1137            ReductionNaN::Include => {
1138                if !best.nan_fixed {
1139                    best.value = (f64::NAN, f64::NAN);
1140                    best.reduce_index = reduce_index;
1141                    best.full_index = full_index;
1142                    best.has_value = true;
1143                    best.nan_fixed = true;
1144                }
1145            }
1146            ReductionNaN::Omit => {}
1147        }
1148        return;
1149    }
1150    if best.nan_fixed {
1151        return;
1152    }
1153
1154    if !best.has_value {
1155        best.value = value;
1156        best.reduce_index = reduce_index;
1157        best.full_index = full_index;
1158        best.has_value = true;
1159        return;
1160    }
1161
1162    if should_replace_complex(best.value, value, comparison) {
1163        best.value = value;
1164        best.reduce_index = reduce_index;
1165        best.full_index = full_index;
1166    }
1167}
1168
1169fn should_replace_real(current: f64, candidate: f64, comparison: ComparisonMethod) -> bool {
1170    match comparison {
1171        ComparisonMethod::Auto | ComparisonMethod::Real => {
1172            if candidate < current {
1173                return true;
1174            }
1175            if candidate > current {
1176                return false;
1177            }
1178            if candidate == 0.0 && current == 0.0 {
1179                return candidate.is_sign_negative() && !current.is_sign_negative();
1180            }
1181            false
1182        }
1183        ComparisonMethod::Abs => {
1184            let curr_abs = current.abs();
1185            let cand_abs = candidate.abs();
1186            if cand_abs < curr_abs {
1187                return true;
1188            }
1189            if cand_abs > curr_abs {
1190                return false;
1191            }
1192            if candidate < current {
1193                return true;
1194            }
1195            if candidate > current {
1196                return false;
1197            }
1198            if candidate == 0.0 && current == 0.0 {
1199                return candidate.is_sign_negative() && !current.is_sign_negative();
1200            }
1201            false
1202        }
1203    }
1204}
1205
1206fn should_replace_complex(
1207    current: (f64, f64),
1208    candidate: (f64, f64),
1209    comparison: ComparisonMethod,
1210) -> bool {
1211    match comparison {
1212        ComparisonMethod::Auto | ComparisonMethod::Abs => {
1213            compare_complex_auto(current, candidate) == Ordering::Greater
1214        }
1215        ComparisonMethod::Real => compare_complex_real(current, candidate) == Ordering::Greater,
1216    }
1217}
1218
1219fn compare_complex_auto(a: (f64, f64), b: (f64, f64)) -> Ordering {
1220    let a_mag = magnitude_squared(a);
1221    let b_mag = magnitude_squared(b);
1222    if a_mag < b_mag {
1223        return Ordering::Less;
1224    }
1225    if a_mag > b_mag {
1226        return Ordering::Greater;
1227    }
1228    // Equal magnitude: tie-break using phase angle.
1229    let a_angle = a.1.atan2(a.0);
1230    let b_angle = b.1.atan2(b.0);
1231    if a_angle < b_angle {
1232        Ordering::Less
1233    } else if a_angle > b_angle {
1234        Ordering::Greater
1235    } else {
1236        Ordering::Equal
1237    }
1238}
1239
1240fn compare_complex_real(a: (f64, f64), b: (f64, f64)) -> Ordering {
1241    if a.0 < b.0 {
1242        return Ordering::Less;
1243    }
1244    if a.0 > b.0 {
1245        return Ordering::Greater;
1246    }
1247    // Equal real parts: use magnitude and phase tie-breakers.
1248    compare_complex_auto(a, b)
1249}
1250
1251fn magnitude_squared(z: (f64, f64)) -> f64 {
1252    z.0.mul_add(z.0, z.1 * z.1)
1253}
1254
1255fn default_dimension_from_shape(shape: &[usize]) -> usize {
1256    if shape.is_empty() {
1257        return 1;
1258    }
1259    for (i, &len) in shape.iter().enumerate() {
1260        if len > 1 {
1261            return i + 1;
1262        }
1263    }
1264    1
1265}
1266
1267fn elementwise_min(value: Value, args: ElementwiseArgs) -> Result<MinEvaluation, String> {
1268    let ElementwiseArgs { other, comparison } = args;
1269    match (value, other) {
1270        (Value::GpuTensor(handle_a), Value::GpuTensor(handle_b)) => {
1271            elementwise_min_gpu_pair(&handle_a, &handle_b, comparison)
1272                .or_else(|| {
1273                    let ta = gpu_helpers::gather_tensor(&handle_a).ok()?;
1274                    let tb = gpu_helpers::gather_tensor(&handle_b).ok()?;
1275                    elementwise_real_or_complex(Value::Tensor(ta), Value::Tensor(tb), comparison)
1276                        .ok()
1277                })
1278                .ok_or_else(|| "min: elementwise GPU path failed".to_string())
1279        }
1280        (Value::GpuTensor(handle), other) => {
1281            elementwise_min_gpu_scalar_left(&handle, &other, comparison)
1282                .or_else(|| {
1283                    let t = gpu_helpers::gather_tensor(&handle).ok()?;
1284                    elementwise_real_or_complex(Value::Tensor(t), other, comparison).ok()
1285                })
1286                .ok_or_else(|| "min: elementwise GPU scalar path failed".to_string())
1287        }
1288        (other, Value::GpuTensor(handle)) => {
1289            elementwise_min_gpu_scalar_right(&other, &handle, comparison)
1290                .or_else(|| {
1291                    let t = gpu_helpers::gather_tensor(&handle).ok()?;
1292                    elementwise_real_or_complex(other, Value::Tensor(t), comparison).ok()
1293                })
1294                .ok_or_else(|| "min: elementwise GPU scalar path failed".to_string())
1295        }
1296        (lhs, rhs) => elementwise_real_or_complex(lhs, rhs, comparison),
1297    }
1298}
1299
1300fn elementwise_min_gpu_pair(
1301    a: &GpuTensorHandle,
1302    b: &GpuTensorHandle,
1303    comparison: ComparisonMethod,
1304) -> Option<MinEvaluation> {
1305    if comparison != ComparisonMethod::Auto {
1306        return None;
1307    }
1308    let provider = runmat_accelerate_api::provider()?;
1309    // Equal-shape fast path
1310    if a.shape == b.shape {
1311        let values = provider.elem_min(a, b).ok()?;
1312        // Try device mask first; if unavailable, compute indices on host while keeping values on device
1313        if let Ok(mask) = provider.elem_le(a, b) {
1314            let mask_host = gpu_helpers::gather_tensor(&mask).ok()?;
1315            let _ = provider.free(&mask);
1316            let mut indices = Vec::with_capacity(mask_host.data.len());
1317            for &m in &mask_host.data {
1318                indices.push(if m != 0.0 { 1.0 } else { 2.0 });
1319            }
1320            let index_tensor = Tensor::new(indices, mask_host.shape.clone()).ok()?;
1321            return Some(MinEvaluation {
1322                values: Value::GpuTensor(values),
1323                indices: tensor::tensor_into_value(index_tensor),
1324            });
1325        } else {
1326            // Host indices only
1327            let ta = gpu_helpers::gather_tensor(a).ok()?;
1328            let tb = gpu_helpers::gather_tensor(b).ok()?;
1329            let mut indices = Vec::with_capacity(ta.data.len());
1330            for i in 0..ta.data.len() {
1331                indices.push(if ta.data[i] <= tb.data[i] { 1.0 } else { 2.0 });
1332            }
1333            let index_tensor = Tensor::new(indices, ta.shape.clone()).ok()?;
1334            return Some(MinEvaluation {
1335                values: Value::GpuTensor(values),
1336                indices: tensor::tensor_into_value(index_tensor),
1337            });
1338        }
1339    }
1340    // Broadcast-compatible path via repmat, then device compare
1341    let (out_shape, reps_a, reps_b) = broadcast_reps(&a.shape, &b.shape)?;
1342    let a_exp = if reps_a.iter().any(|&r| r != 1) {
1343        provider.repmat(a, &reps_a).ok()?
1344    } else {
1345        a.clone()
1346    };
1347    let b_exp = if reps_b.iter().any(|&r| r != 1) {
1348        provider.repmat(b, &reps_b).ok()?
1349    } else {
1350        b.clone()
1351    };
1352    let values = provider.elem_min(&a_exp, &b_exp).ok();
1353    let mask = provider.elem_le(&a_exp, &b_exp).ok();
1354    if !std::ptr::eq(&a_exp, a) {
1355        let _ = provider.free(&a_exp);
1356    }
1357    if !std::ptr::eq(&b_exp, b) {
1358        let _ = provider.free(&b_exp);
1359    }
1360    let values = values?;
1361    if values.shape != out_shape {
1362        let _ = provider.free(&values);
1363        return None;
1364    }
1365    let index_tensor = if let Some(mask) = mask {
1366        let mask_host = gpu_helpers::gather_tensor(&mask).ok()?;
1367        let _ = provider.free(&mask);
1368        let mut indices = Vec::with_capacity(mask_host.data.len());
1369        for &m in &mask_host.data {
1370            indices.push(if m != 0.0 { 1.0 } else { 2.0 });
1371        }
1372        Tensor::new(indices, out_shape).ok()?
1373    } else {
1374        // Host indices fallback
1375        let ta = gpu_helpers::gather_tensor(&a_exp).ok()?;
1376        let tb = gpu_helpers::gather_tensor(&b_exp).ok()?;
1377        let mut indices = Vec::with_capacity(ta.data.len());
1378        for i in 0..ta.data.len() {
1379            indices.push(if ta.data[i] <= tb.data[i] { 1.0 } else { 2.0 });
1380        }
1381        Tensor::new(indices, out_shape).ok()?
1382    };
1383    Some(MinEvaluation {
1384        values: Value::GpuTensor(values),
1385        indices: tensor::tensor_into_value(index_tensor),
1386    })
1387}
1388
1389fn broadcast_reps(a: &[usize], b: &[usize]) -> Option<(Vec<usize>, Vec<usize>, Vec<usize>)> {
1390    let rank = a.len().max(b.len()).max(1);
1391    let mut out = vec![1usize; rank];
1392    let mut aa = vec![1usize; rank];
1393    let mut bb = vec![1usize; rank];
1394    for i in 0..rank {
1395        aa[i] = *a.get(i).unwrap_or(&1);
1396        bb[i] = *b.get(i).unwrap_or(&1);
1397    }
1398    for i in 0..rank {
1399        let (ad, bd) = (aa[i], bb[i]);
1400        if ad == bd {
1401            out[i] = ad;
1402        } else if ad == 1 {
1403            out[i] = bd;
1404        } else if bd == 1 {
1405            out[i] = ad;
1406        } else {
1407            return None;
1408        }
1409    }
1410    let reps_a: Vec<usize> = (0..rank)
1411        .map(|i| if aa[i] == out[i] { 1 } else { out[i] })
1412        .collect();
1413    let reps_b: Vec<usize> = (0..rank)
1414        .map(|i| if bb[i] == out[i] { 1 } else { out[i] })
1415        .collect();
1416    Some((out, reps_a, reps_b))
1417}
1418
1419fn elementwise_min_gpu_scalar_left(
1420    a: &GpuTensorHandle,
1421    other: &Value,
1422    comparison: ComparisonMethod,
1423) -> Option<MinEvaluation> {
1424    if comparison != ComparisonMethod::Auto {
1425        return None;
1426    }
1427    let provider = runmat_accelerate_api::provider()?;
1428    let scalar = extract_scalar(other)?;
1429    let values = provider.scalar_min(a, scalar).ok()?;
1430    // Try device mask; if unavailable, compute on host
1431    let index_tensor = if let Ok(fill) = provider.fill_like(a, scalar) {
1432        if let Ok(mask) = provider.elem_le(a, &fill) {
1433            let _ = provider.free(&fill);
1434            let mask_host = gpu_helpers::gather_tensor(&mask).ok()?;
1435            let _ = provider.free(&mask);
1436            let mut indices = Vec::with_capacity(mask_host.data.len());
1437            for &m in &mask_host.data {
1438                indices.push(if m != 0.0 { 1.0 } else { 2.0 });
1439            }
1440            Tensor::new(indices, mask_host.shape.clone()).ok()?
1441        } else {
1442            let _ = provider.free(&fill);
1443            let ta = gpu_helpers::gather_tensor(a).ok()?;
1444            let mut indices = Vec::with_capacity(ta.data.len());
1445            for &v in &ta.data {
1446                indices.push(if v <= scalar { 1.0 } else { 2.0 });
1447            }
1448            Tensor::new(indices, ta.shape.clone()).ok()?
1449        }
1450    } else {
1451        let ta = gpu_helpers::gather_tensor(a).ok()?;
1452        let mut indices = Vec::with_capacity(ta.data.len());
1453        for &v in &ta.data {
1454            indices.push(if v <= scalar { 1.0 } else { 2.0 });
1455        }
1456        Tensor::new(indices, ta.shape.clone()).ok()?
1457    };
1458    Some(MinEvaluation {
1459        values: Value::GpuTensor(values),
1460        indices: tensor::tensor_into_value(index_tensor),
1461    })
1462}
1463
1464fn elementwise_min_gpu_scalar_right(
1465    other: &Value,
1466    b: &GpuTensorHandle,
1467    comparison: ComparisonMethod,
1468) -> Option<MinEvaluation> {
1469    if comparison != ComparisonMethod::Auto {
1470        return None;
1471    }
1472    let provider = runmat_accelerate_api::provider()?;
1473    let scalar = extract_scalar(other)?;
1474    let values = provider.scalar_min(b, scalar).ok()?;
1475    // Try device mask; if unavailable, compute on host
1476    let index_tensor = if let Ok(fill) = provider.fill_like(b, scalar) {
1477        if let Ok(mask) = provider.elem_le(&fill, b) {
1478            let _ = provider.free(&fill);
1479            let mask_host = gpu_helpers::gather_tensor(&mask).ok()?;
1480            let _ = provider.free(&mask);
1481            let mut indices = Vec::with_capacity(mask_host.data.len());
1482            for &m in &mask_host.data {
1483                indices.push(if m != 0.0 { 1.0 } else { 2.0 });
1484            }
1485            Tensor::new(indices, mask_host.shape.clone()).ok()?
1486        } else {
1487            let _ = provider.free(&fill);
1488            let tb = gpu_helpers::gather_tensor(b).ok()?;
1489            let mut indices = Vec::with_capacity(tb.data.len());
1490            for &v in &tb.data {
1491                indices.push(if scalar <= v { 1.0 } else { 2.0 });
1492            }
1493            Tensor::new(indices, tb.shape.clone()).ok()?
1494        }
1495    } else {
1496        let tb = gpu_helpers::gather_tensor(b).ok()?;
1497        let mut indices = Vec::with_capacity(tb.data.len());
1498        for &v in &tb.data {
1499            indices.push(if scalar <= v { 1.0 } else { 2.0 });
1500        }
1501        Tensor::new(indices, tb.shape.clone()).ok()?
1502    };
1503    Some(MinEvaluation {
1504        values: Value::GpuTensor(values),
1505        indices: tensor::tensor_into_value(index_tensor),
1506    })
1507}
1508
1509fn extract_scalar(v: &Value) -> Option<f64> {
1510    match v {
1511        Value::Num(n) => Some(*n),
1512        Value::Int(i) => Some(i.to_f64()),
1513        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
1514        Value::Tensor(t) if t.data.len() == 1 => t.data.first().copied(),
1515        Value::LogicalArray(l) if l.data.len() == 1 => Some(if l.data[0] != 0 { 1.0 } else { 0.0 }),
1516        _ => None,
1517    }
1518}
1519
1520fn elementwise_real_or_complex(
1521    lhs: Value,
1522    rhs: Value,
1523    comparison: ComparisonMethod,
1524) -> Result<MinEvaluation, String> {
1525    match (
1526        materialize_for_min("min", lhs)?,
1527        materialize_for_min("min", rhs)?,
1528    ) {
1529        (InputData::Complex(a), InputData::Complex(b)) => elementwise_complex_min(a, b, comparison),
1530        (InputData::Complex(a), InputData::Real(b)) => {
1531            let converted = promote_real_tensor_to_complex(b);
1532            elementwise_complex_min(a, converted, comparison)
1533        }
1534        (InputData::Real(a), InputData::Complex(b)) => {
1535            let converted = promote_real_tensor_to_complex(a);
1536            elementwise_complex_min(converted, b, comparison)
1537        }
1538        (InputData::Real(a), InputData::Real(b)) => elementwise_real_min(a, b, comparison),
1539    }
1540}
1541
1542fn elementwise_real_min(
1543    lhs: Tensor,
1544    rhs: Tensor,
1545    comparison: ComparisonMethod,
1546) -> Result<MinEvaluation, String> {
1547    let plan = BroadcastPlan::new(&lhs.shape, &rhs.shape).map_err(|err| format!("min: {}", err))?;
1548    let mut values = vec![0.0f64; plan.len()];
1549    let mut indices = vec![0.0f64; plan.len()];
1550
1551    for (offset, index_a, index_b) in plan.iter() {
1552        let a = lhs.data.get(index_a).copied().unwrap_or(f64::NAN);
1553        let b = rhs.data.get(index_b).copied().unwrap_or(f64::NAN);
1554        let (value, origin) = choose_real_elementwise(a, b, comparison);
1555        values[offset] = value;
1556        indices[offset] = origin;
1557    }
1558
1559    let value_tensor =
1560        Tensor::new(values, plan.output_shape().to_vec()).map_err(|e| format!("min: {e}"))?;
1561    let index_tensor =
1562        Tensor::new(indices, plan.output_shape().to_vec()).map_err(|e| format!("min: {e}"))?;
1563
1564    Ok(MinEvaluation {
1565        values: tensor::tensor_into_value(value_tensor),
1566        indices: tensor::tensor_into_value(index_tensor),
1567    })
1568}
1569
1570fn elementwise_complex_min(
1571    lhs: ComplexTensor,
1572    rhs: ComplexTensor,
1573    comparison: ComparisonMethod,
1574) -> Result<MinEvaluation, String> {
1575    let plan = BroadcastPlan::new(&lhs.shape, &rhs.shape).map_err(|err| format!("min: {}", err))?;
1576    let mut values = vec![(0.0f64, 0.0f64); plan.len()];
1577    let mut indices = vec![0.0f64; plan.len()];
1578
1579    for (offset, index_a, index_b) in plan.iter() {
1580        let a = lhs
1581            .data
1582            .get(index_a)
1583            .copied()
1584            .unwrap_or((f64::NAN, f64::NAN));
1585        let b = rhs
1586            .data
1587            .get(index_b)
1588            .copied()
1589            .unwrap_or((f64::NAN, f64::NAN));
1590        let (value, origin) = choose_complex_elementwise(a, b, comparison);
1591        values[offset] = value;
1592        indices[offset] = origin;
1593    }
1594
1595    let value_tensor = ComplexTensor::new(values, plan.output_shape().to_vec())
1596        .map_err(|e| format!("min: {e}"))?;
1597    let index_tensor =
1598        Tensor::new(indices, plan.output_shape().to_vec()).map_err(|e| format!("min: {e}"))?;
1599
1600    Ok(MinEvaluation {
1601        values: complex_tensor_into_value(value_tensor),
1602        indices: tensor::tensor_into_value(index_tensor),
1603    })
1604}
1605
1606fn promote_real_tensor_to_complex(tensor: Tensor) -> ComplexTensor {
1607    let data = tensor
1608        .data
1609        .iter()
1610        .copied()
1611        .map(|re| (re, 0.0))
1612        .collect::<Vec<_>>();
1613    ComplexTensor {
1614        data,
1615        shape: tensor.shape.clone(),
1616        rows: tensor.rows,
1617        cols: tensor.cols,
1618    }
1619}
1620
1621fn choose_real_elementwise(a: f64, b: f64, comparison: ComparisonMethod) -> (f64, f64) {
1622    match (a.is_nan(), b.is_nan()) {
1623        (true, true) => (f64::NAN, 1.0),
1624        (true, false) => (f64::NAN, 1.0),
1625        (false, true) => (f64::NAN, 2.0),
1626        (false, false) => {
1627            if should_replace_real(a, b, comparison) {
1628                (b, 2.0)
1629            } else {
1630                (a, 1.0)
1631            }
1632        }
1633    }
1634}
1635
1636fn choose_complex_elementwise(
1637    a: (f64, f64),
1638    b: (f64, f64),
1639    comparison: ComparisonMethod,
1640) -> ((f64, f64), f64) {
1641    let a_nan = a.0.is_nan() || a.1.is_nan();
1642    let b_nan = b.0.is_nan() || b.1.is_nan();
1643    match (a_nan, b_nan) {
1644        (true, true) => ((f64::NAN, f64::NAN), 1.0),
1645        (true, false) => ((f64::NAN, f64::NAN), 1.0),
1646        (false, true) => ((f64::NAN, f64::NAN), 2.0),
1647        (false, false) => {
1648            if should_replace_complex(a, b, comparison) {
1649                (b, 2.0)
1650            } else {
1651                (a, 1.0)
1652            }
1653        }
1654    }
1655}
1656
1657#[cfg(test)]
1658mod tests {
1659    use super::*;
1660    #[cfg(any(feature = "doc_export", feature = "wgpu"))]
1661    use crate::builtins::common::test_support;
1662    #[cfg(feature = "wgpu")]
1663    use runmat_accelerate_api::HostTensorView;
1664    use runmat_builtins::{IntValue, Tensor, Value};
1665
1666    fn placeholder() -> Value {
1667        let tensor = Tensor::new(Vec::<f64>::new(), vec![0, 0]).unwrap();
1668        Value::Tensor(tensor)
1669    }
1670
1671    #[test]
1672    fn min_scalar_returns_input() {
1673        let result = min_builtin(Value::Num(5.0), Vec::new()).expect("min");
1674        assert_eq!(result, Value::Num(5.0));
1675    }
1676
1677    #[test]
1678    fn min_vector_with_indices() {
1679        let tensor = Tensor::new(vec![3.0, 1.0, 5.0], vec![3, 1]).unwrap();
1680        let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
1681        let (values, indices) = eval.into_pair();
1682        assert_eq!(values, Value::Num(1.0));
1683        assert_eq!(indices, Value::Num(2.0));
1684    }
1685
1686    #[test]
1687    fn min_matrix_default_dimension() {
1688        let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0, 5.0, 6.0], vec![2, 3]).unwrap();
1689        let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
1690        let (values, indices) = eval.into_pair();
1691        match values {
1692            Value::Tensor(t) => {
1693                assert_eq!(t.shape, vec![1, 3]);
1694                assert_eq!(t.data, vec![3.0, 1.0, 5.0]);
1695            }
1696            other => panic!("expected tensor, got {other:?}"),
1697        }
1698        match indices {
1699            Value::Tensor(t) => {
1700                assert_eq!(t.data, vec![1.0, 1.0, 1.0]);
1701            }
1702            other => panic!("expected tensor, got {other:?}"),
1703        }
1704    }
1705
1706    #[test]
1707    fn min_all_linear_index() {
1708        let tensor =
1709            Tensor::new((1..=12).map(|v| v as f64).collect::<Vec<_>>(), vec![3, 4]).unwrap();
1710        let args = vec![placeholder(), Value::from("all")];
1711        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1712        let (values, indices) = eval.into_pair();
1713        assert_eq!(values, Value::Num(1.0));
1714        assert_eq!(indices, Value::Num(1.0));
1715
1716        let args_linear = vec![placeholder(), Value::from("linear")];
1717        let eval = evaluate(
1718            Value::Tensor(Tensor::new(vec![2.0, 3.0], vec![1, 2]).unwrap()),
1719            &args_linear,
1720        )
1721        .expect("evaluate");
1722        let (values, indices) = eval.into_pair();
1723        assert_eq!(values, Value::Num(2.0));
1724        assert_eq!(indices, Value::Num(1.0));
1725    }
1726
1727    #[test]
1728    fn min_with_omitnan() {
1729        let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0], vec![3, 1]).unwrap();
1730        let args = vec![placeholder(), Value::from("omitnan")];
1731        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1732        let (values, indices) = eval.into_pair();
1733        assert_eq!(values, Value::Num(2.0));
1734        assert_eq!(indices, Value::Num(3.0));
1735    }
1736
1737    #[test]
1738    fn min_omitnan_all_nan_slice() {
1739        let tensor = Tensor::new(vec![f64::NAN, f64::NAN], vec![2, 1]).unwrap();
1740        let args = vec![placeholder(), Value::from("omitnan")];
1741        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1742        let (values, indices) = eval.into_pair();
1743        match values {
1744            Value::Num(v) => assert!(v.is_nan()),
1745            other => panic!("expected scalar NaN, got {other:?}"),
1746        }
1747        match indices {
1748            Value::Num(v) => assert!(v.is_nan()),
1749            other => panic!("expected scalar NaN index, got {other:?}"),
1750        }
1751    }
1752
1753    #[test]
1754    fn min_reduction_abs_comparison() {
1755        let tensor = Tensor::new(vec![1.0, -3.0, -2.0, 4.0], vec![2, 2]).unwrap();
1756        let args = vec![
1757            placeholder(),
1758            Value::from("ComparisonMethod"),
1759            Value::from("abs"),
1760        ];
1761        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1762        let (values, indices) = eval.into_pair();
1763        match values {
1764            Value::Tensor(t) => {
1765                assert_eq!(t.shape, vec![1, 2]);
1766                assert_eq!(t.data, vec![1.0, -2.0]);
1767            }
1768            other => panic!("expected tensor result, got {other:?}"),
1769        }
1770        match indices {
1771            Value::Tensor(t) => {
1772                assert_eq!(t.data, vec![1.0, 1.0]);
1773            }
1774            other => panic!("expected tensor indices, got {other:?}"),
1775        }
1776    }
1777
1778    #[test]
1779    fn min_reduction_complex_real_comparison() {
1780        let tensor = ComplexTensor::new(vec![(1.0, 2.0), (0.5, 5.0)], vec![2, 1]).expect("tensor");
1781        let args = vec![
1782            placeholder(),
1783            Value::from("ComparisonMethod"),
1784            Value::from("real"),
1785        ];
1786        let eval = evaluate(Value::ComplexTensor(tensor), &args).expect("evaluate");
1787        let (values, indices) = eval.into_pair();
1788        match values {
1789            Value::Complex(re, im) => {
1790                assert!((re - 0.5).abs() < 1e-12);
1791                assert!((im - 5.0).abs() < 1e-12);
1792            }
1793            other => panic!("expected complex scalar, got {other:?}"),
1794        }
1795        assert_eq!(indices, Value::Num(2.0));
1796    }
1797
1798    #[test]
1799    fn min_elementwise_broadcast() {
1800        let lhs = Tensor::new(vec![1.0, 4.0, 7.0], vec![1, 3]).unwrap();
1801        let rhs = Tensor::new(vec![2.0, 3.0, 5.0], vec![3, 1]).unwrap();
1802        let eval = evaluate(Value::Tensor(lhs), &[Value::Tensor(rhs)]).expect("evaluate");
1803        let (values, indices) = eval.into_pair();
1804        match values {
1805            Value::Tensor(t) => {
1806                assert_eq!(t.shape, vec![3, 3]);
1807                assert_eq!([t.data[0], t.data[3], t.data[6]], [1.0, 2.0, 2.0]);
1808                assert_eq!([t.data[1], t.data[4], t.data[7]], [1.0, 3.0, 3.0]);
1809                assert_eq!([t.data[2], t.data[5], t.data[8]], [1.0, 4.0, 5.0]);
1810            }
1811            other => panic!("expected tensor, got {other:?}"),
1812        }
1813        match indices {
1814            Value::Tensor(t) => {
1815                assert_eq!(t.shape, vec![3, 3]);
1816                assert_eq!([t.data[0], t.data[3], t.data[6]], [1.0, 2.0, 2.0]);
1817                assert_eq!([t.data[1], t.data[4], t.data[7]], [1.0, 2.0, 2.0]);
1818                assert_eq!([t.data[2], t.data[5], t.data[8]], [1.0, 1.0, 2.0]);
1819            }
1820            other => panic!("expected tensor, got {other:?}"),
1821        }
1822    }
1823
1824    #[test]
1825    fn min_elementwise_abs_comparison() {
1826        let lhs = Tensor::new(vec![-2.0, 1.0], vec![2, 1]).unwrap();
1827        let rhs = Tensor::new(vec![1.5, -3.0], vec![2, 1]).unwrap();
1828        let args = vec![
1829            Value::Tensor(rhs),
1830            Value::from("ComparisonMethod"),
1831            Value::from("abs"),
1832        ];
1833        let eval = evaluate(Value::Tensor(lhs), &args).expect("evaluate");
1834        let (values, indices) = eval.into_pair();
1835        match values {
1836            Value::Tensor(t) => {
1837                assert_eq!(t.data, vec![1.5, 1.0]);
1838            }
1839            other => panic!("expected tensor, got {other:?}"),
1840        }
1841        match indices {
1842            Value::Tensor(t) => {
1843                assert_eq!(t.data, vec![2.0, 1.0]);
1844            }
1845            other => panic!("expected tensor, got {other:?}"),
1846        }
1847    }
1848
1849    #[test]
1850    fn min_elementwise_rejects_reduction_only_keywords() {
1851        let lhs = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1852        let rhs = Tensor::new(vec![3.0, 4.0], vec![2, 1]).unwrap();
1853        let err = evaluate(
1854            Value::Tensor(lhs),
1855            &[Value::Tensor(rhs), Value::from("omitnan")],
1856        )
1857        .expect_err("expected error");
1858        assert!(err.contains("only supported for reduction"));
1859    }
1860
1861    #[test]
1862    fn min_complex_real_comparison() {
1863        let lhs = ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1]).unwrap();
1864        let rhs = ComplexTensor::new(vec![(0.5, 5.0)], vec![1, 1]).unwrap();
1865        let args = vec![
1866            Value::ComplexTensor(rhs),
1867            Value::from("ComparisonMethod"),
1868            Value::from("real"),
1869        ];
1870        let eval = evaluate(Value::ComplexTensor(lhs), &args).expect("evaluate");
1871        let (values, indices) = eval.into_pair();
1872        assert_eq!(values, Value::Complex(0.5, 5.0));
1873        assert_eq!(indices, Value::Num(2.0));
1874    }
1875
1876    #[test]
1877    fn min_dimension_argument_parsing() {
1878        let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0], vec![2, 2]).unwrap();
1879        let dims = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1880        let args = vec![placeholder(), Value::Tensor(dims)];
1881        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1882        let (values, indices) = eval.into_pair();
1883        assert_eq!(values, Value::Num(1.0));
1884        assert_eq!(indices, Value::Num(3.0));
1885    }
1886
1887    #[test]
1888    fn min_vecdim_duplicate_entries() {
1889        let tensor = Tensor::new(vec![5.0, 2.0, 7.0, 1.0], vec![2, 2]).unwrap();
1890        let dims = Tensor::new(vec![1.0, 1.0, 2.0], vec![3, 1]).unwrap();
1891        let args = vec![placeholder(), Value::Tensor(dims)];
1892        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1893        let (values, indices) = eval.into_pair();
1894        assert_eq!(values, Value::Num(1.0));
1895        assert_eq!(indices, Value::Num(4.0));
1896    }
1897
1898    #[test]
1899    fn min_dimension_gpu_argument_errors() {
1900        let tensor = Tensor::new(vec![3.0, 1.0], vec![2, 1]).unwrap();
1901        let dim_handle = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
1902            shape: vec![1, 1],
1903            device_id: 0,
1904            buffer_id: 42,
1905        });
1906        let err = evaluate(Value::Tensor(tensor), &[placeholder(), dim_handle])
1907            .expect_err("expected error");
1908        assert!(err.contains("dimension arguments must reside on the host"));
1909    }
1910
1911    #[test]
1912    fn min_invalid_comparison_method_errors() {
1913        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1914        let args = vec![
1915            placeholder(),
1916            Value::from("ComparisonMethod"),
1917            Value::from("chebyshev"),
1918        ];
1919        let err = evaluate(Value::Tensor(tensor), &args).expect_err("expected error");
1920        assert!(err.contains("unsupported ComparisonMethod"));
1921    }
1922
1923    #[test]
1924    #[cfg(feature = "doc_export")]
1925    fn min_doc_examples_present() {
1926        let blocks = test_support::doc_examples(super::DOC_MD);
1927        assert!(!blocks.is_empty());
1928    }
1929
1930    #[test]
1931    #[cfg(feature = "wgpu")]
1932    fn min_gpu_dim1_matches_cpu() {
1933        let tensor = Tensor::new(vec![3.0, 1.0, 2.0, 4.0], vec![2, 2]).unwrap();
1934        let eval_cpu = evaluate(Value::Tensor(tensor.clone()), &[]).expect("cpu");
1935        let (values_cpu, indices_cpu) = eval_cpu.into_pair();
1936
1937        test_support::with_test_provider(|provider| {
1938            let view = HostTensorView {
1939                data: &tensor.data,
1940                shape: &tensor.shape,
1941            };
1942            let handle = provider.upload(&view).expect("upload");
1943            let eval_gpu = evaluate(Value::GpuTensor(handle), &[]).expect("gpu");
1944            let (values_gpu, indices_gpu) = eval_gpu.into_pair();
1945            match (&values_gpu, &indices_gpu) {
1946                (Value::GpuTensor(_), Value::GpuTensor(_)) => {}
1947                other => panic!("expected GPU tensors, got {other:?}"),
1948            }
1949            let gathered_vals = test_support::gather(values_gpu).expect("gather values");
1950            let gathered_idx = test_support::gather(indices_gpu).expect("gather indices");
1951            let expected_vals = match values_cpu {
1952                Value::Tensor(t) => t,
1953                other => panic!("expected tensor values from cpu eval, got {other:?}"),
1954            };
1955            let expected_idx = match indices_cpu {
1956                Value::Tensor(t) => t,
1957                other => panic!("expected tensor indices from cpu eval, got {other:?}"),
1958            };
1959            assert_eq!(gathered_vals.shape, expected_vals.shape);
1960            assert_eq!(gathered_vals.data, expected_vals.data);
1961            assert_eq!(gathered_idx.shape, expected_idx.shape);
1962            assert_eq!(gathered_idx.data, expected_idx.data);
1963        });
1964    }
1965
1966    #[test]
1967    fn min_dimension_numeric_argument() {
1968        let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0], vec![2, 2]).unwrap();
1969        let args = vec![placeholder(), Value::Num(2.0)];
1970        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
1971        let (values, indices) = eval.into_pair();
1972        match values {
1973            Value::Tensor(t) => {
1974                assert_eq!(t.shape, vec![2, 1]);
1975                assert_eq!(t.data, vec![1.0, 2.0]);
1976            }
1977            other => panic!("expected tensor, got {other:?}"),
1978        }
1979        match indices {
1980            Value::Tensor(t) => {
1981                assert_eq!(t.data, vec![2.0, 2.0]);
1982            }
1983            other => panic!("expected tensor, got {other:?}"),
1984        }
1985    }
1986
1987    #[test]
1988    fn min_complex_auto_comparison() {
1989        let lhs = ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1]).unwrap();
1990        let rhs = ComplexTensor::new(vec![(2.0, 1.0)], vec![1, 1]).unwrap();
1991        let eval =
1992            evaluate(Value::ComplexTensor(lhs), &[Value::ComplexTensor(rhs)]).expect("evaluate");
1993        let (values, indices) = eval.into_pair();
1994        assert_eq!(values, Value::Complex(2.0, 1.0));
1995        assert_eq!(indices, Value::Num(2.0));
1996    }
1997
1998    #[test]
1999    fn min_scalar_pair_arguments() {
2000        let args = vec![Value::Num(2.0)];
2001        let result = min_builtin(Value::Num(3.0), args).expect("min");
2002        assert_eq!(result, Value::Num(2.0));
2003    }
2004
2005    #[test]
2006    fn min_rejects_invalid_dimension() {
2007        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
2008        let args = vec![placeholder(), Value::Int(IntValue::I32(0))];
2009        let err = evaluate(Value::Tensor(tensor), &args).expect_err("expected error");
2010        assert!(err.contains("dimension must be >= 1"));
2011    }
2012}