1use std::convert::TryFrom;
4use std::mem::size_of;
5
6use nalgebra::{linalg::SVD, DMatrix};
7use num_complex::Complex64;
8use runmat_accelerate_api::{GpuTensorHandle, HostTensorView};
9use runmat_builtins::{
10 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
11 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
12 ComplexTensor, Tensor, Value,
13};
14use runmat_macros::runtime_builtin;
15
16use crate::builtins::common::linalg::{
17 matrix_dimensions_for, parse_tolerance_arg, svd_default_tolerance,
18};
19use crate::builtins::common::random_args::complex_tensor_into_value;
20use crate::builtins::common::spec::{
21 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
22 ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
23};
24use crate::builtins::common::{gpu_helpers, tensor};
25use crate::builtins::math::linalg::solve::rref::{
26 default_complex_tolerance, default_real_tolerance, rref_complex_impl, rref_real_impl,
27};
28use crate::builtins::math::linalg::type_resolvers::null_type;
29use crate::{build_runtime_error, BuiltinResult, RuntimeError};
30
31const NAME: &str = "null";
32
33const NULL_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
34 name: "Z",
35 ty: BuiltinParamType::NumericArray,
36 arity: BuiltinParamArity::Required,
37 default: None,
38 description: "Basis for the null space of A.",
39}];
40
41const NULL_INPUTS_A: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
42 name: "A",
43 ty: BuiltinParamType::Any,
44 arity: BuiltinParamArity::Required,
45 default: None,
46 description: "Input matrix.",
47}];
48
49const NULL_INPUTS_A_TOL: [BuiltinParamDescriptor; 2] = [
50 BuiltinParamDescriptor {
51 name: "A",
52 ty: BuiltinParamType::Any,
53 arity: BuiltinParamArity::Required,
54 default: None,
55 description: "Input matrix.",
56 },
57 BuiltinParamDescriptor {
58 name: "tol",
59 ty: BuiltinParamType::NumericScalar,
60 arity: BuiltinParamArity::Optional,
61 default: None,
62 description: "Rank tolerance.",
63 },
64];
65
66const NULL_INPUTS_A_OPTION: [BuiltinParamDescriptor; 2] = [
67 BuiltinParamDescriptor {
68 name: "A",
69 ty: BuiltinParamType::Any,
70 arity: BuiltinParamArity::Required,
71 default: None,
72 description: "Input matrix.",
73 },
74 BuiltinParamDescriptor {
75 name: "option",
76 ty: BuiltinParamType::StringScalar,
77 arity: BuiltinParamArity::Optional,
78 default: None,
79 description: "`\"r\"` requests a row-reduction basis.",
80 },
81];
82
83const NULL_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
84 BuiltinSignatureDescriptor {
85 label: "Z = null(A)",
86 inputs: &NULL_INPUTS_A,
87 outputs: &NULL_OUTPUT,
88 },
89 BuiltinSignatureDescriptor {
90 label: "Z = null(A, tol)",
91 inputs: &NULL_INPUTS_A_TOL,
92 outputs: &NULL_OUTPUT,
93 },
94 BuiltinSignatureDescriptor {
95 label: "Z = null(A, \"r\")",
96 inputs: &NULL_INPUTS_A_OPTION,
97 outputs: &NULL_OUTPUT,
98 },
99];
100
101const NULL_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
102 code: "RM.NULL.INVALID_ARGUMENT",
103 identifier: Some("RunMat:null:InvalidArgument"),
104 when: "Optional tolerance or basis option is malformed or outside accepted bounds.",
105 message: "null: invalid argument",
106};
107
108const NULL_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
109 code: "RM.NULL.INVALID_INPUT",
110 identifier: Some("RunMat:null:InvalidInput"),
111 when: "Input shape/type cannot be processed for null-space evaluation.",
112 message: "null: invalid input",
113};
114
115const NULL_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
116 code: "RM.NULL.INTERNAL",
117 identifier: Some("RunMat:null:Internal"),
118 when: "Runtime fails while computing the null space or executing fallback/upload paths.",
119 message: "null: internal runtime failure",
120};
121
122const NULL_ERRORS: [BuiltinErrorDescriptor; 3] = [
123 NULL_ERROR_INVALID_ARGUMENT,
124 NULL_ERROR_INVALID_INPUT,
125 NULL_ERROR_INTERNAL,
126];
127
128pub const NULL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
129 signatures: &NULL_SIGNATURES,
130 output_mode: BuiltinOutputMode::Fixed,
131 completion_policy: BuiltinCompletionPolicy::Public,
132 errors: &NULL_ERRORS,
133};
134
135#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::linalg::solve::null")]
136pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
137 name: NAME,
138 op_kind: GpuOpKind::Custom("null-space"),
139 supported_precisions: &[ScalarType::F32, ScalarType::F64],
140 broadcast: BroadcastSemantics::None,
141 provider_hooks: &[],
142 constant_strategy: ConstantStrategy::InlineLiteral,
143 residency: ResidencyPolicy::NewHandle,
144 nan_mode: ReductionNaN::Include,
145 two_pass_threshold: None,
146 workgroup_size: None,
147 accepts_nan_mode: false,
148 notes: "`null` gathers GPU inputs to the host null-space implementation and re-uploads real bases when a provider is available.",
149};
150
151fn null_error_with_message(
152 message: impl Into<String>,
153 error: &'static BuiltinErrorDescriptor,
154) -> RuntimeError {
155 let mut builder = build_runtime_error(message).with_builtin(NAME);
156 if let Some(identifier) = error.identifier {
157 builder = builder.with_identifier(identifier);
158 }
159 builder.build()
160}
161
162fn input_error(message: impl Into<String>) -> RuntimeError {
163 null_error_with_message(message, &NULL_ERROR_INVALID_INPUT)
164}
165
166fn argument_error(message: impl Into<String>) -> RuntimeError {
167 null_error_with_message(message, &NULL_ERROR_INVALID_ARGUMENT)
168}
169
170fn internal_error(message: impl Into<String>) -> RuntimeError {
171 null_error_with_message(message, &NULL_ERROR_INTERNAL)
172}
173
174fn map_control_flow(err: RuntimeError) -> RuntimeError {
175 if err.message() == "interaction pending..." {
176 return build_runtime_error("interaction pending...")
177 .with_builtin(NAME)
178 .build();
179 }
180 let mut builder = build_runtime_error(err.message()).with_builtin(NAME);
181 if let Some(identifier) = err.identifier() {
182 builder = builder.with_identifier(identifier.to_string());
183 }
184 if let Some(task_id) = err.context.task_id.clone() {
185 builder = builder.with_task_id(task_id);
186 }
187 if !err.context.call_stack.is_empty() {
188 builder = builder.with_call_stack(err.context.call_stack.clone());
189 }
190 if let Some(phase) = err.context.phase.clone() {
191 builder = builder.with_phase(phase);
192 }
193 builder.with_source(err).build()
194}
195
196#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::linalg::solve::null")]
197pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
198 name: NAME,
199 shape: ShapeRequirements::Any,
200 constant_strategy: ConstantStrategy::InlineLiteral,
201 elementwise: None,
202 reduction: None,
203 emits_nan: true,
204 notes: "`null` is an eager null-space solve and terminates fusion plans.",
205};
206
207#[derive(Clone, Copy, Debug)]
208enum NullMode {
209 Orthonormal { tolerance: Option<f64> },
210 RowReduction,
211}
212
213#[runtime_builtin(
214 name = "null",
215 category = "math/linalg/solve",
216 summary = "Compute a basis for a matrix null space.",
217 keywords = "null,null space,kernel,svd,rref,rational,gpu",
218 accel = "sink",
219 type_resolver(null_type),
220 descriptor(crate::builtins::math::linalg::solve::null::NULL_DESCRIPTOR),
221 builtin_path = "crate::builtins::math::linalg::solve::null"
222)]
223async fn null_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
224 let mode = parse_null_mode(&rest).map_err(argument_error)?;
225 match value {
226 Value::GpuTensor(handle) => null_gpu(handle, mode).await,
227 Value::ComplexTensor(tensor) => null_complex_value(tensor, mode),
228 Value::Complex(re, im) => {
229 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1]).map_err(input_error)?;
230 null_complex_value(tensor, mode)
231 }
232 other => {
233 let tensor = tensor::value_into_tensor_for(NAME, other).map_err(input_error)?;
234 null_real_value(tensor, mode)
235 }
236 }
237}
238
239fn parse_null_mode(args: &[Value]) -> Result<NullMode, String> {
240 match args.len() {
241 0 => Ok(NullMode::Orthonormal { tolerance: None }),
242 1 => {
243 if let Some(option) = string_option_from_value(&args[0]) {
244 let option = option?;
245 let normalized = option.trim().to_ascii_lowercase();
246 return match normalized.as_str() {
247 "r" | "rational" => Ok(NullMode::RowReduction),
248 _ => Err(format!("{NAME}: unsupported basis option '{option}'")),
249 };
250 }
251 let tolerance = parse_tolerance_arg(NAME, args)?;
252 Ok(NullMode::Orthonormal { tolerance })
253 }
254 _ => Err(format!("{NAME}: too many input arguments")),
255 }
256}
257
258fn string_option_from_value(value: &Value) -> Option<Result<String, String>> {
259 match value {
260 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
261 Some(String::try_from(value))
262 }
263 _ => None,
264 }
265}
266
267async fn null_gpu(handle: GpuTensorHandle, mode: NullMode) -> BuiltinResult<Value> {
268 let gathered = gpu_helpers::gather_tensor_async(&handle)
269 .await
270 .map_err(map_control_flow)?;
271 let basis = null_real_tensor(&gathered, mode)?;
272
273 if let Some(provider) = runmat_accelerate_api::provider() {
274 if let Ok(uploaded) = provider.upload(&HostTensorView {
275 data: &basis.data,
276 shape: &basis.shape,
277 }) {
278 return Ok(Value::GpuTensor(uploaded));
279 }
280 }
281
282 Ok(tensor::tensor_into_value(basis))
283}
284
285fn null_real_value(matrix: Tensor, mode: NullMode) -> BuiltinResult<Value> {
286 let basis = null_real_tensor(&matrix, mode)?;
287 Ok(tensor::tensor_into_value(basis))
288}
289
290fn null_complex_value(matrix: ComplexTensor, mode: NullMode) -> BuiltinResult<Value> {
291 let basis = null_complex_tensor(&matrix, mode)?;
292 Ok(complex_tensor_into_value(basis))
293}
294
295fn null_real_tensor(matrix: &Tensor, mode: NullMode) -> BuiltinResult<Tensor> {
296 match mode {
297 NullMode::Orthonormal { tolerance } => null_real_orthonormal_tensor(matrix, tolerance),
298 NullMode::RowReduction => null_real_row_reduction_tensor(matrix),
299 }
300}
301
302fn null_complex_tensor(matrix: &ComplexTensor, mode: NullMode) -> BuiltinResult<ComplexTensor> {
303 match mode {
304 NullMode::Orthonormal { tolerance } => null_complex_orthonormal_tensor(matrix, tolerance),
305 NullMode::RowReduction => null_complex_row_reduction_tensor(matrix),
306 }
307}
308
309fn null_real_orthonormal_tensor(matrix: &Tensor, tol: Option<f64>) -> BuiltinResult<Tensor> {
310 let (rows, cols) = matrix_dimensions_for(NAME, matrix.shape.as_slice()).map_err(input_error)?;
311 if cols == 0 {
312 return Tensor::new(Vec::new(), vec![0, 0])
313 .map_err(|e| internal_error(format!("{NAME}: {e}")));
314 }
315 if rows == 0 {
316 return Tensor::new(identity_basis_real(cols)?, vec![cols, cols])
317 .map_err(|e| internal_error(format!("{NAME}: {e}")));
318 }
319
320 let (basis, basis_cols) = real_orthonormal_basis_from_svd(&matrix.data, rows, cols, tol)?;
321 Tensor::new(basis, vec![cols, basis_cols]).map_err(|e| internal_error(format!("{NAME}: {e}")))
322}
323
324fn null_complex_orthonormal_tensor(
325 matrix: &ComplexTensor,
326 tol: Option<f64>,
327) -> BuiltinResult<ComplexTensor> {
328 let (rows, cols) = matrix_dimensions_for(NAME, matrix.shape.as_slice()).map_err(input_error)?;
329 if cols == 0 {
330 return ComplexTensor::new(Vec::new(), vec![0, 0])
331 .map_err(|e| internal_error(format!("{NAME}: {e}")));
332 }
333 if rows == 0 {
334 let data = identity_basis_complex(cols)?
335 .into_iter()
336 .map(|value| (value.re, value.im))
337 .collect();
338 return ComplexTensor::new(data, vec![cols, cols])
339 .map_err(|e| internal_error(format!("{NAME}: {e}")));
340 }
341
342 let data: Vec<Complex64> = matrix
343 .data
344 .iter()
345 .map(|&(re, im)| Complex64::new(re, im))
346 .collect();
347 let (basis, basis_cols) = complex_orthonormal_basis_from_svd(&data, rows, cols, tol)?;
348 let data: Vec<(f64, f64)> = basis
349 .into_iter()
350 .map(|value| (value.re, value.im))
351 .collect();
352 ComplexTensor::new(data, vec![cols, basis_cols])
353 .map_err(|e| internal_error(format!("{NAME}: {e}")))
354}
355
356fn null_real_row_reduction_tensor(matrix: &Tensor) -> BuiltinResult<Tensor> {
357 let (rows, cols) = matrix_dimensions_for(NAME, matrix.shape.as_slice()).map_err(input_error)?;
358 let tolerance = default_real_tolerance(&matrix.data, rows, cols);
359 let (basis, basis_cols) = real_row_reduction_basis(matrix.data.clone(), rows, cols, tolerance)?;
360 Tensor::new(basis, vec![cols, basis_cols]).map_err(|e| internal_error(format!("{NAME}: {e}")))
361}
362
363fn null_complex_row_reduction_tensor(matrix: &ComplexTensor) -> BuiltinResult<ComplexTensor> {
364 let (rows, cols) = matrix_dimensions_for(NAME, matrix.shape.as_slice()).map_err(input_error)?;
365 let tolerance = default_complex_tolerance(&matrix.data, rows, cols);
366 let data: Vec<Complex64> = matrix
367 .data
368 .iter()
369 .map(|&(re, im)| Complex64::new(re, im))
370 .collect();
371 let (basis, basis_cols) = complex_row_reduction_basis(data, rows, cols, tolerance)?;
372 let data: Vec<(f64, f64)> = basis
373 .into_iter()
374 .map(|value| (value.re, value.im))
375 .collect();
376 ComplexTensor::new(data, vec![cols, basis_cols])
377 .map_err(|e| internal_error(format!("{NAME}: {e}")))
378}
379
380fn real_orthonormal_basis_from_svd(
381 data: &[f64],
382 rows: usize,
383 cols: usize,
384 tol: Option<f64>,
385) -> BuiltinResult<(Vec<f64>, usize)> {
386 let matrix = DMatrix::from_column_slice(rows, cols, data);
387 let svd = SVD::new(matrix, false, true);
388 let tolerance =
389 tol.unwrap_or_else(|| svd_default_tolerance(svd.singular_values.as_slice(), rows, cols));
390 let v_t = svd.v_t.ok_or_else(|| {
391 internal_error(format!("{NAME}: failed to compute right singular vectors"))
392 })?;
393 let v = v_t.adjoint();
394 let mut row_space = Vec::new();
395 let mut basis = Vec::new();
396 for idx in 0..svd.singular_values.len() {
397 let vector: Vec<f64> = (0..cols).map(|row| v[(row, idx)]).collect();
398 if is_rank_singular_value(svd.singular_values[idx], tolerance) {
399 row_space.push(vector);
400 } else {
401 push_orthonormal_real(&mut basis, &row_space, vector, cols);
402 }
403 }
404 let target_nullity = cols.saturating_sub(row_space.len());
405 complete_real_null_basis(&mut basis, &row_space, cols, target_nullity);
406 let basis_cols = basis.len();
407 Ok((flatten_real_columns(basis, cols)?, basis_cols))
408}
409
410fn complex_orthonormal_basis_from_svd(
411 data: &[Complex64],
412 rows: usize,
413 cols: usize,
414 tol: Option<f64>,
415) -> BuiltinResult<(Vec<Complex64>, usize)> {
416 let matrix = DMatrix::from_column_slice(rows, cols, data);
417 let svd = SVD::new(matrix, false, true);
418 let tolerance =
419 tol.unwrap_or_else(|| svd_default_tolerance(svd.singular_values.as_slice(), rows, cols));
420 let v_t = svd.v_t.ok_or_else(|| {
421 internal_error(format!("{NAME}: failed to compute right singular vectors"))
422 })?;
423 let v = v_t.adjoint();
424 let mut row_space = Vec::new();
425 let mut basis = Vec::new();
426 for idx in 0..svd.singular_values.len() {
427 let vector: Vec<Complex64> = (0..cols).map(|row| v[(row, idx)]).collect();
428 if is_rank_singular_value(svd.singular_values[idx], tolerance) {
429 row_space.push(vector);
430 } else {
431 push_orthonormal_complex(&mut basis, &row_space, vector, cols);
432 }
433 }
434 let target_nullity = cols.saturating_sub(row_space.len());
435 complete_complex_null_basis(&mut basis, &row_space, cols, target_nullity);
436 let basis_cols = basis.len();
437 Ok((flatten_complex_columns(basis, cols)?, basis_cols))
438}
439
440fn complete_real_null_basis(
441 basis: &mut Vec<Vec<f64>>,
442 row_space: &[Vec<f64>],
443 dim: usize,
444 target_cols: usize,
445) {
446 for idx in 0..dim {
447 if basis.len() >= target_cols {
448 break;
449 }
450 let mut vector = vec![0.0; dim];
451 vector[idx] = 1.0;
452 push_orthonormal_real(basis, row_space, vector, dim);
453 }
454}
455
456fn complete_complex_null_basis(
457 basis: &mut Vec<Vec<Complex64>>,
458 row_space: &[Vec<Complex64>],
459 dim: usize,
460 target_cols: usize,
461) {
462 for idx in 0..dim {
463 if basis.len() >= target_cols {
464 break;
465 }
466 let mut vector = vec![Complex64::new(0.0, 0.0); dim];
467 vector[idx] = Complex64::new(1.0, 0.0);
468 push_orthonormal_complex(basis, row_space, vector, dim);
469 }
470}
471
472fn push_orthonormal_real(
473 basis: &mut Vec<Vec<f64>>,
474 row_space: &[Vec<f64>],
475 vector: Vec<f64>,
476 dim: usize,
477) {
478 let mut vector = vector;
479 orthogonalize_real(&mut vector, row_space);
480 orthogonalize_real(&mut vector, basis);
481 orthogonalize_real(&mut vector, row_space);
482 orthogonalize_real(&mut vector, basis);
483 let norm = real_vector_norm(&vector);
484 if norm > completion_threshold(dim) {
485 let inv_norm = 1.0 / norm;
486 for value in &mut vector {
487 *value *= inv_norm;
488 }
489 basis.push(vector);
490 }
491}
492
493fn push_orthonormal_complex(
494 basis: &mut Vec<Vec<Complex64>>,
495 row_space: &[Vec<Complex64>],
496 vector: Vec<Complex64>,
497 dim: usize,
498) {
499 let mut vector = vector;
500 orthogonalize_complex(&mut vector, row_space);
501 orthogonalize_complex(&mut vector, basis);
502 orthogonalize_complex(&mut vector, row_space);
503 orthogonalize_complex(&mut vector, basis);
504 let norm = complex_vector_norm(&vector);
505 if norm > completion_threshold(dim) {
506 let inv_norm = 1.0 / norm;
507 for value in &mut vector {
508 *value *= inv_norm;
509 }
510 basis.push(vector);
511 }
512}
513
514fn orthogonalize_real(vector: &mut [f64], columns: &[Vec<f64>]) {
515 for column in columns {
516 let dot = vector
517 .iter()
518 .zip(column.iter())
519 .map(|(lhs, rhs)| lhs * rhs)
520 .sum::<f64>();
521 for (value, basis_value) in vector.iter_mut().zip(column.iter()) {
522 *value -= dot * basis_value;
523 }
524 }
525}
526
527fn orthogonalize_complex(vector: &mut [Complex64], columns: &[Vec<Complex64>]) {
528 for column in columns {
529 let dot = column
530 .iter()
531 .zip(vector.iter())
532 .map(|(basis_value, value)| basis_value.conj() * value)
533 .sum::<Complex64>();
534 for (value, basis_value) in vector.iter_mut().zip(column.iter()) {
535 *value -= basis_value * dot;
536 }
537 }
538}
539
540fn real_vector_norm(vector: &[f64]) -> f64 {
541 vector.iter().map(|value| value * value).sum::<f64>().sqrt()
542}
543
544fn complex_vector_norm(vector: &[Complex64]) -> f64 {
545 vector
546 .iter()
547 .map(|value| value.norm_sqr())
548 .sum::<f64>()
549 .sqrt()
550}
551
552fn completion_threshold(dim: usize) -> f64 {
553 (dim.max(1) as f64) * f64::EPSILON.sqrt()
554}
555
556fn is_rank_singular_value(singular_value: f64, tolerance: f64) -> bool {
557 singular_value.is_infinite() || singular_value > tolerance
558}
559
560fn checked_output_len(rows: usize, cols: usize, context: &str) -> BuiltinResult<usize> {
561 rows.checked_mul(cols)
562 .ok_or_else(|| input_error(format!("{NAME}: {context} dimension product overflow")))
563}
564
565fn zeroed_vec<T: Clone>(len: usize, value: T, context: &str) -> BuiltinResult<Vec<T>> {
566 let max_len = isize::MAX as usize / size_of::<T>();
567 if len > max_len {
568 return Err(input_error(format!(
569 "{NAME}: {context} allocation is too large"
570 )));
571 }
572 let mut data = Vec::new();
573 data.try_reserve_exact(len)
574 .map_err(|err| internal_error(format!("{NAME}: {context} allocation failed: {err}")))?;
575 data.resize(len, value);
576 Ok(data)
577}
578
579fn flatten_real_columns(columns: Vec<Vec<f64>>, rows: usize) -> BuiltinResult<Vec<f64>> {
580 let len = checked_output_len(rows, columns.len(), "orthonormal basis")?;
581 let mut data = Vec::new();
582 data.try_reserve_exact(len).map_err(|err| {
583 internal_error(format!(
584 "{NAME}: orthonormal basis allocation failed: {err}"
585 ))
586 })?;
587 for column in columns {
588 data.extend(column);
589 }
590 Ok(data)
591}
592
593fn flatten_complex_columns(
594 columns: Vec<Vec<Complex64>>,
595 rows: usize,
596) -> BuiltinResult<Vec<Complex64>> {
597 let len = checked_output_len(rows, columns.len(), "orthonormal basis")?;
598 let mut data = Vec::new();
599 data.try_reserve_exact(len).map_err(|err| {
600 internal_error(format!(
601 "{NAME}: orthonormal basis allocation failed: {err}"
602 ))
603 })?;
604 for column in columns {
605 data.extend(column);
606 }
607 Ok(data)
608}
609
610fn identity_basis_real(cols: usize) -> BuiltinResult<Vec<f64>> {
611 let len = checked_output_len(cols, cols, "identity basis")?;
612 let mut data = zeroed_vec(len, 0.0, "identity basis")?;
613 for idx in 0..cols {
614 data[idx + idx * cols] = 1.0;
615 }
616 Ok(data)
617}
618
619fn identity_basis_complex(cols: usize) -> BuiltinResult<Vec<Complex64>> {
620 let len = checked_output_len(cols, cols, "identity basis")?;
621 let mut data = zeroed_vec(len, Complex64::new(0.0, 0.0), "identity basis")?;
622 for idx in 0..cols {
623 data[idx + idx * cols] = Complex64::new(1.0, 0.0);
624 }
625 Ok(data)
626}
627
628fn real_row_reduction_basis(
629 data: Vec<f64>,
630 rows: usize,
631 cols: usize,
632 tolerance: f64,
633) -> BuiltinResult<(Vec<f64>, usize)> {
634 let (reduced, pivots) = rref_real_impl(data, rows, cols, tolerance)
635 .map_err(|err| internal_error(format!("{NAME}: row reduction failed ({err})")))?;
636 basis_from_real_rref(&reduced, rows, cols, &pivots)
637}
638
639fn complex_row_reduction_basis(
640 data: Vec<Complex64>,
641 rows: usize,
642 cols: usize,
643 tolerance: f64,
644) -> BuiltinResult<(Vec<Complex64>, usize)> {
645 let (reduced, pivots) = rref_complex_impl(data, rows, cols, tolerance)
646 .map_err(|err| internal_error(format!("{NAME}: row reduction failed ({err})")))?;
647 basis_from_complex_rref(&reduced, rows, cols, &pivots)
648}
649
650fn basis_from_real_rref(
651 reduced: &[f64],
652 rows: usize,
653 cols: usize,
654 pivots: &[usize],
655) -> BuiltinResult<(Vec<f64>, usize)> {
656 let pivot_cols = normalized_pivot_columns(cols, pivots);
657 let free_len = cols.saturating_sub(pivot_cols.len());
658 let basis_len = checked_output_len(cols, free_len, "row-reduction basis")?;
659 let free_cols = free_columns(cols, &pivot_cols)?;
660 let mut basis = zeroed_vec(basis_len, 0.0, "row-reduction basis")?;
661 for (basis_col, &free_col) in free_cols.iter().enumerate() {
662 basis[free_col + basis_col * cols] = 1.0;
663 for (pivot_row, &pivot_one_based) in pivots.iter().enumerate() {
664 let pivot_col = pivot_one_based - 1;
665 basis[pivot_col + basis_col * cols] = -reduced[pivot_row + free_col * rows];
666 }
667 }
668 Ok((basis, free_cols.len()))
669}
670
671fn basis_from_complex_rref(
672 reduced: &[Complex64],
673 rows: usize,
674 cols: usize,
675 pivots: &[usize],
676) -> BuiltinResult<(Vec<Complex64>, usize)> {
677 let pivot_cols = normalized_pivot_columns(cols, pivots);
678 let free_len = cols.saturating_sub(pivot_cols.len());
679 let basis_len = checked_output_len(cols, free_len, "row-reduction basis")?;
680 let free_cols = free_columns(cols, &pivot_cols)?;
681 let mut basis = zeroed_vec(basis_len, Complex64::new(0.0, 0.0), "row-reduction basis")?;
682 for (basis_col, &free_col) in free_cols.iter().enumerate() {
683 basis[free_col + basis_col * cols] = Complex64::new(1.0, 0.0);
684 for (pivot_row, &pivot_one_based) in pivots.iter().enumerate() {
685 let pivot_col = pivot_one_based - 1;
686 basis[pivot_col + basis_col * cols] = -reduced[pivot_row + free_col * rows];
687 }
688 }
689 Ok((basis, free_cols.len()))
690}
691
692fn normalized_pivot_columns(cols: usize, pivots: &[usize]) -> Vec<usize> {
693 let mut pivot_cols = Vec::new();
694 for &pivot in pivots {
695 if (1..=cols).contains(&pivot) {
696 pivot_cols.push(pivot - 1);
697 }
698 }
699 pivot_cols.sort_unstable();
700 pivot_cols.dedup();
701 pivot_cols
702}
703
704fn free_columns(cols: usize, pivot_cols: &[usize]) -> BuiltinResult<Vec<usize>> {
705 let free_len = cols.saturating_sub(pivot_cols.len());
706 let mut free_cols = Vec::new();
707 free_cols.try_reserve_exact(free_len).map_err(|err| {
708 internal_error(format!("{NAME}: free-column list allocation failed: {err}"))
709 })?;
710 let mut pivot_idx = 0;
711 for col in 0..cols {
712 if pivot_idx < pivot_cols.len() && pivot_cols[pivot_idx] == col {
713 pivot_idx += 1;
714 } else {
715 free_cols.push(col);
716 }
717 }
718 Ok(free_cols)
719}
720
721#[cfg(test)]
722pub(crate) mod tests {
723 use super::*;
724 use crate::builtins::common::test_support;
725 use futures::executor::block_on;
726 use runmat_builtins::{CharArray, IntValue, ResolveContext, StringArray, Type};
727
728 fn unwrap_error(err: crate::RuntimeError) -> crate::RuntimeError {
729 err
730 }
731
732 fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
733 assert_eq!(actual.len(), expected.len(), "length mismatch");
734 for (lhs, rhs) in actual.iter().zip(expected.iter()) {
735 assert!(
736 (lhs - rhs).abs() <= tol,
737 "expected {lhs} ~= {rhs} within {tol}"
738 );
739 }
740 }
741
742 fn assert_complex_close(actual: &[(f64, f64)], expected: &[(f64, f64)], tol: f64) {
743 assert_eq!(actual.len(), expected.len(), "length mismatch");
744 for ((ar, ai), (er, ei)) in actual.iter().zip(expected.iter()) {
745 assert!(
746 (ar - er).abs() <= tol && (ai - ei).abs() <= tol,
747 "expected ({ar}, {ai}) ~= ({er}, {ei}) within {tol}"
748 );
749 }
750 }
751
752 fn assert_az_zero(a: &Tensor, z: &Tensor, tol: f64) {
753 assert_eq!(a.shape.len(), 2);
754 assert_eq!(z.shape.len(), 2);
755 assert_eq!(a.shape[1], z.shape[0], "inner dimension mismatch");
756 let rows = a.shape[0];
757 let inner = a.shape[1];
758 let cols = z.shape[1];
759 for col in 0..cols {
760 for row in 0..rows {
761 let mut sum = 0.0;
762 for k in 0..inner {
763 sum += a.data[row + k * rows] * z.data[k + col * z.shape[0]];
764 }
765 assert!(
766 sum.abs() <= tol,
767 "expected A*Z ~= 0 at ({row},{col}), got {sum}"
768 );
769 }
770 }
771 }
772
773 fn assert_complex_az_zero(a: &ComplexTensor, z: &ComplexTensor, tol: f64) {
774 assert_eq!(a.shape.len(), 2);
775 assert_eq!(z.shape.len(), 2);
776 assert_eq!(a.shape[1], z.shape[0], "inner dimension mismatch");
777 let rows = a.shape[0];
778 let inner = a.shape[1];
779 let cols = z.shape[1];
780 for col in 0..cols {
781 for row in 0..rows {
782 let mut sum = Complex64::new(0.0, 0.0);
783 for k in 0..inner {
784 let lhs = a.data[row + k * rows];
785 let rhs = z.data[k + col * z.shape[0]];
786 sum += Complex64::new(lhs.0, lhs.1) * Complex64::new(rhs.0, rhs.1);
787 }
788 assert!(
789 sum.norm() <= tol,
790 "expected A*Z ~= 0 at ({row},{col}), got {sum}"
791 );
792 }
793 }
794 }
795
796 fn assert_columns_orthonormal(z: &Tensor, tol: f64) {
797 let rows = z.shape[0];
798 let cols = z.shape[1];
799 for col in 0..cols {
800 let norm = (0..rows)
801 .map(|row| z.data[row + col * rows] * z.data[row + col * rows])
802 .sum::<f64>()
803 .sqrt();
804 assert!((norm - 1.0).abs() <= tol, "column norm {norm}");
805 for other in 0..col {
806 let dot = (0..rows)
807 .map(|row| z.data[row + col * rows] * z.data[row + other * rows])
808 .sum::<f64>();
809 assert!(dot.abs() <= tol, "column dot {dot}");
810 }
811 }
812 }
813
814 fn assert_complex_columns_orthonormal(z: &ComplexTensor, tol: f64) {
815 let rows = z.shape[0];
816 let cols = z.shape[1];
817 for col in 0..cols {
818 let norm = (0..rows)
819 .map(|row| {
820 let value = z.data[row + col * rows];
821 value.0 * value.0 + value.1 * value.1
822 })
823 .sum::<f64>()
824 .sqrt();
825 assert!((norm - 1.0).abs() <= tol, "column norm {norm}");
826 for other in 0..col {
827 let dot = (0..rows)
828 .map(|row| {
829 let lhs = z.data[row + other * rows];
830 let rhs = z.data[row + col * rows];
831 Complex64::new(lhs.0, -lhs.1) * Complex64::new(rhs.0, rhs.1)
832 })
833 .sum::<Complex64>();
834 assert!(dot.norm() <= tol, "column dot {dot}");
835 }
836 }
837 }
838
839 #[test]
840 fn null_type_uses_input_column_count_and_unknown_nullity() {
841 let out = null_type(
842 &[Type::Tensor {
843 shape: Some(vec![Some(3), Some(4)]),
844 }],
845 &ResolveContext::new(Vec::new()),
846 );
847 assert_eq!(
848 out,
849 Type::Tensor {
850 shape: Some(vec![Some(4), None])
851 }
852 );
853 }
854
855 #[test]
856 fn null_descriptor_signatures_cover_core_forms() {
857 let labels: Vec<&str> = NULL_DESCRIPTOR
858 .signatures
859 .iter()
860 .map(|signature| signature.label)
861 .collect();
862 assert!(labels.contains(&"Z = null(A)"));
863 assert!(labels.contains(&"Z = null(A, tol)"));
864 assert!(labels.contains(&"Z = null(A, \"r\")"));
865 }
866
867 #[test]
868 fn null_descriptor_errors_have_stable_codes() {
869 let codes: Vec<&str> = NULL_DESCRIPTOR.errors.iter().map(|err| err.code).collect();
870 assert!(codes.contains(&"RM.NULL.INVALID_ARGUMENT"));
871 assert!(codes.contains(&"RM.NULL.INVALID_INPUT"));
872 assert!(codes.contains(&"RM.NULL.INTERNAL"));
873 }
874
875 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
876 #[test]
877 fn null_full_rank_square_returns_empty_basis() {
878 let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 2.0], vec![2, 2]).unwrap();
879 let result = null_builtin(Value::Tensor(tensor), Vec::new()).expect("null");
880 match result {
881 Value::Tensor(out) => {
882 assert_eq!(out.shape, vec![2, 0]);
883 assert!(out.data.is_empty());
884 }
885 other => panic!("expected empty tensor basis, got {other:?}"),
886 }
887 }
888
889 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
890 #[test]
891 fn null_rank_deficient_square_returns_orthonormal_basis() {
892 let tensor = Tensor::new(vec![1.0, 2.0, 2.0, 4.0], vec![2, 2]).unwrap();
893 let result = null_builtin(Value::Tensor(tensor.clone()), Vec::new()).expect("null");
894 match result {
895 Value::Tensor(out) => {
896 assert_eq!(out.shape, vec![2, 1]);
897 assert_az_zero(&tensor, &out, 1e-12);
898 assert_columns_orthonormal(&out, 1e-12);
899 }
900 other => panic!("expected tensor basis, got {other:?}"),
901 }
902 }
903
904 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
905 #[test]
906 fn null_rectangular_wide_matrix_returns_full_nullity() {
907 let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0], vec![2, 3]).unwrap();
908 let result = null_builtin(Value::Tensor(tensor.clone()), Vec::new()).expect("null");
909 match result {
910 Value::Tensor(out) => {
911 assert_eq!(out.shape, vec![3, 1]);
912 assert_az_zero(&tensor, &out, 1e-12);
913 assert_columns_orthonormal(&out, 1e-12);
914 }
915 other => panic!("expected tensor basis, got {other:?}"),
916 }
917 }
918
919 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
920 #[test]
921 fn null_large_scale_default_tolerance_keeps_exact_null_column() {
922 let tensor = Tensor::new(vec![1e20, 0.0], vec![1, 2]).unwrap();
923 let result = null_builtin(Value::Tensor(tensor.clone()), Vec::new()).expect("null");
924 match result {
925 Value::Tensor(out) => {
926 assert_eq!(out.shape, vec![2, 1]);
927 assert_az_zero(&tensor, &out, 1e-6);
928 assert_columns_orthonormal(&out, 1e-12);
929 assert!(out.data[0].abs() <= 1e-12);
930 assert!((out.data[1].abs() - 1.0).abs() <= 1e-12);
931 }
932 other => panic!("expected tensor basis, got {other:?}"),
933 }
934 }
935
936 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
937 #[test]
938 fn null_row_reduction_option_returns_pivot_basis() {
939 let tensor = Tensor::new(vec![1.0, 2.0, 2.0, 4.0, 3.0, 6.0], vec![2, 3]).unwrap();
940 let chars = CharArray::new("r".chars().collect(), 1, 1).unwrap();
941 let result = null_builtin(Value::Tensor(tensor), vec![Value::CharArray(chars)])
942 .expect("null rref basis");
943 match result {
944 Value::Tensor(out) => {
945 assert_eq!(out.shape, vec![3, 2]);
946 assert_close(&out.data, &[-2.0, 1.0, 0.0, -3.0, 0.0, 1.0], 1e-12);
947 }
948 other => panic!("expected tensor basis, got {other:?}"),
949 }
950 }
951
952 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
953 #[test]
954 fn null_row_reduction_accepts_string_array_option() {
955 let tensor = Tensor::new(vec![1.0, 2.0, 2.0, 4.0], vec![2, 2]).unwrap();
956 let option = StringArray::new(vec!["r".to_string()], vec![1, 1]).unwrap();
957 let result = null_builtin(Value::Tensor(tensor), vec![Value::StringArray(option)])
958 .expect("null rref basis");
959 match result {
960 Value::Tensor(out) => {
961 assert_eq!(out.shape, vec![2, 1]);
962 assert_close(&out.data, &[-2.0, 1.0], 1e-12);
963 }
964 other => panic!("expected tensor basis, got {other:?}"),
965 }
966 }
967
968 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
969 #[test]
970 fn null_custom_tolerance_expands_null_space() {
971 let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 1e-8], vec![2, 2]).unwrap();
972 let result = null_builtin(Value::Tensor(tensor), vec![Value::Num(1e-6)]).expect("null");
973 match result {
974 Value::Tensor(out) => {
975 assert_eq!(out.shape, vec![2, 1]);
976 assert_close(&out.data, &[0.0, 1.0], 1e-12);
977 }
978 other => panic!("expected tensor basis, got {other:?}"),
979 }
980 }
981
982 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
983 #[test]
984 fn null_custom_large_tolerance_keeps_full_zero_matrix_basis() {
985 let tensor = Tensor::new(vec![0.0, 0.0], vec![1, 2]).unwrap();
986 let result = null_builtin(Value::Tensor(tensor.clone()), vec![Value::Num(2.0)])
987 .expect("null with large tolerance");
988 match result {
989 Value::Tensor(out) => {
990 assert_eq!(out.shape, vec![2, 2]);
991 assert_az_zero(&tensor, &out, 1e-12);
992 assert_columns_orthonormal(&out, 1e-12);
993 }
994 other => panic!("expected tensor basis, got {other:?}"),
995 }
996 }
997
998 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
999 #[test]
1000 fn null_complex_rank_deficient_matrix() {
1001 let tensor = ComplexTensor::new(
1002 vec![(1.0, 1.0), (2.0, 2.0), (2.0, 0.0), (4.0, 0.0)],
1003 vec![2, 2],
1004 )
1005 .unwrap();
1006 let result = null_builtin(Value::ComplexTensor(tensor.clone()), Vec::new()).expect("null");
1007 match result {
1008 Value::ComplexTensor(out) => {
1009 assert_eq!(out.shape, vec![2, 1]);
1010 assert_complex_az_zero(&tensor, &out, 1e-12);
1011 assert_complex_columns_orthonormal(&out, 1e-12);
1012 }
1013 other => panic!("expected complex tensor basis, got {other:?}"),
1014 }
1015 }
1016
1017 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1018 #[test]
1019 fn null_complex_row_reduction_option_returns_complex_basis() {
1020 let tensor = ComplexTensor::new(
1021 vec![(1.0, 0.0), (2.0, 0.0), (0.0, 1.0), (0.0, 2.0)],
1022 vec![2, 2],
1023 )
1024 .unwrap();
1025 let chars = CharArray::new("r".chars().collect(), 1, 1).unwrap();
1026 let result = null_builtin(Value::ComplexTensor(tensor), vec![Value::CharArray(chars)])
1027 .expect("null rref basis");
1028 match result {
1029 Value::ComplexTensor(out) => {
1030 assert_eq!(out.shape, vec![2, 1]);
1031 assert_complex_close(&out.data, &[(-0.0, -1.0), (1.0, 0.0)], 1e-12);
1032 }
1033 other => panic!("expected complex tensor basis, got {other:?}"),
1034 }
1035 }
1036
1037 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1038 #[test]
1039 fn null_empty_matrices_have_expected_basis_shapes() {
1040 let zero_by_three = Tensor::new(Vec::<f64>::new(), vec![0, 3]).unwrap();
1041 let result = null_builtin(Value::Tensor(zero_by_three), Vec::new()).expect("null");
1042 match result {
1043 Value::Tensor(out) => {
1044 assert_eq!(out.shape, vec![3, 3]);
1045 assert_close(
1046 &out.data,
1047 &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
1048 1e-12,
1049 );
1050 }
1051 other => panic!("expected identity basis, got {other:?}"),
1052 }
1053
1054 let three_by_zero = Tensor::new(Vec::<f64>::new(), vec![3, 0]).unwrap();
1055 let result = null_builtin(Value::Tensor(three_by_zero), Vec::new()).expect("null");
1056 match result {
1057 Value::Tensor(out) => {
1058 assert_eq!(out.shape, vec![0, 0]);
1059 assert!(out.data.is_empty());
1060 }
1061 other => panic!("expected 0x0 tensor basis, got {other:?}"),
1062 }
1063 }
1064
1065 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1066 #[test]
1067 fn null_empty_wide_matrix_rejects_identity_basis_overflow() {
1068 let tensor = Tensor::new(Vec::<f64>::new(), vec![0, usize::MAX]).unwrap();
1069 let err = unwrap_error(null_builtin(Value::Tensor(tensor), Vec::new()).unwrap_err());
1070 assert!(err
1071 .message()
1072 .contains("identity basis dimension product overflow"));
1073 assert_eq!(err.identifier(), NULL_ERROR_INVALID_INPUT.identifier);
1074 }
1075
1076 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1077 #[test]
1078 fn null_rref_basis_rejects_output_size_overflow() {
1079 let err = unwrap_error(basis_from_real_rref(&[], 0, usize::MAX, &[]).unwrap_err());
1080 assert!(err
1081 .message()
1082 .contains("row-reduction basis dimension product overflow"));
1083 assert_eq!(err.identifier(), NULL_ERROR_INVALID_INPUT.identifier);
1084 }
1085
1086 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1087 #[test]
1088 fn null_scalars_follow_matrix_null_space_rules() {
1089 let zero = null_builtin(Value::Bool(false), Vec::new()).expect("null zero");
1090 let nonzero = null_builtin(Value::Int(IntValue::I32(5)), Vec::new()).expect("null int");
1091 match zero {
1092 Value::Num(value) => assert_eq!(value, 1.0),
1093 other => panic!("expected scalar one basis, got {other:?}"),
1094 }
1095 match nonzero {
1096 Value::Tensor(out) => {
1097 assert_eq!(out.shape, vec![1, 0]);
1098 assert!(out.data.is_empty());
1099 }
1100 other => panic!("expected empty scalar null basis, got {other:?}"),
1101 }
1102 }
1103
1104 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1105 #[test]
1106 fn null_rejects_invalid_option() {
1107 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1108 let chars = CharArray::new("q".chars().collect(), 1, 1).unwrap();
1109 let err = unwrap_error(
1110 null_builtin(Value::Tensor(tensor), vec![Value::CharArray(chars)]).unwrap_err(),
1111 );
1112 assert!(err.message().contains("unsupported basis option"));
1113 assert_eq!(err.identifier(), NULL_ERROR_INVALID_ARGUMENT.identifier);
1114 }
1115
1116 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1117 #[test]
1118 fn null_rejects_negative_tolerance() {
1119 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1120 let err = unwrap_error(
1121 null_builtin(Value::Tensor(tensor), vec![Value::Int(IntValue::I32(-1))]).unwrap_err(),
1122 );
1123 assert!(err.message().contains("tolerance"));
1124 assert_eq!(err.identifier(), NULL_ERROR_INVALID_ARGUMENT.identifier);
1125 }
1126
1127 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1128 #[test]
1129 fn null_gpu_round_trip_matches_cpu() {
1130 test_support::with_test_provider(|provider| {
1131 let tensor = Tensor::new(vec![1.0, 2.0, 2.0, 4.0], vec![2, 2]).unwrap();
1132 let view = HostTensorView {
1133 data: &tensor.data,
1134 shape: &tensor.shape,
1135 };
1136 let handle = provider.upload(&view).expect("upload");
1137 let result = null_builtin(Value::GpuTensor(handle), Vec::new()).expect("gpu null");
1138 let gathered = test_support::gather(result).expect("gather");
1139 let cpu = null_real_tensor(&tensor, NullMode::Orthonormal { tolerance: None })
1140 .expect("cpu null");
1141 assert_eq!(gathered.shape, cpu.shape);
1142 assert_close(&gathered.data, &cpu.data, 1e-12);
1143 });
1144 }
1145
1146 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1147 #[test]
1148 #[cfg(feature = "wgpu")]
1149 fn null_wgpu_matches_cpu() {
1150 use runmat_accelerate::backend::wgpu::provider::{
1151 register_wgpu_provider, WgpuProviderOptions,
1152 };
1153
1154 let _ = register_wgpu_provider(WgpuProviderOptions::default());
1155 let tensor = Tensor::new(vec![1.0, 2.0, 2.0, 4.0], vec![2, 2]).unwrap();
1156 let cpu =
1157 null_real_tensor(&tensor, NullMode::Orthonormal { tolerance: None }).expect("cpu null");
1158
1159 let view = HostTensorView {
1160 data: &tensor.data,
1161 shape: &tensor.shape,
1162 };
1163 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1164 let handle = provider.upload(&view).expect("upload");
1165
1166 let gpu_value = null_builtin(Value::GpuTensor(handle), Vec::new()).expect("gpu null");
1167 let gathered = test_support::gather(gpu_value).expect("gather");
1168 assert_eq!(gathered.shape, cpu.shape);
1169 assert_close(&gathered.data, &cpu.data, 5e-5);
1170 }
1171
1172 fn null_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1173 block_on(super::null_builtin(value, rest))
1174 }
1175}