1use 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#[derive(Debug, Clone)]
219pub struct MinEvaluation {
220 values: Value,
221 indices: Value,
222}
223
224impl MinEvaluation {
225 pub fn into_value(self) -> Value {
227 self.values
228 }
229
230 pub fn into_pair(self) -> (Value, Value) {
232 (self.values, self.indices)
233 }
234
235 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
252pub 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 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 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 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 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 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 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 if a.shape == b.shape {
1311 let values = provider.elem_min(a, b).ok()?;
1312 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 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 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 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 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 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}