# bend-blas: the shaped layer. A Mat is one row-major # Array with its shape in the type, a Vec likewise, and every # routine below states the shape rule in its signature, so a wrong # operand is a checker error naming both shapes, not a wrong answer: # Mat.gemm(rows, k, cols, alpha, a, b, beta, c) needs a Mat, a # Mat and a Mat; a transpose is a separate def # (gemm_t, gemm_tn, gemv_t), since it changes the shape. # The sizes are Nat arguments that double as the type indices. The raw # routines of blas.bend and lapack.bend run underneath, unchanged; the # shaped forms answer the same Result, with .try twins, and consume # their operands the same way (Mat.clone keeps a copy). This is the # scipy.linalg to blas.bend's scipy.linalg.blas, plus the shapes. import Base import ./blas.bend as B import ./lapack.bend as L type Mat<-rows: Nat, -cols: Nat> is Type: MatA{a: Array} type Vec<-n: Nat> is Type: VecA{a: Array} # Construction # ------------ # the depth of the smallest power-of-two block holding count floats def depth.go(fuel: Nat, +n: U32, +d: Nat, +cap: U32) -> Nat: match fuel: case 0n: d case 1n+p: Bool.pick(Nat, U32.is_lt(cap, n), depth.go(p, n, 1n+d, (cap * 2 : U32)), d) def depth(+count: Nat) -> Nat: depth.go(40n, U32.from_nat(count), 0n, 1) def fill.go(xs: List<&2, F32>, +i: U32, a: Array) -> Array: match xs: case []: a case x <> rest: fill.go(rest, (i + 1 : U32), Array.set(F32, a, i, x)) # an array of the smallest power-of-two size holding count floats, # the first ones from xs (cut or padded with 0.0), the rest 0.0 def array.of(+count: Nat, xs: List<&2, F32>) -> Array: fill.go(List.take(&2, F32, xs, count), 0, Array.new(F32, depth(count), 0.0)) def take.go(n: Nat, +i: U32, r: Array & F32, acc: List<&2, F32>) -> List<&2, F32>: match n: case 0n: (a, x) = r x <> acc case 1n+p: (a, x) = r take.go(p, (i - 1 : U32), Array.get(F32, a, (i - 1 : U32)), x <> acc) # the first count floats of an array, as a list def list.of(+count: Nat, a: Array) -> List<&2, F32>: match count: case 0n: [] case 1n+p: take.go(p, (U32.from_nat(p) : U32), Array.get(F32, a, U32.from_nat(p)), []) # xs cut into rows of cols def rows.of(rows: Nat, +cols: Nat, +xs: List<&2, F32>) -> List<&2, List<&2, F32>>: match rows: case 0n: [] case 1n+p: List.take(&2, F32, xs, cols) <> rows.of(p, cols, List.drop(&2, F32, xs, cols)) def F32.show.of(x: F32) -> String: F32.show(x) def row.show(xs: List<&2, F32>) -> String: List.show(~&2, ~F32, ~F32.show.of, xs) # rows * cols floats, row-major, cut or padded with 0.0 def Mat.from_list(+rows: Nat, +cols: Nat, xs: List<&2, F32>) -> Mat: MatA{array.of(Nat.mul(rows, cols), xs)} def Mat.zeros(+rows: Nat, +cols: Nat) -> Mat: MatA{Array.new(F32, depth(Nat.mul(rows, cols)), 0.0)} def Mat.to_list(+rows: Nat, +cols: Nat, m: Mat) -> List<&2, F32>: MatA{a} = m list.of(Nat.mul(rows, cols), a) def Mat.to_rows(+rows: Nat, +cols: Nat, m: Mat) -> List<&2, List<&2, F32>>: rows.of(rows, cols, Mat.to_list(rows, cols, m)) def Mat.show(+rows: Nat, +cols: Nat, m: Mat) -> String: List.show(~&2, ~List<&2, F32>, ~row.show, Mat.to_rows(rows, cols, m)) def Mat.clone.fin(-rows: Nat, -cols: Nat, r: Array & Array) -> Mat & Mat: (x, y) = r (MatA{x}, MatA{y}) # a Mat is affine: two copies, to be opened by matching in a def of its own def Mat.clone(+rows: Nat, +cols: Nat, m: Mat) -> Mat & Mat: MatA{a} = m Mat.clone.fin(rows, cols, Array.clone(F32, a)) def Vec.from_list(+n: Nat, xs: List<&2, F32>) -> Vec: VecA{array.of(n, xs)} def Vec.zeros(+n: Nat) -> Vec: VecA{Array.new(F32, depth(n), 0.0)} def Vec.to_list(+n: Nat, v: Vec) -> List<&2, F32>: VecA{a} = v list.of(n, a) def Vec.show(+n: Nat, v: Vec) -> String: row.show(Vec.to_list(n, v)) def Vec.clone.fin(-n: Nat, r: Array & Array) -> Vec & Vec: (x, y) = r (VecA{x}, VecA{y}) def Vec.clone(+n: Nat, v: Vec) -> Vec & Vec: VecA{a} = v Vec.clone.fin(n, Array.clone(F32, a)) # the raw answers, shaped def Mat.wrap(-rows: Nat, -cols: Nat, r: Result<&1, &1, U32 & String, Array>) -> IO(Result<&1, &1, U32 & String, Mat>): IO.pure(Result<&1, &1, U32 & String, Mat>, Result.map(&1, &1, U32 & String, Array, Mat, x => MatA{x}, r)) def Vec.wrap(-n: Nat, r: Result<&1, &1, U32 & String, Array>) -> IO(Result<&1, &1, U32 & String, Vec>): IO.pure(Result<&1, &1, U32 & String, Vec>, Result.map(&1, &1, U32 & String, Array, Vec, x => VecA{x}, r)) # BLAS # ---- # C = alpha A B + beta C def Mat.gemm(+rows: Nat, +k: Nat, +cols: Nat, alpha: F32, a: Mat, b: Mat, beta: F32, c: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): MatA{x} = a MatA{y} = b MatA{z} = c IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, B.Blas.sgemm(0, 0, U32.from_nat(rows), U32.from_nat(cols), U32.from_nat(k), alpha, x, y, beta, z), r => Mat.wrap(rows, cols, r)) def Mat.gemm.try(+rows: Nat, +k: Nat, +cols: Nat, alpha: F32, a: Mat, b: Mat, beta: F32, c: Mat) -> IO(Mat): IO.try(Mat, Mat.gemm(rows, k, cols, alpha, a, b, beta, c)) # C = alpha A B^T + beta C, B stored cols x k def Mat.gemm_t(+rows: Nat, +k: Nat, +cols: Nat, alpha: F32, a: Mat, b: Mat, beta: F32, c: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): MatA{x} = a MatA{y} = b MatA{z} = c IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, B.Blas.sgemm(0, 1, U32.from_nat(rows), U32.from_nat(cols), U32.from_nat(k), alpha, x, y, beta, z), r => Mat.wrap(rows, cols, r)) def Mat.gemm_t.try(+rows: Nat, +k: Nat, +cols: Nat, alpha: F32, a: Mat, b: Mat, beta: F32, c: Mat) -> IO(Mat): IO.try(Mat, Mat.gemm_t(rows, k, cols, alpha, a, b, beta, c)) # C = alpha A^T B + beta C, A stored k x rows def Mat.gemm_tn(+rows: Nat, +k: Nat, +cols: Nat, alpha: F32, a: Mat, b: Mat, beta: F32, c: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): MatA{x} = a MatA{y} = b MatA{z} = c IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, B.Blas.sgemm(1, 0, U32.from_nat(rows), U32.from_nat(cols), U32.from_nat(k), alpha, x, y, beta, z), r => Mat.wrap(rows, cols, r)) def Mat.gemm_tn.try(+rows: Nat, +k: Nat, +cols: Nat, alpha: F32, a: Mat, b: Mat, beta: F32, c: Mat) -> IO(Mat): IO.try(Mat, Mat.gemm_tn(rows, k, cols, alpha, a, b, beta, c)) # y = alpha A x + beta y def Mat.gemv(+rows: Nat, +cols: Nat, alpha: F32, a: Mat, x: Vec, beta: F32, y: Vec) -> IO(Result<&1, &1, U32 & String, Vec>): MatA{m} = a VecA{xs} = x VecA{ys} = y IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Vec>, B.Blas.sgemv(0, U32.from_nat(rows), U32.from_nat(cols), alpha, m, xs, beta, ys), r => Vec.wrap(rows, r)) def Mat.gemv.try(+rows: Nat, +cols: Nat, alpha: F32, a: Mat, x: Vec, beta: F32, y: Vec) -> IO(Vec): IO.try(Vec, Mat.gemv(rows, cols, alpha, a, x, beta, y)) # y = alpha A^T x + beta y def Mat.gemv_t(+rows: Nat, +cols: Nat, alpha: F32, a: Mat, x: Vec, beta: F32, y: Vec) -> IO(Result<&1, &1, U32 & String, Vec>): MatA{m} = a VecA{xs} = x VecA{ys} = y IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Vec>, B.Blas.sgemv(1, U32.from_nat(rows), U32.from_nat(cols), alpha, m, xs, beta, ys), r => Vec.wrap(cols, r)) def Mat.gemv_t.try(+rows: Nat, +cols: Nat, alpha: F32, a: Mat, x: Vec, beta: F32, y: Vec) -> IO(Vec): IO.try(Vec, Mat.gemv_t(rows, cols, alpha, a, x, beta, y)) # A = alpha x y^T + A def Mat.ger(+rows: Nat, +cols: Nat, alpha: F32, x: Vec, y: Vec, a: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): VecA{xs} = x VecA{ys} = y MatA{m} = a IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, B.Blas.sger(U32.from_nat(rows), U32.from_nat(cols), alpha, xs, ys, m), r => Mat.wrap(rows, cols, r)) def Mat.ger.try(+rows: Nat, +cols: Nat, alpha: F32, x: Vec, y: Vec, a: Mat) -> IO(Mat): IO.try(Mat, Mat.ger(rows, cols, alpha, x, y, a)) def Vec.dot(+n: Nat, x: Vec, y: Vec) -> IO(Result<&1, &1, U32 & String, F32>): VecA{xs} = x VecA{ys} = y B.Blas.sdot(U32.from_nat(n), xs, ys) def Vec.dot.try(+n: Nat, x: Vec, y: Vec) -> IO(F32): IO.try(F32, Vec.dot(n, x, y)) def Vec.nrm2(+n: Nat, x: Vec) -> IO(Result<&1, &1, U32 & String, F32>): VecA{xs} = x B.Blas.snrm2(U32.from_nat(n), xs) def Vec.nrm2.try(+n: Nat, x: Vec) -> IO(F32): IO.try(F32, Vec.nrm2(n, x)) def Vec.asum(+n: Nat, x: Vec) -> IO(Result<&1, &1, U32 & String, F32>): VecA{xs} = x B.Blas.sasum(U32.from_nat(n), xs) def Vec.asum.try(+n: Nat, x: Vec) -> IO(F32): IO.try(F32, Vec.asum(n, x)) def Vec.iamax(+n: Nat, x: Vec) -> IO(Result<&1, &1, U32 & String, U32>): VecA{xs} = x B.Blas.isamax(U32.from_nat(n), xs) def Vec.iamax.try(+n: Nat, x: Vec) -> IO(U32): IO.try(U32, Vec.iamax(n, x)) # y = alpha x + y def Vec.axpy(+n: Nat, alpha: F32, x: Vec, y: Vec) -> IO(Result<&1, &1, U32 & String, Vec>): VecA{xs} = x VecA{ys} = y IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Vec>, B.Blas.saxpy(U32.from_nat(n), alpha, xs, ys), r => Vec.wrap(n, r)) def Vec.axpy.try(+n: Nat, alpha: F32, x: Vec, y: Vec) -> IO(Vec): IO.try(Vec, Vec.axpy(n, alpha, x, y)) def Vec.scal(+n: Nat, alpha: F32, x: Vec) -> IO(Result<&1, &1, U32 & String, Vec>): VecA{xs} = x IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Vec>, B.Blas.sscal(U32.from_nat(n), alpha, xs), r => Vec.wrap(n, r)) def Vec.scal.try(+n: Nat, alpha: F32, x: Vec) -> IO(Vec): IO.try(Vec, Vec.scal(n, alpha, x)) # LAPACK # ------ # X = A^-1 B; Fail with the zero pivot as the code when A is singular def Mat.solve(+n: Nat, +nrhs: Nat, a: Mat, b: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): MatA{x} = a MatA{y} = b IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, L.Lapack.sgesv(U32.from_nat(n), U32.from_nat(nrhs), x, y), r => Mat.wrap(n, nrhs, r)) def Mat.solve.try(+n: Nat, +nrhs: Nat, a: Mat, b: Mat) -> IO(Mat): IO.try(Mat, Mat.solve(n, nrhs, a, b)) # the lower Cholesky factor L of a symmetric positive definite A def Mat.cholesky(+n: Nat, a: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): MatA{x} = a IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, L.Lapack.spotrf(U32.from_nat(n), x), r => Mat.wrap(n, n, r)) def Mat.cholesky.try(+n: Nat, a: Mat) -> IO(Mat): IO.try(Mat, Mat.cholesky(n, a)) # X = A^-1 B given L from Mat.cholesky def Mat.cholesky_solve(+n: Nat, +nrhs: Nat, l: Mat, b: Mat) -> IO(Result<&1, &1, U32 & String, Mat>): MatA{x} = l MatA{y} = b IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Mat>, L.Lapack.spotrs(U32.from_nat(n), U32.from_nat(nrhs), x, y), r => Mat.wrap(n, nrhs, r)) def Mat.cholesky_solve.try(+n: Nat, +nrhs: Nat, l: Mat, b: Mat) -> IO(Mat): IO.try(Mat, Mat.cholesky_solve(n, nrhs, l, b)) # the singular values, largest first def Mat.svd_values(+m: Nat, +n: Nat, a: Mat) -> IO(Result<&1, &1, U32 & String, Vec>): MatA{x} = a IO.bind(Result<&1, &1, U32 & String, Array>, Result<&1, &1, U32 & String, Vec>, L.Lapack.sgesvd_s(U32.from_nat(m), U32.from_nat(n), x), r => Vec.wrap(Nat.min(m, n), r)) def Mat.svd_values.try(+m: Nat, +n: Nat, a: Mat) -> IO(Vec): IO.try(Vec, Mat.svd_values(m, n, a)) def Mat.svd.fin(-m: Nat, -n: Nat, s: Array, uv: Array & Array) -> IO(Result<&1, &1, U32 & String, Vec & (Mat & Mat)>): match uv: case (u, vt): IO.pure(Result<&1, &1, U32 & String, Vec & (Mat & Mat)>, Done{(VecA{s}, (MatA{u}, MatA{vt}))}) def Mat.svd.wrap(-m: Nat, -n: Nat, r: Result<&1, &1, U32 & String, Array & (Array & Array)>) -> IO(Result<&1, &1, U32 & String, Vec & (Mat & Mat)>): match r: case Fail{e}: IO.pure(Result<&1, &1, U32 & String, Vec & (Mat & Mat)>, Fail{e}) case Done{v}: match v: case (s, uv): Mat.svd.fin(m, n, s, uv) # the thin SVD: (s, (u, vt)) with A = u diag(s) vt def Mat.svd(+m: Nat, +n: Nat, a: Mat) -> IO(Result<&1, &1, U32 & String, Vec & (Mat & Mat)>): MatA{x} = a IO.bind(Result<&1, &1, U32 & String, Array & (Array & Array)>, Result<&1, &1, U32 & String, Vec & (Mat & Mat)>, L.Lapack.sgesvd(U32.from_nat(m), U32.from_nat(n), x), r => Mat.svd.wrap(m, n, r)) def Mat.svd.try(+m: Nat, +n: Nat, a: Mat) -> IO(Vec & (Mat & Mat)): IO.try(Vec & (Mat & Mat), Mat.svd(m, n, a)) def Mat.eigh.wrap(-n: Nat, r: Result<&1, &1, U32 & String, Array & Array>) -> IO(Result<&1, &1, U32 & String, Vec & Mat>): match r: case Fail{e}: IO.pure(Result<&1, &1, U32 & String, Vec & Mat>, Fail{e}) case Done{v}: match v: case (w, vs): IO.pure(Result<&1, &1, U32 & String, Vec & Mat>, Done{(VecA{w}, MatA{vs})}) # the eigenvalues ascending and the eigenvectors as the rows of the Mat def Mat.eigh(+n: Nat, a: Mat) -> IO(Result<&1, &1, U32 & String, Vec & Mat>): MatA{x} = a IO.bind(Result<&1, &1, U32 & String, Array & Array>, Result<&1, &1, U32 & String, Vec & Mat>, L.Lapack.ssyev(U32.from_nat(n), x), r => Mat.eigh.wrap(n, r)) def Mat.eigh.try(+n: Nat, a: Mat) -> IO(Vec & Mat): IO.try(Vec & Mat, Mat.eigh(n, a))