-module(viva_math@matrix_dense). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/viva_math/matrix_dense.gleam"). -export([rows/1, cols/1, shape/1, zeros/2, identity/1, from_list/3, get/3, row/2, to_rows/1, byte_size/1, add/2, sub/2, hadamard/2, scale/2, transpose/1, mul/2, frobenius/1, trace/1]). -export_type([dense_mat/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Dense matrix backed by `BitArray` (binary row-major).\n" "\n" " Counterpart to `viva_math/matrix.MatN`. Where `MatN` stores rows as\n" " nested `List(Float)`, `DenseMat` keeps a single contiguous binary\n" " buffer with one IEEE-754 little-endian double per element. This:\n" "\n" " - **Saves memory**: 8 bytes/element vs ~24 bytes/element for cons-cell\n" " lists on 64-bit BEAM.\n" " - **Speeds up indexed access**: O(1) random read of any cell via\n" " `byte_offset = (row · cols + col) · 8`.\n" " - **Avoids list reversal**: `transpose` and `matmul` rebuild a binary\n" " instead of allocating thousands of cons cells.\n" "\n" " ## Scope\n" "\n" " Suitable for matrices up to a few thousand rows × cols. For tensor-scale\n" " work (batched GEMM, GPU acceleration) defer to `viva_tensor`. This\n" " module is the middle ground between the trivially-correct `MatN` and\n" " the heavy `viva_tensor`.\n" "\n" " ## Limitations\n" "\n" " - Float-only (`Float` ↔ 64-bit IEEE 754 little-endian).\n" " - Sequential algorithms (no parallelism, no SIMD).\n" " - Conversion to/from `MatN` available for interop.\n" ). -opaque dense_mat() :: {dense_mat, integer(), integer(), bitstring()}. -file("src/viva_math/matrix_dense.gleam", 46). ?DOC(" Number of rows in the matrix.\n"). -spec rows(dense_mat()) -> integer(). rows(M) -> erlang:element(2, M). -file("src/viva_math/matrix_dense.gleam", 51). ?DOC(" Number of columns in the matrix.\n"). -spec cols(dense_mat()) -> integer(). cols(M) -> erlang:element(3, M). -file("src/viva_math/matrix_dense.gleam", 56). ?DOC(" Shape as `#(rows, cols)`.\n"). -spec shape(dense_mat()) -> {integer(), integer()}. shape(M) -> {erlang:element(2, M), erlang:element(3, M)}. -file("src/viva_math/matrix_dense.gleam", 75). -spec build_zeros(integer(), bitstring()) -> bitstring(). build_zeros(N, Acc) -> case N =< 0 of true -> Acc; false -> build_zeros(N - 1, <>) end. -file("src/viva_math/matrix_dense.gleam", 65). ?DOC(" Zero matrix of given shape. Errors on non-positive dimensions.\n"). -spec zeros(integer(), integer()) -> {ok, dense_mat()} | {error, nil}. zeros(Rows, Cols) -> case (Rows =< 0) orelse (Cols =< 0) of true -> {error, nil}; false -> Zero_data = build_zeros(Rows * Cols, <<>>), {ok, {dense_mat, Rows, Cols, Zero_data}} end. -file("src/viva_math/matrix_dense.gleam", 93). -spec build_identity(integer(), integer(), integer(), bitstring()) -> bitstring(). build_identity(N, I, J, Acc) -> case I >= N of true -> Acc; false -> V = case I =:= J of true -> 1.0; false -> +0.0 end, Next_acc = <>, case (J + 1) >= N of true -> build_identity(N, I + 1, 0, Next_acc); false -> build_identity(N, I, J + 1, Next_acc) end end. -file("src/viva_math/matrix_dense.gleam", 83). ?DOC(" Identity matrix of size n.\n"). -spec identity(integer()) -> {ok, dense_mat()} | {error, nil}. identity(N) -> case N =< 0 of true -> {error, nil}; false -> Data = build_identity(N, 0, 0, <<>>), {ok, {dense_mat, N, N, Data}} end. -file("src/viva_math/matrix_dense.gleam", 125). -spec pack_floats(list(float()), bitstring()) -> bitstring(). pack_floats(Xs, Acc) -> case Xs of [] -> Acc; [X | Rest] -> pack_floats(Rest, <>) end. -file("src/viva_math/matrix_dense.gleam", 111). ?DOC(" Build from row-major list of values. Errors if shape doesn't match.\n"). -spec from_list(integer(), integer(), list(float())) -> {ok, dense_mat()} | {error, nil}. from_list(Rows, Cols, Values) -> case ((Rows =< 0) orelse (Cols =< 0)) orelse (erlang:length(Values) /= (Rows * Cols)) of true -> {error, nil}; false -> Data = pack_floats(Values, <<>>), {ok, {dense_mat, Rows, Cols, Data}} end. -file("src/viva_math/matrix_dense.gleam", 137). ?DOC(" Random-access read in O(1). Errors on out-of-bounds.\n"). -spec get(dense_mat(), integer(), integer()) -> {ok, float()} | {error, nil}. get(M, Row, Col) -> case (((Row < 0) orelse (Row >= erlang:element(2, M))) orelse (Col < 0)) orelse (Col >= erlang:element(3, M)) of true -> {error, nil}; false -> Index = (Row * erlang:element(3, M)) + Col, Bit_offset = Index * 64, case erlang:element(4, M) of <<_:Bit_offset, Value:64/float-little, _/bitstring>> -> {ok, Value}; _ -> {error, nil} end end. -file("src/viva_math/matrix_dense.gleam", 159). -spec extract_row(dense_mat(), integer(), integer(), list(float())) -> list(float()). extract_row(M, Row_idx, Col_idx, Acc) -> case Col_idx >= erlang:element(3, M) of true -> lists:reverse(Acc); false -> case get(M, Row_idx, Col_idx) of {ok, V} -> extract_row(M, Row_idx, Col_idx + 1, [V | Acc]); {error, _} -> lists:reverse(Acc) end end. -file("src/viva_math/matrix_dense.gleam", 152). ?DOC(" Get an entire row as a list.\n"). -spec row(dense_mat(), integer()) -> {ok, list(float())} | {error, nil}. row(M, Row_idx) -> case (Row_idx < 0) orelse (Row_idx >= erlang:element(2, M)) of true -> {error, nil}; false -> {ok, extract_row(M, Row_idx, 0, [])} end. -file("src/viva_math/matrix_dense.gleam", 180). -spec build_rows(dense_mat(), integer(), list(list(float()))) -> list(list(float())). build_rows(M, I, Acc) -> case I >= erlang:element(2, M) of true -> lists:reverse(Acc); false -> case row(M, I) of {ok, R} -> build_rows(M, I + 1, [R | Acc]); {error, _} -> lists:reverse(Acc) end end. -file("src/viva_math/matrix_dense.gleam", 176). ?DOC(" Convert to nested-list representation for interop with `MatN`.\n"). -spec to_rows(dense_mat()) -> list(list(float())). to_rows(M) -> build_rows(M, 0, []). -file("src/viva_math/matrix_dense.gleam", 196). ?DOC(" Number of bytes consumed by the data buffer. Useful for benchmarking.\n"). -spec byte_size(dense_mat()) -> integer(). byte_size(M) -> erlang:bit_size(erlang:element(4, M)) div 8. -file("src/viva_math/matrix_dense.gleam", 255). -spec zip_data( bitstring(), bitstring(), fun((float(), float()) -> float()), bitstring() ) -> bitstring(). zip_data(A, B, F, Acc) -> case {A, B} of {<>, <>} -> zip_data( Arest, Brest, F, <> ); {_, _} -> Acc end. -file("src/viva_math/matrix_dense.gleam", 238). -spec zip_with(dense_mat(), dense_mat(), fun((float(), float()) -> float())) -> dense_mat(). zip_with(A, B, F) -> New_data = zip_data(erlang:element(4, A), erlang:element(4, B), F, <<>>), {dense_mat, erlang:element(2, A), erlang:element(3, A), New_data}. -file("src/viva_math/matrix_dense.gleam", 209). ?DOC(" Element-wise add. Errors on shape mismatch.\n"). -spec add(dense_mat(), dense_mat()) -> {ok, dense_mat()} | {error, nil}. add(A, B) -> case (erlang:element(2, A) /= erlang:element(2, B)) orelse (erlang:element( 3, A ) /= erlang:element(3, B)) of true -> {error, nil}; false -> {ok, zip_with(A, B, fun(X, Y) -> X + Y end)} end. -file("src/viva_math/matrix_dense.gleam", 217). ?DOC(" Element-wise subtract.\n"). -spec sub(dense_mat(), dense_mat()) -> {ok, dense_mat()} | {error, nil}. sub(A, B) -> case (erlang:element(2, A) /= erlang:element(2, B)) orelse (erlang:element( 3, A ) /= erlang:element(3, B)) of true -> {error, nil}; false -> {ok, zip_with(A, B, fun(X, Y) -> X - Y end)} end. -file("src/viva_math/matrix_dense.gleam", 225). ?DOC(" Element-wise Hadamard product.\n"). -spec hadamard(dense_mat(), dense_mat()) -> {ok, dense_mat()} | {error, nil}. hadamard(A, B) -> case (erlang:element(2, A) /= erlang:element(2, B)) orelse (erlang:element( 3, A ) /= erlang:element(3, B)) of true -> {error, nil}; false -> {ok, zip_with(A, B, fun(X, Y) -> X * Y end)} end. -file("src/viva_math/matrix_dense.gleam", 247). -spec map_data(bitstring(), fun((float()) -> float()), bitstring()) -> bitstring(). map_data(Data, F, Acc) -> case Data of <> -> map_data(Rest, F, <>); _ -> Acc end. -file("src/viva_math/matrix_dense.gleam", 233). ?DOC(" Scalar multiplication.\n"). -spec scale(dense_mat(), float()) -> dense_mat(). scale(M, S) -> New_data = map_data(erlang:element(4, M), fun(X) -> X * S end, <<>>), {dense_mat, erlang:element(2, M), erlang:element(3, M), New_data}. -file("src/viva_math/matrix_dense.gleam", 280). -spec transpose_loop(dense_mat(), integer(), integer(), bitstring()) -> bitstring(). transpose_loop(M, New_row, New_col, Acc) -> case New_row >= erlang:element(3, M) of true -> Acc; false -> V = case get(M, New_col, New_row) of {ok, X} -> X; {error, _} -> +0.0 end, Next_acc = <>, case (New_col + 1) >= erlang:element(2, M) of true -> transpose_loop(M, New_row + 1, 0, Next_acc); false -> transpose_loop(M, New_row, New_col + 1, Next_acc) end end. -file("src/viva_math/matrix_dense.gleam", 275). ?DOC(" Transpose. O(rows · cols) but with contiguous writes.\n"). -spec transpose(dense_mat()) -> dense_mat(). transpose(M) -> New_data = transpose_loop(M, 0, 0, <<>>), {dense_mat, erlang:element(3, M), erlang:element(2, M), New_data}. -file("src/viva_math/matrix_dense.gleam", 336). -spec dot_row_col( dense_mat(), dense_mat(), integer(), integer(), integer(), float() ) -> float(). dot_row_col(A, B, I, J, K, Acc) -> case K >= erlang:element(3, A) of true -> Acc; false -> Av = case get(A, I, K) of {ok, X} -> X; {error, _} -> +0.0 end, Bv = case get(B, K, J) of {ok, X@1} -> X@1; {error, _} -> +0.0 end, dot_row_col(A, B, I, J, K + 1, Acc + (Av * Bv)) end. -file("src/viva_math/matrix_dense.gleam", 316). -spec matmul_loop(dense_mat(), dense_mat(), integer(), integer(), bitstring()) -> bitstring(). matmul_loop(A, B, I, J, Acc) -> case I >= erlang:element(2, A) of true -> Acc; false -> V = dot_row_col(A, B, I, J, 0, +0.0), Next_acc = <>, case (J + 1) >= erlang:element(3, B) of true -> matmul_loop(A, B, I + 1, 0, Next_acc); false -> matmul_loop(A, B, I, J + 1, Next_acc) end end. -file("src/viva_math/matrix_dense.gleam", 306). ?DOC( " Matrix product A · B. Errors on shape mismatch.\n" "\n" " Naïve triple-loop O(n³). For larger matrices defer to `viva_tensor` which\n" " dispatches to BLAS / cuBLAS.\n" ). -spec mul(dense_mat(), dense_mat()) -> {ok, dense_mat()} | {error, nil}. mul(A, B) -> case erlang:element(3, A) /= erlang:element(2, B) of true -> {error, nil}; false -> New_data = matmul_loop(A, B, 0, 0, <<>>), {ok, {dense_mat, erlang:element(2, A), erlang:element(3, B), New_data}} end. -file("src/viva_math/matrix_dense.gleam", 365). -spec sum_squared(bitstring(), float()) -> float(). sum_squared(Data, Acc) -> case Data of <> -> sum_squared(Rest, Acc + (X * X)); _ -> Acc end. -file("src/viva_math/matrix_dense.gleam", 361). ?DOC(" Frobenius norm √(Σ aᵢⱼ²).\n"). -spec frobenius(dense_mat()) -> float(). frobenius(M) -> math:sqrt(sum_squared(erlang:element(4, M), +0.0)). -file("src/viva_math/matrix_dense.gleam", 380). -spec trace_loop(dense_mat(), integer(), float()) -> float(). trace_loop(M, I, Acc) -> case I >= erlang:element(2, M) of true -> Acc; false -> case get(M, I, I) of {ok, V} -> trace_loop(M, I + 1, Acc + V); {error, _} -> Acc end end. -file("src/viva_math/matrix_dense.gleam", 373). ?DOC(" Trace of a square matrix.\n"). -spec trace(dense_mat()) -> {ok, float()} | {error, nil}. trace(M) -> case erlang:element(2, M) /= erlang:element(3, M) of true -> {error, nil}; false -> {ok, trace_loop(M, 0, +0.0)} end.