Packages

Some basic filters like Kalman filters implemented in Elixir with Nx. Largely based on a port of the excellent https://github.com/rlabbe/filterpy library.

Current section

Files

Jump to
filter_ex lib utils.ex
Raw

lib/utils.ex

defmodule FilterEx.Utils do
def tt(x) do
Nx.transpose(x)
end
def zeros({m,n}, def \\ 0.0) do
Nx.broadcast(def, {m,n})
end
@doc """
Convert scalar, list, 1d-tensor to a 2d tensor.
iex> 1.0 |> FilterEx.Utils.to_tensor_2d()
Nx.tensor([[1.0]])
iex> [1.0, 2.0] |> FilterEx.Utils.to_tensor_2d()
Nx.tensor([[1.0], [2.0]])
iex> [[1.0, 2.0], [3.0, 4.0]] |> FilterEx.Utils.to_tensor_2d()
Nx.tensor([[1.0, 2.0], [3.0, 4.0]])
"""
def to_tensor_2d(value) do
case value do
value when is_number(value) ->
Nx.tensor([[value]], type: :f32)
[] ->
raise %ArgumentError{message: "unable to convert tensor shape to 2d"}
[v | _] when is_list(value) and (is_number(v) or v == nil or v == :nan or v == :inf or v == :neg_inf) ->
Nx.tensor([value], type: :f32)
|> Nx.transpose()
value when is_list(value) ->
Nx.tensor(value, type: :f32)
%Nx.Tensor{shape: {_}} ->
Nx.tensor([value], type: :f32)
%Nx.Tensor{shape: {_, _}} ->
value
%Nx.Tensor{} ->
raise %ArgumentError{message: "unable to convert tensor shape to 2d"}
end
end
@doc ~s"""
ensure z is a (dim_z, 1) shaped vector
## Examples
iex> FilterEx.Utils.reshape_z(3.0, 1, 1)
Nx.tensor([ 3.0 ], names: [:x])
iex> FilterEx.Utils.reshape_z(3.0, 1, 2)
Nx.tensor([[ 3.0 ]], names: [:x, :y])
iex> FilterEx.Utils.reshape_z([3.0], 1, 1)
Nx.tensor([ 3.0 ], names: [:x])
iex> FilterEx.Utils.reshape_z([3.0], 1, 2)
Nx.tensor([[3.0]], names: [:x, :y])
iex> FilterEx.Utils.reshape_z([3.0,2.0], 2, 1)
Nx.tensor([3.0, 2.0], names: [:x])
iex> FilterEx.Utils.reshape_z([3.0,2.0], 2, 2)
Nx.tensor([[3.0], [2.0]], names: [:x, :y])
"""
def reshape_z(z, dim_z, ndim) do
# IO.inspect(z, label: "reshape z::")
# IO.inspect({dim_z, ndim}, label: "reshape {dim_z, ndim}::")
z = if is_number(z) do Nx.broadcast(z, {1,1}) else z |> Nx.tensor() end
z = case z |> Nx.shape() do
{n} -> z |> Nx.reshape({1,n})
_other -> z
end
z = z |> Nx.tensor(names: [:y, :x])
# IO.inspect(z, label: Zz)
z = if z |> Nx.shape() |> elem(1) == dim_z do z |> tt() else z end
# IO.inspect(z, label: Zzz)
if Nx.shape(z) != {dim_z, 1} do
raise %ArgumentError{ message: "z (shape #{Nx.shape(z)}) must be convertible to shape (#{dim_z}, 1)" }
end
z = if ndim == 1 do z[y: 0] else z end
# IO.inspect(z, label: Zzza)
z = if ndim == 0 do z[0][0] else z end
# IO.inspect(z, label: Zzzb)
z
end
@doc """
Returns the Q matrix for the Discrete Constant White Noise
Model. dim may be either 2, 3, or 4 dt is the time step, and sigma
is the variance in the noise.
Q is computed as the G * G^T * variance, where G is the process noise per
time step. In other words, G = [[.5dt^2][dt]]^T for the constant velocity
model.
Parameters
-----------
dim : int (2, 3, or 4)
dimension for Q, where the final dimension is (dim x dim)
dt : float, default=1.0
time step in whatever units your filter is using for time. i.e. the
amount of time between innovations
var : float, default=1.0
variance in the noise
block_size : int >= 1
If your state variable contains more than one dimension, such as
a 3d constant velocity model [x x' y y' z z']^T, then Q must be
a block diagonal matrix.
order_by_dim : bool, default=True
Defines ordering of variables in the state vector. `True` orders
by keeping all derivatives of each dimensions)
[x x' x'' y y' y'']
whereas `False` interleaves the dimensions
[x y z x' y' z' x'' y'' z'']
Examples
--------
iex> # constant velocity model in a 3D world with a 10 Hz update rate
...> FilterEx.Utils.q_discrete_white_noise(2, dt: 0.1, var: 1.0, block_size: 3)
Nx.tensor([[0.000025, 0.0005 , 0.0 , 0.0 , 0.0 , 0.0 ],
[0.0005 , 0.01 , 0.0 , 0.0 , 0.0 , 0.0 ],
[0.0 , 0.0 , 0.000025, 0.0005 , 0.0 , 0.0 ],
[0.0 , 0.0 , 0.0005 , 0.01 , 0.0 , 0.0 ],
[0.0 , 0.0 , 0.0 , 0.0 , 0.000025, 0.0005 ],
[0.0 , 0.0 , 0.0 , 0.0 , 0.0005 , 0.01 ]
], type: :f32)
iex> FilterEx.Utils.q_discrete_white_noise(2, dt: 0.1, var: 1.0, block_size: 3, order_by_dim: false)
Nx.tensor([
[2.499999936844688e-5, 0.0, 0.0, 5.000000237487257e-4, 0.0, 0.0],
[0.0, 2.499999936844688e-5, 0.0, 0.0, 5.000000237487257e-4, 0.0],
[0.0, 0.0, 2.499999936844688e-5, 0.0, 0.0, 5.000000237487257e-4],
[5.000000237487257e-4, 0.0, 0.0, 0.009999999776482582, 0.0, 0.0],
[0.0, 5.000000237487257e-4, 0.0, 0.0, 0.009999999776482582, 0.0],
[0.0, 0.0, 5.000000237487257e-4, 0.0, 0.0, 0.009999999776482582]
], type: :f32)
References
----------
Bar-Shalom. "Estimation with Applications To Tracking and Navigation".
John Wiley & Sons, 2001. Page 274.
"""
def q_discrete_white_noise(dim, opts) do
dt = opts |> Keyword.get(:dt, 1.0)
var = opts |> Keyword.get(:var, 1.0)
block_size = opts |> Keyword.get(:block_size, 1)
order_by_dim = opts |> Keyword.get(:order_by_dim, true)
mtyp = opts |> Keyword.get(:mtyp, :f32)
if !(dim in [2, 3, 4]) do
raise %ArgumentError{message: "dim must be between 2 and 4"}
end
qQ =
case dim do
2 ->
[[0.25*dt**4, 0.5*dt**3],
[0.5*dt**3, dt**2]]
3 ->
[[0.25*dt**4, 0.5*dt**3, 0.5*dt**2],
[0.5*dt**3, dt**2, dt],
[0.5*dt**2, dt, 1]]
_other ->
[[(dt**6)/36, (dt**5)/12, (dt**4)/6, (dt**3)/6],
[(dt**5)/12, (dt**4)/4, (dt**3)/2, (dt**2)/2],
[(dt**4)/6, (dt**3)/2, dt**2, dt],
[(dt**3)/6, (dt**2)/2 , dt, 1.0]]
end
|> Nx.tensor(type: mtyp)
# IO.inspect(qQ, label: "q_diag: qq: ")
if order_by_dim do
# block_diag(*[qQ]*block_size) * var # wtf python...
mats = 1..block_size |> Enum.map(fn _ -> qQ end)
block_diag(mats) |> Nx.multiply(var)
else
order_by_derivative(qQ, dim, block_size) |> Nx.multiply(var)
end
end
@doc """
Creates a block diagonal matrix from a list of matrices using Nx.
## Examples
iex> qQ = Nx.tensor([[1, 2], [3, 4]], type: :f32)
...> matrices = [qQ, qQ, qQ] # List of matrices to place on the diagonal
...> FilterEx.Utils.block_diag(matrices)
Nx.tensor([
[1.0, 2.0, 0.0, 0.0, 0.0, 0.0],
[3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 2.0, 0.0, 0.0],
[0.0, 0.0, 3.0, 4.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0, 2.0],
[0.0, 0.0, 0.0, 0.0, 3.0, 4.0]
], type: :f32)
"""
def block_diag(matrices) when is_list(matrices) do
# Calculate the total size of the resulting matrix
# IO.inspect(matrices, label: "block_diag:matrices")
{total_rows, total_cols, {styp, ssz}} =
for mat <- matrices, reduce: {0, 0, nil} do
{rows, cols, _typ} ->
rows = rows + elem(Nx.shape(mat), 0)
cols = cols + elem(Nx.shape(mat), 1)
{rows, cols, Nx.type(mat)}
end
# IO.inspect({total_rows, total_cols, {styp, ssz}}, label: "block_diag:total indx")
# Initialize an empty matrix of zeros
result =
Nx.tensor(0, type: :"#{styp}#{ssz}")
|> Nx.broadcast({total_rows, total_cols})
# Fill in each block along the diagonal
for mat <- matrices, reduce: {result, 0, 0} do
{acc, start_row, start_col} ->
{rows, cols} = Nx.shape(mat)
updated_matrix =
acc
|> Nx.put_slice([start_row, start_col], mat)
{updated_matrix, start_row + rows, start_col + cols}
end
|> elem(0)
end
@doc """
Flattens the input tensor into a 1D tensor using Nx.
## Examples
iex> Nx.tensor([[1, 2, 3], [4, 5, 6]], type: :f32) |> FilterEx.Utils.ravel()
Nx.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], type: :f32)
"""
def ravel(tensor) do
# Get the total number of elements in the tensor
num_elements = Nx.size(tensor)
# Reshape the tensor into a 1D tensor
Nx.reshape(tensor, {num_elements})
end
@doc """
Given a matrix Q, ordered assuming state space
[x y z x' y' z' x'' y'' z''...]
return a reordered matrix assuming an ordering of
[ x x' x'' y y' y'' z z' y'']
This works for any covariance matrix or state transition function
Parameters
----------
Q : np.array, square
The matrix to reorder
dim : int >= 1
number of independent state variables. 3 for x, y, z
block_size : int >= 0
Size of derivatives. Second derivative would be a block size of 3
(x, x', x'')
"""
def order_by_derivative(qQ, dim, block_size) do
nN = dim * block_size
dD = zeros({nN, nN})
# qQ = array(qQ)
for {x, i} <- Enum.with_index(qQ |> ravel() |> Nx.to_list()), reduce: dD do
dD ->
f = Nx.eye(block_size) |> Nx.multiply(x)
ix = div(i, dim) * block_size
iy = rem(i, dim) * block_size
# dD[ix .. (ix + block_size)][iy .. (iy + block_size)] = f
dD |> Nx.put_slice([ix, iy], f)
end
end
@doc """
Get scalar number from a 1, 2, or 3 dimension tensor from zeroth index.
"""
def to_scalar(mat) do
case mat |> Nx.shape() do
{} ->
mat
{_n} ->
mat[0]
{_m, _n} ->
mat[0][0]
{_m, _n, _o} ->
mat[0][0][0]
end
|> Nx.to_number()
end
end