1use runmat_builtins::{
4 BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
5 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
6 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
7};
8use std::cmp::max;
9
10use runmat_accelerate_api::{GpuTensorHandle, GpuTensorStorage};
11use runmat_builtins::{
12 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
13 BuiltinExtensionMode, BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor,
14 BuiltinParamType, BuiltinSignatureDescriptor, ResolveContext, Type,
15};
16use runmat_value::{
17 ComplexStorage, ComplexTensor, IntegerComplexStorage, IntegerStorage, NumericDType,
18 NumericStorage, Tensor, Value,
19};
20
21use crate::builtins::array::type_resolvers::size_vector_len;
22use runmat_macros::runtime_builtin;
23
24use crate::build_runtime_error;
25use crate::builtins::common::gpu_helpers;
26use crate::builtins::common::random_args::{complex_tensor_into_value, keyword_of};
27use crate::builtins::common::residency::{sequence_gpu_preference, SequenceIntent};
28use crate::builtins::common::spec::{
29 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
30 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
31};
32use crate::builtins::common::tensor;
33
34const MESHGRID_LIKE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
35 id: "meshgrid-like",
36 mode: BuiltinExtensionMode::RunMatOnly,
37 description: "the meshgrid \"like\" prototype selector is a RunMat extension",
38 error_identifier: Some("RunMat:compatibility:MeshgridLikeExtension"),
39};
40
41const MESHGRID_COMPLEX_AXES_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
42 id: "meshgrid-complex-axes",
43 mode: BuiltinExtensionMode::RunMatOnly,
44 description: "complex meshgrid axes are a RunMat extension",
45 error_identifier: Some("RunMat:compatibility:MeshgridComplexAxesExtension"),
46};
47
48pub const MESHGRID_EXTENSIONS: [BuiltinExtensionDescriptor; 2] =
49 [MESHGRID_LIKE_EXTENSION, MESHGRID_COMPLEX_AXES_EXTENSION];
50
51#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::creation::meshgrid")]
52pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
53 name: "meshgrid",
54 op_kind: GpuOpKind::Custom("array_construct"),
55 supported_precisions: &[ScalarType::F32, ScalarType::F64],
56 broadcast: BroadcastSemantics::Matlab,
57 provider_hooks: &[ProviderHook::Custom("meshgrid")],
58 constant_strategy: ConstantStrategy::InlineLiteral,
59 residency: ResidencyPolicy::NewHandle,
60 nan_mode: ReductionNaN::Include,
61 two_pass_threshold: None,
62 workgroup_size: None,
63 accepts_nan_mode: false,
64 notes: "Providers may supply a dedicated meshgrid hook; until then the runtime builds grids on the host and uploads them when GPU residency is requested.",
65};
66
67fn builtin_error(message: impl Into<String>) -> crate::RuntimeError {
68 build_runtime_error(message)
69 .with_builtin("meshgrid")
70 .build()
71}
72
73#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::array::creation::meshgrid")]
74pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
75 name: "meshgrid",
76 shape: ShapeRequirements::Any,
77 constant_strategy: ConstantStrategy::InlineLiteral,
78 elementwise: None,
79 reduction: None,
80 emits_nan: false,
81 notes:
82 "Meshgrid explicitly materialises dense coordinate arrays and therefore bypasses fusion.",
83};
84
85fn meshgrid_type(args: &[Type], _context: &ResolveContext) -> Type {
86 if args.is_empty() {
87 return Type::Unknown;
88 }
89 let mut axis_count = args.len();
90 if axis_count >= 2 && matches!(args[axis_count - 2], Type::String) {
91 axis_count = axis_count.saturating_sub(2);
92 }
93 if axis_count == 0 {
94 return Type::Unknown;
95 }
96 let axis_args = &args[..axis_count];
97 let len_x = axis_args.get(0).and_then(size_vector_len);
98 let len_y = axis_args.get(1).and_then(size_vector_len).or(len_x);
99 let len_z = axis_args.get(2).and_then(size_vector_len);
100 let shape = if axis_count >= 3 {
101 vec![len_y, len_x, len_z]
102 } else {
103 vec![len_y, len_x]
104 };
105 Type::Tensor { shape: Some(shape) }
106}
107
108const MESHGRID_OUTPUT_XY: [BuiltinParamDescriptor; 2] = [
109 BuiltinParamDescriptor {
110 name: "X",
111 ty: BuiltinParamType::NumericArray,
112 arity: BuiltinParamArity::Required,
113 default: None,
114 description: "Grid coordinates along X-axis.",
115 },
116 BuiltinParamDescriptor {
117 name: "Y",
118 ty: BuiltinParamType::NumericArray,
119 arity: BuiltinParamArity::Required,
120 default: None,
121 description: "Grid coordinates along Y-axis.",
122 },
123];
124
125const MESHGRID_OUTPUT_XYZ: [BuiltinParamDescriptor; 3] = [
126 BuiltinParamDescriptor {
127 name: "X",
128 ty: BuiltinParamType::NumericArray,
129 arity: BuiltinParamArity::Required,
130 default: None,
131 description: "Grid coordinates along X-axis.",
132 },
133 BuiltinParamDescriptor {
134 name: "Y",
135 ty: BuiltinParamType::NumericArray,
136 arity: BuiltinParamArity::Required,
137 default: None,
138 description: "Grid coordinates along Y-axis.",
139 },
140 BuiltinParamDescriptor {
141 name: "Z",
142 ty: BuiltinParamType::NumericArray,
143 arity: BuiltinParamArity::Optional,
144 default: None,
145 description: "Grid coordinates along Z-axis.",
146 },
147];
148
149const MESHGRID_SIG_X_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
150 name: "x",
151 ty: BuiltinParamType::NumericArray,
152 arity: BuiltinParamArity::Required,
153 default: None,
154 description: "X-axis vector.",
155}];
156
157const MESHGRID_SIG_XY_INPUTS: [BuiltinParamDescriptor; 2] = [
158 BuiltinParamDescriptor {
159 name: "x",
160 ty: BuiltinParamType::NumericArray,
161 arity: BuiltinParamArity::Required,
162 default: None,
163 description: "X-axis vector.",
164 },
165 BuiltinParamDescriptor {
166 name: "y",
167 ty: BuiltinParamType::NumericArray,
168 arity: BuiltinParamArity::Required,
169 default: None,
170 description: "Y-axis vector.",
171 },
172];
173
174const MESHGRID_SIG_XYZ_INPUTS: [BuiltinParamDescriptor; 3] = [
175 BuiltinParamDescriptor {
176 name: "x",
177 ty: BuiltinParamType::NumericArray,
178 arity: BuiltinParamArity::Required,
179 default: None,
180 description: "X-axis vector.",
181 },
182 BuiltinParamDescriptor {
183 name: "y",
184 ty: BuiltinParamType::NumericArray,
185 arity: BuiltinParamArity::Required,
186 default: None,
187 description: "Y-axis vector.",
188 },
189 BuiltinParamDescriptor {
190 name: "z",
191 ty: BuiltinParamType::NumericArray,
192 arity: BuiltinParamArity::Optional,
193 default: None,
194 description: "Z-axis vector.",
195 },
196];
197
198const MESHGRID_SIG_X_LIKE_INPUTS: [BuiltinParamDescriptor; 3] = [
199 BuiltinParamDescriptor {
200 name: "x",
201 ty: BuiltinParamType::NumericArray,
202 arity: BuiltinParamArity::Required,
203 default: None,
204 description: "X-axis vector.",
205 },
206 BuiltinParamDescriptor {
207 name: "like_kw",
208 ty: BuiltinParamType::StringScalar,
209 arity: BuiltinParamArity::Required,
210 default: Some("\"like\""),
211 description: "Like keyword.",
212 },
213 BuiltinParamDescriptor {
214 name: "prototype",
215 ty: BuiltinParamType::LikePrototype,
216 arity: BuiltinParamArity::Required,
217 default: None,
218 description: "Prototype controlling class/device residency.",
219 },
220];
221
222const MESHGRID_SIG_XY_LIKE_INPUTS: [BuiltinParamDescriptor; 4] = [
223 BuiltinParamDescriptor {
224 name: "x",
225 ty: BuiltinParamType::NumericArray,
226 arity: BuiltinParamArity::Required,
227 default: None,
228 description: "X-axis vector.",
229 },
230 BuiltinParamDescriptor {
231 name: "y",
232 ty: BuiltinParamType::NumericArray,
233 arity: BuiltinParamArity::Required,
234 default: None,
235 description: "Y-axis vector.",
236 },
237 BuiltinParamDescriptor {
238 name: "like_kw",
239 ty: BuiltinParamType::StringScalar,
240 arity: BuiltinParamArity::Required,
241 default: Some("\"like\""),
242 description: "Like keyword.",
243 },
244 BuiltinParamDescriptor {
245 name: "prototype",
246 ty: BuiltinParamType::LikePrototype,
247 arity: BuiltinParamArity::Required,
248 default: None,
249 description: "Prototype controlling class/device residency.",
250 },
251];
252
253const MESHGRID_SIG_XYZ_LIKE_INPUTS: [BuiltinParamDescriptor; 5] = [
254 BuiltinParamDescriptor {
255 name: "x",
256 ty: BuiltinParamType::NumericArray,
257 arity: BuiltinParamArity::Required,
258 default: None,
259 description: "X-axis vector.",
260 },
261 BuiltinParamDescriptor {
262 name: "y",
263 ty: BuiltinParamType::NumericArray,
264 arity: BuiltinParamArity::Required,
265 default: None,
266 description: "Y-axis vector.",
267 },
268 BuiltinParamDescriptor {
269 name: "z",
270 ty: BuiltinParamType::NumericArray,
271 arity: BuiltinParamArity::Optional,
272 default: None,
273 description: "Z-axis vector.",
274 },
275 BuiltinParamDescriptor {
276 name: "like_kw",
277 ty: BuiltinParamType::StringScalar,
278 arity: BuiltinParamArity::Required,
279 default: Some("\"like\""),
280 description: "Like keyword.",
281 },
282 BuiltinParamDescriptor {
283 name: "prototype",
284 ty: BuiltinParamType::LikePrototype,
285 arity: BuiltinParamArity::Required,
286 default: None,
287 description: "Prototype controlling class/device residency.",
288 },
289];
290
291const MESHGRID_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
292 BuiltinSignatureDescriptor {
293 label: "[X,Y] = meshgrid(x)",
294 inputs: &MESHGRID_SIG_X_INPUTS,
295 outputs: &MESHGRID_OUTPUT_XY,
296 },
297 BuiltinSignatureDescriptor {
298 label: "[X,Y] = meshgrid(x, y)",
299 inputs: &MESHGRID_SIG_XY_INPUTS,
300 outputs: &MESHGRID_OUTPUT_XY,
301 },
302 BuiltinSignatureDescriptor {
303 label: "[X,Y,Z] = meshgrid(x, y, z)",
304 inputs: &MESHGRID_SIG_XYZ_INPUTS,
305 outputs: &MESHGRID_OUTPUT_XYZ,
306 },
307 BuiltinSignatureDescriptor {
308 label: "[X,Y] = meshgrid(x, \"like\", prototype)",
309 inputs: &MESHGRID_SIG_X_LIKE_INPUTS,
310 outputs: &MESHGRID_OUTPUT_XY,
311 },
312 BuiltinSignatureDescriptor {
313 label: "[X,Y] = meshgrid(x, y, \"like\", prototype)",
314 inputs: &MESHGRID_SIG_XY_LIKE_INPUTS,
315 outputs: &MESHGRID_OUTPUT_XY,
316 },
317 BuiltinSignatureDescriptor {
318 label: "[X,Y,Z] = meshgrid(x, y, z, \"like\", prototype)",
319 inputs: &MESHGRID_SIG_XYZ_LIKE_INPUTS,
320 outputs: &MESHGRID_OUTPUT_XYZ,
321 },
322];
323
324const MESHGRID_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
325 [BuiltinIntegerInputCapability {
326 name: "x, y, z",
327 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
328 availability: BuiltinIntegerInputAvailability::Documented,
329 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
330 notes: "Every documented integer axis class is replicated without conversion and each corresponding grid preserves that axis class.",
331 }];
332
333pub const MESHGRID_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
334 [BuiltinIntegerCapabilityDescriptor {
335 form: "[X,Y,Z] = meshgrid(integer_x, integer_y, integer_z)",
336 inputs: &MESHGRID_INTEGER_INPUTS,
337 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
338 output_class: BuiltinIntegerOutputClassRule::PreserveInput,
339 overflow: BuiltinIntegerOverflowRule::NotApplicable,
340 backend: BuiltinIntegerBackendRule::GpuRestricted,
341 overload: BuiltinIntegerOverloadKind::Multiple,
342 notes: "Host axes preserve exact native storage independently. MATLAB documents gpuArray axes only for single and double; explicit integer gpuArray axes reject, while automatic integer residency gathers transparently and returns exact host grids.",
343 }];
344
345const MESHGRID_ERRORS: [BuiltinErrorDescriptor; 11] = [
346 BuiltinErrorDescriptor {
347 code: "RM.MESHGRID.MISSING_AXIS",
348 identifier: None,
349 when: "No axis vectors are provided.",
350 message: "meshgrid: at least one input vector is required",
351 },
352 BuiltinErrorDescriptor {
353 code: "RM.MESHGRID.TOO_MANY_AXES",
354 identifier: None,
355 when: "More than three axis vectors are provided.",
356 message: "meshgrid: expected at most three input vectors",
357 },
358 BuiltinErrorDescriptor {
359 code: "RM.MESHGRID.LIKE_EXPECTED_PROTOTYPE",
360 identifier: None,
361 when: "The 'like' keyword is provided without a prototype argument.",
362 message: "meshgrid: expected prototype after 'like'",
363 },
364 BuiltinErrorDescriptor {
365 code: "RM.MESHGRID.MULTIPLE_LIKE",
366 identifier: None,
367 when: "The 'like' keyword is provided multiple times.",
368 message: "meshgrid: multiple 'like' specifications are not supported",
369 },
370 BuiltinErrorDescriptor {
371 code: "RM.MESHGRID.LIKE_POSITION",
372 identifier: None,
373 when: "The 'like' keyword is in an invalid position or not final.",
374 message: "meshgrid: 'like' must be the final argument",
375 },
376 BuiltinErrorDescriptor {
377 code: "RM.MESHGRID.UNRECOGNIZED_OPTION",
378 identifier: None,
379 when: "A trailing option string is not recognized.",
380 message: "meshgrid: unrecognised option",
381 },
382 BuiltinErrorDescriptor {
383 code: "RM.MESHGRID.INVALID_AXIS_INPUT",
384 identifier: None,
385 when: "Axis inputs are non-numeric or non-vector shapes.",
386 message: "meshgrid: input argument must be numeric vector data",
387 },
388 BuiltinErrorDescriptor {
389 code: "RM.MESHGRID.INVALID_PROTOTYPE",
390 identifier: None,
391 when: "The 'like' prototype is unsupported.",
392 message: "meshgrid: prototypes must be numeric arrays",
393 },
394 BuiltinErrorDescriptor {
395 code: "RM.MESHGRID.OUTPUT_COUNT_EXCEEDED",
396 identifier: None,
397 when: "Requested outputs exceed available outputs for provided axes.",
398 message:
399 "meshgrid: supports at most two outputs for 2-axis inputs and three for 3-axis inputs",
400 },
401 BuiltinErrorDescriptor {
402 code: "RM.MESHGRID.THIRD_OUTPUT_UNAVAILABLE",
403 identifier: None,
404 when: "A third output is requested without supplying a Z-axis vector.",
405 message: "meshgrid: third output requested but no Z vector was supplied",
406 },
407 BuiltinErrorDescriptor {
408 code: "RM.MESHGRID.COMPLEX_REAL_CONVERSION",
409 identifier: None,
410 when: "Complex axis values cannot be represented in requested real output class.",
411 message: "meshgrid: cannot represent complex values in a real output",
412 },
413];
414
415pub const MESHGRID_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
416 signatures: &MESHGRID_SIGNATURES,
417 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
418 completion_policy: BuiltinCompletionPolicy::Public,
419 errors: &MESHGRID_ERRORS,
420};
421
422#[runtime_builtin(
423 name = "meshgrid",
424 category = "array/creation",
425 summary = "Generate coordinate matrices for 2-D and 3-D grids.",
426 keywords = "meshgrid,grid,gpu,like,3d",
427 accel = "array_construct",
428 type_resolver(meshgrid_type),
429 descriptor(crate::builtins::array::creation::meshgrid::MESHGRID_DESCRIPTOR),
430 integer_capabilities(
431 crate::builtins::array::creation::meshgrid::MESHGRID_INTEGER_CAPABILITIES
432 ),
433 extensions(crate::builtins::array::creation::meshgrid::MESHGRID_EXTENSIONS),
434 builtin_path = "crate::builtins::array::creation::meshgrid"
435)]
436async fn meshgrid_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
437 let eval = evaluate(&rest).await?;
438 if let Some(out_count) = crate::output_count::current_output_count() {
439 if out_count == 0 {
440 return Ok(Value::OutputList(Vec::new()));
441 }
442 let available = eval.output_count();
443 if out_count > available {
444 let msg = if available == 2 {
445 "meshgrid with two inputs supports at most two outputs"
446 } else {
447 "meshgrid supports at most three outputs"
448 };
449 return Err(builtin_error(msg));
450 }
451 let mut outputs = Vec::with_capacity(out_count);
452 let first = eval.first().await?;
453 outputs.push(first);
454 if out_count >= 2 {
455 outputs.push(eval.second().await?);
456 }
457 if out_count >= 3 {
458 outputs.push(eval.third().await?);
459 }
460 return Ok(Value::OutputList(outputs));
461 }
462 eval.first().await
463}
464
465pub async fn evaluate(args: &[Value]) -> crate::BuiltinResult<MeshgridEval> {
467 let parsed = ParsedMeshgrid::parse(args).await?;
468 let (x_axis, y_axis, z_axis) = normalise_axes(&parsed.axes);
469
470 let require_complex = parsed.axes.iter().any(|axis| axis.is_complex);
471
472 let target_class = match &parsed.template {
473 OutputTemplate::Default => {
474 if require_complex {
475 PrototypeClass::Complex
476 } else {
477 PrototypeClass::Real
478 }
479 }
480 OutputTemplate::Like(spec) => {
481 if require_complex {
482 PrototypeClass::Complex
483 } else {
484 spec.class
485 }
486 }
487 };
488
489 let target_residency = match &parsed.template {
490 OutputTemplate::Default => {
491 if parsed.prefer_gpu {
492 DevicePreference::Gpu(parsed.preferred_device_id.unwrap_or(0))
493 } else {
494 DevicePreference::Host
495 }
496 }
497 OutputTemplate::Like(spec) => spec.residency,
498 };
499
500 let mut outputs: Vec<MeshgridOutput> = Vec::new();
501
502 if matches!(target_residency, DevicePreference::Gpu(_)) {
503 if let Some(gpu) = try_meshgrid_gpu_from_vector_axes(&x_axis, &y_axis, z_axis.as_ref())? {
504 outputs = gpu;
505 }
506 }
507
508 if outputs.is_empty() {
509 let x_host = axis_to_host_async(&x_axis).await?;
511 let y_host = axis_to_host_async(&y_axis).await?;
512 let z_host = match z_axis.as_ref() {
513 Some(axis) => Some(axis_to_host_async(axis).await?),
514 None => None,
515 };
516 outputs = build_outputs(&x_host, &y_host, z_host.as_ref())
517 .into_iter()
518 .map(MeshgridOutput::Host)
519 .collect();
520 }
521
522 Ok(MeshgridEval {
523 outputs,
524 target_class,
525 target_residency,
526 })
527}
528
529#[derive(Clone)]
530struct ParsedMeshgrid {
531 axes: Vec<AxisData>,
532 template: OutputTemplate,
533 prefer_gpu: bool,
534 preferred_device_id: Option<u32>,
535}
536
537impl ParsedMeshgrid {
538 async fn parse(args: &[Value]) -> crate::BuiltinResult<Self> {
539 if args.is_empty() {
540 return Err(builtin_error(
541 "meshgrid: at least one input vector is required",
542 ));
543 }
544 let mut axis_values: Vec<Value> = Vec::new();
545 let mut like_proto: Option<Value> = None;
546 let mut prefer_gpu = false;
547 let mut preferred_device_id = None;
548 let mut idx = 0;
549 while idx < args.len() {
550 let value = args[idx].clone();
551 if let Some(keyword) = keyword_of(&value) {
552 match keyword.as_str() {
553 "like" => {
554 crate::compatibility::ensure_builtin_extension_enabled(
555 &MESHGRID_LIKE_EXTENSION,
556 "meshgrid",
557 )?;
558 if like_proto.is_some() {
559 return Err(builtin_error(
560 "meshgrid: multiple 'like' specifications are not supported",
561 ));
562 }
563 if axis_values.is_empty() {
564 return Err(builtin_error(
565 "meshgrid: 'like' must follow at least one input vector",
566 ));
567 }
568 let Some(proto) = args.get(idx + 1).cloned() else {
569 return Err(builtin_error("meshgrid: expected prototype after 'like'"));
570 };
571 like_proto = Some(proto);
572 idx += 2;
573 if idx < args.len() {
574 return Err(builtin_error(
575 "meshgrid: 'like' must be the final argument",
576 ));
577 }
578 break;
579 }
580 other => {
581 return Err(builtin_error(format!(
582 "meshgrid: unrecognised option '{other}'"
583 )));
584 }
585 }
586 }
587
588 axis_values.push(value);
589 idx += 1;
590 }
591
592 if axis_values.is_empty() {
593 return Err(builtin_error(
594 "meshgrid: at least one input vector is required",
595 ));
596 }
597 if axis_values.len() > 3 {
598 return Err(builtin_error(
599 "meshgrid: expected at most three input vectors",
600 ));
601 }
602
603 let force_host_for_integer_residency = axis_values.iter().any(|value| {
604 matches!(value, Value::GpuTensor(handle)
605 if runmat_accelerate_api::handle_integer_type(handle).is_some()
606 && runmat_accelerate_api::handle_storage(handle) == GpuTensorStorage::Real)
607 });
608
609 let mut axes = Vec::with_capacity(max(axis_values.len(), 2));
610 for (i, value) in axis_values.into_iter().enumerate() {
611 if let Value::GpuTensor(handle) = &value {
612 preferred_device_id.get_or_insert(handle.device_id);
613 }
614 let mut consumed_gpu = false;
615 let data = axis_from_value(value, i, &mut consumed_gpu).await?;
616 if data.is_complex {
617 crate::compatibility::ensure_builtin_extension_enabled(
618 &MESHGRID_COMPLEX_AXES_EXTENSION,
619 "meshgrid",
620 )?;
621 }
622 if consumed_gpu {
623 prefer_gpu = true;
624 }
625 axes.push(data);
626 }
627
628 if !prefer_gpu {
629 if let Some(max_len) = axes.iter().map(|axis| axis.len).max() {
630 if max_len > 0
631 && !axes.iter().any(|axis| {
632 axis.real_storage().is_some_and(real_storage_is_integer)
633 || matches!(axis.complex_storage(), Some(ComplexStorage::Integer(_)))
634 })
635 && sequence_gpu_preference(max_len, SequenceIntent::MeshAxis, false).prefer_gpu
636 {
637 prefer_gpu = true;
638 }
639 }
640 }
641 if force_host_for_integer_residency {
642 prefer_gpu = false;
643 }
644
645 let template = if let Some(proto) = like_proto {
646 OutputTemplate::Like(analyse_like_prototype(&proto)?)
647 } else {
648 OutputTemplate::Default
649 };
650
651 Ok(Self {
652 axes,
653 template,
654 prefer_gpu,
655 preferred_device_id,
656 })
657 }
658}
659
660#[derive(Clone)]
661enum OutputTemplate {
662 Default,
663 Like(PrototypeSpec),
664}
665
666#[derive(Clone)]
667struct PrototypeSpec {
668 residency: DevicePreference,
669 class: PrototypeClass,
670}
671
672#[derive(Clone, Copy, PartialEq, Eq)]
673enum PrototypeClass {
674 Real,
675 Complex,
676}
677
678#[derive(Clone, Copy)]
679enum DevicePreference {
680 Host,
681 Gpu(u32),
682}
683
684fn analyse_like_prototype(proto: &Value) -> crate::BuiltinResult<PrototypeSpec> {
685 match proto {
686 Value::GpuTensor(handle) => {
687 let class = if runmat_accelerate_api::handle_storage(handle)
688 == GpuTensorStorage::ComplexInterleaved
689 {
690 PrototypeClass::Complex
691 } else {
692 PrototypeClass::Real
693 };
694 Ok(PrototypeSpec {
695 residency: DevicePreference::Gpu(handle.device_id),
696 class,
697 })
698 }
699 Value::ComplexTensor(_) | Value::Complex(_, _) => Ok(PrototypeSpec {
700 residency: DevicePreference::Host,
701 class: PrototypeClass::Complex,
702 }),
703 Value::Tensor(_)
704 | Value::SparseTensor(_)
705 | Value::Num(_)
706 | Value::Int(_)
707 | Value::Bool(_)
708 | Value::LogicalArray(_) => Ok(PrototypeSpec {
709 residency: DevicePreference::Host,
710 class: PrototypeClass::Real,
711 }),
712 Value::CharArray(_) | Value::String(_) | Value::StringArray(_) => Err(builtin_error(
713 "meshgrid: prototypes must be numeric or gpuArray values",
714 )),
715 Value::Symbolic(_) | Value::SymbolicArray(_) => Err(builtin_error(
716 "meshgrid: prototypes must be numeric or gpuArray values",
717 )),
718 Value::Cell(_)
719 | Value::Struct(_)
720 | Value::ObjectArray(_)
721 | Value::Object(_)
722 | Value::HandleObject(_)
723 | Value::Listener(_)
724 | Value::FunctionHandle(_)
725 | Value::ExternalFunctionHandle(_)
726 | Value::MethodFunctionHandle(_)
727 | Value::BoundFunctionHandle { .. }
728 | Value::Closure(_)
729 | Value::ClassRef(_)
730 | Value::MException(_)
731 | Value::Future(_)
732 | Value::Task(_)
733 | Value::Pool(_)
734 | Value::Job(_)
735 | Value::Foreign(_)
736 | Value::OutputList(_) => Err(builtin_error("meshgrid: prototypes must be numeric arrays")),
737 }
738}
739
740#[derive(Clone)]
741struct AxisData {
742 len: usize,
743 is_complex: bool,
744 storage: AxisStorage,
745}
746
747#[derive(Clone)]
748enum AxisStorage {
749 Real(NumericStorage),
750 Complex(ComplexStorage),
751 GpuReal(GpuTensorHandle),
752}
753
754impl AxisData {
755 fn real_storage(&self) -> Option<&NumericStorage> {
756 match &self.storage {
757 AxisStorage::Real(storage) => Some(storage),
758 AxisStorage::Complex(_) | AxisStorage::GpuReal(_) => None,
759 }
760 }
761
762 fn complex_storage(&self) -> Option<&ComplexStorage> {
763 match &self.storage {
764 AxisStorage::Complex(storage) => Some(storage),
765 AxisStorage::Real(_) | AxisStorage::GpuReal(_) => None,
766 }
767 }
768
769 fn gpu_real(&self) -> Option<&GpuTensorHandle> {
770 match &self.storage {
771 AxisStorage::GpuReal(handle) => Some(handle),
772 AxisStorage::Real(_) | AxisStorage::Complex(_) => None,
773 }
774 }
775}
776
777fn real_storage_is_integer(storage: &NumericStorage) -> bool {
778 !matches!(
779 storage.numeric_dtype(),
780 NumericDType::F64 | NumericDType::F32
781 )
782}
783
784async fn axis_from_value(
785 value: Value,
786 index: usize,
787 prefer_gpu: &mut bool,
788) -> crate::BuiltinResult<AxisData> {
789 match value {
790 Value::Tensor(tensor) => axis_from_tensor(tensor, index),
791 Value::LogicalArray(logical) => {
792 let tensor = tensor::logical_to_tensor(&logical)?;
793 axis_from_tensor(tensor, index)
794 }
795 Value::Num(n) => Ok(AxisData {
796 len: 1,
797 is_complex: false,
798 storage: AxisStorage::Real(NumericStorage::F64(vec![n])),
799 }),
800 Value::Int(i) => Ok(AxisData {
801 len: 1,
802 is_complex: false,
803 storage: AxisStorage::Real(NumericStorage::from_integer_storage(
804 IntegerStorage::from_scalar(i),
805 )),
806 }),
807 Value::Bool(b) => Ok(AxisData {
808 len: 1,
809 is_complex: false,
810 storage: AxisStorage::Real(NumericStorage::F64(vec![if b { 1.0 } else { 0.0 }])),
811 }),
812 Value::Complex(re, im) => Ok(AxisData {
813 len: 1,
814 is_complex: im != 0.0,
815 storage: AxisStorage::Complex(ComplexStorage::F64(vec![(re, im)])),
816 }),
817 Value::ComplexTensor(tensor) => axis_from_complex_tensor(tensor, index),
818 Value::GpuTensor(handle) => {
819 if runmat_accelerate_api::handle_integer_type(&handle).is_some() {
820 if runmat_accelerate_api::handle_storage(&handle)
821 == GpuTensorStorage::ComplexInterleaved
822 {
823 *prefer_gpu = true;
824 let gathered =
825 gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
826 return match gathered {
827 Value::ComplexTensor(tensor) => axis_from_complex_tensor(tensor, index),
828 other => Err(builtin_error(format!(
829 "meshgrid: expected paired complex integer GPU axis, got {other:?}"
830 ))),
831 };
832 }
833 if runmat_accelerate_api::handle_is_explicit(&handle) {
834 return Err(builtin_error(
835 "meshgrid: integer gpuArray axes are not supported",
836 ));
837 }
838 let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
839 return match gathered {
840 Value::Tensor(tensor) => axis_from_tensor(tensor, index),
841 Value::Int(value) => Ok(AxisData {
842 len: 1,
843 is_complex: false,
844 storage: AxisStorage::Real(NumericStorage::from_integer_storage(
845 IntegerStorage::from_scalar(value),
846 )),
847 }),
848 other => Err(builtin_error(format!(
849 "meshgrid: expected integer GPU axis, got {other:?}"
850 ))),
851 };
852 }
853 let is_complex = runmat_accelerate_api::handle_storage(&handle)
854 == GpuTensorStorage::ComplexInterleaved;
855 if is_vector_shape(&handle.shape) && !is_complex {
858 *prefer_gpu = true;
859 return Ok(AxisData {
860 len: vector_len_from_shape(&handle.shape),
861 is_complex,
862 storage: AxisStorage::GpuReal(handle),
863 });
864 }
865
866 *prefer_gpu = true;
868 let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
869 match gathered {
870 Value::Tensor(tensor) => {
871 if is_vector_shape(&tensor.shape) {
872 *prefer_gpu = true;
873 }
874 axis_from_tensor(tensor, index)
875 }
876 Value::ComplexTensor(tensor) => {
877 if is_vector_shape(&tensor.shape) {
878 *prefer_gpu = true;
879 }
880 axis_from_complex_tensor(tensor, index)
881 }
882 other => Err(builtin_error(format!(
883 "meshgrid: input argument {} must be numeric, got {other:?}",
884 index + 1
885 ))),
886 }
887 }
888 other => Err(builtin_error(format!(
889 "meshgrid: input argument {} must be numeric, got {other:?}",
890 index + 1
891 ))),
892 }
893}
894
895fn axis_from_tensor(tensor: Tensor, index: usize) -> crate::BuiltinResult<AxisData> {
896 if is_vector_shape(&tensor.shape) {
897 let storage = tensor
898 .into_numeric_storage()
899 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
900 let len = storage.len();
901 return Ok(AxisData {
902 len,
903 is_complex: false,
904 storage: AxisStorage::Real(storage),
905 });
906 }
907
908 let shape = tensor.shape.clone();
914 if let Some(axis) = axis_from_meshgrid_matrix_real(tensor, index)? {
915 return Ok(axis);
916 }
917
918 Err(builtin_error(format!(
919 "meshgrid: input argument {} must be a vector (1xN or Nx1), got shape {:?}",
920 index + 1,
921 shape
922 )))
923}
924
925fn axis_from_complex_tensor(tensor: ComplexTensor, index: usize) -> crate::BuiltinResult<AxisData> {
926 if is_vector_shape(&tensor.shape) {
927 let len = tensor::complex_tensor_element_len(&tensor);
928 let is_complex = match tensor.integer_storage() {
929 Some(storage) => storage
930 .imag
931 .exact_values()
932 .into_iter()
933 .any(|value| !value.is_zero()),
934 None => tensor
935 .materialize_f64()
936 .iter()
937 .any(|&(_, imag)| !imag.is_nan() && imag != 0.0),
938 };
939 return Ok(AxisData {
940 len,
941 is_complex,
942 storage: AxisStorage::Complex(tensor.into_complex_storage()),
943 });
944 }
945
946 if tensor.integer_storage().is_some() {
947 return Err(builtin_error(format!(
948 "meshgrid: input argument {} must be a vector (1xN or Nx1), got shape {:?}",
949 index + 1,
950 tensor.shape
951 )));
952 }
953
954 if let Some(axis) = axis_from_meshgrid_matrix_complex(&tensor, index)? {
955 return Ok(axis);
956 }
957
958 Err(builtin_error(format!(
959 "meshgrid: input argument {} must be a vector (1xN or Nx1), got shape {:?}",
960 index + 1,
961 tensor.shape
962 )))
963}
964
965fn axis_from_meshgrid_matrix_real(
966 tensor: Tensor,
967 index: usize,
968) -> crate::BuiltinResult<Option<AxisData>> {
969 let (rows, cols) = match tensor.shape.as_slice() {
970 [r, c] => (*r, *c),
971 _ => return Ok(None),
972 };
973 if rows <= 1 || cols <= 1 {
974 return Ok(None);
975 }
976
977 let storage = tensor
978 .into_numeric_storage()
979 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
980
981 let expect_rows_constant = index == 0;
984
985 if expect_rows_constant {
986 for row in 1..rows {
987 for col in 0..cols {
988 if storage.value_at(row + rows * col) != storage.value_at(rows * col) {
989 return Ok(None);
990 }
991 }
992 }
993 let indices: Vec<usize> = (0..cols).map(|col| rows * col).collect();
994 let storage = storage
995 .gather(&indices)
996 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
997 return Ok(Some(AxisData {
998 len: cols,
999 is_complex: false,
1000 storage: AxisStorage::Real(storage),
1001 }));
1002 }
1003
1004 for col in 1..cols {
1005 for row in 0..rows {
1006 if storage.value_at(row + rows * col) != storage.value_at(row) {
1007 return Ok(None);
1008 }
1009 }
1010 }
1011 let indices: Vec<usize> = (0..rows).collect();
1012 let storage = storage
1013 .gather(&indices)
1014 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
1015 Ok(Some(AxisData {
1016 len: rows,
1017 is_complex: false,
1018 storage: AxisStorage::Real(storage),
1019 }))
1020}
1021
1022fn axis_from_meshgrid_matrix_complex(
1023 tensor: &ComplexTensor,
1024 index: usize,
1025) -> crate::BuiltinResult<Option<AxisData>> {
1026 let (rows, cols) = match tensor.shape.as_slice() {
1027 [r, c] => (*r, *c),
1028 _ => return Ok(None),
1029 };
1030 if rows <= 1 || cols <= 1 {
1031 return Ok(None);
1032 }
1033
1034 let values = tensor.materialize_f64();
1035 let expect_rows_constant = index == 0;
1036 if expect_rows_constant {
1037 if !matrix_rows_are_identical_complex(&values, rows, cols) {
1038 return Ok(None);
1039 }
1040 let indices: Vec<usize> = (0..cols).map(|col| rows * col).collect();
1041 let storage = tensor
1042 .complex_storage()
1043 .gather(&indices)
1044 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
1045 let is_complex = storage
1046 .materialize_f64()
1047 .iter()
1048 .any(|&(_, im)| !im.is_nan() && im != 0.0);
1049 return Ok(Some(AxisData {
1050 len: storage.len(),
1051 is_complex,
1052 storage: AxisStorage::Complex(storage),
1053 }));
1054 }
1055
1056 if !matrix_cols_are_identical_complex(&values, rows, cols) {
1057 return Ok(None);
1058 }
1059 let indices: Vec<usize> = (0..rows).collect();
1060 let storage = tensor
1061 .complex_storage()
1062 .gather(&indices)
1063 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
1064 let is_complex = storage
1065 .materialize_f64()
1066 .iter()
1067 .any(|&(_, im)| !im.is_nan() && im != 0.0);
1068 Ok(Some(AxisData {
1069 len: storage.len(),
1070 is_complex,
1071 storage: AxisStorage::Complex(storage),
1072 }))
1073}
1074
1075fn matrix_rows_are_identical_complex(values: &[(f64, f64)], rows: usize, cols: usize) -> bool {
1076 for row in 1..rows {
1077 for col in 0..cols {
1078 let idx0 = rows * col;
1079 let idx = row + rows * col;
1080 if values[idx] != values[idx0] {
1081 return false;
1082 }
1083 }
1084 }
1085 true
1086}
1087
1088fn matrix_cols_are_identical_complex(values: &[(f64, f64)], rows: usize, cols: usize) -> bool {
1089 for col in 1..cols {
1090 for row in 0..rows {
1091 let idx0 = row;
1092 let idx = row + rows * col;
1093 if values[idx] != values[idx0] {
1094 return false;
1095 }
1096 }
1097 }
1098 true
1099}
1100
1101fn is_vector_shape(shape: &[usize]) -> bool {
1102 if shape.is_empty() {
1103 return true;
1104 }
1105 let mut non_singleton = 0usize;
1106 for &dim in shape {
1107 if dim > 1 {
1108 non_singleton += 1;
1109 }
1110 }
1111 non_singleton <= 1
1112}
1113
1114fn vector_len_from_shape(shape: &[usize]) -> usize {
1115 if shape.is_empty() {
1116 return 1;
1117 }
1118 shape.iter().copied().max().unwrap_or(0)
1119}
1120
1121async fn axis_to_host_async(axis: &AxisData) -> crate::BuiltinResult<AxisData> {
1122 if axis.gpu_real().is_none() {
1123 return Ok(axis.clone());
1124 }
1125 let handle = axis.gpu_real().expect("checked gpu_real is_some");
1126 let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone())).await?;
1127 match gathered {
1129 Value::Tensor(tensor) => axis_from_tensor(tensor, 0),
1130 Value::ComplexTensor(tensor) => axis_from_complex_tensor(tensor, 0),
1131 Value::Num(n) => Ok(AxisData {
1132 len: 1,
1133 is_complex: false,
1134 storage: AxisStorage::Complex(ComplexStorage::F64(vec![(n, 0.0)])),
1135 }),
1136 Value::Complex(re, im) => Ok(AxisData {
1137 len: 1,
1138 is_complex: im != 0.0,
1139 storage: AxisStorage::Complex(ComplexStorage::F64(vec![(re, im)])),
1140 }),
1141 other => Err(builtin_error(format!(
1142 "meshgrid: expected numeric GPU axis, got {other:?}"
1143 ))),
1144 }
1145}
1146
1147fn try_meshgrid_gpu_from_vector_axes(
1148 x_axis: &AxisData,
1149 y_axis: &AxisData,
1150 z_axis: Option<&AxisData>,
1151) -> crate::BuiltinResult<Option<Vec<MeshgridOutput>>> {
1152 let Some(x_handle) = x_axis.gpu_real() else {
1153 return Ok(None);
1154 };
1155 let Some(y_handle) = y_axis.gpu_real() else {
1156 return Ok(None);
1157 };
1158
1159 let z_handle = match z_axis {
1160 Some(axis) => match axis.gpu_real() {
1161 Some(h) => Some(h),
1162 None => return Ok(None),
1163 },
1164 None => None,
1165 };
1166
1167 let Some(provider) = runmat_accelerate_api::provider_for_handle(x_handle) else {
1168 return Ok(None);
1169 };
1170 let Some(y_provider) = runmat_accelerate_api::provider_for_handle(y_handle) else {
1171 return Ok(None);
1172 };
1173 if y_provider.device_id() != provider.device_id() {
1174 return Ok(None);
1175 }
1176 if let Some(z) = z_handle {
1177 let Some(z_provider) = runmat_accelerate_api::provider_for_handle(z) else {
1178 return Ok(None);
1179 };
1180 if z_provider.device_id() != provider.device_id() {
1181 return Ok(None);
1182 }
1183 }
1184
1185 let nx = x_axis.len;
1186 let ny = y_axis.len;
1187 let nz = z_axis.map(|axis| axis.len).unwrap_or(1);
1188
1189 let x_row = provider
1191 .reshape(x_handle, &[1, nx])
1192 .map_err(|e| builtin_error(format!("meshgrid: reshape X failed: {e}")))?;
1193 let y_col = provider
1194 .reshape(y_handle, &[ny, 1])
1195 .map_err(|e| builtin_error(format!("meshgrid: reshape Y failed: {e}")))?;
1196
1197 let mut outputs = Vec::with_capacity(if z_handle.is_some() { 3 } else { 2 });
1198 if let Some(z) = z_handle {
1199 let x_base = provider
1200 .reshape(&x_row, &[1, nx, 1])
1201 .map_err(|e| builtin_error(format!("meshgrid: reshape X(3d) failed: {e}")))?;
1202 let y_base = provider
1203 .reshape(&y_col, &[ny, 1, 1])
1204 .map_err(|e| builtin_error(format!("meshgrid: reshape Y(3d) failed: {e}")))?;
1205
1206 let x_grid = provider
1207 .repmat(&x_base, &[ny, 1, nz])
1208 .map_err(|e| builtin_error(format!("meshgrid: repmat X failed: {e}")))?;
1209 let y_grid = provider
1210 .repmat(&y_base, &[1, nx, nz])
1211 .map_err(|e| builtin_error(format!("meshgrid: repmat Y failed: {e}")))?;
1212
1213 outputs.push(MeshgridOutput::Gpu(x_grid));
1214 outputs.push(MeshgridOutput::Gpu(y_grid));
1215 let z_axis_row = provider
1216 .reshape(z, &[1, nz])
1217 .map_err(|e| builtin_error(format!("meshgrid: reshape Z failed: {e}")))?;
1218 let z_base = provider
1219 .reshape(&z_axis_row, &[1, 1, nz])
1220 .map_err(|e| builtin_error(format!("meshgrid: reshape Z(3d) failed: {e}")))?;
1221 let z_grid = provider
1222 .repmat(&z_base, &[ny, nx, 1])
1223 .map_err(|e| builtin_error(format!("meshgrid: repmat Z failed: {e}")))?;
1224 outputs.push(MeshgridOutput::Gpu(z_grid));
1225 } else {
1226 let x_grid = provider
1227 .repmat(&x_row, &[ny, 1])
1228 .map_err(|e| builtin_error(format!("meshgrid: repmat X failed: {e}")))?;
1229 let y_grid = provider
1230 .repmat(&y_col, &[1, nx])
1231 .map_err(|e| builtin_error(format!("meshgrid: repmat Y failed: {e}")))?;
1232 outputs.push(MeshgridOutput::Gpu(x_grid));
1233 outputs.push(MeshgridOutput::Gpu(y_grid));
1234 }
1235
1236 Ok(Some(outputs))
1237}
1238
1239fn normalise_axes(axes: &[AxisData]) -> (AxisData, AxisData, Option<AxisData>) {
1240 match axes.len() {
1241 1 => {
1242 let x = axes[0].clone();
1243 (x.clone(), x, None)
1244 }
1245 2 => {
1246 let x = axes[0].clone();
1247 let y = axes[1].clone();
1248 (x, y, None)
1249 }
1250 3 => {
1251 let x = axes[0].clone();
1252 let y = axes[1].clone();
1253 let z = axes[2].clone();
1254 (x, y, Some(z))
1255 }
1256 _ => unreachable!(),
1257 }
1258}
1259
1260fn build_outputs(
1261 x_axis: &AxisData,
1262 y_axis: &AxisData,
1263 z_axis: Option<&AxisData>,
1264) -> Vec<GridOutput> {
1265 let nx = x_axis.len;
1266 let ny = y_axis.len;
1267 let nz = z_axis.map(|axis| axis.len).unwrap_or(1);
1268 let total = nx * ny * nz;
1269 let mut x_indices = Vec::with_capacity(total);
1270 let mut y_indices = Vec::with_capacity(total);
1271 let mut z_indices = z_axis.map(|_| Vec::with_capacity(total));
1272
1273 for k in 0..nz {
1274 for col in 0..nx {
1275 for row in 0..ny {
1276 x_indices.push(col);
1277 y_indices.push(row);
1278 if let Some(indices) = z_indices.as_mut() {
1279 indices.push(k);
1280 }
1281 }
1282 }
1283 }
1284
1285 let mut outputs = Vec::new();
1286 let base_shape = if nz == 1 {
1287 vec![ny, nx]
1288 } else {
1289 vec![ny, nx, nz]
1290 };
1291 outputs.push(GridOutput {
1292 shape: base_shape.clone(),
1293 storage: GridStorage::from_axis(x_axis),
1294 indices: x_indices,
1295 });
1296 outputs.push(GridOutput {
1297 shape: base_shape.clone(),
1298 storage: GridStorage::from_axis(y_axis),
1299 indices: y_indices,
1300 });
1301 if let (Some(axis), Some(indices)) = (z_axis, z_indices) {
1302 outputs.push(GridOutput {
1303 shape: base_shape,
1304 storage: GridStorage::from_axis(axis),
1305 indices,
1306 });
1307 }
1308 outputs
1309}
1310
1311struct GridOutput {
1312 shape: Vec<usize>,
1313 storage: GridStorage,
1314 indices: Vec<usize>,
1315}
1316
1317enum GridStorage {
1318 Real(NumericStorage),
1319 Complex(ComplexStorage),
1320}
1321
1322impl GridStorage {
1323 fn from_axis(axis: &AxisData) -> Self {
1324 match &axis.storage {
1325 AxisStorage::Real(storage) => Self::Real(storage.clone()),
1326 AxisStorage::Complex(storage) => Self::Complex(storage.clone()),
1327 AxisStorage::GpuReal(_) => {
1328 unreachable!("meshgrid host output construction requires a gathered axis")
1329 }
1330 }
1331 }
1332}
1333
1334impl GridOutput {
1335 fn to_value(
1336 &self,
1337 class: PrototypeClass,
1338 residency: DevicePreference,
1339 ) -> crate::BuiltinResult<Value> {
1340 match class {
1341 PrototypeClass::Real => self.to_real_value(residency),
1342 PrototypeClass::Complex => self.to_complex_value(residency),
1343 }
1344 }
1345
1346 fn to_real_value(&self, residency: DevicePreference) -> crate::BuiltinResult<Value> {
1347 let storage = match &self.storage {
1348 GridStorage::Real(prototype) => prototype
1349 .gather(&self.indices)
1350 .map_err(|e| builtin_error(format!("meshgrid: {e}")))?,
1351 GridStorage::Complex(storage) => {
1352 let storage = storage
1353 .gather(&self.indices)
1354 .map_err(|e| builtin_error(format!("meshgrid: {e}")))?;
1355 complex_storage_into_real(storage)?
1356 }
1357 };
1358 let tensor = Tensor::from_numeric_storage(storage, self.shape.clone())
1359 .map_err(|e| builtin_error(format!("meshgrid: {e}")))?;
1360 match residency {
1361 DevicePreference::Host => Ok(Value::Tensor(tensor)),
1362 DevicePreference::Gpu(device_id) => to_gpu_tensor_value(tensor, device_id),
1363 }
1364 }
1365
1366 fn to_complex_value(&self, residency: DevicePreference) -> crate::BuiltinResult<Value> {
1367 let storage = match &self.storage {
1368 GridStorage::Real(storage) => {
1369 if !matches!(
1370 storage.numeric_dtype(),
1371 NumericDType::F64 | NumericDType::F32
1372 ) {
1373 return Err(builtin_error(
1374 "meshgrid: complex output for typed integer axes is not supported",
1375 ));
1376 }
1377 match storage
1378 .gather(&self.indices)
1379 .map_err(|e| builtin_error(format!("meshgrid: {e}")))?
1380 {
1381 NumericStorage::F64(values) => {
1382 ComplexStorage::F64(values.into_iter().map(|value| (value, 0.0)).collect())
1383 }
1384 NumericStorage::F32(values) => {
1385 ComplexStorage::F32(values.into_iter().map(|value| (value, 0.0)).collect())
1386 }
1387 _ => unreachable!("integer storage was rejected above"),
1388 }
1389 }
1390 GridStorage::Complex(storage) => storage
1391 .gather(&self.indices)
1392 .map_err(|e| builtin_error(format!("meshgrid: {e}")))?,
1393 };
1394 let tensor = ComplexTensor::from_complex_storage(storage, self.shape.clone())
1395 .map_err(|e| builtin_error(format!("meshgrid: {e}")))?;
1396 match residency {
1397 DevicePreference::Host => Ok(complex_tensor_into_value(tensor)),
1398 DevicePreference::Gpu(device_id) => to_complex_gpu_tensor_value(tensor, device_id),
1399 }
1400 }
1401}
1402
1403fn complex_storage_into_real(storage: ComplexStorage) -> crate::BuiltinResult<NumericStorage> {
1404 match storage {
1405 ComplexStorage::F64(values) => values
1406 .into_iter()
1407 .map(|(real, imag)| {
1408 if imag == 0.0 {
1409 Ok(real)
1410 } else {
1411 Err(builtin_error(
1412 "meshgrid: cannot represent complex values in a real output",
1413 ))
1414 }
1415 })
1416 .collect::<crate::BuiltinResult<Vec<_>>>()
1417 .map(NumericStorage::F64),
1418 ComplexStorage::F32(values) => values
1419 .into_iter()
1420 .map(|(real, imag)| {
1421 if imag == 0.0 {
1422 Ok(real)
1423 } else {
1424 Err(builtin_error(
1425 "meshgrid: cannot represent complex values in a real output",
1426 ))
1427 }
1428 })
1429 .collect::<crate::BuiltinResult<Vec<_>>>()
1430 .map(NumericStorage::F32),
1431 ComplexStorage::Integer(storage) => {
1432 if storage
1433 .imag
1434 .exact_values()
1435 .into_iter()
1436 .any(|value| !value.is_zero())
1437 {
1438 return Err(builtin_error(
1439 "meshgrid: cannot represent complex values in a real output",
1440 ));
1441 }
1442 Ok(NumericStorage::from(storage.real))
1443 }
1444 }
1445}
1446
1447fn to_gpu_tensor_value(tensor: Tensor, device_id: u32) -> crate::BuiltinResult<Value> {
1448 let provider = runmat_accelerate_api::provider_for_device(device_id)
1449 .ok_or_else(|| builtin_error("meshgrid: no acceleration provider owns the GPU output"))?;
1450 gpu_helpers::upload_tensor(provider, &tensor)
1451 .map(Value::GpuTensor)
1452 .map_err(|error| builtin_error(format!("meshgrid: GPU output upload failed: {error}")))
1453}
1454
1455fn to_complex_gpu_tensor_value(
1456 tensor: ComplexTensor,
1457 device_id: u32,
1458) -> crate::BuiltinResult<Value> {
1459 let provider = runmat_accelerate_api::provider_for_device(device_id)
1460 .ok_or_else(|| builtin_error("meshgrid: no acceleration provider owns the GPU output"))?;
1461 gpu_helpers::upload_complex_tensor(provider, &tensor)
1462 .map(gpu_helpers::complex_gpu_value)
1463 .map_err(|error| builtin_error(format!("meshgrid: GPU output upload failed: {error}")))
1464}
1465
1466fn tensor_to_complex_tensor(tensor: Tensor) -> crate::BuiltinResult<ComplexTensor> {
1467 let shape = tensor.shape.clone();
1468 let storage = match tensor
1469 .into_numeric_storage()
1470 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?
1471 {
1472 NumericStorage::F64(values) => {
1473 ComplexStorage::F64(values.into_iter().map(|real| (real, 0.0)).collect())
1474 }
1475 NumericStorage::F32(values) => {
1476 ComplexStorage::F32(values.into_iter().map(|real| (real, 0.0)).collect())
1477 }
1478 storage => {
1479 let real = storage
1480 .into_integer_storage()
1481 .expect("non-floating meshgrid storage is integer");
1482 let imag = real.zeros_like(real.len());
1483 ComplexStorage::Integer(
1484 IntegerComplexStorage::new(real, imag)
1485 .map_err(|error| builtin_error(format!("meshgrid: {error}")))?,
1486 )
1487 }
1488 };
1489 ComplexTensor::from_complex_storage(storage, shape)
1490 .map_err(|e| builtin_error(format!("meshgrid: {e}")))
1491}
1492
1493fn tensor_to_complex_value(tensor: Tensor) -> crate::BuiltinResult<Value> {
1494 let complex = tensor_to_complex_tensor(tensor)?;
1495 Ok(complex_tensor_into_value(complex))
1496}
1497
1498enum MeshgridOutput {
1499 Host(GridOutput),
1500 Gpu(GpuTensorHandle),
1501}
1502
1503impl MeshgridOutput {
1504 async fn to_value(
1505 &self,
1506 class: PrototypeClass,
1507 residency: DevicePreference,
1508 ) -> crate::BuiltinResult<Value> {
1509 match self {
1510 MeshgridOutput::Host(host) => host.to_value(class, residency),
1511 MeshgridOutput::Gpu(handle) => match (class, residency) {
1512 (PrototypeClass::Real, DevicePreference::Gpu(_)) => {
1513 Ok(Value::GpuTensor(handle.clone()))
1514 }
1515 (PrototypeClass::Real, DevicePreference::Host) => {
1516 let tensor = gpu_helpers::gather_tensor_async(handle).await?;
1517 Ok(tensor::tensor_into_value(tensor))
1518 }
1519 (PrototypeClass::Complex, DevicePreference::Host) => {
1520 match gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone())).await?
1521 {
1522 Value::ComplexTensor(tensor) => Ok(complex_tensor_into_value(tensor)),
1523 Value::Complex(re, im) => Ok(Value::Complex(re, im)),
1524 Value::Tensor(tensor) => tensor_to_complex_value(tensor),
1525 Value::Num(n) => Ok(Value::Complex(n, 0.0)),
1526 other => Err(builtin_error(format!(
1527 "meshgrid: expected numeric GPU output, got {other:?}"
1528 ))),
1529 }
1530 }
1531 (PrototypeClass::Complex, DevicePreference::Gpu(device_id)) => {
1532 if runmat_accelerate_api::handle_storage(handle)
1533 == GpuTensorStorage::ComplexInterleaved
1534 {
1535 Ok(gpu_helpers::complex_gpu_value(handle.clone()))
1536 } else {
1537 let tensor = gpu_helpers::gather_tensor_async(handle).await?;
1538 to_complex_gpu_tensor_value(tensor_to_complex_tensor(tensor)?, device_id)
1539 }
1540 }
1541 },
1542 }
1543 }
1544}
1545
1546pub struct MeshgridEval {
1549 outputs: Vec<MeshgridOutput>,
1550 target_class: PrototypeClass,
1551 target_residency: DevicePreference,
1552}
1553
1554impl MeshgridEval {
1555 pub fn output_count(&self) -> usize {
1556 self.outputs.len()
1557 }
1558
1559 pub async fn first(&self) -> crate::BuiltinResult<Value> {
1560 self.outputs[0]
1561 .to_value(self.target_class, self.target_residency)
1562 .await
1563 }
1564
1565 pub async fn second(&self) -> crate::BuiltinResult<Value> {
1566 if self.outputs.len() < 2 {
1567 Err(builtin_error("meshgrid: second output unavailable"))
1568 } else {
1569 self.outputs[1]
1570 .to_value(self.target_class, self.target_residency)
1571 .await
1572 }
1573 }
1574
1575 pub async fn third(&self) -> crate::BuiltinResult<Value> {
1576 if self.outputs.len() < 3 {
1577 Err(builtin_error(
1578 "meshgrid: third output requested but no Z vector was supplied",
1579 ))
1580 } else {
1581 self.outputs[2]
1582 .to_value(self.target_class, self.target_residency)
1583 .await
1584 }
1585 }
1586}
1587
1588#[cfg(test)]
1589pub(crate) mod tests {
1590 use super::*;
1591 use crate::builtins::common::test_support;
1592 use futures::executor::block_on;
1593 #[cfg(feature = "wgpu")]
1594 use runmat_accelerate_api::AccelProvider;
1595
1596 use runmat_accelerate_api::HostTensorView;
1597 use runmat_value::IntValue;
1598
1599 fn evaluate(args: &[Value]) -> crate::BuiltinResult<MeshgridEval> {
1600 block_on(super::evaluate(args))
1601 }
1602
1603 fn eval_first(eval: &MeshgridEval) -> crate::BuiltinResult<Value> {
1604 block_on(eval.first())
1605 }
1606
1607 fn eval_second(eval: &MeshgridEval) -> crate::BuiltinResult<Value> {
1608 block_on(eval.second())
1609 }
1610
1611 fn eval_third(eval: &MeshgridEval) -> crate::BuiltinResult<Value> {
1612 block_on(eval.third())
1613 }
1614
1615 #[test]
1616 fn meshgrid_preserves_all_exact_real_integer_classes() {
1617 let storages = [
1618 IntegerStorage::I8(vec![-2, 7, 9]),
1619 IntegerStorage::I16(vec![-300, 400, 900]),
1620 IntegerStorage::I32(vec![i32::MIN, 0, i32::MAX]),
1621 IntegerStorage::I64(vec![i64::MIN, 0, i64::MAX]),
1622 IntegerStorage::U8(vec![0, 7, u8::MAX]),
1623 IntegerStorage::U16(vec![0, 700, u16::MAX]),
1624 IntegerStorage::U32(vec![0, 9_007_199, u32::MAX]),
1625 IntegerStorage::U64(vec![0, 9_007_199_254_740_993, u64::MAX]),
1626 ];
1627
1628 for storage in storages {
1629 let values = storage.exact_values();
1630 let axis = Tensor::new_integer(storage.clone(), vec![1, 3]).expect("axis");
1631 let eval = evaluate(&[Value::Tensor(axis)]).expect("meshgrid");
1632 let Value::Tensor(x) = eval_first(&eval).expect("X") else {
1633 panic!("expected real integer X output");
1634 };
1635 let Value::Tensor(y) = eval_second(&eval).expect("Y") else {
1636 panic!("expected real integer Y output");
1637 };
1638 assert_eq!(x.shape, vec![3, 3]);
1639 assert_eq!(
1640 x.integer_storage(),
1641 Some(
1642 &storage
1643 .from_exact_values_like(vec![
1644 values[0].clone(),
1645 values[0].clone(),
1646 values[0].clone(),
1647 values[1].clone(),
1648 values[1].clone(),
1649 values[1].clone(),
1650 values[2].clone(),
1651 values[2].clone(),
1652 values[2].clone(),
1653 ])
1654 .expect("expected X")
1655 )
1656 );
1657 assert_eq!(
1658 y.integer_storage(),
1659 Some(
1660 &storage
1661 .from_exact_values_like(vec![
1662 values[0].clone(),
1663 values[1].clone(),
1664 values[2].clone(),
1665 values[0].clone(),
1666 values[1].clone(),
1667 values[2].clone(),
1668 values[0].clone(),
1669 values[1].clone(),
1670 values[2].clone(),
1671 ])
1672 .expect("expected Y")
1673 )
1674 );
1675 }
1676
1677 let eval = evaluate(&[Value::Int(IntValue::U64(u64::MAX))]).expect("scalar meshgrid");
1678 let Value::Tensor(output) = eval_first(&eval).expect("scalar X") else {
1679 panic!("expected real integer scalar output");
1680 };
1681 assert_eq!(
1682 output.integer_storage(),
1683 Some(&IntegerStorage::U64(vec![u64::MAX]))
1684 );
1685 }
1686
1687 #[test]
1688 fn meshgrid_reads_typed_integer_axis_length_from_storage_without_mirror() {
1689 let axis = Tensor::new_integer(
1690 IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1691 vec![1, 2],
1692 )
1693 .expect("axis");
1694
1695 let eval = evaluate(&[Value::Tensor(axis)]).expect("meshgrid");
1696 let Value::Tensor(x) = eval_first(&eval).expect("X") else {
1697 panic!("expected real integer X output");
1698 };
1699 let Value::Tensor(y) = eval_second(&eval).expect("Y") else {
1700 panic!("expected real integer Y output");
1701 };
1702 assert_eq!(x.shape, vec![2, 2]);
1703 assert_eq!(
1704 x.integer_storage(),
1705 Some(&IntegerStorage::U64(vec![
1706 9_007_199_254_740_993,
1707 9_007_199_254_740_993,
1708 u64::MAX,
1709 u64::MAX,
1710 ]))
1711 );
1712 assert_eq!(
1713 y.integer_storage(),
1714 Some(&IntegerStorage::U64(vec![
1715 9_007_199_254_740_993,
1716 u64::MAX,
1717 9_007_199_254_740_993,
1718 u64::MAX,
1719 ]))
1720 );
1721 }
1722
1723 #[test]
1724 fn meshgrid_automatic_integer_residency_gathers_exactly_but_explicit_rejects() {
1725 test_support::with_test_provider(|provider| {
1726 let axis = Tensor::new_integer(
1727 IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1728 vec![1, 2],
1729 )
1730 .expect("axis");
1731 let automatic = gpu_helpers::upload_tensor(provider, &axis).expect("upload");
1732 let automatic =
1733 automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1734 let eval = evaluate(&[Value::GpuTensor(automatic)]).expect("automatic gather");
1735 let Value::Tensor(output) = eval_first(&eval).expect("host grid") else {
1736 panic!("automatic integer residency must return exact host output");
1737 };
1738 assert_eq!(
1739 output.integer_storage(),
1740 Some(&IntegerStorage::U64(vec![
1741 9_007_199_254_740_993,
1742 9_007_199_254_740_993,
1743 u64::MAX,
1744 u64::MAX,
1745 ]))
1746 );
1747
1748 let explicit = gpu_helpers::upload_tensor(provider, &axis).expect("upload");
1749 let explicit =
1750 explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1751 let error = match evaluate(&[Value::GpuTensor(explicit)]) {
1752 Ok(_) => panic!("explicit integer gpuArray must reject"),
1753 Err(error) => error,
1754 };
1755 assert!(error.message().contains("integer gpuArray axes"));
1756 });
1757 }
1758
1759 #[test]
1760 fn meshgrid_reads_typed_complex_integer_axis_length_from_storage_without_mirror() {
1761 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1762 let storage = IntegerComplexStorage::new(
1763 IntegerStorage::I16(vec![-3, 5]),
1764 IntegerStorage::I16(vec![7, -11]),
1765 )
1766 .unwrap();
1767 let axis = ComplexTensor::new_integer(storage, vec![1, 2]).expect("axis");
1768
1769 let eval = evaluate(&[Value::ComplexTensor(axis)]).expect("meshgrid");
1770 let Value::ComplexTensor(x) = eval_first(&eval).expect("X") else {
1771 panic!("expected typed complex integer X output");
1772 };
1773 assert_eq!(x.shape, vec![2, 2]);
1774 assert_eq!(
1775 x.integer_storage().cloned(),
1776 Some(
1777 IntegerComplexStorage::new(
1778 IntegerStorage::I16(vec![-3, -3, 5, 5]),
1779 IntegerStorage::I16(vec![7, 7, -11, -11]),
1780 )
1781 .unwrap()
1782 )
1783 );
1784 }
1785
1786 #[test]
1787 fn meshgrid_recovers_exact_integer_axes_from_coordinate_matrices() {
1788 let x = Tensor::new_integer(
1789 IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1790 vec![1, 2],
1791 )
1792 .expect("X axis");
1793 let y = Tensor::new_integer(IntegerStorage::I64(vec![i64::MIN, 7]), vec![2, 1])
1794 .expect("Y axis");
1795 let initial = evaluate(&[Value::Tensor(x), Value::Tensor(y)]).expect("initial meshgrid");
1796 let x_grid = eval_first(&initial).expect("X grid");
1797 let y_grid = eval_second(&initial).expect("Y grid");
1798 let recovered = evaluate(&[x_grid, y_grid]).expect("recovered meshgrid");
1799 let Value::Tensor(x_out) = eval_first(&recovered).expect("recovered X") else {
1800 panic!("expected real integer X output");
1801 };
1802 let Value::Tensor(y_out) = eval_second(&recovered).expect("recovered Y") else {
1803 panic!("expected real integer Y output");
1804 };
1805 assert_eq!(
1806 x_out.integer_storage(),
1807 Some(&IntegerStorage::U64(vec![
1808 9_007_199_254_740_993,
1809 9_007_199_254_740_993,
1810 u64::MAX,
1811 u64::MAX,
1812 ]))
1813 );
1814 assert_eq!(
1815 y_out.integer_storage(),
1816 Some(&IntegerStorage::I64(vec![i64::MIN, 7, i64::MIN, 7]))
1817 );
1818 }
1819
1820 #[test]
1821 fn meshgrid_does_not_lossily_promote_typed_integer_axes_to_complex() {
1822 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1823 let axis = Tensor::new_integer(
1824 IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1825 vec![1, 2],
1826 )
1827 .expect("axis");
1828 let prototype = ComplexTensor::new(vec![(1.0, 1.0)], vec![1, 1]).expect("prototype");
1829 let eval = evaluate(&[
1830 Value::Tensor(axis),
1831 Value::String("like".to_string()),
1832 Value::ComplexTensor(prototype),
1833 ])
1834 .expect("parse meshgrid");
1835 let error = eval_first(&eval).expect_err("typed integer complex output must reject");
1836 assert!(error
1837 .message()
1838 .contains("complex output for typed integer axes is not supported"));
1839 }
1840
1841 #[test]
1842 fn meshgrid_tensor_to_complex_reads_typed_integer_storage_exactly() {
1843 let tensor =
1844 Tensor::new_integer(IntegerStorage::I64(vec![-3, 5]), vec![1, 2]).expect("tensor");
1845
1846 let out = tensor_to_complex_tensor(tensor).expect("complex tensor");
1847 assert_eq!(out.shape, vec![1, 2]);
1848 assert_eq!(out.materialize_f64(), vec![(-3.0, 0.0), (5.0, 0.0)]);
1849 assert_eq!(
1850 out.integer_storage().cloned(),
1851 Some(
1852 IntegerComplexStorage::new(
1853 IntegerStorage::I64(vec![-3, 5]),
1854 IntegerStorage::I64(vec![0, 0]),
1855 )
1856 .expect("typed complex storage")
1857 )
1858 );
1859 }
1860
1861 fn tensor_from_vec(data: Vec<f64>, rows: usize, cols: usize) -> Tensor {
1862 Tensor::new(data, vec![rows, cols]).unwrap()
1863 }
1864
1865 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1866 #[test]
1867 fn meshgrid_single_input_duplicates_axis() {
1868 let x = tensor_from_vec(vec![-1.0, 0.0, 1.0], 1, 3);
1869 let eval = evaluate(&[Value::Tensor(x)]).expect("meshgrid");
1870 assert_eq!(eval.output_count(), 2);
1871 let x_out = test_support::gather(eval_first(&eval).expect("X")).expect("host");
1872 assert_eq!(x_out.shape, vec![3, 3]);
1873 assert_eq!(
1874 x_out.materialize_f64(),
1875 vec![-1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
1876 );
1877 let y_out = test_support::gather(eval_second(&eval).expect("Y")).expect("host");
1878 assert_eq!(y_out.shape, vec![3, 3]);
1879 assert_eq!(
1880 y_out.materialize_f64(),
1881 vec![-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0]
1882 );
1883 }
1884
1885 #[test]
1886 fn meshgrid_preserves_native_single_storage() {
1887 let x = Tensor::from_f32(vec![-1.0, 0.0, 1.0], vec![1, 3]).expect("single axis");
1888 let eval = evaluate(&[Value::Tensor(x)]).expect("meshgrid");
1889 let Value::Tensor(x_out) = eval_first(&eval).expect("X") else {
1890 panic!("expected tensor X output");
1891 };
1892 let Value::Tensor(y_out) = eval_second(&eval).expect("Y") else {
1893 panic!("expected tensor Y output");
1894 };
1895 assert_eq!(
1896 x_out.into_numeric_storage().expect("single X storage"),
1897 NumericStorage::F32(vec![-1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0])
1898 );
1899 assert_eq!(
1900 y_out.into_numeric_storage().expect("single Y storage"),
1901 NumericStorage::F32(vec![-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0])
1902 );
1903 }
1904
1905 #[test]
1906 fn meshgrid_like_follows_compatibility_mode() {
1907 let axis = || Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("axis");
1908 {
1909 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1910 let error =
1911 match evaluate(&[Value::Tensor(axis()), Value::from("like"), Value::Num(0.0)]) {
1912 Ok(_) => panic!("MATLAB mode rejects meshgrid like"),
1913 Err(error) => error,
1914 };
1915 assert_eq!(
1916 error.identifier(),
1917 Some("RunMat:compatibility:MeshgridLikeExtension")
1918 );
1919 }
1920 {
1921 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1922 evaluate(&[Value::Tensor(axis()), Value::from("like"), Value::Num(0.0)])
1923 .expect("RunMat mode accepts meshgrid like");
1924 }
1925 }
1926
1927 #[test]
1928 fn meshgrid_complex_axes_follow_compatibility_mode() {
1929 let axis = || ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![1, 2]).expect("axis");
1930 {
1931 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1932 let error = match evaluate(&[Value::ComplexTensor(axis())]) {
1933 Ok(_) => panic!("MATLAB mode rejects complex meshgrid axes"),
1934 Err(error) => error,
1935 };
1936 assert_eq!(
1937 error.identifier(),
1938 Some("RunMat:compatibility:MeshgridComplexAxesExtension")
1939 );
1940 }
1941 {
1942 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1943 evaluate(&[Value::ComplexTensor(axis())])
1944 .expect("RunMat mode accepts complex meshgrid axes");
1945 }
1946 }
1947
1948 #[test]
1949 fn meshgrid_type_infers_rank_from_axis_count() {
1950 let ctx = ResolveContext::new(Vec::new());
1951 assert_eq!(
1952 meshgrid_type(&[Type::Num, Type::Num], &ctx),
1953 Type::Tensor {
1954 shape: Some(vec![Some(1), Some(1)])
1955 }
1956 );
1957 assert_eq!(
1958 meshgrid_type(&[Type::Num, Type::Num, Type::Num], &ctx),
1959 Type::Tensor {
1960 shape: Some(vec![Some(1), Some(1), Some(1)])
1961 }
1962 );
1963 }
1964
1965 #[test]
1966 fn meshgrid_type_uses_vector_lengths() {
1967 let ctx = ResolveContext::new(Vec::new());
1968 assert_eq!(
1969 meshgrid_type(
1970 &[
1971 Type::Tensor {
1972 shape: Some(vec![Some(1), Some(201)]),
1973 },
1974 Type::Tensor {
1975 shape: Some(vec![Some(1), Some(101)]),
1976 },
1977 ],
1978 &ctx,
1979 ),
1980 Type::Tensor {
1981 shape: Some(vec![Some(101), Some(201)])
1982 }
1983 );
1984 }
1985
1986 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1987 #[test]
1988 fn meshgrid_rectangular_inputs() {
1989 let x = tensor_from_vec(vec![0.0, 0.5, 1.0], 1, 3);
1990 let y = tensor_from_vec(vec![10.0, 20.0], 2, 1);
1991 let eval = evaluate(&[Value::Tensor(x), Value::Tensor(y)]).expect("meshgrid");
1992 assert_eq!(eval.output_count(), 2);
1993 let x_out = test_support::gather(eval_first(&eval).expect("X")).expect("host");
1994 assert_eq!(x_out.shape, vec![2, 3]);
1995 assert_eq!(x_out.materialize_f64(), vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0]);
1996 let y_out = test_support::gather(eval_second(&eval).expect("Y")).expect("host");
1997 assert_eq!(y_out.shape, vec![2, 3]);
1998 assert_eq!(
1999 y_out.materialize_f64(),
2000 vec![10.0, 20.0, 10.0, 20.0, 10.0, 20.0]
2001 );
2002 }
2003
2004 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2005 #[test]
2006 fn meshgrid_three_inputs_volume() {
2007 let x = tensor_from_vec(vec![1.0, 2.0], 1, 2);
2008 let y = tensor_from_vec(vec![5.0, 6.0, 7.0], 3, 1);
2009 let z = tensor_from_vec(vec![0.0, 1.0], 1, 2);
2010 let eval =
2011 evaluate(&[Value::Tensor(x), Value::Tensor(y), Value::Tensor(z)]).expect("meshgrid");
2012 assert_eq!(eval.output_count(), 3);
2013 let x_out = test_support::gather(eval_first(&eval).expect("X")).expect("host");
2014 assert_eq!(x_out.shape, vec![3, 2, 2]);
2015 assert_eq!(
2016 x_out.materialize_f64(),
2017 vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
2018 );
2019 let z_out = test_support::gather(eval_third(&eval).expect("Z")).expect("host");
2020 assert_eq!(z_out.shape, vec![3, 2, 2]);
2021 assert_eq!(
2022 z_out.materialize_f64(),
2023 vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
2024 );
2025 }
2026
2027 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2028 #[test]
2029 fn meshgrid_like_keeps_gpu_residency() {
2030 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2031 test_support::with_test_provider(|provider| {
2032 let x = tensor_from_vec(vec![-1.0, 0.0, 1.0], 1, 3);
2033 let y = tensor_from_vec(vec![2.0, 4.0], 2, 1);
2034 let proto = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
2035 let proto_view = HostTensorView {
2036 data: &proto.materialize_f64(),
2037 shape: &proto.shape,
2038 };
2039 let proto_handle = provider.upload(&proto_view).expect("upload");
2040 let eval = evaluate(&[
2041 Value::Tensor(x),
2042 Value::Tensor(y),
2043 Value::from("like"),
2044 Value::GpuTensor(proto_handle),
2045 ])
2046 .expect("meshgrid");
2047 let x_value = eval_first(&eval).expect("X");
2048 assert!(matches!(x_value, Value::GpuTensor(_)));
2049 let gathered = test_support::gather(x_value).expect("gather");
2050 assert_eq!(gathered.shape, vec![2, 3]);
2051 });
2052 }
2053
2054 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2055 #[test]
2056 fn meshgrid_gpu_inputs_roundtrip() {
2057 test_support::with_test_provider(|provider| {
2058 let x = tensor_from_vec(vec![0.0, 0.5], 1, 2);
2059 let y = tensor_from_vec(vec![1.0, 2.0], 2, 1);
2060 let x_view = HostTensorView {
2061 data: &x.materialize_f64(),
2062 shape: &x.shape,
2063 };
2064 let y_view = HostTensorView {
2065 data: &y.materialize_f64(),
2066 shape: &y.shape,
2067 };
2068 let x_handle = provider.upload(&x_view).expect("upload");
2069 let y_handle = provider.upload(&y_view).expect("upload");
2070 let eval = evaluate(&[Value::GpuTensor(x_handle), Value::GpuTensor(y_handle)])
2071 .expect("meshgrid");
2072 assert!(matches!(eval_first(&eval).expect("X"), Value::GpuTensor(_)));
2073 assert!(matches!(
2074 eval_second(&eval).expect("Y"),
2075 Value::GpuTensor(_)
2076 ));
2077 });
2078 }
2079
2080 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2081 #[test]
2082 #[cfg(feature = "wgpu")]
2083 fn meshgrid_wgpu_matches_cpu() {
2084 let _guard = test_support::accel_test_lock();
2085 let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2086 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2087 ) else {
2088 return;
2089 };
2090
2091 let x = tensor_from_vec(vec![-1.0, 0.0, 1.0, 2.0], 1, 4);
2092 let y = tensor_from_vec(vec![5.0, 6.0], 2, 1);
2093
2094 let cpu_eval =
2095 evaluate(&[Value::Tensor(x.clone()), Value::Tensor(y.clone())]).expect("meshgrid cpu");
2096 let cpu_x =
2097 test_support::gather(eval_first(&cpu_eval).expect("X cpu")).expect("gather X cpu");
2098 let cpu_y =
2099 test_support::gather(eval_second(&cpu_eval).expect("Y cpu")).expect("gather Y cpu");
2100
2101 let x_view = HostTensorView {
2102 data: &x.materialize_f64(),
2103 shape: &x.shape,
2104 };
2105 let y_view = HostTensorView {
2106 data: &y.materialize_f64(),
2107 shape: &y.shape,
2108 };
2109 let x_gpu = provider.upload(&x_view).expect("upload x");
2110 let y_gpu = provider.upload(&y_view).expect("upload y");
2111
2112 let gpu_eval =
2113 evaluate(&[Value::GpuTensor(x_gpu), Value::GpuTensor(y_gpu)]).expect("meshgrid gpu");
2114 let gpu_x_value = eval_first(&gpu_eval).expect("X gpu");
2115 let gpu_y_value = eval_second(&gpu_eval).expect("Y gpu");
2116
2117 assert!(matches!(gpu_x_value, Value::GpuTensor(_)));
2118 assert!(matches!(gpu_y_value, Value::GpuTensor(_)));
2119
2120 let gathered_x = test_support::gather(gpu_x_value).expect("gather X gpu");
2121 let gathered_y = test_support::gather(gpu_y_value).expect("gather Y gpu");
2122
2123 assert_eq!(gathered_x.shape, cpu_x.shape);
2124 assert_eq!(gathered_x.materialize_f64(), cpu_x.materialize_f64());
2125 assert_eq!(gathered_y.shape, cpu_y.shape);
2126 assert_eq!(gathered_y.materialize_f64(), cpu_y.materialize_f64());
2127 }
2128
2129 #[test]
2130 #[cfg(feature = "wgpu")]
2131 fn meshgrid_wgpu_integer_residency_obeys_automatic_and_explicit_policy() {
2132 let _guard = test_support::accel_test_lock();
2133 let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2134 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2135 ) else {
2136 return;
2137 };
2138 let axis = Tensor::new_integer(
2139 IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
2140 vec![1, 2],
2141 )
2142 .expect("axis");
2143
2144 let automatic = gpu_helpers::upload_tensor(provider, &axis).expect("automatic upload");
2145 let automatic =
2146 automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
2147 let eval = evaluate(&[Value::GpuTensor(automatic)]).expect("automatic gather");
2148 let Value::Tensor(output) = eval_first(&eval).expect("exact host output") else {
2149 panic!("automatic integer residency must become host output");
2150 };
2151 assert_eq!(
2152 output.integer_storage(),
2153 Some(&IntegerStorage::U64(vec![
2154 9_007_199_254_740_993,
2155 9_007_199_254_740_993,
2156 u64::MAX,
2157 u64::MAX,
2158 ]))
2159 );
2160
2161 let explicit = gpu_helpers::upload_tensor(provider, &axis).expect("explicit upload");
2162 let explicit =
2163 explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
2164 let error = match evaluate(&[Value::GpuTensor(explicit)]) {
2165 Ok(_) => panic!("explicit integer gpuArray must reject"),
2166 Err(error) => error,
2167 };
2168 assert!(error.message().contains("integer gpuArray axes"));
2169 }
2170
2171 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2172 #[test]
2173 fn meshgrid_complex_inputs_produce_complex_outputs() {
2174 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2175 let complex = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
2176 let eval = evaluate(&[Value::ComplexTensor(complex)]).expect("meshgrid");
2177 let x_value = eval_first(&eval).expect("X");
2178 match x_value {
2179 Value::ComplexTensor(ct) => {
2180 assert_eq!(ct.shape, vec![2, 2]);
2181 }
2182 Value::Complex(_, _) => {}
2183 other => panic!("expected complex output, got {other:?}"),
2184 }
2185 }
2186
2187 #[test]
2188 fn meshgrid_preserves_native_complex_single_storage() {
2189 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2190 let complex = ComplexTensor::from_f32(vec![(1.25, -2.5), (3.75, 4.5)], vec![1, 2]).unwrap();
2191 let eval = evaluate(&[Value::ComplexTensor(complex)]).expect("meshgrid");
2192 let Value::ComplexTensor(output) = eval_first(&eval).expect("X") else {
2193 panic!("expected complex single output");
2194 };
2195 assert_eq!(output.numeric_dtype(), NumericDType::F32);
2196 assert_eq!(output.shape, vec![2, 2]);
2197 assert_eq!(
2198 output.as_f32_slice(),
2199 Some(&[(1.25, -2.5), (1.25, -2.5), (3.75, 4.5), (3.75, 4.5),][..])
2200 );
2201 }
2202
2203 #[test]
2204 fn meshgrid_like_complex_gpu_prototype_keeps_complex_residency() {
2205 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2206 test_support::with_test_provider(|provider| {
2207 let x = tensor_from_vec(vec![1.0, 2.0], 1, 2);
2208 let proto = ComplexTensor::new(vec![(0.0, 1.0)], vec![1, 1]).unwrap();
2209 let proto_handle =
2210 gpu_helpers::upload_complex_tensor(provider, &proto).expect("upload");
2211
2212 let eval = evaluate(&[
2213 Value::Tensor(x),
2214 Value::from("like"),
2215 Value::GpuTensor(proto_handle),
2216 ])
2217 .expect("meshgrid");
2218 let x_value = eval_first(&eval).expect("X");
2219 let Value::GpuTensor(handle) = x_value else {
2220 panic!("expected complex gpu tensor");
2221 };
2222 assert_eq!(
2223 runmat_accelerate_api::handle_storage(&handle),
2224 GpuTensorStorage::ComplexInterleaved
2225 );
2226 let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle)))
2227 .expect("gather");
2228 let Value::ComplexTensor(tensor) = gathered else {
2229 panic!("expected complex tensor");
2230 };
2231 assert_eq!(tensor.shape, vec![2, 2]);
2232 assert_eq!(
2233 tensor.materialize_f64(),
2234 vec![(1.0, 0.0), (1.0, 0.0), (2.0, 0.0), (2.0, 0.0)]
2235 );
2236 });
2237 }
2238
2239 #[test]
2240 fn meshgrid_complex_gpu_axis_stays_resident() {
2241 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2242 test_support::with_test_provider(|provider| {
2243 let axis = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
2244 let axis_handle = gpu_helpers::upload_complex_tensor(provider, &axis).expect("upload");
2245
2246 let eval = evaluate(&[Value::GpuTensor(axis_handle)]).expect("meshgrid");
2247 let x_value = eval_first(&eval).expect("X");
2248 let Value::GpuTensor(handle) = x_value else {
2249 panic!("expected complex gpu tensor");
2250 };
2251 assert_eq!(
2252 runmat_accelerate_api::handle_storage(&handle),
2253 GpuTensorStorage::ComplexInterleaved
2254 );
2255 let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle)))
2256 .expect("gather");
2257 let Value::ComplexTensor(tensor) = gathered else {
2258 panic!("expected complex tensor");
2259 };
2260 assert_eq!(tensor.shape, vec![2, 2]);
2261 assert_eq!(
2262 tensor.materialize_f64(),
2263 vec![(1.0, 1.0), (1.0, 1.0), (2.0, -1.0), (2.0, -1.0)]
2264 );
2265 });
2266 }
2267
2268 #[test]
2269 fn meshgrid_paired_complex_integer_gpu_axis_stays_resident_and_exact() {
2270 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2271 test_support::with_test_provider(|provider| {
2272 let wide = (1_u64 << 53) + 1;
2273 let axis = ComplexTensor::new_integer(
2274 IntegerComplexStorage::new(
2275 IntegerStorage::U64(vec![wide, u64::MAX]),
2276 IntegerStorage::U64(vec![u64::MAX, wide]),
2277 )
2278 .expect("paired axis storage"),
2279 vec![1, 2],
2280 )
2281 .expect("paired axis");
2282 let axis_handle =
2283 gpu_helpers::upload_complex_tensor(provider, &axis).expect("upload paired axis");
2284
2285 let eval = evaluate(&[Value::GpuTensor(axis_handle.clone())]).expect("meshgrid");
2286 let Value::GpuTensor(output) = eval_first(&eval).expect("X") else {
2287 panic!("expected resident paired grid");
2288 };
2289 assert_eq!(
2290 runmat_accelerate_api::handle_storage(&output),
2291 GpuTensorStorage::ComplexInterleaved
2292 );
2293 assert_eq!(
2294 runmat_accelerate_api::handle_integer_type(&output),
2295 Some(runmat_accelerate_api::IntegerElementType::U64)
2296 );
2297 assert!(runmat_accelerate_api::provider_for_handle(&output)
2298 .is_some_and(|owner| std::ptr::eq(owner, provider)));
2299 let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(
2300 output.clone(),
2301 )))
2302 .expect("gather paired grid");
2303 let Value::ComplexTensor(gathered) = gathered else {
2304 panic!("expected paired tensor");
2305 };
2306 let storage = gathered.integer_storage().expect("paired storage");
2307 assert_eq!(
2308 storage.real,
2309 IntegerStorage::U64(vec![wide, wide, u64::MAX, u64::MAX])
2310 );
2311 assert_eq!(
2312 storage.imag,
2313 IntegerStorage::U64(vec![u64::MAX, u64::MAX, wide, wide])
2314 );
2315 provider.free(&axis_handle).expect("free axis");
2316 provider.free(&output).expect("free output");
2317 });
2318 }
2319
2320 #[test]
2321 #[cfg(feature = "wgpu")]
2322 fn meshgrid_wgpu_complex_axis_matches_cpu_and_stays_resident() {
2323 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2324 let _guard = test_support::accel_test_lock();
2325 let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2326 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2327 ) else {
2328 return;
2329 };
2330
2331 let axis = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
2332 let cpu_eval = evaluate(&[Value::ComplexTensor(axis.clone())]).expect("meshgrid cpu");
2333 let cpu_x = match eval_first(&cpu_eval).expect("X cpu") {
2334 Value::ComplexTensor(tensor) => tensor,
2335 other => panic!("expected cpu complex tensor, got {other:?}"),
2336 };
2337
2338 let axis_handle = gpu_helpers::upload_complex_tensor(provider, &axis).expect("upload");
2339 let gpu_eval = evaluate(&[Value::GpuTensor(axis_handle)]).expect("meshgrid gpu");
2340 let gpu_x = eval_first(&gpu_eval).expect("X gpu");
2341 let Value::GpuTensor(handle) = gpu_x else {
2342 panic!("expected complex gpu tensor");
2343 };
2344 assert_eq!(
2345 runmat_accelerate_api::handle_storage(&handle),
2346 GpuTensorStorage::ComplexInterleaved
2347 );
2348 let gathered =
2349 block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle))).expect("gather");
2350 let Value::ComplexTensor(gpu_tensor) = gathered else {
2351 panic!("expected complex tensor");
2352 };
2353 assert_eq!(gpu_tensor.shape, cpu_x.shape);
2354 assert_eq!(gpu_tensor.materialize_f64(), cpu_x.materialize_f64());
2355 }
2356
2357 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2358 #[test]
2359 fn meshgrid_like_host_prototype() {
2360 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2361 let x = tensor_from_vec(vec![1.0, 2.0], 1, 2);
2362 let eval =
2363 evaluate(&[Value::Tensor(x), Value::from("like"), Value::Num(0.0)]).expect("meshgrid");
2364 let x_out = eval_first(&eval).expect("X");
2365 assert!(matches!(x_out, Value::Tensor(_) | Value::Num(_)));
2366 }
2367}