Current section
Files
Jump to
Current section
Files
lib/vector.ex
defmodule Numy.Vector do
@moduledoc """
Typical vector operations with `[float]` List.
When list is long, we call NIF functions to speed up computation.
Note: What we call "vector" here is just a list of numbers of type float,
it is not a new type.
**Important:** Make sure **ALL** elements of vector(s) have type float before calling
functions of this module. NIF C functions assume all elements have type float,
they will fail (usually with "bad argument" error) if some element is not float.
Author: Igor Lesik 2019
"""
@on_load :load_nifs
@doc """
Callback on module's load. Loads NIF shared library.
"""
def load_nifs do
mix_app = Mix.Project.config[:app]
path = :filename.join(:code.priv_dir(mix_app), 'libnumy_vector')
:erlang.load_nif(path, 0)
end
@doc """
Convert list of numbers to list of floats (what we call vector).
"""
@spec from_list([number]) :: [float]
def from_list(list) do
Numy.Enumy.all_to_float(list)
end
# hmm, length() calculated by traversing, maybe having this guard is not a good idea
defguard is_two_vectors_equal_length(vector1, vector2) when
is_list(vector1) and is_list(vector2) and length(vector1) == length(vector2)
# Vector size when we use NIF implementation instead of pure Elixir code.
@nif_watermark 100_000
@doc """
The dot product is the sum of the products of the corresponding entries
of the two sequences (vectors) of numbers.
When vectors are long we call `nif_dot_product`.
**Warning:** make sure all elements are float,
`use vec = Numy.Vector.from_list(your_list)`
"""
@spec dot_product([float], [float]) :: float
def dot_product(vector1, vector2) when is_two_vectors_equal_length(vector1, vector2) do
if length(vector1) < @nif_watermark do
Numy.Enumy.dot_product(vector1, vector2)
else
nif_dot_product(vector1, vector2)
end
end
@doc """
NIF implementation of dot product.
## Benchmarks
iex(16)> import Numy.Vector
iex(17)> vec = Numy.Vector.from_list(Enum.to_list(1..100))
iex(18)> Benchee.run(%{"elixir" => fn -> dot_product(vec, vec) end, \\
...(18)> "C code" => fn -> nif_dot_product(vec, vec) end})
Comparison:
C code 173.44 K
elixir 149.06 K - 1.16x slower +0.94 μs
iex(19)> vec = Numy.Vector.from_list(Enum.to_list(1..1_000))
Comparison:
C code 24.11 K
elixir 14.77 K - 1.63x slower +26.25 μs
iex(24)> vec = Numy.Vector.from_list(Enum.to_list(1..99_000))
Comparison:
C code 248.26
elixir 94.39 - 2.63x slower +6.57 ms
"""
def nif_dot_product(vector1, vector2) when is_two_vectors_equal_length(vector1, vector2) do
raise "nif_dot_product/2 not implemented"
end
@doc """
Multiply each element of vector by scalar constant factor.
For long vectors scaling is done in parallel with Flow.
## Examples
iex(2)> scale([2.0,3.0,4.0], 0.5)
[1.0, 1.5, 2.0]
## Benchmarks
iex> import Numy.Vector
iex> vec1 = Enum.to_list(1..1000) |> from_list
iex> vec10 = Enum.to_list(1..10_000) |> from_list
iex> vec99 = Enum.to_list(1..99_000) |> from_list
iex> vec101 = Enum.to_list(1..101_000) |> from_list
iex> vec999 = Enum.to_list(1..999_000) |> from_list
iex(7)> Benchee.run(%{"1" => fn -> scale(vec1, 0.5) end,
...(7)> "10" => fn -> scale(vec10, 0.5) end,
...(7)> "99" => fn -> scale(vec99, 0.5) end,
...(7)> "101" => fn -> scale(vec101, 0.5) end,
...(7)> "999" => fn -> scale(vec999, 0.5) end})
Name ips average deviation median 99th %
1 18824.26 0.0531 ms ±18.54% 0.0504 ms 0.0785 ms
10 1612.85 0.62 ms ±5.92% 0.61 ms 0.75 ms
99 118.20 8.46 ms ±10.34% 8.30 ms 13.34 ms
101 16.46 60.75 ms ±9.79% 59.47 ms 80.31 ms
999 1.68 593.90 ms ±8.80% 578.90 ms 704.34 ms
"""
@spec scale([float], float) :: [float]
def scale(vector, factor) do #when length(vector) < 100_000 do
Enum.map(vector, fn x -> x * factor end)
end
#def scale(vector, factor) do
# vector
# |> Flow.from_enumerable()
# |> Flow.map(fn x -> x * factor end)
# |> Enum.to_list
#end
@doc """
## Examples
iex(5)> add([1.0,2.0,3.0],[2.0,3.0,4.0])
[3.0, 5.0, 7.0]
"""
@spec add([float], [float]) :: [float]
def add(vec1, vec2) when vec1 == [] or vec2 == [], do: []
def add(vec1, vec2) when length(vec1) < 10_000 do
Enum.zip(vec1,vec2) |> Enum.map(fn {a,b} -> a + b end)
end
def add(vec1, vec2) do
Enum.zip(vec1,vec2)
|> Flow.from_enumerable
|> Flow.map(fn {a,b} -> a + b end)
|> Enum.to_list
end
@spec subtract([float], [float]) :: [float]
def subtract(vec1, vec2) when vec1 == [] or vec2 == [], do: []
def subtract(vec1, vec2) when length(vec1) < 10_000 do
Enum.zip(vec1,vec2) |> Enum.map(fn {a,b} -> a - b end)
end
def subtract(vec1, vec2) do
Enum.zip(vec1,vec2)
|> Flow.from_enumerable
|> Flow.map(fn {a,b} -> a - b end)
|> Enum.to_list
end
@spec element_multiply([float], [float]) :: [float]
def element_multiply(vec1, vec2) when vec1 == [] or vec2 == [], do: []
def element_multiply(vec1, vec2) when length(vec1) < 10_000 do
Enum.zip(vec1,vec2) |> Enum.map(fn {a,b} -> a - b end)
end
def element_multiply(vec1, vec2) do
Enum.zip(vec1,vec2)
|> Flow.from_enumerable
|> Flow.map(fn {a,b} -> a * b end)
|> Enum.to_list
end
@spec mean_sq_err([float], [float]) :: float
def mean_sq_err(vec1, vec2) do
sum_sq_err = Enum.zip(vec1,vec2)
|> Flow.from_enumerable
|> Flow.map(fn {a,b} -> (a - b)*(a - b) end)
#|> Flow.reduce( TODO figure out how reduce works
# fn -> %{} end,
# fn x, acc -> acc + x end
# )
|> Enum.sum
sum_sq_err / length(vec1)
end
@spec root_mean_sq_err([float], [float]) :: float
def root_mean_sq_err(vec1, vec2) do
:math.sqrt(mean_sq_err(vec1,vec2))
end
end