Current section
Files
Jump to
Current section
Files
lib/annex/data.ex
defmodule Annex.Data do
@moduledoc """
Annex.Data defines the callbacks and helpers for data structures used
by Annex.
An implementer of the Annex.Layer behaviour must return an Annex.Data
implementer from the `c:data_type/0` callback.
"""
alias Annex.{
AnnexError,
Data,
Data.List1D,
Data.List2D,
Shape
}
require Shape
@typedoc """
A module that implements the Annex.Data Behaviour.
"""
@type type :: module()
@type flat_data :: [float(), ...]
@type data :: struct() | flat_data() | [flat_data()]
@type op :: any()
@type args :: list(any())
defguard is_flat_data(data) when is_list(data) and is_float(hd(data))
@callback cast(data, Shape.t()) :: data()
@callback to_flat_list(data) :: list(float())
@callback shape(data) :: Shape.t()
@callback is_type?(any) :: boolean
@callback apply_op(data(), op(), args()) :: data()
defmacro __using__(_) do
quote do
require Annex.Data
require Annex.Shape
alias Annex.Data
alias Annex.Shape
@behaviour Annex.Data
end
end
@doc """
Annex.Data.cast/4 calls cast/3 for an Annex.Data behaviour implementing module.
Valid shapes are a non-empty tuple of positive integers or any the atom :any.
e.g. `{2, 3}` or `{3, :any}`
"""
def cast(type, data, []) when is_list(data) do
message = "Annex.Data.cast/3 got an empty list for shape"
raise AnnexError.build(message, type: type, data: data)
end
def cast(type, data, shape) when Shape.is_shape(shape) and is_atom(type) do
type.cast(data, shape)
end
@spec cast(Data.data(), Shape.t()) :: Data.data()
def cast(data, shape) do
data
|> infer_type()
|> cast(data, shape)
end
@doc """
Flattens an Annex.Data into a list of floats via the type's callback.
"""
@spec to_flat_list(type(), data()) :: Data.flat_data()
def to_flat_list(type, data), do: type.to_flat_list(data)
@doc """
Flattens an Annex.Data into a list of floats via Enum.into/2.
"""
@spec to_flat_list(Data.data()) :: Data.flat_data()
def to_flat_list(data) do
data
|> infer_type()
|> to_flat_list(data)
end
@doc """
Given an Annex.Data `type` and the `data` returns the shape of the data.
The shape of data is used to cast between the expected shapes from one Annex.Layer
to the next or from one Annex.Sequence to the next.
"""
@spec shape(type(), data()) :: Shape.t()
def shape(type, data), do: type.shape(data)
@spec shape(data()) :: Shape.t()
def shape(data), do: data |> infer_type() |> shape(data)
@doc """
Given a type (Data implementing module) and some `data` returns true or false if the
data is of the correct type.
Calls `c:is_type?/1` of the `type`.
"""
def is_type?(nil, _), do: false
def is_type?(type, data), do: type.is_type?(data)
@doc """
Given a `type`, `data`, and a `target_shape` converts the data to the `type` and `target_shape`
If the `data` matches the `type` and the `data_shape` matches the `target_shape` the
data is returned unaltered.
If either the `type` or `target_shape` do not match the `data` the data is casted using
`Data.cast/3`.
"""
def convert(type, data, target_shape) do
if is_type?(type, data) do
data_shape = shape(type, data)
do_convert(type, data, data_shape, target_shape)
else
flat = Data.to_flat_list(data)
data_shape = List1D.shape(flat)
do_convert(type, flat, data_shape, target_shape)
end
end
defp do_convert(type, data, data_shape, target_shape) do
new_shape = Shape.convert_abstract_to_concrete(target_shape, data_shape)
cast(type, data, new_shape)
end
def flat_data_to_tensor(flat_data, shape) when Shape.is_shape(shape) do
shape
|> Enum.reverse()
|> Enum.reduce(flat_data, fn chunk_size, acc ->
Enum.chunk_every(acc, chunk_size)
end)
|> unwrap()
end
defp unwrap([unwrapped]), do: unwrapped
@spec infer_type(data()) :: any
def infer_type(%module{} = item) do
if function_exported?(module, :data_type, 1) do
module.data_type(item)
else
module
end
end
def infer_type([]) do
raise %AnnexError{
message: """
#{inspect(__MODULE__)}.infer_type/1 was given an empty list.
An empty list is not valid Data.
""",
details: []
}
end
def infer_type(data) when is_flat_data(data) do
List1D
end
def infer_type([row | _]) when is_flat_data(row) do
List2D
end
@spec apply_op(data(), any, list(any)) :: data
def apply_op(data, name, args) do
data
|> infer_type()
|> apply_op(data, name, args)
end
@spec apply_op(module, data(), any, list(any)) :: data()
def apply_op(type, data, name, args) when is_atom(type) do
type.apply_op(data, name, args)
end
@spec error(Data.data(), Data.data()) :: Data.flat_data()
def error(outputs, labels) do
labels = Data.to_flat_list(labels)
outputs
|> Data.to_flat_list()
|> Data.apply_op(:subtract, [labels])
end
end