numrs2 0.3.3

A Rust implementation inspired by NumPy for numerical computing (NumRS2)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
use crate::array::Array;
use crate::error::{NumRs2Error, Result};
use scirs2_core::ndarray::Axis;

/// Enum to handle both single axis and multiple axes for concatenation
pub enum AxisArg {
    Single(usize),
    Multiple(Vec<usize>),
}

impl From<usize> for AxisArg {
    fn from(axis: usize) -> Self {
        AxisArg::Single(axis)
    }
}

impl From<&[usize]> for AxisArg {
    fn from(axes: &[usize]) -> Self {
        AxisArg::Multiple(axes.to_vec())
    }
}

impl From<Vec<usize>> for AxisArg {
    fn from(axes: Vec<usize>) -> Self {
        AxisArg::Multiple(axes)
    }
}

/// Join a sequence of arrays along an existing axis
pub fn concatenate<T: Clone>(arrays: &[&Array<T>], axis: impl Into<AxisArg>) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation(
            "No arrays to concatenate".into(),
        ));
    }

    match axis.into() {
        AxisArg::Single(axis) => concatenate_single_axis(arrays, axis),
        AxisArg::Multiple(axes) => concatenate_multiple_axes(arrays, &axes),
    }
}

/// Concatenate arrays along a single axis
fn concatenate_single_axis<T: Clone>(arrays: &[&Array<T>], axis: usize) -> Result<Array<T>> {
    let first_shape = arrays[0].shape();

    if axis >= first_shape.len() {
        return Err(NumRs2Error::DimensionMismatch(format!(
            "Axis {} out of bounds for array of dimension {}",
            axis,
            first_shape.len()
        )));
    }

    // Check that all arrays have compatible shapes
    for (_i, arr) in arrays.iter().enumerate().skip(1) {
        let shape = arr.shape();

        if shape.len() != first_shape.len() {
            return Err(NumRs2Error::ShapeMismatch {
                expected: first_shape.clone(),
                actual: shape,
            });
        }

        for (j, (&s1, &s2)) in first_shape.iter().zip(shape.iter()).enumerate() {
            if j != axis && s1 != s2 {
                return Err(NumRs2Error::ShapeMismatch {
                    expected: first_shape.clone(),
                    actual: shape,
                });
            }
        }
    }

    // Calculate the output shape
    let mut output_shape = first_shape.clone();
    output_shape[axis] = arrays.iter().map(|arr| arr.shape()[axis]).sum();

    // Create views for all arrays to concatenate
    let views: Result<Vec<_>> = arrays.iter().map(|arr| Ok(arr.array().view())).collect();

    let views = views?;

    // Use ndarray's concatenate function
    let result = scirs2_core::ndarray::concatenate(Axis(axis), &views).map_err(|e| {
        NumRs2Error::InvalidOperation(format!("Failed to concatenate arrays: {}", e))
    })?;

    // Convert the result back to our Array type
    Ok(Array::from_ndarray(result))
}

/// Concatenate arrays along multiple axes
fn concatenate_multiple_axes<T: Clone>(arrays: &[&Array<T>], axes: &[usize]) -> Result<Array<T>> {
    if axes.is_empty() {
        return Err(NumRs2Error::InvalidOperation(
            "No axes provided for concatenation".into(),
        ));
    }

    // Process one axis at a time
    let mut result = arrays[0].clone();

    // We'll concatenate along each axis, starting with only the first array in the sequence
    for (i, &axis) in axes.iter().enumerate() {
        // For the first concatenation, we need to concatenate all arrays
        if i == 0 {
            result = concatenate_single_axis(arrays, axis)?;
        } else {
            // For subsequent concatenations, we would need arrays with matching shapes
            // This is a simplified implementation - for better user experience,
            // we should allow arrays to be broadcast to the correct shape
            result = concatenate_single_axis(&[&result, arrays[1]], axis)?;
        }
    }

    Ok(result)
}

