1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6 CellArray, LiteralValue, LogicalArray, NumericDType, ResolveContext, Tensor, Type, Value,
7};
8use runmat_macros::runtime_builtin;
9
10use super::type_resolvers::logical_output_type;
11use crate::builtins::common::{
12 gpu_helpers,
13 spec::{
14 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15 ReductionNaN, ResidencyPolicy, ShapeRequirements,
16 },
17 tensor,
18};
19use crate::{build_runtime_error, BuiltinResult, RuntimeError};
20
21const NAME: &str = "ismembertol";
22const DEFAULT_TOL_DOUBLE: f64 = 1.0e-12;
23const DEFAULT_TOL_SINGLE: f64 = 1.0e-6;
24
25#[runmat_macros::register_gpu_spec(
26 builtin_path = "crate::builtins::array::sorting_sets::ismembertol"
27)]
28pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
29 name: NAME,
30 op_kind: GpuOpKind::Custom("ismembertol"),
31 supported_precisions: &[],
32 broadcast: BroadcastSemantics::None,
33 provider_hooks: &[],
34 constant_strategy: ConstantStrategy::InlineLiteral,
35 residency: ResidencyPolicy::GatherImmediately,
36 nan_mode: ReductionNaN::Include,
37 two_pass_threshold: None,
38 workgroup_size: None,
39 accepts_nan_mode: false,
40 notes: "Tolerance membership is host materialised today; gpuArray inputs are gathered before comparison.",
41};
42
43#[runmat_macros::register_fusion_spec(
44 builtin_path = "crate::builtins::array::sorting_sets::ismembertol"
45)]
46pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
47 name: NAME,
48 shape: ShapeRequirements::Any,
49 constant_strategy: ConstantStrategy::InlineLiteral,
50 elementwise: None,
51 reduction: None,
52 emits_nan: false,
53 notes: "`ismembertol` materialises logical and index outputs and terminates fusion chains.",
54};
55
56const OUTPUT_MASK: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
57 name: "LIA",
58 ty: BuiltinParamType::LogicalArray,
59 arity: BuiltinParamArity::Required,
60 default: None,
61 description: "Logical mask over A.",
62}];
63
64const OUTPUT_MASK_LOC: [BuiltinParamDescriptor; 2] = [
65 BuiltinParamDescriptor {
66 name: "LIA",
67 ty: BuiltinParamType::LogicalArray,
68 arity: BuiltinParamArity::Required,
69 default: None,
70 description: "Logical mask over A.",
71 },
72 BuiltinParamDescriptor {
73 name: "LocB",
74 ty: BuiltinParamType::Any,
75 arity: BuiltinParamArity::Required,
76 default: None,
77 description: "First or all matching indices into B.",
78 },
79];
80
81const INPUTS_AB: [BuiltinParamDescriptor; 2] = [
82 BuiltinParamDescriptor {
83 name: "A",
84 ty: BuiltinParamType::NumericArray,
85 arity: BuiltinParamArity::Required,
86 default: None,
87 description: "Query values or rows.",
88 },
89 BuiltinParamDescriptor {
90 name: "B",
91 ty: BuiltinParamType::NumericArray,
92 arity: BuiltinParamArity::Required,
93 default: None,
94 description: "Reference values or rows.",
95 },
96];
97
98const INPUTS_AB_OPTIONS: [BuiltinParamDescriptor; 4] = [
99 BuiltinParamDescriptor {
100 name: "A",
101 ty: BuiltinParamType::NumericArray,
102 arity: BuiltinParamArity::Required,
103 default: None,
104 description: "Query values or rows.",
105 },
106 BuiltinParamDescriptor {
107 name: "B",
108 ty: BuiltinParamType::NumericArray,
109 arity: BuiltinParamArity::Required,
110 default: None,
111 description: "Reference values or rows.",
112 },
113 BuiltinParamDescriptor {
114 name: "tol",
115 ty: BuiltinParamType::NumericScalar,
116 arity: BuiltinParamArity::Optional,
117 default: Some("1e-12 for double, 1e-6 for single"),
118 description: "Relative tolerance.",
119 },
120 BuiltinParamDescriptor {
121 name: "Name,Value",
122 ty: BuiltinParamType::Any,
123 arity: BuiltinParamArity::Variadic,
124 default: None,
125 description: "Name-value options: ByRows, DataScale, OutputAllIndices.",
126 },
127];
128
129const SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
130 BuiltinSignatureDescriptor {
131 label: "LIA = ismembertol(A, B)",
132 inputs: &INPUTS_AB,
133 outputs: &OUTPUT_MASK,
134 },
135 BuiltinSignatureDescriptor {
136 label: "LIA = ismembertol(A, B, tol, Name, Value)",
137 inputs: &INPUTS_AB_OPTIONS,
138 outputs: &OUTPUT_MASK,
139 },
140 BuiltinSignatureDescriptor {
141 label: "[LIA, LocB] = ismembertol(A, B)",
142 inputs: &INPUTS_AB,
143 outputs: &OUTPUT_MASK_LOC,
144 },
145 BuiltinSignatureDescriptor {
146 label: "[LIA, LocB] = ismembertol(A, B, tol, Name, Value)",
147 inputs: &INPUTS_AB_OPTIONS,
148 outputs: &OUTPUT_MASK_LOC,
149 },
150];
151
152const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
153 code: "RM.ISMEMBERTOL.INVALID_INPUT",
154 identifier: Some("RunMat:ismembertol:InvalidInput"),
155 when: "A or B is not a supported real full numeric input.",
156 message: "ismembertol: inputs must be real full numeric arrays",
157};
158
159const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
160 code: "RM.ISMEMBERTOL.INVALID_ARGUMENT",
161 identifier: Some("RunMat:ismembertol:InvalidArgument"),
162 when: "Tolerance or name-value arguments are malformed.",
163 message: "ismembertol: invalid argument",
164};
165
166const ERROR_ROWS_COLUMN_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
167 code: "RM.ISMEMBERTOL.ROWS_COLUMN_MISMATCH",
168 identifier: Some("RunMat:ismembertol:RowsColumnMismatch"),
169 when: "ByRows is true and A/B column counts differ.",
170 message: "ismembertol: inputs must have the same number of columns when ByRows is true",
171};
172
173const ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
174 code: "RM.ISMEMBERTOL.INTERNAL",
175 identifier: Some("RunMat:ismembertol:Internal"),
176 when: "Internal conversion or allocation fails.",
177 message: "ismembertol: internal error",
178};
179
180const ERRORS: [BuiltinErrorDescriptor; 4] = [
181 ERROR_INVALID_INPUT,
182 ERROR_INVALID_ARGUMENT,
183 ERROR_ROWS_COLUMN_MISMATCH,
184 ERROR_INTERNAL,
185];
186
187pub const ISMEMBERTOL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
188 signatures: &SIGNATURES,
189 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
190 completion_policy: BuiltinCompletionPolicy::Public,
191 errors: &ERRORS,
192};
193
194fn ismembertol_output_type(args: &[Type], ctx: &ResolveContext) -> Type {
195 if literal_by_rows_enabled(ctx) {
196 return match args.first().and_then(type_row_count) {
197 Some(rows) => Type::Logical {
198 shape: Some(vec![rows, Some(1)]),
199 },
200 None => Type::logical(),
201 };
202 }
203 logical_output_type(args, ctx)
204}
205
206fn literal_by_rows_enabled(ctx: &ResolveContext) -> bool {
207 let mut idx = 2usize;
208 if !matches!(ctx.literal_args.get(idx), Some(LiteralValue::String(_))) {
209 idx += 1;
210 }
211 while idx + 1 < ctx.literal_args.len() {
212 if matches!(
213 ctx.literal_args.get(idx),
214 Some(LiteralValue::String(name)) if name.eq_ignore_ascii_case("ByRows")
215 ) {
216 return literal_truthy(ctx.literal_args.get(idx + 1));
217 }
218 idx += 2;
219 }
220 false
221}
222
223fn literal_truthy(value: Option<&LiteralValue>) -> bool {
224 match value {
225 Some(LiteralValue::Bool(flag)) => *flag,
226 Some(LiteralValue::Number(num)) => *num == 1.0,
227 _ => false,
228 }
229}
230
231fn type_row_count(ty: &Type) -> Option<Option<usize>> {
232 match ty {
233 Type::Tensor { shape: Some(shape) } | Type::Logical { shape: Some(shape) } => {
234 if shape.is_empty() {
235 Some(Some(1))
236 } else {
237 Some(shape[0])
238 }
239 }
240 Type::Tensor { shape: None } | Type::Logical { shape: None } => Some(None),
241 Type::Num | Type::Int | Type::Bool => Some(Some(1)),
242 _ => None,
243 }
244}
245
246#[runtime_builtin(
247 name = "ismembertol",
248 category = "array/sorting_sets",
249 summary = "Identify numeric array elements or rows that are within tolerance of another array.",
250 keywords = "ismembertol,membership,tolerance,set,rows,indices",
251 accel = "array_construct",
252 sink = true,
253 type_resolver(ismembertol_output_type),
254 descriptor(crate::builtins::array::sorting_sets::ismembertol::ISMEMBERTOL_DESCRIPTOR),
255 builtin_path = "crate::builtins::array::sorting_sets::ismembertol"
256)]
257async fn ismembertol_builtin(a: Value, b: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
258 if let Some(out_count) = crate::output_count::current_output_count() {
259 if out_count == 0 {
260 return Ok(Value::OutputList(Vec::new()));
261 }
262 let opts = parse_options(&rest)?;
263 if out_count == 1 {
264 let eval = evaluate_with_options(a, b, opts, LocRequest::None).await?;
265 return Ok(Value::OutputList(vec![eval.into_mask_value()]));
266 }
267 let loc_request = if opts.output_all_indices {
268 LocRequest::All
269 } else {
270 LocRequest::First
271 };
272 let eval = evaluate_with_options(a, b, opts, loc_request).await?;
273 let (mask, loc) = eval.into_pair();
274 return Ok(crate::output_count::output_list_with_padding(
275 out_count,
276 vec![mask, loc],
277 ));
278 }
279 let opts = parse_options(&rest)?;
280 let eval = evaluate_with_options(a, b, opts, LocRequest::None).await?;
281 Ok(eval.into_mask_value())
282}
283
284pub async fn evaluate(a: Value, b: Value, rest: &[Value]) -> BuiltinResult<IsMemberTolEvaluation> {
285 let opts = parse_options(rest)?;
286 let loc_request = if opts.output_all_indices {
287 LocRequest::All
288 } else {
289 LocRequest::First
290 };
291 evaluate_with_options(a, b, opts, loc_request).await
292}
293
294async fn evaluate_with_options(
295 a: Value,
296 b: Value,
297 opts: IsMemberTolOptions,
298 loc_request: LocRequest,
299) -> BuiltinResult<IsMemberTolEvaluation> {
300 let a = gather_if_gpu(a).await?;
301 let b = gather_if_gpu(b).await?;
302 let tensor_a =
303 tensor::value_into_tensor_for(NAME, a).map_err(|_| error(&ERROR_INVALID_INPUT))?;
304 let tensor_b =
305 tensor::value_into_tensor_for(NAME, b).map_err(|_| error(&ERROR_INVALID_INPUT))?;
306 evaluate_tensors(tensor_a, tensor_b, opts, loc_request)
307}
308
309async fn gather_if_gpu(value: Value) -> BuiltinResult<Value> {
310 match value {
311 Value::GpuTensor(handle) => gpu_helpers::gather_value_async(&Value::GpuTensor(handle))
312 .await
313 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}"))),
314 other => Ok(other),
315 }
316}
317
318fn evaluate_tensors(
319 a: Tensor,
320 b: Tensor,
321 mut opts: IsMemberTolOptions,
322 loc_request: LocRequest,
323) -> BuiltinResult<IsMemberTolEvaluation> {
324 if opts.tol.is_none() {
325 opts.tol = Some(default_tolerance(a.dtype, b.dtype));
326 }
327 if opts.by_rows {
328 evaluate_rows(a, b, opts, loc_request)
329 } else {
330 evaluate_elements(a, b, opts, loc_request)
331 }
332}
333
334#[derive(Debug, Clone)]
335struct IsMemberTolOptions {
336 tol: Option<f64>,
337 by_rows: bool,
338 data_scale: Option<Vec<f64>>,
339 output_all_indices: bool,
340}
341
342#[derive(Debug, Copy, Clone, Eq, PartialEq)]
343enum LocRequest {
344 None,
345 First,
346 All,
347}
348
349fn parse_options(rest: &[Value]) -> BuiltinResult<IsMemberTolOptions> {
350 let mut opts = IsMemberTolOptions {
351 tol: None,
352 by_rows: false,
353 data_scale: None,
354 output_all_indices: false,
355 };
356
357 let mut idx = 0usize;
358 if let Some(first) = rest.first() {
359 if option_name(first).is_none() {
360 opts.tol = Some(parse_positive_scalar(first, "tolerance")?);
361 idx = 1;
362 }
363 }
364
365 let remaining = &rest[idx..];
366 if !remaining.len().is_multiple_of(2) {
367 return Err(error_with(
368 &ERROR_INVALID_ARGUMENT,
369 "ismembertol: name-value arguments must appear in pairs",
370 ));
371 }
372
373 for pair in remaining.chunks_exact(2) {
374 let name = option_name(&pair[0])
375 .ok_or_else(|| {
376 error_with(&ERROR_INVALID_ARGUMENT, "ismembertol: expected option name")
377 })?
378 .trim()
379 .to_ascii_lowercase();
380 match name.as_str() {
381 "byrows" => opts.by_rows = parse_bool_option(&pair[1])?,
382 "outputallindices" => opts.output_all_indices = parse_bool_option(&pair[1])?,
383 "datascale" => opts.data_scale = Some(parse_data_scale(&pair[1])?),
384 other => {
385 return Err(error_with(
386 &ERROR_INVALID_ARGUMENT,
387 format!("ismembertol: unknown option '{other}'"),
388 ))
389 }
390 }
391 }
392
393 Ok(opts)
394}
395
396fn option_name(value: &Value) -> Option<String> {
397 match value {
398 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
399 tensor::value_to_string(value)
400 }
401 _ => None,
402 }
403}
404
405fn evaluate_elements(
406 a: Tensor,
407 b: Tensor,
408 opts: IsMemberTolOptions,
409 loc_request: LocRequest,
410) -> BuiltinResult<IsMemberTolEvaluation> {
411 let tol = opts.tol.expect("defaulted tolerance");
412 let scale = opts
413 .data_scale
414 .as_ref()
415 .map(|values| {
416 if values.len() == 1 {
417 Ok(values[0])
418 } else {
419 Err(error_with(
420 &ERROR_INVALID_ARGUMENT,
421 "ismembertol: DataScale must be scalar unless ByRows is true",
422 ))
423 }
424 })
425 .transpose()?
426 .unwrap_or_else(|| default_element_scale(&a, &b));
427 let threshold = tol * scale.abs();
428
429 let mut mask_data = Vec::with_capacity(a.data.len());
430 let mut loc_data = match loc_request {
431 LocRequest::First => Vec::with_capacity(a.data.len()),
432 LocRequest::None | LocRequest::All => Vec::new(),
433 };
434 let mut all_cells = match loc_request {
435 LocRequest::All => Vec::with_capacity(a.data.len()),
436 LocRequest::None | LocRequest::First => Vec::new(),
437 };
438
439 for &value in &a.data {
440 match loc_request {
441 LocRequest::None => {
442 mask_data.push(
443 if first_element_match(value, &b.data, threshold).is_some() {
444 1
445 } else {
446 0
447 },
448 );
449 }
450 LocRequest::First => {
451 let first = first_element_match(value, &b.data, threshold);
452 mask_data.push(if first.is_some() { 1 } else { 0 });
453 loc_data.push(first.unwrap_or(0) as f64);
454 }
455 LocRequest::All => {
456 let matches = all_element_matches(value, &b.data, threshold);
457 mask_data.push(if matches.is_empty() { 0 } else { 1 });
458 all_cells.push(indices_cell(matches)?);
459 }
460 }
461 }
462
463 let mask = LogicalArray::new(mask_data, a.shape.clone())
464 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?;
465 let loc = match loc_request {
466 LocRequest::None => None,
467 LocRequest::First => Some(LocOutput::First(
468 Tensor::new(loc_data, a.shape.clone())
469 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?,
470 )),
471 LocRequest::All => Some(LocOutput::All(cell_array_from_linear_matches(
472 all_cells,
473 a.shape.clone(),
474 )?)),
475 };
476 Ok(IsMemberTolEvaluation { mask, loc })
477}
478
479fn evaluate_rows(
480 a: Tensor,
481 b: Tensor,
482 opts: IsMemberTolOptions,
483 loc_request: LocRequest,
484) -> BuiltinResult<IsMemberTolEvaluation> {
485 let (rows_a, cols_a) = rows_cols(&a)?;
486 let (rows_b, cols_b) = rows_cols(&b)?;
487 if cols_a != cols_b {
488 return Err(error(&ERROR_ROWS_COLUMN_MISMATCH));
489 }
490 let tol = opts.tol.expect("defaulted tolerance");
491 let scales = row_scales(&a, rows_a, &b, rows_b, opts.data_scale.as_deref(), cols_a)?;
492 let thresholds: Vec<f64> = scales.iter().map(|scale| tol * scale.abs()).collect();
493
494 let mut mask_data = vec![0u8; rows_a];
495 let mut loc_data = match loc_request {
496 LocRequest::First => vec![0.0f64; rows_a],
497 LocRequest::None | LocRequest::All => Vec::new(),
498 };
499 let mut all_cells = match loc_request {
500 LocRequest::All => Vec::with_capacity(rows_a),
501 LocRequest::None | LocRequest::First => Vec::new(),
502 };
503
504 for row_a in 0..rows_a {
505 match loc_request {
506 LocRequest::None => {
507 mask_data[row_a] = if first_row_match(
508 &a,
509 row_a,
510 rows_a,
511 &b,
512 rows_b,
513 cols_a,
514 &thresholds,
515 )
516 .is_some()
517 {
518 1
519 } else {
520 0
521 };
522 }
523 LocRequest::First => {
524 let first = first_row_match(&a, row_a, rows_a, &b, rows_b, cols_a, &thresholds);
525 mask_data[row_a] = if first.is_some() { 1 } else { 0 };
526 loc_data[row_a] = first.unwrap_or(0) as f64;
527 }
528 LocRequest::All => {
529 let matches = all_row_matches(&a, row_a, rows_a, &b, rows_b, cols_a, &thresholds);
530 mask_data[row_a] = if matches.is_empty() { 0 } else { 1 };
531 all_cells.push(indices_cell(matches)?);
532 }
533 }
534 }
535
536 let shape = vec![rows_a, 1];
537 let mask = LogicalArray::new(mask_data, shape.clone())
538 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?;
539 let loc = match loc_request {
540 LocRequest::None => None,
541 LocRequest::First => Some(LocOutput::First(
542 Tensor::new(loc_data, shape)
543 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))?,
544 )),
545 LocRequest::All => Some(LocOutput::All(cell_array_from_linear_matches(
546 all_cells, shape,
547 )?)),
548 };
549 Ok(IsMemberTolEvaluation { mask, loc })
550}
551
552fn first_element_match(value: f64, candidates: &[f64], threshold: f64) -> Option<usize> {
553 candidates.iter().enumerate().find_map(|(idx, &candidate)| {
554 within_tolerance(value, candidate, threshold).then_some(idx + 1)
555 })
556}
557
558fn all_element_matches(value: f64, candidates: &[f64], threshold: f64) -> Vec<usize> {
559 candidates
560 .iter()
561 .enumerate()
562 .filter_map(|(idx, &candidate)| {
563 within_tolerance(value, candidate, threshold).then_some(idx + 1)
564 })
565 .collect()
566}
567
568fn first_row_match(
569 a: &Tensor,
570 row_a: usize,
571 rows_a: usize,
572 b: &Tensor,
573 rows_b: usize,
574 cols: usize,
575 thresholds: &[f64],
576) -> Option<usize> {
577 (0..rows_b).find_map(|row_b| {
578 rows_match(a, row_a, rows_a, b, row_b, rows_b, cols, thresholds).then_some(row_b + 1)
579 })
580}
581
582fn all_row_matches(
583 a: &Tensor,
584 row_a: usize,
585 rows_a: usize,
586 b: &Tensor,
587 rows_b: usize,
588 cols: usize,
589 thresholds: &[f64],
590) -> Vec<usize> {
591 (0..rows_b)
592 .filter_map(|row_b| {
593 rows_match(a, row_a, rows_a, b, row_b, rows_b, cols, thresholds).then_some(row_b + 1)
594 })
595 .collect()
596}
597
598fn rows_match(
599 a: &Tensor,
600 row_a: usize,
601 rows_a: usize,
602 b: &Tensor,
603 row_b: usize,
604 rows_b: usize,
605 cols: usize,
606 thresholds: &[f64],
607) -> bool {
608 for col in 0..cols {
609 let threshold = thresholds[col];
610 if threshold.is_infinite() {
611 continue;
612 }
613 let lhs = a.data[row_a + col * rows_a];
614 let rhs = b.data[row_b + col * rows_b];
615 if !within_tolerance(lhs, rhs, threshold) {
616 return false;
617 }
618 }
619 true
620}
621
622fn within_tolerance(lhs: f64, rhs: f64, threshold: f64) -> bool {
623 if lhs.is_nan() || rhs.is_nan() {
624 return false;
625 }
626 if lhs == rhs {
627 return true;
628 }
629 (lhs - rhs).abs() <= threshold
630}
631
632fn default_tolerance(a: NumericDType, b: NumericDType) -> f64 {
633 if matches!(a, NumericDType::F32) || matches!(b, NumericDType::F32) {
634 DEFAULT_TOL_SINGLE
635 } else {
636 DEFAULT_TOL_DOUBLE
637 }
638}
639
640fn default_element_scale(a: &Tensor, b: &Tensor) -> f64 {
641 a.data
642 .iter()
643 .chain(b.data.iter())
644 .filter_map(|value| (!value.is_nan()).then_some(value.abs()))
645 .fold(0.0, f64::max)
646}
647
648fn row_scales(
649 a: &Tensor,
650 rows_a: usize,
651 b: &Tensor,
652 rows_b: usize,
653 supplied: Option<&[f64]>,
654 cols: usize,
655) -> BuiltinResult<Vec<f64>> {
656 if let Some(values) = supplied {
657 if values.len() == 1 {
658 return Ok(vec![values[0]; cols]);
659 }
660 if values.len() == cols {
661 return Ok(values.to_vec());
662 }
663 return Err(error_with(
664 &ERROR_INVALID_ARGUMENT,
665 "ismembertol: DataScale vector length must match the number of columns",
666 ));
667 }
668
669 let mut scales = vec![0.0f64; cols];
670 for (col, scale) in scales.iter_mut().enumerate() {
671 for row in 0..rows_a {
672 let value = a.data[row + col * rows_a];
673 if !value.is_nan() {
674 *scale = (*scale).max(value.abs());
675 }
676 }
677 for row in 0..rows_b {
678 let value = b.data[row + col * rows_b];
679 if !value.is_nan() {
680 *scale = (*scale).max(value.abs());
681 }
682 }
683 }
684 Ok(scales)
685}
686
687fn rows_cols(tensor: &Tensor) -> BuiltinResult<(usize, usize)> {
688 match tensor.shape.len() {
689 0 => Ok((1, 1)),
690 1 => Ok((tensor.shape[0], 1)),
691 2 => Ok((tensor.shape[0], tensor.shape[1])),
692 _ => Err(error_with(
693 &ERROR_INVALID_ARGUMENT,
694 "ismembertol: ByRows requires 2-D arrays",
695 )),
696 }
697}
698
699fn indices_cell(indices: Vec<usize>) -> BuiltinResult<Value> {
700 if indices.is_empty() {
701 return Ok(Value::Tensor(Tensor::new(vec![0.0], vec![1, 1]).map_err(
702 |e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")),
703 )?));
704 }
705 let data = indices
706 .into_iter()
707 .map(|idx| idx as f64)
708 .collect::<Vec<_>>();
709 let len = data.len();
710 Tensor::new(data, vec![len, 1])
711 .map(Value::Tensor)
712 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))
713}
714
715fn cell_array_from_linear_matches(
716 cells_column_major: Vec<Value>,
717 shape: Vec<usize>,
718) -> BuiltinResult<CellArray> {
719 let cells = if shape.len() == 2 && shape[0] > 1 && shape[1] > 1 {
720 let rows = shape[0];
721 let cols = shape[1];
722 let mut row_major = Vec::with_capacity(cells_column_major.len());
723 for row in 0..rows {
724 for col in 0..cols {
725 row_major.push(cells_column_major[row + col * rows].clone());
726 }
727 }
728 row_major
729 } else {
730 cells_column_major
731 };
732 CellArray::new_with_shape(cells, shape)
733 .map_err(|e| error_with(&ERROR_INTERNAL, format!("{NAME}: {e}")))
734}
735
736fn parse_positive_scalar(value: &Value, label: &str) -> BuiltinResult<f64> {
737 let scalar = numeric_scalar(value).ok_or_else(|| {
738 error_with(
739 &ERROR_INVALID_ARGUMENT,
740 format!("ismembertol: {label} must be a positive real scalar"),
741 )
742 })?;
743 if !scalar.is_finite() || scalar <= 0.0 {
744 return Err(error_with(
745 &ERROR_INVALID_ARGUMENT,
746 format!("ismembertol: {label} must be a positive real scalar"),
747 ));
748 }
749 Ok(scalar)
750}
751
752fn parse_bool_option(value: &Value) -> BuiltinResult<bool> {
753 match numeric_scalar(value) {
754 Some(0.0) => Ok(false),
755 Some(1.0) => Ok(true),
756 _ => Err(error_with(
757 &ERROR_INVALID_ARGUMENT,
758 "ismembertol: boolean options must be true/false or 0/1",
759 )),
760 }
761}
762
763fn parse_data_scale(value: &Value) -> BuiltinResult<Vec<f64>> {
764 match value {
765 Value::Num(n) => validate_data_scale(vec![*n]),
766 Value::Int(i) => validate_data_scale(vec![i.to_i64() as f64]),
767 Value::Bool(flag) => validate_data_scale(vec![if *flag { 1.0 } else { 0.0 }]),
768 Value::Tensor(tensor) => validate_data_scale(tensor.data.clone()),
769 Value::LogicalArray(logical) => validate_data_scale(
770 logical
771 .data
772 .iter()
773 .map(|&v| if v != 0 { 1.0 } else { 0.0 })
774 .collect(),
775 ),
776 _ => Err(error_with(
777 &ERROR_INVALID_ARGUMENT,
778 "ismembertol: DataScale must be a numeric scalar or vector",
779 )),
780 }
781}
782
783fn validate_data_scale(values: Vec<f64>) -> BuiltinResult<Vec<f64>> {
784 if values.is_empty() || values.iter().any(|value| value.is_nan() || *value < 0.0) {
785 return Err(error_with(
786 &ERROR_INVALID_ARGUMENT,
787 "ismembertol: DataScale values must be non-negative or Inf",
788 ));
789 }
790 Ok(values)
791}
792
793fn numeric_scalar(value: &Value) -> Option<f64> {
794 match value {
795 Value::Num(n) => Some(*n),
796 Value::Int(i) => Some(i.to_i64() as f64),
797 Value::Bool(flag) => Some(if *flag { 1.0 } else { 0.0 }),
798 Value::Tensor(t) if t.data.len() == 1 => Some(t.data[0]),
799 Value::LogicalArray(logical) if logical.data.len() == 1 => {
800 Some(if logical.data[0] != 0 { 1.0 } else { 0.0 })
801 }
802 _ => None,
803 }
804}
805
806#[derive(Debug, Clone)]
807pub struct IsMemberTolEvaluation {
808 mask: LogicalArray,
809 loc: Option<LocOutput>,
810}
811
812#[derive(Debug, Clone)]
813enum LocOutput {
814 First(Tensor),
815 All(CellArray),
816}
817
818impl IsMemberTolEvaluation {
819 pub fn into_mask_value(self) -> Value {
820 logical_array_into_value(self.mask)
821 }
822
823 pub fn mask_value(&self) -> Value {
824 logical_array_into_value(self.mask.clone())
825 }
826
827 pub fn into_pair(self) -> (Value, Value) {
828 let mask = logical_array_into_value(self.mask);
829 let loc = match self.loc.expect("LocB was not requested") {
830 LocOutput::First(tensor) => tensor::tensor_into_value(tensor),
831 LocOutput::All(cell) => Value::Cell(cell),
832 };
833 (mask, loc)
834 }
835}
836
837fn logical_array_into_value(logical: LogicalArray) -> Value {
838 if logical.data.len() == 1 {
839 Value::Bool(logical.data[0] != 0)
840 } else {
841 Value::LogicalArray(logical)
842 }
843}
844
845fn error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
846 error_with(error, error.message)
847}
848
849fn error_with(error: &'static BuiltinErrorDescriptor, message: impl Into<String>) -> RuntimeError {
850 let mut builder = build_runtime_error(message).with_builtin(NAME);
851 if let Some(identifier) = error.identifier {
852 builder = builder.with_identifier(identifier);
853 }
854 builder.build()
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use crate::builtins::common::test_support;
861 use futures::executor::block_on;
862 use runmat_builtins::{LiteralValue, ResolveContext, Type};
863
864 fn eval(a: Value, b: Value, rest: &[Value]) -> BuiltinResult<IsMemberTolEvaluation> {
865 block_on(evaluate(a, b, rest))
866 }
867
868 #[test]
869 fn type_resolver_returns_logical_shape() {
870 assert_eq!(
871 ismembertol_output_type(
872 &[Type::Tensor {
873 shape: Some(vec![Some(1), Some(3)])
874 }],
875 &ResolveContext::new(Vec::new()),
876 ),
877 Type::Logical {
878 shape: Some(vec![Some(1), Some(3)])
879 }
880 );
881 }
882
883 #[test]
884 fn type_resolver_returns_row_shape_for_literal_by_rows() {
885 assert_eq!(
886 ismembertol_output_type(
887 &[Type::Tensor {
888 shape: Some(vec![Some(4), Some(3)])
889 }],
890 &ResolveContext::new(vec![
891 LiteralValue::Unknown,
892 LiteralValue::Unknown,
893 LiteralValue::Number(0.01),
894 LiteralValue::String("ByRows".to_string()),
895 LiteralValue::Bool(true),
896 ]),
897 ),
898 Type::Logical {
899 shape: Some(vec![Some(4), Some(1)])
900 }
901 );
902 }
903
904 #[test]
905 fn default_tolerance_uses_scaled_double_data() {
906 let a = Tensor::new(vec![0.1, 1.0e10], vec![1, 2]).unwrap();
907 let b = Tensor::new(vec![0.1 + 1.0e-14, 1.0e10 + 5.0e-3], vec![1, 2]).unwrap();
908 let result = eval(Value::Tensor(a), Value::Tensor(b), &[]).unwrap();
909 assert_eq!(result.mask.data, vec![1, 1]);
910 }
911
912 #[test]
913 fn data_scale_one_uses_absolute_tolerance() {
914 let a = Tensor::new(vec![1.0e10], vec![1, 1]).unwrap();
915 let b = Tensor::new(vec![1.0e10 + 5.0e-3], vec![1, 1]).unwrap();
916 let result = eval(
917 Value::Tensor(a),
918 Value::Tensor(b),
919 &[
920 Value::Num(1.0e-6),
921 Value::from("DataScale"),
922 Value::Num(1.0),
923 ],
924 )
925 .unwrap();
926 assert_eq!(result.mask_value(), Value::Bool(false));
927 }
928
929 #[test]
930 fn loc_returns_first_matching_index() {
931 let a = Tensor::new(vec![1.0, 2.0, 4.0], vec![1, 3]).unwrap();
932 let b = Tensor::new(vec![2.01, 2.02, 4.2], vec![1, 3]).unwrap();
933 let (_, loc) = eval(Value::Tensor(a), Value::Tensor(b), &[Value::Num(0.05)])
934 .unwrap()
935 .into_pair();
936 match loc {
937 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![0.0, 1.0, 3.0]),
938 other => panic!("expected tensor loc, got {other:?}"),
939 }
940 }
941
942 #[test]
943 fn mask_only_request_does_not_materialize_locations() {
944 let a = Tensor::new(vec![2.0, 9.0], vec![1, 2]).unwrap();
945 let b = Tensor::new(vec![1.99, 2.01, 9.5], vec![1, 3]).unwrap();
946 let opts = parse_options(&[
947 Value::Num(0.01),
948 Value::from("OutputAllIndices"),
949 Value::Bool(true),
950 ])
951 .unwrap();
952 let result = block_on(evaluate_with_options(
953 Value::Tensor(a),
954 Value::Tensor(b),
955 opts,
956 LocRequest::None,
957 ))
958 .unwrap();
959 assert_eq!(result.mask.data, vec![1, 0]);
960 assert!(result.loc.is_none());
961 }
962
963 #[test]
964 fn output_all_indices_returns_cell_array() {
965 let a = Tensor::new(vec![2.0, 9.0], vec![1, 2]).unwrap();
966 let b = Tensor::new(vec![1.99, 2.01, 9.5], vec![1, 3]).unwrap();
967 let (_, loc) = eval(
968 Value::Tensor(a),
969 Value::Tensor(b),
970 &[
971 Value::Num(0.01),
972 Value::from("OutputAllIndices"),
973 Value::Bool(true),
974 ],
975 )
976 .unwrap()
977 .into_pair();
978 let Value::Cell(cell) = loc else {
979 panic!("expected cell loc");
980 };
981 assert_eq!(cell.shape, vec![1, 2]);
982 match &cell.data[0] {
983 Value::Tensor(indices) => assert_eq!(indices.data, vec![1.0, 2.0]),
984 other => panic!("expected tensor indices, got {other:?}"),
985 }
986 match &cell.data[1] {
987 Value::Tensor(indices) => assert_eq!(indices.data, vec![0.0]),
988 other => panic!("expected tensor zero indices, got {other:?}"),
989 }
990 }
991
992 #[test]
993 fn output_all_indices_matrix_cells_follow_public_cell_indexing() {
994 let a = Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap();
995 let b = Tensor::new(vec![10.0, 2.0, 3.0, 20.0], vec![1, 4]).unwrap();
996 let (_, loc) = eval(
997 Value::Tensor(a),
998 Value::Tensor(b),
999 &[
1000 Value::Num(1.0e-12),
1001 Value::from("OutputAllIndices"),
1002 Value::Bool(true),
1003 ],
1004 )
1005 .unwrap()
1006 .into_pair();
1007 let Value::Cell(cell) = loc else {
1008 panic!("expected cell loc");
1009 };
1010 assert_eq!(cell.shape, vec![2, 2]);
1011 let top_right = cell.get(0, 1).unwrap();
1012 match top_right {
1013 Value::Tensor(indices) => assert_eq!(indices.data, vec![2.0]),
1014 other => panic!("expected tensor indices, got {other:?}"),
1015 }
1016 let bottom_left = cell.get(1, 0).unwrap();
1017 match bottom_left {
1018 Value::Tensor(indices) => assert_eq!(indices.data, vec![3.0]),
1019 other => panic!("expected tensor indices, got {other:?}"),
1020 }
1021 }
1022
1023 #[test]
1024 fn by_rows_uses_column_scales_and_inf_ignores_column() {
1025 let a = Tensor::new(vec![0.0, 10.0, 0.5, 20.0], vec![2, 2]).unwrap();
1026 let b = Tensor::new(vec![0.05, 10.2, 999.0, -999.0], vec![2, 2]).unwrap();
1027 let result = eval(
1028 Value::Tensor(a),
1029 Value::Tensor(b),
1030 &[
1031 Value::Num(0.1),
1032 Value::from("ByRows"),
1033 Value::Bool(true),
1034 Value::from("DataScale"),
1035 Value::Tensor(Tensor::new(vec![1.0, f64::INFINITY], vec![1, 2]).unwrap()),
1036 ],
1037 )
1038 .unwrap();
1039 assert_eq!(result.mask.data, vec![1, 0]);
1040 let (_, loc) = result.into_pair();
1041 match loc {
1042 Value::Tensor(tensor) => {
1043 assert_eq!(tensor.shape, vec![2, 1]);
1044 assert_eq!(tensor.data, vec![1.0, 0.0]);
1045 }
1046 other => panic!("expected tensor loc, got {other:?}"),
1047 }
1048 }
1049
1050 #[test]
1051 fn by_rows_output_all_indices_returns_row_cells() {
1052 let a = Tensor::new(vec![0.0, 1.0], vec![1, 2]).unwrap();
1053 let b = Tensor::new(vec![0.01, -0.01, 5.0, 1.0, 1.0, 5.0], vec![3, 2]).unwrap();
1054 let (_, loc) = eval(
1055 Value::Tensor(a),
1056 Value::Tensor(b),
1057 &[
1058 Value::Num(0.02),
1059 Value::from("ByRows"),
1060 Value::Bool(true),
1061 Value::from("OutputAllIndices"),
1062 Value::Bool(true),
1063 ],
1064 )
1065 .unwrap()
1066 .into_pair();
1067 let Value::Cell(cell) = loc else {
1068 panic!("expected cell loc");
1069 };
1070 assert_eq!(cell.shape, vec![1, 1]);
1071 match &cell.data[0] {
1072 Value::Tensor(indices) => assert_eq!(indices.data, vec![1.0, 2.0]),
1073 other => panic!("expected tensor indices, got {other:?}"),
1074 }
1075 }
1076
1077 #[test]
1078 fn rejects_bad_options_and_shape_mismatch() {
1079 let err = parse_options(&[Value::Num(-1.0)]).unwrap_err();
1080 assert_eq!(
1081 err.identifier.as_deref(),
1082 Some("RunMat:ismembertol:InvalidArgument")
1083 );
1084
1085 let a = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
1086 let b = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1087 let err = eval(
1088 Value::Tensor(a),
1089 Value::Tensor(b),
1090 &[Value::from("ByRows"), Value::Bool(true)],
1091 )
1092 .unwrap_err();
1093 assert_eq!(
1094 err.identifier.as_deref(),
1095 Some("RunMat:ismembertol:RowsColumnMismatch")
1096 );
1097 }
1098
1099 #[test]
1100 fn gpu_inputs_gather_to_host() {
1101 test_support::with_test_provider(|provider| {
1102 let a = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1103 let b = Tensor::new(vec![1.0 + 1e-13, 3.0], vec![2, 1]).unwrap();
1104 let handle_a = provider
1105 .upload(&runmat_accelerate_api::HostTensorView {
1106 data: &a.data,
1107 shape: &a.shape,
1108 })
1109 .expect("upload a");
1110 let handle_b = provider
1111 .upload(&runmat_accelerate_api::HostTensorView {
1112 data: &b.data,
1113 shape: &b.shape,
1114 })
1115 .expect("upload b");
1116 let result = eval(Value::GpuTensor(handle_a), Value::GpuTensor(handle_b), &[]).unwrap();
1117 assert_eq!(result.mask.data, vec![1, 0]);
1118 });
1119 }
1120}