4  Tensors

La Linea wraps dtype-next tensors in RealTensors — thin wrappers that add type identity, custom printing, and structural equality while preserving zero-copy interop. This chapter covers the scicloj.lalinea.tensor namespace (alias t/).

(ns lalinea-book.tensors
  (:require
   [scicloj.lalinea.tensor :as t]
   [scicloj.lalinea.linalg :as la]
   [scicloj.lalinea.elementwise :as el]
   [scicloj.kindly.v4.kind :as kind]
   [clojure.math :as math])
  (:import [org.ejml.data DMatrixRMaj]))

Construction

t/matrix creates a RealTensor from nested sequences or an existing tensor:

(t/matrix [[1 2 3]
           [4 5 6]])
#la/R [:float64 [2 3]
       [[1.000 2.000 3.000]
        [4.000 5.000 6.000]]]

t/column and t/row create column [n 1] and row [1 n] vectors:

(t/column [1 2 3])
#la/R [:float64 [3 1]
       [[1.000]
        [2.000]
        [3.000]]]
(t/row [1 2 3])
#la/R [:float64 [1 3]
       [[1.000 2.000 3.000]]]

t/eye, t/zeros, and t/ones create standard matrices:

(t/eye 3)
#la/R [:float64 [3 3]
       [[1.000 0.000 0.000]
        [0.000 1.000 0.000]
        [0.000 0.000 1.000]]]
(t/zeros 2 3)
#la/R [:float64 [2 3]
       [[0.000 0.000 0.000]
        [0.000 0.000 0.000]]]

t/compute-matrix constructs from a function of (i, j):

(t/compute-matrix 3 3 (fn [i j] (if (== i j) 1.0 0.0)))
#la/R [:float64 [3 3]
       [[1.000 0.000 0.000]
        [0.000 1.000 0.000]
        [0.000 0.000 1.000]]]

t/compute-tensor creates a lazy tensor of arbitrary shape:

(t/compute-tensor [3 3]
                  (fn [i j] (if (== i j) 1.0 0.0))
                  :float64)
#la/R [:float64 [3 3]
       [[1.000 0.000 0.000]
        [0.000 1.000 0.000]
        [0.000 0.000 1.000]]]

Printed form

RealTensors print as #la/R tagged literals. This representation round-trips through pr-str / read-string:

(t/matrix [[1 2] [3 4]])
#la/R [:float64 [2 2]
       [[1.000 2.000]
        [3.000 4.000]]]

Element access

Tensors are callable — (m i j) reads element [i,j]:

(let [m (t/matrix [[10 20] [30 40]])]
  [(m 0 1) (m 1 0)])
[20.0 30.0]

Structural operations

Shape and reshape

(t/shape (t/matrix [[1 2 3] [4 5 6]]))
[2 3]

t/reshape is zero-copy — it wraps the same backing array in a different shape:

(t/reshape (t/matrix [1 2 3 4 5 6]) [2 3])
#la/R [:float64 [2 3]
       [[1.000 2.000 3.000]
        [4.000 5.000 6.000]]]

Select and submatrix

t/select slices along dimensions. It returns a view:

(let [A (t/matrix [[1 2 3]
                   [4 5 6]])]
  (t/select A 0 :all))
#la/R [:float64 [3] [1.000 2.000 3.000]]

t/submatrix extracts a contiguous submatrix (always a copy):

(let [A (t/matrix [[1 2 3] [4 5 6] [7 8 9]])]
  (t/submatrix A (range 2) (range 2)))
#la/R [:float64 [2 2]
       [[1.000 2.000]
        [4.000 5.000]]]

Flatten

t/flatten reshapes to 1D:

(t/flatten (t/matrix [[1 2] [3 4]]))
#la/R [:float64 [4] [1.000 2.000 3.000 4.000]]

Materialization

Lazy results (from el/+, el/sqrt, etc.) recompute on every access. Call t/materialize to ensure a concrete array (no-op if already concrete), or t/clone to force a fresh copy:

(let [a (t/matrix [1 4 9])
      s (el/sqrt a)
      cloned (t/clone s)]
  {:lazy-array? (some? (t/array-buffer s))
   :cloned-array? (some? (t/array-buffer cloned))})
{:lazy-array? false, :cloned-array? true}

Mutation: t/mset!

t/mset! mutates a tensor element in place:

(let [m (t/matrix [[1 2] [3 4]])]
  (t/mset! m 0 1 99.0)
  (m 0 1))
99.0

Only works on tensors backed by a concrete array. Lazy tensors cannot be mutated.

Element-wise operations

The scicloj.lalinea.elementwise namespace (alias el/) provides tape-aware element-wise functions:

(let [x (t/matrix [[1 4] [9 16]])]
  (el/sqrt x))
#la/R [:float64 [2 2]
       [[1.000 2.000]
        [3.000 4.000]]]

el/+, el/-, el/scale, and el/* also work element-wise on matching-shape tensors:

(let [a (t/matrix [[1 2] [3 4]])
      b (t/matrix [[10 20] [30 40]])]
  (el/+ a b))
#la/R [:float64 [2 2]
       [[11.00 22.00]
        [33.00 44.00]]]

EJML interop

t/tensor->dmat and t/dmat->tensor convert between RealTensors and EJML’s DMatrixRMaj — zero-copy both ways.

(let [m (t/matrix [[1.0 2.0] [3.0 4.0]])
      dm (t/tensor->dmat m)]
  {:identical? (identical? (t/->double-array m)
                           (.data ^DMatrixRMaj dm))
   :rows (.numRows ^DMatrixRMaj dm)
   :cols (.numCols ^DMatrixRMaj dm)})
{:identical? true, :rows 2, :cols 2}

Mutations through either view are immediately visible in the other:

(let [m (t/matrix [[1.0 0.0] [0.0 1.0]])
      dm (t/tensor->dmat m)]
  (.set ^DMatrixRMaj dm 0 1 99.0)
  (m 0 1))
99.0

Matrix operations (preview)

The scicloj.lalinea.linalg namespace (alias la/) provides a functional API for matrix operations. A taste:

Matrix multiply: \(A \cdot I = A\)

(la/mmul (t/matrix [[1 2] [3 4]])
         (t/eye 2))
#la/R [:float64 [2 2]
       [[1.000 2.000]
        [3.000 4.000]]]

Inverse:

(la/invert (t/matrix [[1 2] [3 4]]))
#la/R [:float64 [2 2]
       [[-2.000   1.000]
        [ 1.500 -0.5000]]]

Frobenius norm: \(\|A\|_F = \sqrt{\sum a_{ij}^2}\)

(la/norm (t/matrix [[1 2 3] [4 5 6]]))
9.539392014169456
source: notebooks/lalinea_book/tensors.clj