1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
5 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
6 BuiltinIntegerClass, BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
7 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
8 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
9 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10 LiteralValue, ResolveContext, Type,
11};
12use runmat_macros::runtime_builtin;
13use runmat_value::{CellArray, LogicalArray, NumericDType, Tensor, Value};
14
15use super::type_resolvers::logical_output_type;
16use crate::builtins::common::{
17 gpu_helpers,
18 spec::{
19 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
20 ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
21 },
22 tensor,
23};
24use crate::{build_runtime_error, BuiltinResult, RuntimeError};
25
26const NAME: &str = "ismembertol";
27const DEFAULT_TOL_DOUBLE: f64 = 1.0e-12;
28const DEFAULT_TOL_SINGLE: f64 = 1.0e-6;
29
30#[runmat_macros::register_gpu_spec(
31 builtin_path = "crate::builtins::array::sorting_sets::ismembertol"
32)]
33pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
34 name: NAME,
35 op_kind: GpuOpKind::Custom("ismembertol"),
36 supported_precisions: &[ScalarType::F32, ScalarType::F64],
37 broadcast: BroadcastSemantics::None,
38 provider_hooks: &[],
39 constant_strategy: ConstantStrategy::InlineLiteral,
40 residency: ResidencyPolicy::NewHandle,
41 nan_mode: ReductionNaN::Include,
42 two_pass_threshold: None,
43 workgroup_size: None,
44 accepts_nan_mode: false,
45 notes: "Tolerance membership uses authoritative typed gather fallback and restores documented logical and double outputs to the resident input owner; RunMat-only all-indices cell output remains host materialised.",
46};
47
48#[runmat_macros::register_fusion_spec(
49 builtin_path = "crate::builtins::array::sorting_sets::ismembertol"
50)]
51pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
52 name: NAME,
53 shape: ShapeRequirements::Any,
54 constant_strategy: ConstantStrategy::InlineLiteral,
55 elementwise: None,
56 reduction: None,
57 emits_nan: false,
58 notes: "`ismembertol` materialises logical and index outputs and terminates fusion chains.",
59};
60
61const OUTPUT_MASK: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
62 name: "LIA",
63 ty: BuiltinParamType::LogicalArray,
64 arity: BuiltinParamArity::Required,
65 default: None,
66 description: "Logical mask over A.",
67}];
68
69const OUTPUT_MASK_LOC: [BuiltinParamDescriptor; 2] = [
70 BuiltinParamDescriptor {
71 name: "LIA",
72 ty: BuiltinParamType::LogicalArray,
73 arity: BuiltinParamArity::Required,
74 default: None,
75 description: "Logical mask over A.",
76 },
77 BuiltinParamDescriptor {
78 name: "LocB",
79 ty: BuiltinParamType::Any,
80 arity: BuiltinParamArity::Required,
81 default: None,
82 description: "First or all matching indices into B.",
83 },
84];
85
86const INPUTS_AB: [BuiltinParamDescriptor; 2] = [
87 BuiltinParamDescriptor {
88 name: "A",
89 ty: BuiltinParamType::NumericArray,
90 arity: BuiltinParamArity::Required,
91 default: None,
92 description: "Query values or rows.",
93 },
94 BuiltinParamDescriptor {
95 name: "B",
96 ty: BuiltinParamType::NumericArray,
97 arity: BuiltinParamArity::Required,
98 default: None,
99 description: "Reference values or rows.",
100 },
101];
102
103const INPUTS_AB_OPTIONS: [BuiltinParamDescriptor; 4] = [
104 BuiltinParamDescriptor {
105 name: "A",
106 ty: BuiltinParamType::NumericArray,
107 arity: BuiltinParamArity::Required,
108 default: None,
109 description: "Query values or rows.",
110 },
111 BuiltinParamDescriptor {
112 name: "B",
113 ty: BuiltinParamType::NumericArray,
114 arity: BuiltinParamArity::Required,
115 default: None,
116 description: "Reference values or rows.",
117 },
118 BuiltinParamDescriptor {
119 name: "tol",
120 ty: BuiltinParamType::NumericScalar,
121 arity: BuiltinParamArity::Optional,
122 default: Some("1e-12 for double, 1e-6 for single"),
123 description: "Relative tolerance.",
124 },
125 BuiltinParamDescriptor {
126 name: "Name,Value",
127 ty: BuiltinParamType::Any,
128 arity: BuiltinParamArity::Variadic,
129 default: None,
130 description: "Name-value options: ByRows, DataScale, OutputAllIndices.",
131 },
132];
133
134const SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
135 BuiltinSignatureDescriptor {
136 label: "LIA = ismembertol(A, B)",
137 inputs: &INPUTS_AB,
138 outputs: &OUTPUT_MASK,
139 },
140 BuiltinSignatureDescriptor {
141 label: "LIA = ismembertol(A, B, tol, Name, Value)",
142 inputs: &INPUTS_AB_OPTIONS,
143 outputs: &OUTPUT_MASK,
144 },
145 BuiltinSignatureDescriptor {
146 label: "[LIA, LocB] = ismembertol(A, B)",
147 inputs: &INPUTS_AB,
148 outputs: &OUTPUT_MASK_LOC,
149 },
150 BuiltinSignatureDescriptor {
151 label: "[LIA, LocB] = ismembertol(A, B, tol, Name, Value)",
152 inputs: &INPUTS_AB_OPTIONS,
153 outputs: &OUTPUT_MASK_LOC,
154 },
155];
156
157const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
158 code: "RM.ISMEMBERTOL.INVALID_INPUT",
159 identifier: Some("RunMat:ismembertol:InvalidInput"),
160 when: "A or B is not a supported real full numeric input.",
161 message: "ismembertol: inputs must be real full numeric arrays",
162};
163
164const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
165 code: "RM.ISMEMBERTOL.INVALID_ARGUMENT",
166 identifier: Some("RunMat:ismembertol:InvalidArgument"),
167 when: "Tolerance or name-value arguments are malformed.",
168 message: "ismembertol: invalid argument",
169};
170
171const ERROR_ROWS_COLUMN_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
172 code: "RM.ISMEMBERTOL.ROWS_COLUMN_MISMATCH",
173 identifier: Some("RunMat:ismembertol:RowsColumnMismatch"),
174 when: "ByRows is true and A/B column counts differ.",
175 message: "ismembertol: inputs must have the same number of columns when ByRows is true",
176};
177
178const ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
179 code: "RM.ISMEMBERTOL.INTERNAL",
180 identifier: Some("RunMat:ismembertol:Internal"),
181 when: "Internal conversion or allocation fails.",
182 message: "ismembertol: internal error",
183};
184
185const ERRORS: [BuiltinErrorDescriptor; 4] = [
186 ERROR_INVALID_INPUT,
187 ERROR_INVALID_ARGUMENT,
188 ERROR_ROWS_COLUMN_MISMATCH,
189 ERROR_INTERNAL,
190];
191
192const HOST_INTEGER_DATA_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
193 id: "ismembertol-host-integer-data",
194 mode: BuiltinExtensionMode::RunMatOnly,
195 description: "ismembertol with host typed-integer A or B data is a RunMat extension",
196 error_identifier: Some("RunMat:compatibility:IsmembertolHostIntegerDataExtension"),
197};
198
199const HOST_LOGICAL_DATA_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
200 id: "ismembertol-host-logical-data",
201 mode: BuiltinExtensionMode::RunMatOnly,
202 description: "ismembertol with host logical A or B data is a RunMat extension",
203 error_identifier: Some("RunMat:compatibility:IsmembertolHostLogicalDataExtension"),
204};
205
206const TYPED_TOLERANCE_CONTROL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
207 id: "ismembertol-typed-tolerance-control",
208 mode: BuiltinExtensionMode::RunMatOnly,
209 description: "ismembertol with a typed-integer tolerance or DataScale is a RunMat extension",
210 error_identifier: Some("RunMat:compatibility:IsmembertolTypedToleranceControlExtension"),
211};
212
213const LOGICAL_TOLERANCE_CONTROL_EXTENSION: BuiltinExtensionDescriptor =
214 BuiltinExtensionDescriptor {
215 id: "ismembertol-logical-tolerance-control",
216 mode: BuiltinExtensionMode::RunMatOnly,
217 description: "ismembertol with a logical tolerance or DataScale is a RunMat extension",
218 error_identifier: Some("RunMat:compatibility:IsmembertolLogicalToleranceControlExtension"),
219 };
220
221const GPU_WIDE_INTEGER_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
222 id: "ismembertol-gpu-wide-integer-input",
223 mode: BuiltinExtensionMode::RunMatOnly,
224 description: "ismembertol with a resident 64-bit integer input is a RunMat extension",
225 error_identifier: Some("RunMat:compatibility:IsmembertolGpuWideIntegerInputExtension"),
226};
227
228const GPU_OPTIONS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
229 id: "ismembertol-gpu-options",
230 mode: BuiltinExtensionMode::RunMatOnly,
231 description: "ismembertol ByRows or OutputAllIndices with resident input is a RunMat extension",
232 error_identifier: Some("RunMat:compatibility:IsmembertolGpuOptionsExtension"),
233};
234
235const EXTENSIONS: [BuiltinExtensionDescriptor; 6] = [
236 GPU_OPTIONS_EXTENSION,
237 GPU_WIDE_INTEGER_EXTENSION,
238 HOST_INTEGER_DATA_EXTENSION,
239 HOST_LOGICAL_DATA_EXTENSION,
240 LOGICAL_TOLERANCE_CONTROL_EXTENSION,
241 TYPED_TOLERANCE_CONTROL_EXTENSION,
242];
243
244const WIDE_INTEGER_CLASSES: [BuiltinIntegerClass; 2] =
245 [BuiltinIntegerClass::Int64, BuiltinIntegerClass::Uint64];
246
247const HOST_INTEGER_DATA_INPUTS: [BuiltinIntegerInputCapability; 2] = [
248 BuiltinIntegerInputCapability {
249 name: "A",
250 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
251 availability: BuiltinIntegerInputAvailability::RunMatOnly,
252 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
253 notes: "The documented host data domain is single or double; RunMat mode additionally accepts every real integer class.",
254 },
255 BuiltinIntegerInputCapability {
256 name: "B",
257 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
258 availability: BuiltinIntegerInputAvailability::RunMatOnly,
259 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
260 notes: "The documented host data domain is single or double; RunMat mode additionally accepts every real integer class.",
261 },
262];
263
264const GPU_INTEGER_DATA_INPUTS: [BuiltinIntegerInputCapability; 2] = [
265 BuiltinIntegerInputCapability {
266 name: "resident_A",
267 classes: &crate::builtins::common::integer_capability::INTEGER_CLASSES_THROUGH_32_BITS,
268 availability: BuiltinIntegerInputAvailability::Documented,
269 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
270 notes: "Interactive GPU arrays document signed and unsigned integer data through 32 bits.",
271 },
272 BuiltinIntegerInputCapability {
273 name: "resident_B",
274 classes: &crate::builtins::common::integer_capability::INTEGER_CLASSES_THROUGH_32_BITS,
275 availability: BuiltinIntegerInputAvailability::Documented,
276 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
277 notes: "Interactive GPU arrays document signed and unsigned integer data through 32 bits.",
278 },
279];
280
281const GPU_WIDE_INTEGER_DATA_INPUT: [BuiltinIntegerInputCapability; 1] =
282 [BuiltinIntegerInputCapability {
283 name: "resident_A_or_B",
284 classes: &WIDE_INTEGER_CLASSES,
285 availability: BuiltinIntegerInputAvailability::RunMatOnly,
286 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
287 notes: "Public interactive GPU support excludes 64-bit integers; RunMat mode preserves them through typed gather before the tolerance boundary.",
288 }];
289
290const TYPED_TOLERANCE_INPUTS: [BuiltinIntegerInputCapability; 1] =
291 [BuiltinIntegerInputCapability {
292 name: "tol_or_DataScale",
293 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
294 availability: BuiltinIntegerInputAvailability::RunMatOnly,
295 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
296 notes: "The documented tolerance and DataScale classes are single and double; RunMat mode additionally accepts typed integers and converts them once into the floating tolerance domain.",
297 }];
298
299const INTEGER_BOOLEAN_OPTION_INPUTS: [BuiltinIntegerInputCapability; 1] =
300 [BuiltinIntegerInputCapability {
301 name: "ByRows_or_OutputAllIndices",
302 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
303 availability: BuiltinIntegerInputAvailability::Documented,
304 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
305 notes: "The documented numeric boolean controls accept only zero or one; resident ByRows and OutputAllIndices forms remain independently restricted.",
306 }];
307
308const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 5] = [
309 BuiltinIntegerCapabilityDescriptor {
310 form: "[LIA, LocB] = ismembertol(host_integer_A, host_integer_B, options)",
311 inputs: &HOST_INTEGER_DATA_INPUTS,
312 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
313 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
314 overflow: BuiltinIntegerOverflowRule::NotApplicable,
315 backend: BuiltinIntegerBackendRule::HostOnly,
316 overload: BuiltinIntegerOverloadKind::Multiple,
317 notes: "RunMat-only host integer observations retain authoritative storage until one explicit f64 tolerance/DataScale boundary; LIA is logical and first locations are double.",
318 },
319 BuiltinIntegerCapabilityDescriptor {
320 form: "[LIA, LocB] = ismembertol(resident_integer_A, resident_integer_B, options)",
321 inputs: &GPU_INTEGER_DATA_INPUTS,
322 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
323 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
324 overflow: BuiltinIntegerOverflowRule::NotApplicable,
325 backend: BuiltinIntegerBackendRule::GpuRestricted,
326 overload: BuiltinIntegerOverloadKind::Multiple,
327 notes: "Documented resident integer inputs through 32 bits use typed gather fallback and restore logical/double outputs to the input owner; ByRows and OutputAllIndices remain mode-gated.",
328 },
329 BuiltinIntegerCapabilityDescriptor {
330 form: "[LIA, LocB] = ismembertol(resident_int64_or_uint64_A_or_B, options)",
331 inputs: &GPU_WIDE_INTEGER_DATA_INPUT,
332 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
333 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
334 overflow: BuiltinIntegerOverflowRule::NotApplicable,
335 backend: BuiltinIntegerBackendRule::GatherFallback,
336 overload: BuiltinIntegerOverloadKind::Multiple,
337 notes: "Resident 64-bit integer data is a RunMat-only extension and enters the same explicit floating tolerance domain after authoritative typed gather.",
338 },
339 BuiltinIntegerCapabilityDescriptor {
340 form: "LIA = ismembertol(A, B, integer_tol_or_DataScale, options)",
341 inputs: &TYPED_TOLERANCE_INPUTS,
342 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
343 output_class: BuiltinIntegerOutputClassRule::Logical,
344 overflow: BuiltinIntegerOverflowRule::NotApplicable,
345 backend: BuiltinIntegerBackendRule::GatherFallback,
346 overload: BuiltinIntegerOverloadKind::StructuralParameter,
347 notes: "Typed integer tolerance controls are RunMat-only and convert once to f64 before threshold arithmetic.",
348 },
349 BuiltinIntegerCapabilityDescriptor {
350 form: "LIA = ismembertol(A, B, ByRows_or_OutputAllIndices=integer_zero_or_one)",
351 inputs: &INTEGER_BOOLEAN_OPTION_INPUTS,
352 computation_domain: BuiltinIntegerComputationDomain::Structural,
353 output_class: BuiltinIntegerOutputClassRule::Logical,
354 overflow: BuiltinIntegerOverflowRule::NotApplicable,
355 backend: BuiltinIntegerBackendRule::GpuRestricted,
356 overload: BuiltinIntegerOverloadKind::StructuralParameter,
357 notes: "Typed integer zero/one boolean controls are documented numeric forms; interactive GPU execution excludes enabled ByRows and OutputAllIndices.",
358 },
359];
360
361pub const ISMEMBERTOL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
362 signatures: &SIGNATURES,
363 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
364 completion_policy: BuiltinCompletionPolicy::Public,
365 errors: &ERRORS,
366};
367
368fn ismembertol_output_type(args: &[Type], ctx: &ResolveContext) -> Type {
369 if literal_by_rows_enabled(ctx) {
370 return match args.first().and_then(type_row_count) {
371 Some(rows) => Type::Logical {
372 shape: Some(vec![rows, Some(1)]),
373 },
374 None => Type::logical(),
375 };
376 }
377 logical_output_type(args, ctx)
378}
379
380fn literal_by_rows_enabled(ctx: &ResolveContext) -> bool {
381 let mut idx = 2usize;
382 if !matches!(ctx.literal_args.get(idx), Some(LiteralValue::String(_))) {
383 idx += 1;
384 }
385 while idx + 1 < ctx.literal_args.len() {
386 if matches!(
387 ctx.literal_args.get(idx),
388 Some(LiteralValue::String(name)) if name.eq_ignore_ascii_case("ByRows")
389 ) {
390 return literal_truthy(ctx.literal_args.get(idx + 1));
391 }
392 idx += 2;
393 }
394 false
395}
396
397fn literal_truthy(value: Option<&LiteralValue>) -> bool {
398 match value {
399 Some(LiteralValue::Bool(flag)) => *flag,
400 Some(LiteralValue::Number(num)) => *num == 1.0,
401 _ => false,
402 }
403}
404
405fn type_row_count(ty: &Type) -> Option<Option<usize>> {
406 match ty {
407 Type::Tensor { shape: Some(shape) } | Type::Logical { shape: Some(shape) } => {
408 if shape.is_empty() {
409 Some(Some(1))
410 } else {
411 Some(shape[0])
412 }
413 }
414 Type::Tensor { shape: None } | Type::Logical { shape: None } => Some(None),
415 Type::Num | Type::Int | Type::Bool => Some(Some(1)),
416 _ => None,
417 }
418}
419
420#[runtime_builtin(
421 name = "ismembertol",
422 category = "array/sorting_sets",
423 summary = "Identify numeric array elements or rows that are within tolerance of another array.",
424 keywords = "ismembertol,membership,tolerance,set,rows,indices",
425 accel = "array_construct",
426 sink = true,
427 type_resolver(ismembertol_output_type),
428 descriptor(crate::builtins::array::sorting_sets::ismembertol::ISMEMBERTOL_DESCRIPTOR),
429 extensions(EXTENSIONS),
430 integer_capabilities(INTEGER_CAPABILITIES),
431 builtin_path = "crate::builtins::array::sorting_sets::ismembertol"
432)]
433async fn ismembertol_builtin(a: Value, b: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
434 let requested_outputs = crate::output_count::current_output_count();
435 if matches!(requested_outputs, Some(count) if count > 2) {
436 return Err(error_with(
437 &ERROR_INVALID_ARGUMENT,
438 "ismembertol: too many output arguments; maximum is 2",
439 ));
440 }
441 let opts = parse_options(&rest)?;
442 ensure_compatibility(&a, &b, &opts)?;
443 let host_cell_output = opts.output_all_indices && requested_outputs == Some(2);
444 let output_provider = (!host_cell_output)
445 .then(|| super::set_output_provider(&a, &b))
446 .flatten();
447 let loc_request = match requested_outputs {
448 Some(0) | Some(1) | None => LocRequest::None,
449 Some(2) if opts.output_all_indices => LocRequest::All,
450 Some(2) => LocRequest::First,
451 Some(_) => unreachable!("excess output count rejected"),
452 };
453 let eval = evaluate_with_options(a, b, opts, loc_request).await?;
454 let mut outputs = match loc_request {
455 LocRequest::None => vec![eval.into_mask_value()],
456 LocRequest::First | LocRequest::All => {
457 let (mask, loc) = eval.into_pair();
458 vec![mask, loc]
459 }
460 };
461 if requested_outputs == Some(0) {
462 outputs.clear();
463 } else {
464 outputs =
465 super::restore_set_outputs(output_provider, NAME, outputs, internal_error_from_string)?;
466 }
467 if requested_outputs.is_some() {
468 Ok(Value::OutputList(outputs))
469 } else {
470 Ok(outputs.pop().expect("ismembertol mask output"))
471 }
472}
473
474pub async fn evaluate(a: Value, b: Value, rest: &[Value]) -> BuiltinResult<IsMemberTolEvaluation> {
475 let opts = parse_options(rest)?;
476 ensure_compatibility(&a, &b, &opts)?;
477 let loc_request = if opts.output_all_indices {
478 LocRequest::All
479 } else {
480 LocRequest::First
481 };
482 evaluate_with_options(a, b, opts, loc_request).await
483}
484
485async fn evaluate_with_options(
486 a: Value,
487 b: Value,
488 opts: IsMemberTolOptions,
489 loc_request: LocRequest,
490) -> BuiltinResult<IsMemberTolEvaluation> {
491 let a = gather_if_gpu(a).await?;
492 let b = gather_if_gpu(b).await?;
493 let tensor_a =
494 tensor::value_into_tensor_for(NAME, a).map_err(|_| error(&ERROR_INVALID_INPUT))?;
495 let tensor_b =
496 tensor::value_into_tensor_for(NAME, b).map_err(|_| error(&ERROR_INVALID_INPUT))?;
497 evaluate_tensors(tensor_a, tensor_b, opts, loc_request)
498}
499
500async fn gather_if_gpu(value: Value) -> BuiltinResult<Value> {
501 match value {
502 Value::GpuTensor(handle) => gpu_helpers::gather_value_async(&Value::GpuTensor(handle))
503 .await
504 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}"))),
505 other => Ok(other),
506 }
507}
508
509fn evaluate_tensors(
510 a: Tensor,
511 b: Tensor,
512 mut opts: IsMemberTolOptions,
513 loc_request: LocRequest,
514) -> BuiltinResult<IsMemberTolEvaluation> {
515 if opts.tol.is_none() {
516 opts.tol = Some(default_tolerance(a.numeric_dtype(), b.numeric_dtype()));
517 }
518 if opts.by_rows {
519 evaluate_rows(a, b, opts, loc_request)
520 } else {
521 evaluate_elements(a, b, opts, loc_request)
522 }
523}
524
525#[derive(Debug, Clone)]
526struct IsMemberTolOptions {
527 tol: Option<f64>,
528 by_rows: bool,
529 data_scale: Option<Vec<f64>>,
530 output_all_indices: bool,
531 typed_tolerance: bool,
532 typed_data_scale: bool,
533 logical_tolerance: bool,
534 logical_data_scale: bool,
535}
536
537#[derive(Debug, Copy, Clone, Eq, PartialEq)]
538enum LocRequest {
539 None,
540 First,
541 All,
542}
543
544fn parse_options(rest: &[Value]) -> BuiltinResult<IsMemberTolOptions> {
545 let mut opts = IsMemberTolOptions {
546 tol: None,
547 by_rows: false,
548 data_scale: None,
549 output_all_indices: false,
550 typed_tolerance: false,
551 typed_data_scale: false,
552 logical_tolerance: false,
553 logical_data_scale: false,
554 };
555
556 let mut idx = 0usize;
557 if let Some(first) = rest.first() {
558 if option_name(first).is_none() {
559 opts.typed_tolerance = is_typed_integer_value(first);
560 opts.logical_tolerance = is_logical_value(first);
561 opts.tol = Some(parse_positive_scalar(first, "tolerance")?);
562 idx = 1;
563 }
564 }
565
566 let remaining = &rest[idx..];
567 if !remaining.len().is_multiple_of(2) {
568 return Err(error_with(
569 &ERROR_INVALID_ARGUMENT,
570 "ismembertol: name-value arguments must appear in pairs",
571 ));
572 }
573
574 for pair in remaining.chunks_exact(2) {
575 let name = option_name(&pair[0])
576 .ok_or_else(|| {
577 error_with(&ERROR_INVALID_ARGUMENT, "ismembertol: expected option name")
578 })?
579 .trim()
580 .to_ascii_lowercase();
581 match name.as_str() {
582 "byrows" => opts.by_rows = parse_bool_option(&pair[1])?,
583 "outputallindices" => opts.output_all_indices = parse_bool_option(&pair[1])?,
584 "datascale" => {
585 opts.typed_data_scale = is_typed_integer_value(&pair[1]);
586 opts.logical_data_scale = is_logical_value(&pair[1]);
587 opts.data_scale = Some(parse_data_scale(&pair[1])?);
588 }
589 other => {
590 return Err(error_with(
591 &ERROR_INVALID_ARGUMENT,
592 format!("ismembertol: unknown option '{other}'"),
593 ))
594 }
595 }
596 }
597
598 Ok(opts)
599}
600
601fn ensure_compatibility(a: &Value, b: &Value, opts: &IsMemberTolOptions) -> BuiltinResult<()> {
602 for value in [a, b] {
603 match value {
604 Value::GpuTensor(handle) if super::is_unsupported_set_gpu_integer(handle) => {
605 crate::compatibility::ensure_builtin_extension_enabled(
606 &GPU_WIDE_INTEGER_EXTENSION,
607 NAME,
608 )?;
609 }
610 Value::GpuTensor(_) => {}
611 _ if is_typed_integer_value(value) => {
612 crate::compatibility::ensure_builtin_extension_enabled(
613 &HOST_INTEGER_DATA_EXTENSION,
614 NAME,
615 )?;
616 }
617 _ if is_logical_value(value) => {
618 crate::compatibility::ensure_builtin_extension_enabled(
619 &HOST_LOGICAL_DATA_EXTENSION,
620 NAME,
621 )?;
622 }
623 _ => {}
624 }
625 }
626 if opts.typed_tolerance || opts.typed_data_scale {
627 crate::compatibility::ensure_builtin_extension_enabled(
628 &TYPED_TOLERANCE_CONTROL_EXTENSION,
629 NAME,
630 )?;
631 }
632 if opts.logical_tolerance || opts.logical_data_scale {
633 crate::compatibility::ensure_builtin_extension_enabled(
634 &LOGICAL_TOLERANCE_CONTROL_EXTENSION,
635 NAME,
636 )?;
637 }
638 if super::set_output_provider(a, b).is_some() && (opts.by_rows || opts.output_all_indices) {
639 crate::compatibility::ensure_builtin_extension_enabled(&GPU_OPTIONS_EXTENSION, NAME)?;
640 }
641 Ok(())
642}
643
644fn is_typed_integer_value(value: &Value) -> bool {
645 matches!(value, Value::Int(_))
646 || matches!(value, Value::Tensor(tensor) if tensor.integer_storage().is_some())
647 || matches!(
648 value,
649 Value::GpuTensor(handle)
650 if runmat_accelerate_api::handle_integer_type(handle).is_some()
651 )
652}
653
654fn is_logical_value(value: &Value) -> bool {
655 matches!(value, Value::Bool(_) | Value::LogicalArray(_))
656 || matches!(
657 value,
658 Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_logical(handle)
659 )
660}
661
662fn option_name(value: &Value) -> Option<String> {
663 match value {
664 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
665 tensor::value_to_string(value)
666 }
667 _ => None,
668 }
669}
670
671fn evaluate_elements(
672 a: Tensor,
673 b: Tensor,
674 opts: IsMemberTolOptions,
675 loc_request: LocRequest,
676) -> BuiltinResult<IsMemberTolEvaluation> {
677 let a_shape = a.shape.clone();
678 let a_values = materialize_tolerance_values(a)?;
679 let b_values = materialize_tolerance_values(b)?;
680 let tol = opts.tol.expect("defaulted tolerance");
681 let scale = opts
682 .data_scale
683 .as_ref()
684 .map(|values| {
685 if values.len() == 1 {
686 Ok(values[0])
687 } else {
688 Err(error_with(
689 &ERROR_INVALID_ARGUMENT,
690 "ismembertol: DataScale must be scalar unless ByRows is true",
691 ))
692 }
693 })
694 .transpose()?
695 .unwrap_or_else(|| default_element_scale(&a_values, &b_values));
696 let threshold = tol * scale.abs();
697
698 let mut mask_data = Vec::with_capacity(a_values.len());
699 let mut loc_data = match loc_request {
700 LocRequest::First => Vec::with_capacity(a_values.len()),
701 LocRequest::None | LocRequest::All => Vec::new(),
702 };
703 let mut all_cells = match loc_request {
704 LocRequest::All => Vec::with_capacity(a_values.len()),
705 LocRequest::None | LocRequest::First => Vec::new(),
706 };
707
708 for &value in a_values.iter() {
709 match loc_request {
710 LocRequest::None => {
711 mask_data.push(
712 if first_element_match(value, &b_values, threshold).is_some() {
713 1
714 } else {
715 0
716 },
717 );
718 }
719 LocRequest::First => {
720 let first = first_element_match(value, &b_values, threshold);
721 mask_data.push(if first.is_some() { 1 } else { 0 });
722 loc_data.push(first.unwrap_or(0) as f64);
723 }
724 LocRequest::All => {
725 let matches = all_element_matches(value, &b_values, threshold);
726 mask_data.push(if matches.is_empty() { 0 } else { 1 });
727 all_cells.push(indices_cell(matches)?);
728 }
729 }
730 }
731
732 let mask = LogicalArray::new(mask_data, a_shape.clone())
733 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?;
734 let loc = match loc_request {
735 LocRequest::None => None,
736 LocRequest::First => Some(LocOutput::First(
737 Tensor::new(loc_data, a_shape.clone())
738 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?,
739 )),
740 LocRequest::All => Some(LocOutput::All(cell_array_from_linear_matches(
741 all_cells, a_shape,
742 )?)),
743 };
744 Ok(IsMemberTolEvaluation { mask, loc })
745}
746
747fn evaluate_rows(
748 a: Tensor,
749 b: Tensor,
750 opts: IsMemberTolOptions,
751 loc_request: LocRequest,
752) -> BuiltinResult<IsMemberTolEvaluation> {
753 let (rows_a, cols_a) = rows_cols(&a)?;
754 let (rows_b, cols_b) = rows_cols(&b)?;
755 if cols_a != cols_b {
756 return Err(error(&ERROR_ROWS_COLUMN_MISMATCH));
757 }
758 let a_values = materialize_tolerance_values(a)?;
759 let b_values = materialize_tolerance_values(b)?;
760 let tol = opts.tol.expect("defaulted tolerance");
761 let scales = row_scales(
762 &a_values,
763 rows_a,
764 &b_values,
765 rows_b,
766 opts.data_scale.as_deref(),
767 cols_a,
768 )?;
769 let thresholds: Vec<f64> = scales.iter().map(|scale| tol * scale.abs()).collect();
770
771 let mut mask_data = vec![0u8; rows_a];
772 let mut loc_data = match loc_request {
773 LocRequest::First => vec![0.0f64; rows_a],
774 LocRequest::None | LocRequest::All => Vec::new(),
775 };
776 let mut all_cells = match loc_request {
777 LocRequest::All => Vec::with_capacity(rows_a),
778 LocRequest::None | LocRequest::First => Vec::new(),
779 };
780
781 for row_a in 0..rows_a {
782 match loc_request {
783 LocRequest::None => {
784 mask_data[row_a] = if first_row_match(
785 &a_values,
786 row_a,
787 rows_a,
788 &b_values,
789 rows_b,
790 cols_a,
791 &thresholds,
792 )
793 .is_some()
794 {
795 1
796 } else {
797 0
798 };
799 }
800 LocRequest::First => {
801 let first = first_row_match(
802 &a_values,
803 row_a,
804 rows_a,
805 &b_values,
806 rows_b,
807 cols_a,
808 &thresholds,
809 );
810 mask_data[row_a] = if first.is_some() { 1 } else { 0 };
811 loc_data[row_a] = first.unwrap_or(0) as f64;
812 }
813 LocRequest::All => {
814 let matches = all_row_matches(
815 &a_values,
816 row_a,
817 rows_a,
818 &b_values,
819 rows_b,
820 cols_a,
821 &thresholds,
822 );
823 mask_data[row_a] = if matches.is_empty() { 0 } else { 1 };
824 all_cells.push(indices_cell(matches)?);
825 }
826 }
827 }
828
829 let shape = vec![rows_a, 1];
830 let mask = LogicalArray::new(mask_data, shape.clone())
831 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?;
832 let loc = match loc_request {
833 LocRequest::None => None,
834 LocRequest::First => Some(LocOutput::First(
835 Tensor::new(loc_data, shape)
836 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?,
837 )),
838 LocRequest::All => Some(LocOutput::All(cell_array_from_linear_matches(
839 all_cells, shape,
840 )?)),
841 };
842 Ok(IsMemberTolEvaluation { mask, loc })
843}
844
845fn materialize_tolerance_values(tensor: Tensor) -> BuiltinResult<Vec<f64>> {
846 tensor
848 .into_numeric_storage()
849 .map(|storage| storage.materialize_f64())
850 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))
851}
852
853fn first_element_match(value: f64, candidates: &[f64], threshold: f64) -> Option<usize> {
854 candidates.iter().enumerate().find_map(|(idx, &candidate)| {
855 within_tolerance(value, candidate, threshold).then_some(idx + 1)
856 })
857}
858
859fn all_element_matches(value: f64, candidates: &[f64], threshold: f64) -> Vec<usize> {
860 candidates
861 .iter()
862 .enumerate()
863 .filter_map(|(idx, &candidate)| {
864 within_tolerance(value, candidate, threshold).then_some(idx + 1)
865 })
866 .collect()
867}
868
869fn first_row_match(
870 a: &[f64],
871 row_a: usize,
872 rows_a: usize,
873 b: &[f64],
874 rows_b: usize,
875 cols: usize,
876 thresholds: &[f64],
877) -> Option<usize> {
878 (0..rows_b).find_map(|row_b| {
879 rows_match(a, row_a, rows_a, b, row_b, rows_b, cols, thresholds).then_some(row_b + 1)
880 })
881}
882
883fn all_row_matches(
884 a: &[f64],
885 row_a: usize,
886 rows_a: usize,
887 b: &[f64],
888 rows_b: usize,
889 cols: usize,
890 thresholds: &[f64],
891) -> Vec<usize> {
892 (0..rows_b)
893 .filter_map(|row_b| {
894 rows_match(a, row_a, rows_a, b, row_b, rows_b, cols, thresholds).then_some(row_b + 1)
895 })
896 .collect()
897}
898
899fn rows_match(
900 a: &[f64],
901 row_a: usize,
902 rows_a: usize,
903 b: &[f64],
904 row_b: usize,
905 rows_b: usize,
906 cols: usize,
907 thresholds: &[f64],
908) -> bool {
909 for col in 0..cols {
910 let threshold = thresholds[col];
911 if threshold.is_infinite() {
912 continue;
913 }
914 let lhs = a[row_a + col * rows_a];
915 let rhs = b[row_b + col * rows_b];
916 if !within_tolerance(lhs, rhs, threshold) {
917 return false;
918 }
919 }
920 true
921}
922
923fn within_tolerance(lhs: f64, rhs: f64, threshold: f64) -> bool {
924 if lhs.is_nan() || rhs.is_nan() {
925 return false;
926 }
927 if lhs == rhs {
928 return true;
929 }
930 (lhs - rhs).abs() <= threshold
931}
932
933fn default_tolerance(a: NumericDType, b: NumericDType) -> f64 {
934 if matches!(a, NumericDType::F32) || matches!(b, NumericDType::F32) {
935 DEFAULT_TOL_SINGLE
936 } else {
937 DEFAULT_TOL_DOUBLE
938 }
939}
940
941fn default_element_scale(a: &[f64], b: &[f64]) -> f64 {
942 a.iter()
943 .chain(b.iter())
944 .filter_map(|value| (!value.is_nan()).then_some(value.abs()))
945 .fold(0.0, f64::max)
946}
947
948fn row_scales(
949 a: &[f64],
950 rows_a: usize,
951 b: &[f64],
952 rows_b: usize,
953 supplied: Option<&[f64]>,
954 cols: usize,
955) -> BuiltinResult<Vec<f64>> {
956 if let Some(values) = supplied {
957 if values.len() == 1 {
958 return Ok(vec![values[0]; cols]);
959 }
960 if values.len() == cols {
961 return Ok(values.to_vec());
962 }
963 return Err(error_with(
964 &ERROR_INVALID_ARGUMENT,
965 "ismembertol: DataScale vector length must match the number of columns",
966 ));
967 }
968
969 let mut scales = vec![0.0f64; cols];
970 for (col, scale) in scales.iter_mut().enumerate() {
971 for row in 0..rows_a {
972 let value = a[row + col * rows_a];
973 if !value.is_nan() {
974 *scale = (*scale).max(value.abs());
975 }
976 }
977 for row in 0..rows_b {
978 let value = b[row + col * rows_b];
979 if !value.is_nan() {
980 *scale = (*scale).max(value.abs());
981 }
982 }
983 }
984 Ok(scales)
985}
986
987fn rows_cols(tensor: &Tensor) -> BuiltinResult<(usize, usize)> {
988 match tensor.shape.len() {
989 0 => Ok((1, 1)),
990 1 => Ok((tensor.shape[0], 1)),
991 2 => Ok((tensor.shape[0], tensor.shape[1])),
992 _ => Err(error_with(
993 &ERROR_INVALID_ARGUMENT,
994 "ismembertol: ByRows requires 2-D arrays",
995 )),
996 }
997}
998
999fn indices_cell(indices: Vec<usize>) -> BuiltinResult<Value> {
1000 if indices.is_empty() {
1001 return Ok(Value::Tensor(Tensor::new(vec![0.0], vec![1, 1]).map_err(
1002 |e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")),
1003 )?));
1004 }
1005 let data = indices
1006 .into_iter()
1007 .map(|idx| idx as f64)
1008 .collect::<Vec<_>>();
1009 let len = data.len();
1010 Tensor::new(data, vec![len, 1])
1011 .map(Value::Tensor)
1012 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))
1013}
1014
1015fn cell_array_from_linear_matches(
1016 cells_column_major: Vec<Value>,
1017 shape: Vec<usize>,
1018) -> BuiltinResult<CellArray> {
1019 let cells = if shape.len() == 2 && shape[0] > 1 && shape[1] > 1 {
1020 let rows = shape[0];
1021 let cols = shape[1];
1022 let mut row_major = Vec::with_capacity(cells_column_major.len());
1023 for row in 0..rows {
1024 for col in 0..cols {
1025 row_major.push(cells_column_major[row + col * rows].clone());
1026 }
1027 }
1028 row_major
1029 } else {
1030 cells_column_major
1031 };
1032 CellArray::new_with_shape(cells, shape)
1033 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))
1034}
1035
1036fn parse_positive_scalar(value: &Value, label: &str) -> BuiltinResult<f64> {
1037 let scalar = numeric_scalar(value).ok_or_else(|| {
1038 error_with(
1039 &ERROR_INVALID_ARGUMENT,
1040 format!("ismembertol: {label} must be a positive real scalar"),
1041 )
1042 })?;
1043 if !scalar.is_finite() || scalar <= 0.0 {
1044 return Err(error_with(
1045 &ERROR_INVALID_ARGUMENT,
1046 format!("ismembertol: {label} must be a positive real scalar"),
1047 ));
1048 }
1049 Ok(scalar)
1050}
1051
1052fn parse_bool_option(value: &Value) -> BuiltinResult<bool> {
1053 match numeric_scalar(value) {
1054 Some(0.0) => Ok(false),
1055 Some(1.0) => Ok(true),
1056 _ => Err(error_with(
1057 &ERROR_INVALID_ARGUMENT,
1058 "ismembertol: boolean options must be true/false or 0/1",
1059 )),
1060 }
1061}
1062
1063fn parse_data_scale(value: &Value) -> BuiltinResult<Vec<f64>> {
1064 match value {
1065 Value::Num(n) => validate_data_scale(vec![*n]),
1066 Value::Int(i) => validate_data_scale(vec![i.to_f64()]),
1067 Value::Bool(flag) => validate_data_scale(vec![if *flag { 1.0 } else { 0.0 }]),
1068 Value::Tensor(tensor) => validate_data_scale(tensor::tensor_values_f64(tensor)),
1069 Value::LogicalArray(logical) => validate_data_scale(
1070 logical
1071 .data
1072 .iter()
1073 .map(|&v| if v != 0 { 1.0 } else { 0.0 })
1074 .collect(),
1075 ),
1076 _ => Err(error_with(
1077 &ERROR_INVALID_ARGUMENT,
1078 "ismembertol: DataScale must be a numeric scalar or vector",
1079 )),
1080 }
1081}
1082
1083fn validate_data_scale(values: Vec<f64>) -> BuiltinResult<Vec<f64>> {
1084 if values.is_empty() || values.iter().any(|value| value.is_nan() || *value < 0.0) {
1085 return Err(error_with(
1086 &ERROR_INVALID_ARGUMENT,
1087 "ismembertol: DataScale values must be non-negative or Inf",
1088 ));
1089 }
1090 Ok(values)
1091}
1092
1093fn numeric_scalar(value: &Value) -> Option<f64> {
1094 match value {
1095 Value::Num(n) => Some(*n),
1096 Value::Int(i) => Some(i.to_f64()),
1097 Value::Bool(flag) => Some(if *flag { 1.0 } else { 0.0 }),
1098 Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
1099 tensor::tensor_values_f64(t).first().copied()
1100 }
1101 Value::LogicalArray(logical) if logical.data.len() == 1 => {
1102 Some(if logical.data[0] != 0 { 1.0 } else { 0.0 })
1103 }
1104 _ => None,
1105 }
1106}
1107
1108#[derive(Debug, Clone)]
1109pub struct IsMemberTolEvaluation {
1110 mask: LogicalArray,
1111 loc: Option<LocOutput>,
1112}
1113
1114#[derive(Debug, Clone)]
1115enum LocOutput {
1116 First(Tensor),
1117 All(CellArray),
1118}
1119
1120impl IsMemberTolEvaluation {
1121 pub fn into_mask_value(self) -> Value {
1122 logical_array_into_value(self.mask)
1123 }
1124
1125 pub fn mask_value(&self) -> Value {
1126 logical_array_into_value(self.mask.clone())
1127 }
1128
1129 pub fn into_pair(self) -> (Value, Value) {
1130 let mask = logical_array_into_value(self.mask);
1131 let loc = match self.loc.expect("LocB was not requested") {
1132 LocOutput::First(tensor) => tensor::tensor_into_value(tensor),
1133 LocOutput::All(cell) => Value::Cell(cell),
1134 };
1135 (mask, loc)
1136 }
1137}
1138
1139fn logical_array_into_value(logical: LogicalArray) -> Value {
1140 if logical.data.len() == 1 {
1141 Value::Bool(logical.data[0] != 0)
1142 } else {
1143 Value::LogicalArray(logical)
1144 }
1145}
1146
1147fn error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
1148 error_with(error, error.message)
1149}
1150
1151fn error_with(error: &'static BuiltinErrorDescriptor, message: impl Into<String>) -> RuntimeError {
1152 let mut builder = build_runtime_error(message).with_builtin(NAME);
1153 if let Some(identifier) = error.identifier {
1154 builder = builder.with_identifier(identifier);
1155 }
1156 builder.build()
1157}
1158
1159fn internal_error_from_string(message: String) -> RuntimeError {
1160 error_with(&ERROR_INTERNAL, message)
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use crate::builtins::common::test_support;
1167 use futures::executor::block_on;
1168 use runmat_builtins::{LiteralValue, ResolveContext, Type};
1169 use runmat_value::IntegerStorage;
1170
1171 fn eval(a: Value, b: Value, rest: &[Value]) -> BuiltinResult<IsMemberTolEvaluation> {
1172 block_on(evaluate(a, b, rest))
1173 }
1174
1175 fn builtin(a: Value, b: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1176 block_on(ismembertol_builtin(a, b, rest))
1177 }
1178
1179 #[test]
1180 fn type_resolver_returns_logical_shape() {
1181 assert_eq!(
1182 ismembertol_output_type(
1183 &[Type::Tensor {
1184 shape: Some(vec![Some(1), Some(3)])
1185 }],
1186 &ResolveContext::new(Vec::new()),
1187 ),
1188 Type::Logical {
1189 shape: Some(vec![Some(1), Some(3)])
1190 }
1191 );
1192 }
1193
1194 #[test]
1195 fn type_resolver_returns_row_shape_for_literal_by_rows() {
1196 assert_eq!(
1197 ismembertol_output_type(
1198 &[Type::Tensor {
1199 shape: Some(vec![Some(4), Some(3)])
1200 }],
1201 &ResolveContext::new(vec![
1202 LiteralValue::Unknown,
1203 LiteralValue::Unknown,
1204 LiteralValue::Number(0.01),
1205 LiteralValue::String("ByRows".to_string()),
1206 LiteralValue::Bool(true),
1207 ]),
1208 ),
1209 Type::Logical {
1210 shape: Some(vec![Some(4), Some(1)])
1211 }
1212 );
1213 }
1214
1215 #[test]
1216 fn default_tolerance_uses_scaled_double_data() {
1217 let a = Tensor::new(vec![0.1, 1.0e10], vec![1, 2]).unwrap();
1218 let b = Tensor::new(vec![0.1 + 1.0e-14, 1.0e10 + 5.0e-3], vec![1, 2]).unwrap();
1219 let result = eval(Value::Tensor(a), Value::Tensor(b), &[]).unwrap();
1220 assert_eq!(result.mask.data, vec![1, 1]);
1221 }
1222
1223 #[test]
1224 fn default_tolerance_uses_native_single_class_before_materialization() {
1225 let single_a = Tensor::from_f32(vec![1.0], vec![1, 1]).unwrap();
1226 let single_b = Tensor::from_f32(vec![1.0 + 5.0e-7], vec![1, 1]).unwrap();
1227 let single = eval(Value::Tensor(single_a), Value::Tensor(single_b), &[]).unwrap();
1228 assert_eq!(single.mask_value(), Value::Bool(true));
1229
1230 let double_a = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1231 let double_b = Tensor::new(vec![1.0 + 5.0e-7], vec![1, 1]).unwrap();
1232 let double = eval(Value::Tensor(double_a), Value::Tensor(double_b), &[]).unwrap();
1233 assert_eq!(double.mask_value(), Value::Bool(false));
1234 }
1235
1236 #[test]
1237 fn data_scale_one_uses_absolute_tolerance() {
1238 let a = Tensor::new(vec![1.0e10], vec![1, 1]).unwrap();
1239 let b = Tensor::new(vec![1.0e10 + 5.0e-3], vec![1, 1]).unwrap();
1240 let result = eval(
1241 Value::Tensor(a),
1242 Value::Tensor(b),
1243 &[
1244 Value::Num(1.0e-6),
1245 Value::from("DataScale"),
1246 Value::Num(1.0),
1247 ],
1248 )
1249 .unwrap();
1250 assert_eq!(result.mask_value(), Value::Bool(false));
1251 }
1252
1253 #[test]
1254 fn typed_integer_inputs_are_compared_from_exact_storage() {
1255 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1256 let a = Tensor::new_integer(IntegerStorage::I16(vec![10, 20, 30]), vec![1, 3]).unwrap();
1257 let b = Tensor::new_integer(IntegerStorage::I16(vec![9, 31]), vec![1, 2]).unwrap();
1258
1259 let result = eval(
1260 Value::Tensor(a),
1261 Value::Tensor(b),
1262 &[Value::Num(0.2), Value::from("DataScale"), Value::Num(10.0)],
1263 )
1264 .unwrap();
1265
1266 assert_eq!(result.mask.data, vec![1, 0, 1]);
1267 }
1268
1269 #[test]
1270 fn wide_integers_enter_explicit_f64_tolerance_domain() {
1271 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1272 let a = Tensor::new_integer(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1])
1273 .unwrap();
1274 let b = Tensor::new_integer(IntegerStorage::U64(vec![9_007_199_254_740_992]), vec![1, 1])
1275 .unwrap();
1276
1277 let result = eval(
1278 Value::Tensor(a),
1279 Value::Tensor(b),
1280 &[
1281 Value::Num(1.0e-12),
1282 Value::from("DataScale"),
1283 Value::Num(1.0),
1284 ],
1285 )
1286 .unwrap();
1287
1288 assert_eq!(result.mask_value(), Value::Bool(true));
1289 }
1290
1291 #[test]
1292 fn typed_integer_tolerance_and_datascale_read_exact_storage() {
1293 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1294 let a = Tensor::new(vec![10.0, 20.0], vec![1, 2]).unwrap();
1295 let b = Tensor::new(vec![11.0, 24.0], vec![1, 2]).unwrap();
1296 let tolerance = Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1]).unwrap();
1297 let scale = Tensor::new_integer(IntegerStorage::U16(vec![1, 4]), vec![1, 2]).unwrap();
1298
1299 let result = eval(
1300 Value::Tensor(a),
1301 Value::Tensor(b),
1302 &[
1303 Value::Tensor(tolerance),
1304 Value::from("DataScale"),
1305 Value::Tensor(scale),
1306 Value::from("ByRows"),
1307 Value::Bool(true),
1308 ],
1309 )
1310 .unwrap();
1311
1312 assert_eq!(result.mask.data, vec![1]);
1313 }
1314
1315 #[test]
1316 fn loc_returns_first_matching_index() {
1317 let a = Tensor::new(vec![1.0, 2.0, 4.0], vec![1, 3]).unwrap();
1318 let b = Tensor::new(vec![2.01, 2.02, 4.2], vec![1, 3]).unwrap();
1319 let (_, loc) = eval(Value::Tensor(a), Value::Tensor(b), &[Value::Num(0.05)])
1320 .unwrap()
1321 .into_pair();
1322 match loc {
1323 Value::Tensor(tensor) => assert_eq!(tensor.materialize_f64(), vec![0.0, 1.0, 3.0]),
1324 other => panic!("expected tensor loc, got {other:?}"),
1325 }
1326 }
1327
1328 #[test]
1329 fn mask_only_request_does_not_materialize_locations() {
1330 let a = Tensor::new(vec![2.0, 9.0], vec![1, 2]).unwrap();
1331 let b = Tensor::new(vec![1.99, 2.01, 9.5], vec![1, 3]).unwrap();
1332 let opts = parse_options(&[
1333 Value::Num(0.01),
1334 Value::from("OutputAllIndices"),
1335 Value::Bool(true),
1336 ])
1337 .unwrap();
1338 let result = block_on(evaluate_with_options(
1339 Value::Tensor(a),
1340 Value::Tensor(b),
1341 opts,
1342 LocRequest::None,
1343 ))
1344 .unwrap();
1345 assert_eq!(result.mask.data, vec![1, 0]);
1346 assert!(result.loc.is_none());
1347 }
1348
1349 #[test]
1350 fn output_all_indices_returns_cell_array() {
1351 let a = Tensor::new(vec![2.0, 9.0], vec![1, 2]).unwrap();
1352 let b = Tensor::new(vec![1.99, 2.01, 9.5], vec![1, 3]).unwrap();
1353 let (_, loc) = eval(
1354 Value::Tensor(a),
1355 Value::Tensor(b),
1356 &[
1357 Value::Num(0.01),
1358 Value::from("OutputAllIndices"),
1359 Value::Bool(true),
1360 ],
1361 )
1362 .unwrap()
1363 .into_pair();
1364 let Value::Cell(cell) = loc else {
1365 panic!("expected cell loc");
1366 };
1367 assert_eq!(cell.shape, vec![1, 2]);
1368 match &cell.data[0] {
1369 Value::Tensor(indices) => assert_eq!(indices.materialize_f64(), vec![1.0, 2.0]),
1370 other => panic!("expected tensor indices, got {other:?}"),
1371 }
1372 match &cell.data[1] {
1373 Value::Tensor(indices) => assert_eq!(indices.materialize_f64(), vec![0.0]),
1374 other => panic!("expected tensor zero indices, got {other:?}"),
1375 }
1376 }
1377
1378 #[test]
1379 fn output_all_indices_matrix_cells_follow_public_cell_indexing() {
1380 let a = Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap();
1381 let b = Tensor::new(vec![10.0, 2.0, 3.0, 20.0], vec![1, 4]).unwrap();
1382 let (_, loc) = eval(
1383 Value::Tensor(a),
1384 Value::Tensor(b),
1385 &[
1386 Value::Num(1.0e-12),
1387 Value::from("OutputAllIndices"),
1388 Value::Bool(true),
1389 ],
1390 )
1391 .unwrap()
1392 .into_pair();
1393 let Value::Cell(cell) = loc else {
1394 panic!("expected cell loc");
1395 };
1396 assert_eq!(cell.shape, vec![2, 2]);
1397 let top_right = cell.get(0, 1).unwrap();
1398 match top_right {
1399 Value::Tensor(indices) => assert_eq!(indices.materialize_f64(), vec![2.0]),
1400 other => panic!("expected tensor indices, got {other:?}"),
1401 }
1402 let bottom_left = cell.get(1, 0).unwrap();
1403 match bottom_left {
1404 Value::Tensor(indices) => assert_eq!(indices.materialize_f64(), vec![3.0]),
1405 other => panic!("expected tensor indices, got {other:?}"),
1406 }
1407 }
1408
1409 #[test]
1410 fn by_rows_uses_column_scales_and_inf_ignores_column() {
1411 let a = Tensor::new(vec![0.0, 10.0, 0.5, 20.0], vec![2, 2]).unwrap();
1412 let b = Tensor::new(vec![0.05, 10.2, 999.0, -999.0], vec![2, 2]).unwrap();
1413 let result = eval(
1414 Value::Tensor(a),
1415 Value::Tensor(b),
1416 &[
1417 Value::Num(0.1),
1418 Value::from("ByRows"),
1419 Value::Bool(true),
1420 Value::from("DataScale"),
1421 Value::Tensor(Tensor::new(vec![1.0, f64::INFINITY], vec![1, 2]).unwrap()),
1422 ],
1423 )
1424 .unwrap();
1425 assert_eq!(result.mask.data, vec![1, 0]);
1426 let (_, loc) = result.into_pair();
1427 match loc {
1428 Value::Tensor(tensor) => {
1429 assert_eq!(tensor.shape, vec![2, 1]);
1430 assert_eq!(tensor.materialize_f64(), vec![1.0, 0.0]);
1431 }
1432 other => panic!("expected tensor loc, got {other:?}"),
1433 }
1434 }
1435
1436 #[test]
1437 fn by_rows_typed_integer_inputs_are_compared_from_exact_storage() {
1438 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1439 let a = Tensor::new_integer(IntegerStorage::U16(vec![10, 20, 30, 40]), vec![2, 2]).unwrap();
1440 let b = Tensor::new_integer(IntegerStorage::U16(vec![11, 21, 31, 41]), vec![2, 2]).unwrap();
1441
1442 let result = eval(
1443 Value::Tensor(a),
1444 Value::Tensor(b),
1445 &[
1446 Value::Num(0.2),
1447 Value::from("ByRows"),
1448 Value::Bool(true),
1449 Value::from("DataScale"),
1450 Value::Num(10.0),
1451 ],
1452 )
1453 .unwrap();
1454
1455 assert_eq!(result.mask.data, vec![1, 1]);
1456 }
1457
1458 #[test]
1459 fn by_rows_output_all_indices_returns_row_cells() {
1460 let a = Tensor::new(vec![0.0, 1.0], vec![1, 2]).unwrap();
1461 let b = Tensor::new(vec![0.01, -0.01, 5.0, 1.0, 1.0, 5.0], vec![3, 2]).unwrap();
1462 let (_, loc) = eval(
1463 Value::Tensor(a),
1464 Value::Tensor(b),
1465 &[
1466 Value::Num(0.02),
1467 Value::from("ByRows"),
1468 Value::Bool(true),
1469 Value::from("OutputAllIndices"),
1470 Value::Bool(true),
1471 ],
1472 )
1473 .unwrap()
1474 .into_pair();
1475 let Value::Cell(cell) = loc else {
1476 panic!("expected cell loc");
1477 };
1478 assert_eq!(cell.shape, vec![1, 1]);
1479 match &cell.data[0] {
1480 Value::Tensor(indices) => assert_eq!(indices.materialize_f64(), vec![1.0, 2.0]),
1481 other => panic!("expected tensor indices, got {other:?}"),
1482 }
1483 }
1484
1485 #[test]
1486 fn rejects_bad_options_and_shape_mismatch() {
1487 let err = parse_options(&[Value::Num(-1.0)]).unwrap_err();
1488 assert_eq!(
1489 err.identifier.as_deref(),
1490 Some("RunMat:ismembertol:InvalidArgument")
1491 );
1492
1493 let a = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
1494 let b = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1495 let err = eval(
1496 Value::Tensor(a),
1497 Value::Tensor(b),
1498 &[Value::from("ByRows"), Value::Bool(true)],
1499 )
1500 .unwrap_err();
1501 assert_eq!(
1502 err.identifier.as_deref(),
1503 Some("RunMat:ismembertol:RowsColumnMismatch")
1504 );
1505 }
1506
1507 #[test]
1508 fn host_integer_logical_and_typed_tolerance_forms_are_mode_gated() {
1509 let integer = Tensor::new_integer(IntegerStorage::I16(vec![1, 2]), vec![2, 1]).unwrap();
1510 {
1511 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1512 let error = eval(
1513 Value::Tensor(integer.clone()),
1514 Value::Tensor(integer.clone()),
1515 &[],
1516 )
1517 .expect_err("host integer data must be gated");
1518 assert_eq!(
1519 error.identifier(),
1520 HOST_INTEGER_DATA_EXTENSION.error_identifier
1521 );
1522
1523 let error = eval(Value::Bool(true), Value::Bool(true), &[])
1524 .expect_err("host logical data must be gated");
1525 assert_eq!(
1526 error.identifier(),
1527 HOST_LOGICAL_DATA_EXTENSION.error_identifier
1528 );
1529
1530 let tolerance = Tensor::new_integer(IntegerStorage::U8(vec![1]), vec![1, 1]).unwrap();
1531 let error = eval(
1532 Value::Num(1.0),
1533 Value::Num(1.0),
1534 &[Value::Tensor(tolerance)],
1535 )
1536 .expect_err("typed tolerance must be gated");
1537 assert_eq!(
1538 error.identifier(),
1539 TYPED_TOLERANCE_CONTROL_EXTENSION.error_identifier
1540 );
1541
1542 let error = eval(
1543 Value::Num(1.0),
1544 Value::Num(1.0),
1545 &[Value::from("DataScale"), Value::Bool(true)],
1546 )
1547 .expect_err("logical DataScale must be gated");
1548 assert_eq!(
1549 error.identifier(),
1550 LOGICAL_TOLERANCE_CONTROL_EXTENSION.error_identifier
1551 );
1552 }
1553 {
1554 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1555 assert_eq!(
1556 eval(Value::Tensor(integer.clone()), Value::Tensor(integer), &[])
1557 .unwrap()
1558 .mask,
1559 LogicalArray::new(vec![1, 1], vec![2, 1]).unwrap()
1560 );
1561 assert_eq!(
1562 eval(Value::Bool(true), Value::Bool(true), &[])
1563 .unwrap()
1564 .mask_value(),
1565 Value::Bool(true)
1566 );
1567 }
1568 }
1569
1570 #[test]
1571 fn documented_resident_integer_outputs_restore_and_excess_arity_rejects() {
1572 test_support::with_test_provider(|provider| {
1573 let a = Tensor::new_integer(IntegerStorage::I32(vec![1, 3]), vec![2, 1]).unwrap();
1574 let b = Tensor::new_integer(IntegerStorage::I32(vec![1, 2]), vec![2, 1]).unwrap();
1575 let a = Value::GpuTensor(gpu_helpers::upload_tensor(provider, &a).unwrap());
1576 let b = Value::GpuTensor(gpu_helpers::upload_tensor(provider, &b).unwrap());
1577 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1578 let _outputs = crate::output_count::push_output_count(Some(2));
1579 let Value::OutputList(outputs) = builtin(a, b, Vec::new()).unwrap() else {
1580 panic!("expected output list");
1581 };
1582 assert_eq!(outputs.len(), 2);
1583 let Value::GpuTensor(mask) = &outputs[0] else {
1584 panic!("expected resident logical mask");
1585 };
1586 assert!(runmat_accelerate_api::handle_is_logical(mask));
1587 assert!(matches!(outputs[1], Value::GpuTensor(_)));
1588 assert_eq!(
1589 test_support::gather(outputs[0].clone())
1590 .unwrap()
1591 .materialize_f64(),
1592 vec![1.0, 0.0]
1593 );
1594 assert_eq!(
1595 test_support::gather(outputs[1].clone())
1596 .unwrap()
1597 .materialize_f64(),
1598 vec![1.0, 0.0]
1599 );
1600 });
1601
1602 let _outputs = crate::output_count::push_output_count(Some(3));
1603 let error = builtin(Value::Num(1.0), Value::Num(1.0), Vec::new())
1604 .expect_err("excess outputs must reject");
1605 assert_eq!(error.identifier(), ERROR_INVALID_ARGUMENT.identifier);
1606 }
1607
1608 #[test]
1609 fn resident_wide_integer_and_restricted_options_are_mode_gated_before_gather() {
1610 test_support::with_test_provider(|provider| {
1611 let wide = Tensor::new_integer(IntegerStorage::U64(vec![1, 2]), vec![2, 1]).unwrap();
1612 let wide_handle = gpu_helpers::upload_tensor(provider, &wide).unwrap();
1613 {
1614 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1615 let error = eval(
1616 Value::GpuTensor(wide_handle.clone()),
1617 Value::GpuTensor(wide_handle.clone()),
1618 &[],
1619 )
1620 .expect_err("resident uint64 must be gated");
1621 assert_eq!(
1622 error.identifier(),
1623 GPU_WIDE_INTEGER_EXTENSION.error_identifier
1624 );
1625 }
1626 {
1627 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1628 assert_eq!(
1629 eval(
1630 Value::GpuTensor(wide_handle.clone()),
1631 Value::GpuTensor(wide_handle),
1632 &[]
1633 )
1634 .unwrap()
1635 .mask
1636 .data,
1637 vec![1, 1]
1638 );
1639 }
1640
1641 let data = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1642 let data_handle = gpu_helpers::upload_tensor(provider, &data).unwrap();
1643 let options = [
1644 Value::from("ByRows"),
1645 Value::Int(runmat_value::IntValue::I8(1)),
1646 ];
1647 {
1648 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1649 let error = eval(
1650 Value::GpuTensor(data_handle.clone()),
1651 Value::GpuTensor(data_handle.clone()),
1652 &options,
1653 )
1654 .expect_err("resident ByRows must be gated");
1655 assert_eq!(error.identifier(), GPU_OPTIONS_EXTENSION.error_identifier);
1656 }
1657 {
1658 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1659 assert_eq!(
1660 eval(
1661 Value::GpuTensor(data_handle.clone()),
1662 Value::GpuTensor(data_handle.clone()),
1663 &options,
1664 )
1665 .unwrap()
1666 .mask
1667 .data,
1668 vec![1, 1]
1669 );
1670
1671 let _outputs = crate::output_count::push_output_count(Some(2));
1672 let Value::OutputList(outputs) = builtin(
1673 Value::GpuTensor(data_handle.clone()),
1674 Value::GpuTensor(data_handle),
1675 vec![
1676 Value::from("OutputAllIndices"),
1677 Value::Int(runmat_value::IntValue::I8(1)),
1678 ],
1679 )
1680 .unwrap() else {
1681 panic!("expected output list");
1682 };
1683 assert!(matches!(outputs[0], Value::LogicalArray(_)));
1684 assert!(matches!(outputs[1], Value::Cell(_)));
1685 }
1686 });
1687 }
1688
1689 #[test]
1690 fn gpu_inputs_gather_to_host() {
1691 test_support::with_test_provider(|provider| {
1692 let a = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1693 let b = Tensor::new(vec![1.0 + 1e-13, 3.0], vec![2, 1]).unwrap();
1694 let handle_a = provider
1695 .upload(&runmat_accelerate_api::HostTensorView {
1696 data: &a.materialize_f64(),
1697 shape: &a.shape,
1698 })
1699 .expect("upload a");
1700 let handle_b = provider
1701 .upload(&runmat_accelerate_api::HostTensorView {
1702 data: &b.materialize_f64(),
1703 shape: &b.shape,
1704 })
1705 .expect("upload b");
1706 let result = eval(Value::GpuTensor(handle_a), Value::GpuTensor(handle_b), &[]).unwrap();
1707 assert_eq!(result.mask.data, vec![1, 0]);
1708 });
1709 }
1710}