Skip to content

Commit c06b31e

Browse files
doc/refactor: reduce nd-dot code duplication using ArrayRef and add docs
1 parent 2ae90c2 commit c06b31e

1 file changed

Lines changed: 72 additions & 81 deletions

File tree

src/linalg/impl_linalg.rs

Lines changed: 72 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,22 @@ unsafe fn blas_1d_params<A>(ptr: *const A, len: usize, stride: isize) -> (*const
157157
///
158158
/// For two-dimensional arrays, the dot method computes the matrix
159159
/// multiplication.
160+
///
161+
/// For higher-dimensional arrays (3-D through 6-D, and dynamic-dimensional),
162+
/// `Dot<Ix2>` contracts the last axis of the left-hand side with the first
163+
/// axis of the right-hand side, following NumPy semantics. For example, if
164+
/// `self` has shape *I* × *J* × *K* and `rhs` has shape *K* × *N*, the
165+
/// result has shape *I* × *J* × *N*.
166+
///
167+
/// ```
168+
/// use ndarray::{Array3, Array2};
169+
/// use ndarray::linalg::Dot;
170+
///
171+
/// let a = Array3::<f64>::zeros((3, 4, 5));
172+
/// let b = Array2::<f64>::zeros((5, 6));
173+
/// let c = a.dot(&b);
174+
/// assert_eq!(c.shape(), &[3, 4, 6]);
175+
/// ```
160176
pub trait Dot<Rhs: ?Sized>
161177
{
162178
/// The result of the operation.
@@ -222,13 +238,58 @@ impl_dots!(Ix1, Ix2);
222238
impl_dots!(Ix2, Ix1);
223239
impl_dots!(Ix2, Ix2);
224240

225-
fn nd_dot_non_contiguous<A, S1, S2, S3>(
226-
lhs: &ArrayBase<S1, IxDyn>, rhs: &ArrayBase<S2, Ix2>, out: &mut ArrayBase<S3, IxDyn>,
241+
/// Compute the dot product of an N-dimensional LHS array with a 2-D RHS matrix.
242+
///
243+
/// Contracts the last axis of `lhs` with the first axis of `rhs`.
244+
/// If `lhs` has shape *d₀ × d₁ × … × dₙ₋₁ × K* and `rhs` has shape
245+
/// *K × N*, the result has shape *d₀ × d₁ × … × dₙ₋₁ × N*.
246+
///
247+
/// Two paths are used internally:
248+
/// - **C-contiguous LHS**: the leading axes are flattened into a single
249+
/// 2-D view (zero-copy) and delegated to the optimised 2-D `dot`.
250+
/// - **Non-contiguous LHS**: a result array is pre-allocated and filled
251+
/// in-place via recursive `general_mat_mul` calls (no intermediate copies).
252+
#[track_caller]
253+
fn nd_dot<A: LinalgScalar>(lhs: &ArrayRef<A, IxDyn>, rhs: &ArrayRef<A, Ix2>) -> Array<A, IxDyn>
254+
{
255+
let ndim = lhs.ndim();
256+
let k = lhs.shape()[ndim - 1];
257+
let k2 = rhs.shape()[0];
258+
let n = rhs.shape()[1];
259+
if k != k2 {
260+
panic!(
261+
"shapes {:?} and {:?} are not compatible for nd dot \
262+
(last axis of lhs must equal first axis of rhs)",
263+
lhs.shape(),
264+
rhs.shape()
265+
);
266+
}
267+
268+
let mut out_shape = lhs.shape().to_vec();
269+
*out_shape.last_mut().unwrap() = n;
270+
271+
if lhs.is_standard_layout() {
272+
// C-contiguous: to_shape returns a *view* (no copy of LHS data).
273+
let rows = lhs.len() / k;
274+
// unwrap: rows * k == lhs.len() by construction, so reshape always succeeds.
275+
let lhs_2d = lhs.to_shape((rows, k)).unwrap();
276+
let result_2d = lhs_2d.dot(rhs);
277+
// unwrap: result_2d is a fresh C-contiguous owned array of the correct size.
278+
result_2d.into_shape_with_order(IxDyn(&out_shape)).unwrap()
279+
} else {
280+
// Non-contiguous: pre-allocate and fill in-place to avoid whole-array copying.
281+
let mut out = Array::zeros(IxDyn(&out_shape));
282+
nd_dot_non_contiguous(&lhs.view(), &rhs.view(), &mut out.view_mut());
283+
out
284+
}
285+
}
286+
287+
/// Recursive helper: writes the N-D × 2-D product directly into `out`
288+
/// using `general_mat_mul` at the 2-D base case.
289+
fn nd_dot_non_contiguous<A>(
290+
lhs: &ArrayRef<A, IxDyn>, rhs: &ArrayRef<A, Ix2>, out: &mut ArrayRef<A, IxDyn>,
227291
) where
228292
A: LinalgScalar,
229-
S1: Data<Elem = A>,
230-
S2: Data<Elem = A>,
231-
S3: DataMut<Elem = A>,
232293
{
233294
let ndim = lhs.ndim();
234295
if ndim == 2 {
@@ -237,8 +298,8 @@ fn nd_dot_non_contiguous<A, S1, S2, S3>(
237298
let mut out_2d = out.view_mut().into_dimensionality::<Ix2>().unwrap();
238299
general_mat_mul(A::one(), &lhs_2d, rhs, A::zero(), &mut out_2d);
239300
} else {
240-
Zip::from(lhs.axis_iter(Axis(0)))
241-
.and(out.axis_iter_mut(Axis(0)))
301+
Zip::from(lhs.view().axis_iter(Axis(0)))
302+
.and(out.view_mut().axis_iter_mut(Axis(0)))
242303
.for_each(|lhs_slice, mut out_slice| {
243304
nd_dot_non_contiguous(&lhs_slice, rhs, &mut out_slice);
244305
});
@@ -255,46 +316,9 @@ macro_rules! impl_dot_nd_ix2 {
255316
#[track_caller]
256317
fn dot(&self, rhs: &ArrayRef<A, Ix2>) -> Array<A, $dim>
257318
{
258-
let ndim = self.ndim();
259-
let k = self.shape()[ndim - 1];
260-
let k2 = rhs.shape()[0];
261-
let n = rhs.shape()[1];
262-
if k != k2 {
263-
panic!(
264-
"shapes {:?} and {:?} are not compatible for nd dot \
265-
(last axis of lhs must equal first axis of rhs)",
266-
self.shape(),
267-
rhs.shape()
268-
);
269-
}
270-
271-
if self.is_standard_layout() {
272-
// C-contiguous: to_shape returns a *view* (no copy of LHS data).
273-
let rows = self.len() / k;
274-
// unwrap: rows * k == self.len() by construction, so reshape always succeeds.
275-
let lhs_2d = self.to_shape((rows, k)).unwrap();
276-
let result_2d = lhs_2d.dot(rhs);
277-
278-
let mut out_dim = <$dim>::zeros(ndim);
279-
for i in 0..ndim - 1 {
280-
out_dim[i] = self.shape()[i];
281-
}
282-
out_dim[ndim - 1] = n;
283-
284-
// unwrap: result_2d is a fresh C-contiguous owned array of the correct size.
285-
result_2d.into_shape_with_order(out_dim).unwrap()
286-
} else {
287-
// Non-contiguous: iterate over the first axis and write results directly
288-
// into the pre-allocated output array to avoid whole-array copying.
289-
let mut out_dim = <$dim>::zeros(ndim);
290-
for i in 0..ndim - 1 {
291-
out_dim[i] = self.shape()[i];
292-
}
293-
out_dim[ndim - 1] = n;
294-
let mut out = Array::zeros(out_dim);
295-
nd_dot_non_contiguous(&self.view().into_dyn(), &rhs.view(), &mut out.view_mut().into_dyn());
296-
out
297-
}
319+
let result_dyn = nd_dot(&self.view().into_dyn(), rhs);
320+
// unwrap: nd_dot preserves the number of dimensions.
321+
result_dyn.into_dimensionality::<$dim>().unwrap()
298322
}
299323
}
300324

@@ -315,40 +339,7 @@ where A: LinalgScalar
315339
#[track_caller]
316340
fn dot(&self, rhs: &ArrayRef<A, Ix2>) -> Array<A, IxDyn>
317341
{
318-
let ndim = self.ndim();
319-
let k = self.shape()[ndim - 1];
320-
let k2 = rhs.shape()[0];
321-
let n = rhs.shape()[1];
322-
if k != k2 {
323-
panic!(
324-
"shapes {:?} and {:?} are not compatible for nd dot \
325-
(last axis of lhs must equal first axis of rhs)",
326-
self.shape(),
327-
rhs.shape()
328-
);
329-
}
330-
331-
if self.is_standard_layout() {
332-
// C-contiguous: to_shape returns a *view* (no copy of LHS data).
333-
let rows = self.len() / k;
334-
// unwrap: rows * k == self.len() by construction, so reshape always succeeds.
335-
let lhs_2d = self.to_shape((rows, k)).unwrap();
336-
let result_2d = lhs_2d.dot(rhs);
337-
338-
let mut out_shape = self.shape().to_vec();
339-
*out_shape.last_mut().unwrap() = n;
340-
341-
// unwrap: result_2d is a fresh C-contiguous owned array of the correct size.
342-
result_2d.into_shape_with_order(IxDyn(&out_shape)).unwrap()
343-
} else {
344-
// Non-contiguous: iterate recursively over the first axis and write results
345-
// directly into the pre-allocated output array to avoid whole-array copying.
346-
let mut out_shape = self.shape().to_vec();
347-
*out_shape.last_mut().unwrap() = n;
348-
let mut out = Array::zeros(IxDyn(&out_shape));
349-
nd_dot_non_contiguous(&self.view(), &rhs.view(), &mut out.view_mut());
350-
out
351-
}
342+
nd_dot(&self.view().into_dyn(), rhs)
352343
}
353344
}
354345

0 commit comments

Comments
 (0)