Read what an MlCommand answered.
Latu latches the whole MlCommandResult onto the finished execution rather than picking an
arm, because which arm is set is part of the answer. This is where the picking happens.
Clauses match maps rather than named messages, as Latu.Result.Literal does: the shape is
what matters, and a pattern is not a call into the protocol layer.
Summary
Types
A loaded operator, as a Read described it.
Functions
The object reference a Fit put in the ML cache.
What a Read answered with: a loaded operator, as data.
The cache ids a Fetch answered with, where the answer was a list of cached objects.
The value an attribute Fetch answered with.
The cache id a Fetch answered with, where the answer was another cached object.
Types
@type operator_info() :: %{ ref: String.t() | nil, name: String.t() | nil, uid: String.t() | nil, params: [Latu.ML.Plan.param()] }
A loaded operator, as a Read described it.
ref is set for a model and nil for everything else; name is the reverse. params are
the values the saved metadata carried, in the shape Latu.ML.Plan.params/1 takes them.
Functions
@spec object_ref(struct()) :: {:ok, Latu.ML.Plan.object_ref()} | {:error, Latu.Error.t()}
The object reference a Fit put in the ML cache.
A Fit populates obj_ref and nothing else — uid, params and warning_message all come
back nil — which is why a model's uid is the client's own, not the server's.
@spec operator_info(struct()) :: {:ok, operator_info()} | {:error, Latu.Error.t()}
What a Read answered with: a loaded operator, as data.
The one answer the server fills in. A Fit populates obj_ref and nothing else — a fitted
model's uid and params are the client's own, copied from the estimator — where this carries
uid and params too, because both came off the saved metadata and no client could have
known them.
A model carries a cache reference as well: reading one registers it in the session's ML cache exactly as a fit does, and it is the caller's to release. An estimator, transformer or evaluator carries its class name instead, and nothing is cached.
Each param is typed from the arm the server chose rather than from the registry, so a class
the registry never heard of loads as readily as one it knows. The two agree everywhere they
both apply: PySpark re-types a loaded param through its declared converter, and every
disagreement that could produce — a :float param, an :int past 32 bits — collapses to
the same literal on the way back out.
@spec operators(struct()) :: {:ok, [Latu.ML.Plan.object_ref()]} | {:error, Latu.Error.t()}
The cache ids a Fetch answered with, where the answer was a list of cached objects.
MlCommandResult's operator_info arm again, but the server has packed several ids into one
ObjectRef by joining them with commas — MLHandler does ids.mkString(",") and PySpark
splits them apart. There is no repeated field to read instead, so this is the format rather
than a convention.
Each id is a separate cache entry: the server registered every sub-model on its way out, so a forest of a hundred trees is a hundred entries the caller now owns.
@spec param(struct()) :: {:ok, term()} | {:error, Latu.Error.t()}
The value an attribute Fetch answered with.
A Vector or Matrix arrives as a UDT-typed struct literal and comes back as an
Nx.Tensor or a Latu.ML.SparseVector; everything else is whatever
Latu.Result.Literal.value/1 makes of it.
@spec summary(struct()) :: {:ok, String.t()} | {:error, Latu.Error.t()}
The cache id a Fetch answered with, where the answer was another cached object.
MlCommandResult's third arm, and a bare string: asking a model for its summary gets the
id of the summary rather than the summary itself, because a summary is a server-side object
like the model is. Latu.ML.summary/1 does not send this — it composes the reference from
the model's own, as PySpark does — so this is here for the paths that do, evaluate first
among them.