# `Latu.ML.Linalg`
[🔗](https://github.com/zero-one-group/latu_ml/blob/v0.4.2/lib/latu/ml/linalg.ex#L1)

Spark's `Vector` and `Matrix`, both ways.

Spark serialises these as struct literals typed by a JVM class rather than by field names,
so Latu hands them back as `Latu.Result.UDT` — the class, and the elements in the order that
class defines them, with nothing asserted about how many. Asserting is this module's job, and
the layouts are PySpark's `pyspark/ml/connect/serialize.py`, element for element.

A dense vector becomes an `Nx.Tensor`; a sparse one becomes a `Latu.ML.SparseVector`, because
`Nx` has no sparse tensor and quietly densifying a vector that is sparse on purpose is not a
decision this package makes. A matrix always comes back **row-major**, whatever
`isTransposed` said on the wire — that flag is a storage detail, not a shape.

# `decoded`

```elixir
@type decoded() :: Nx.Tensor.t() | Latu.ML.SparseVector.t() | nil
```

What a `Vector` or `Matrix` literal decodes to.

`nil` where the server sent an empty one. That is not a failure and not a missing value: some
attributes are genuinely empty on some models, and `Nx` cannot represent a tensor with a
zero-sized dimension.

# `from_udt`

```elixir
@spec from_udt(Latu.Result.UDT.t()) :: {:ok, decoded()} | {:error, Latu.Error.t()}
```

Turn a `Latu.Result.UDT` into a tensor or a `Latu.ML.SparseVector`.

    iex> udt = %Latu.Result.UDT{class: "org.apache.spark.ml.linalg.VectorUDT",
    ...>                        elements: [1, nil, nil, [0.5, -1.5]]}
    iex> {:ok, tensor} = Latu.ML.Linalg.from_udt(udt)
    iex> Nx.to_flat_list(tensor)
    [0.5, -1.5]

    iex> udt = %Latu.Result.UDT{class: "org.apache.spark.ml.linalg.VectorUDT",
    ...>                        elements: [0, 4, [1, 3], [2.0, 4.0]]}
    iex> Latu.ML.Linalg.from_udt(udt)
    {:ok, %Latu.ML.SparseVector{size: 4, indices: [1, 3], values: [2.0, 4.0]}}

An empty answer is an answer, and the server does send them — `NaiveBayesModel.sigma` is a
0x0 Matrix on a multinomial model, because sigma belongs to the gaussian one. `Nx` has no
empty tensor at all (a dimension must be a positive integer), so this is `nil` rather than a
shape nothing can hold:

    iex> udt = %Latu.Result.UDT{class: "org.apache.spark.ml.linalg.MatrixUDT",
    ...>                        elements: [1, 0, 0, nil, nil, [], true]}
    iex> Latu.ML.Linalg.from_udt(udt)
    {:ok, nil}

# `from_udt!`

```elixir
@spec from_udt!(Latu.Result.UDT.t()) :: decoded()
```

See `from_udt/1`. Raises instead of returning an error.

# `to_literal`

```elixir
@spec to_literal(Nx.Tensor.t() | Latu.ML.SparseVector.t()) :: Latu.ML.Plan.literal()
```

Turn a tensor or a `Latu.ML.SparseVector` into the literal Spark expects.

A rank-1 tensor is a dense vector, a rank-2 tensor a dense matrix sent row-major — which is
what `isTransposed: true` means on the wire, and why it is set. Element order and the null
placeholders are PySpark's `serialize_param`, so the oracle can diff this byte for byte.

Raises rather than returning an error: this is a value on its way out, and a caller who hands
over a rank-3 tensor has a bug, not a runtime condition.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