/// Join a sequence of arrays along a new axis
pub fn stack<T: Clone>(arrays: &[&Array<T>], axis: usize) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation("No arrays to stack".into()));
    }

    let first_shape = arrays[0].shape();

    if axis > first_shape.len() {
        return Err(NumRs2Error::DimensionMismatch(format!(
            "Axis {} out of bounds for array of dimension {}",
            axis,
            first_shape.len()
        )));
    }

    // Check that all arrays have the same shape
    for arr in arrays.iter().skip(1) {
        let shape = arr.shape();

        if shape != first_shape {
            return Err(NumRs2Error::ShapeMismatch {
                expected: first_shape.clone(),
                actual: shape,
            });
        }
    }

    // Calculate the output shape - insert a new dimension at the specified axis
    let mut output_shape = first_shape.clone();
    output_shape.insert(axis, arrays.len());

    // For a complete implementation, we would use ndarray's stack operation
    // For now, we'll use a simple implementation based on concatenate

    // First reshape each array to add a new dimension
    let mut reshaped_arrays = Vec::with_capacity(arrays.len());
    for &arr in arrays {
        let mut new_shape = first_shape.clone();
        new_shape.insert(axis, 1);
        let reshaped = arr.reshape(&new_shape);
        reshaped_arrays.push(reshaped);
    }

    // Then concatenate along the new dimension
    let mut result_refs: Vec<&Array<T>> = Vec::with_capacity(reshaped_arrays.len());
    for arr in &reshaped_arrays {
        result_refs.push(arr);
    }

    concatenate(&result_refs, axis)
}

/// Construct a block array from nested lists of blocks
pub fn block<T: Clone>(blocks: &[Vec<&Array<T>>]) -> Result<Array<T>> {
    if blocks.is_empty() {
        return Err(NumRs2Error::InvalidOperation(
            "Empty block structure".into(),
        ));
    }

    // Normalize dimensions of arrays within each row
    let mut processed_rows = Vec::with_capacity(blocks.len());

    for (row_idx, row) in blocks.iter().enumerate() {
        if row.is_empty() {
            return Err(NumRs2Error::InvalidOperation(format!(
                "Empty row at index {} in block structure",
                row_idx
            )));
        }

        // Process each array in the row to ensure compatible dimensions
        let mut processed_row = Vec::with_capacity(row.len());

        // Determine the maximum number of dimensions in this row
        let max_ndim = row.iter().map(|arr| arr.ndim()).max().unwrap_or(1);

        for arr in row.iter() {
            let arr_ndim = arr.ndim();

            if arr_ndim < max_ndim {
                // Reshape to add missing dimensions with size 1
                let mut new_shape = arr.shape().to_vec();
                while new_shape.len() < max_ndim {
                    // For 1D arrays, add dimension at the end to make it a column vector
                    if arr_ndim == 1 {
                        new_shape.push(1);
                    } else {
                        // Otherwise add dimensions at the beginning
                        new_shape.insert(0, 1);
                    }
                }
                processed_row.push(arr.reshape(&new_shape));
            } else {
                processed_row.push((*arr).clone());
            }
        }

        processed_rows.push(processed_row);
    }

    // Now we can proceed with concatenation
    let mut rows_result = Vec::with_capacity(processed_rows.len());

    for row in &processed_rows {
        // Make sure all arrays in this row have the same number of dimensions
        let ndim = row[0].ndim();

        if !row.iter().all(|arr| arr.ndim() == ndim) {
            return Err(NumRs2Error::InvalidOperation(
                "Arrays in each row must have the same number of dimensions".into(),
            ));
        }

        // For single arrays in a row, we don't need to concatenate
        if row.len() == 1 {
            rows_result.push(row[0].clone());
            continue;
        }

        // Concatenate along the last axis (which would be 1 for 2D arrays)
        let row_refs: Vec<&Array<T>> = row.iter().collect();

        // Use the last axis for concatenation
        let axis = ndim - 1;
        let concatenated_row = concatenate(&row_refs, axis)?;
        rows_result.push(concatenated_row);
    }

    // For a single row, we're done
    if rows_result.len() == 1 {
        return Ok(rows_result[0].clone());
    }

    // Now concatenate the rows - verify they have compatible dimensions
    let row_ndim = rows_result[0].ndim();

    if !rows_result.iter().all(|arr| arr.ndim() == row_ndim) {
        return Err(NumRs2Error::InvalidOperation(
            "All rows must have the same number of dimensions after processing".into(),
        ));
    }

    // Concatenate rows along the first axis (which would be 0 for 2D arrays)
    let row_refs: Vec<&Array<T>> = rows_result.iter().collect();
    let axis = 0;
    concatenate(&row_refs, axis)
}

/// Translate slice objects to concatenation along the first axis
pub fn r_<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    concatenate(arrays, 0)
}

/// Translate slice objects to concatenation along the second axis  
pub fn c_<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation(
            "No arrays to concatenate".into(),
        ));
    }

    // Check if all arrays are 1D
    let all_1d = arrays.iter().all(|arr| arr.ndim() == 1);

    if all_1d {
        // For 1D arrays, convert them to column vectors and then concatenate along axis 1
        let mut column_vectors = Vec::with_capacity(arrays.len());
        for &arr in arrays {
            let shape = arr.shape();
            let new_shape = vec![shape[0], 1]; // Convert [n] to [n, 1]
            column_vectors.push(arr.reshape(&new_shape));
        }

        // Create references for concatenation
        let column_refs: Vec<&Array<T>> = column_vectors.iter().collect();
        concatenate(&column_refs, 1)
    } else {
        // For arrays with 2+ dimensions, concatenate directly along axis 1
        concatenate(arrays, 1)
    }
}

/// Stack arrays in sequence vertically (row wise).
///
/// This is equivalent to concatenation along the first axis after 1-D arrays
/// of shape (N,) have been reshaped to (1,N). Rebuilds arrays divided by vsplit.
///
/// # Arguments
///
/// * `arrays` - A slice of arrays to stack
///
/// # Returns
///
/// The array formed by stacking the given arrays
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
///
/// let a = Array::from_vec(vec![1, 2, 3]);
/// let b = Array::from_vec(vec![4, 5, 6]);
/// let stacked = vstack(&[&a, &b]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![2, 3]);
/// assert_eq!(stacked.to_vec(), vec![1, 2, 3, 4, 5, 6]);
///
/// let c = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let stacked = vstack(&[&c, &d]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![4, 2]);
/// ```
pub fn vstack<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation("No arrays to stack".into()));
    }

    // Convert 1D arrays to 2D arrays with shape (1, N)
    let mut reshaped_arrays = Vec::with_capacity(arrays.len());
    for &arr in arrays {
        if arr.ndim() == 1 {
            let shape = arr.shape();
            reshaped_arrays.push(arr.reshape(&[1, shape[0]]));
        } else {
            reshaped_arrays.push(arr.clone());
        }
    }

    // Create references for concatenation
    let array_refs: Vec<&Array<T>> = reshaped_arrays.iter().collect();
    concatenate(&array_refs, 0)
}

/// Stack arrays in sequence horizontally (column wise).
///
/// This is equivalent to concatenation along the second axis, except for 1-D
/// arrays where it concatenates along the first axis.
///
/// # Arguments
///
/// * `arrays` - A slice of arrays to stack
///
/// # Returns
///
/// The array formed by stacking the given arrays
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
///
/// let a = Array::from_vec(vec![1, 2, 3]);
/// let b = Array::from_vec(vec![4, 5, 6]);
/// let stacked = hstack(&[&a, &b]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![6]);
/// assert_eq!(stacked.to_vec(), vec![1, 2, 3, 4, 5, 6]);
///
/// let c = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let stacked = hstack(&[&c, &d]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![2, 4]);
/// ```
pub fn hstack<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation("No arrays to stack".into()));
    }

    // Check if all arrays are 1D
    let all_1d = arrays.iter().all(|arr| arr.ndim() == 1);

    if all_1d {
        // For 1D arrays, concatenate along axis 0
        concatenate(arrays, 0)
    } else {
        // For arrays with 2+ dimensions, concatenate along axis 1
        concatenate(arrays, 1)
    }
}

/// Stack arrays in sequence depth wise (along third axis).
///
/// This is equivalent to concatenation along the third axis after 2-D arrays
/// of shape (M,N) have been reshaped to (M,N,1) and 1-D arrays of shape (N,)
/// have been reshaped to (1,N,1).
///
/// # Arguments
///
/// * `arrays` - A slice of arrays to stack
///
/// # Returns
///
/// The array formed by stacking the given arrays
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
///
/// let a = Array::from_vec(vec![1, 2, 3]);
/// let b = Array::from_vec(vec![4, 5, 6]);
/// let stacked = dstack(&[&a, &b]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![1, 3, 2]);
///
/// let c = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let stacked = dstack(&[&c, &d]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![2, 2, 2]);
/// ```
pub fn dstack<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation("No arrays to stack".into()));
    }

    // Reshape arrays to have at least 3 dimensions
    let mut reshaped_arrays = Vec::with_capacity(arrays.len());
    for &arr in arrays {
        let shape = arr.shape();
        let reshaped = match arr.ndim() {
            1 => {
                // 1D array: reshape from (N,) to (1, N, 1)
                arr.reshape(&[1, shape[0], 1])
            }
            2 => {
                // 2D array: reshape from (M, N) to (M, N, 1)
                arr.reshape(&[shape[0], shape[1], 1])
            }
            _ => {
                // 3D+ array: use as is
                arr.clone()
            }
        };
        reshaped_arrays.push(reshaped);
    }

    // Create references for concatenation
    let array_refs: Vec<&Array<T>> = reshaped_arrays.iter().collect();
    concatenate(&array_refs, 2)
}

/// Stack arrays as rows.
///
/// This is an alias for vstack. Stack arrays in sequence vertically (row wise).
///
/// # Arguments
///
/// * `arrays` - A slice of arrays to stack
///
/// # Returns
///
/// The array formed by stacking the given arrays
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
///
/// let a = Array::from_vec(vec![1, 2, 3]);
/// let b = Array::from_vec(vec![4, 5, 6]);
/// let stacked = row_stack(&[&a, &b]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![2, 3]);
/// assert_eq!(stacked.to_vec(), vec![1, 2, 3, 4, 5, 6]);
/// ```
pub fn row_stack<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    vstack(arrays)
}

/// Stack 1-D arrays as columns into a 2-D array.
///
/// Take a sequence of 1-D arrays and stack them as columns to make a single 2-D array.
/// 2-D arrays are stacked as-is, similar to hstack.
///
/// # Arguments
///
/// * `arrays` - A slice of arrays to stack
///
/// # Returns
///
/// 2-D array formed by stacking the given arrays
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
/// use numrs2::array_ops::joining::column_stack;
///
/// // Stack 1-D arrays as columns
/// let a = Array::from_vec(vec![1, 2, 3]);
/// let b = Array::from_vec(vec![4, 5, 6]);
/// let stacked = column_stack(&[&a, &b]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![3, 2]);
/// assert_eq!(stacked.to_vec(), vec![1, 2, 3, 4, 5, 6]);
///
/// // 2-D arrays are stacked horizontally
/// let c = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let stacked = column_stack(&[&c, &d]).expect("operation should succeed");
/// assert_eq!(stacked.shape(), vec![2, 4]);
/// assert_eq!(stacked.to_vec(), vec![1, 3, 2, 4, 5, 7, 6, 8]);
/// ```
pub fn column_stack<T: Clone>(arrays: &[&Array<T>]) -> Result<Array<T>> {
    if arrays.is_empty() {
        return Err(NumRs2Error::InvalidOperation("No arrays to stack".into()));
    }

    // Check if all arrays are 1D
    let all_1d = arrays.iter().all(|arr| arr.ndim() == 1);

    if all_1d {
        // For 1D arrays, reshape them to column vectors and then concatenate horizontally
        let mut column_vectors = Vec::with_capacity(arrays.len());
        for &arr in arrays {
            let shape = arr.shape();
            let new_shape = vec![shape[0], 1]; // Convert [n] to [n, 1]
            column_vectors.push(arr.reshape(&new_shape));
        }

        // Create references for concatenation
        let column_refs: Vec<&Array<T>> = column_vectors.iter().collect();
        concatenate(&column_refs, 1)
    } else {
        // For arrays with 2+ dimensions, use hstack
        hstack(arrays)
    }
}

/// Interpret an object as a matrix and build a matrix from string representations
///
/// This function emulates NumPy's `bmat` functionality, allowing you to create
/// matrices from string representations like "A B; C D" or nested arrays.
///
/// # Arguments
///
/// * `obj` - String description of the matrix or nested array structure
///
/// # Returns
///
/// A 2D array built from the specified blocks
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
/// use numrs2::array_ops::joining::bmat_from_string;
///
/// // Create a 2x2 block matrix from string description
/// // "A B; C D" where A, B, C, D are variable names in scope
/// let a = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let b = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let c = Array::from_vec(vec![9, 10, 11, 12]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![13, 14, 15, 16]).reshape(&[2, 2]);
///
/// // The result would be a 4x4 matrix with these blocks
/// ```
pub fn bmat_from_string<T: Clone>(_description: &str) -> Result<Array<T>> {
    // This is a simplified implementation - in practice, this would need
    // a more sophisticated parser to handle variable references
    Err(NumRs2Error::InvalidOperation(
        "String-based bmat not yet implemented - use bmat_from_arrays instead".to_string(),
    ))
}

/// Create a matrix from an array-like object or nested sequence of arrays
///
/// This function creates a matrix by treating each sub-array as a block.
/// It's similar to `block()` but specifically designed for 2D matrix construction.
///
/// # Arguments
///
/// * `obj` - Nested array structure representing the matrix blocks
///
/// # Returns
///
/// A 2D array built from the specified blocks
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
/// use numrs2::array_ops::joining::bmat_from_arrays;
///
/// // Create individual matrices
/// let a = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let b = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let c = Array::from_vec(vec![9, 10, 11, 12]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![13, 14, 15, 16]).reshape(&[2, 2]);
///
/// // Create a 2x2 block matrix: [A B]
/// //                            [C D]
/// let blocks = vec![
///     vec![&a, &b],
///     vec![&c, &d],
/// ];
/// let result = bmat_from_arrays(&blocks).expect("operation should succeed");
///
/// // Result is a 4x4 matrix:
/// // [ 1  2  5  6]
/// // [ 3  4  7  8]
/// // [ 9 10 13 14]
/// // [11 12 15 16]
/// assert_eq!(result.shape(), vec![4, 4]);
/// ```
pub fn bmat_from_arrays<T: Clone>(obj: &[Vec<&Array<T>>]) -> Result<Array<T>> {
    if obj.is_empty() {
        return Err(NumRs2Error::InvalidOperation(
            "Empty block matrix specification".to_string(),
        ));
    }

    // Ensure all arrays in the matrix are 2D
    for (row_idx, row) in obj.iter().enumerate() {
        if row.is_empty() {
            return Err(NumRs2Error::InvalidOperation(format!(
                "Empty row {} in block matrix",
                row_idx
            )));
        }

        for (col_idx, &arr) in row.iter().enumerate() {
            match arr.ndim() {
                0 => {
                    return Err(NumRs2Error::InvalidOperation(format!(
                        "Scalar at position ({}, {}) - bmat requires 2D arrays",
                        row_idx, col_idx
                    )));
                }
                1 => {
                    return Err(NumRs2Error::InvalidOperation(format!(
                        "1D array at position ({}, {}) - bmat requires 2D arrays",
                        row_idx, col_idx
                    )));
                }
                2 => {} // Good - this is what we expect
                _ => {
                    return Err(NumRs2Error::InvalidOperation(format!(
                        "{}D array at position ({}, {}) - bmat requires 2D arrays",
                        arr.ndim(),
                        row_idx,
                        col_idx
                    )));
                }
            }
        }
    }

    // Verify that all rows have the same number of columns
    let num_cols = obj[0].len();
    for (row_idx, row) in obj.iter().enumerate() {
        if row.len() != num_cols {
            return Err(NumRs2Error::InvalidOperation(format!(
                "Row {} has {} blocks, expected {}",
                row_idx,
                row.len(),
                num_cols
            )));
        }
    }

    // Verify that blocks in each row have compatible heights
    // and blocks in each column have compatible widths
    for row_idx in 0..obj.len() {
        let row = &obj[row_idx];
        let expected_height = row[0].shape()[0];

        for (col_idx, &arr) in row.iter().enumerate() {
            let height = arr.shape()[0];
            if height != expected_height {
                return Err(NumRs2Error::InvalidOperation(format!(
                    "Block at ({}, {}) has height {}, but row {} requires height {}",
                    row_idx, col_idx, height, row_idx, expected_height
                )));
            }
        }
    }

    for col_idx in 0..num_cols {
        let expected_width = obj[0][col_idx].shape()[1];

        for (row_idx, row) in obj.iter().enumerate() {
            let width = row[col_idx].shape()[1];
            if width != expected_width {
                return Err(NumRs2Error::InvalidOperation(format!(
                    "Block at ({}, {}) has width {}, but column {} requires width {}",
                    row_idx, col_idx, width, col_idx, expected_width
                )));
            }
        }
    }

    // Now we can safely concatenate
    // First, concatenate each row horizontally
    let mut concatenated_rows = Vec::with_capacity(obj.len());

    for row in obj.iter() {
        if row.len() == 1 {
            // Single block in this row
            concatenated_rows.push(row[0].clone());
        } else {
            // Multiple blocks - concatenate horizontally
            let concatenated_row = concatenate(row, 1)?;
            concatenated_rows.push(concatenated_row);
        }
    }

    // Then concatenate all rows vertically
    if concatenated_rows.len() == 1 {
        Ok(concatenated_rows[0].clone())
    } else {
        let row_refs: Vec<&Array<T>> = concatenated_rows.iter().collect();
        concatenate(&row_refs, 0)
    }
}

/// Convenient alias for bmat_from_arrays
///
/// This provides the main `bmat` functionality for creating block matrices.
///
/// # Arguments
///
/// * `obj` - Nested array structure representing the matrix blocks
///
/// # Returns
///
/// A 2D array built from the specified blocks
///
/// # Examples
///
/// ```
/// use numrs2::prelude::*;
/// use numrs2::array_ops::joining::bmat;
///
/// let a = Array::from_vec(vec![1, 2, 3, 4]).reshape(&[2, 2]);
/// let b = Array::from_vec(vec![5, 6, 7, 8]).reshape(&[2, 2]);
/// let c = Array::from_vec(vec![9, 10, 11, 12]).reshape(&[2, 2]);
/// let d = Array::from_vec(vec![13, 14, 15, 16]).reshape(&[2, 2]);
///
/// let blocks = vec![
///     vec![&a, &b],
///     vec![&c, &d],
/// ];
/// let result = bmat(&blocks).expect("operation should succeed");
/// assert_eq!(result.shape(), vec![4, 4]);
/// ```
pub fn bmat<T: Clone>(obj: &[Vec<&Array<T>>]) -> Result<Array<T>> {
    bmat_from_arrays(obj)
}