defmodule Chi2fit.Utilities do
# Copyright 2015-2017 Pieter Rijken
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@moduledoc """
Provides various utilities:
* Bootstrapping
* Creating Cumulative Distribution Functions / Histograms from sample data
* Autocorrelation coefficients
"""
alias Chi2fit.Distribution, as: D
alias Chi2fit.Distribution.Utilities, as: U
alias Chi2fit.Fit, as: F
alias Chi2fit.Math, as: M
alias Chi2fit.Matrix, as: Mx
alias Chi2fit.Statistics, as: S
@typedoc "Average and standard deviationm (error)"
@type avgsd :: {avg :: float, sd :: float}
@doc """
Reads data from a file specified by `filename` and returns a stream with the data parsed as floats.
It expects a single data point on a separate line and removes entries that:
* are not floats, and
* smaller than zero (0)
"""
@spec read_data(filename::String.t) :: Enumerable.t
def read_data(filename) do
filename
|> File.stream!([],:line)
|> Stream.flat_map(&String.split(&1,"\r",trim: true))
|> Stream.filter(&is_tuple(Float.parse(&1)))
|> Stream.map(&elem(Float.parse(&1),0))
|> Stream.filter(&(&1 >= 0.0))
end
@doc """
Unzips lists of 1-, 2-, 3-, 4-, 5-, 6-, 7-, and 8-tuples.
"""
@spec unzip(list::[tuple]) :: tuple
def unzip([]), do: {}
def unzip([{}|_]), do: {}
def unzip(list=[{_}|_]), do: {Enum.map(list,fn {x}->x end)}
def unzip(list=[{_,_}|_]), do: Enum.unzip(list)
def unzip(list=[{_,_,_}|_]) do
{
list |> Enum.map(&elem(&1,0)),
list |> Enum.map(&elem(&1,1)),
list |> Enum.map(&elem(&1,2))
}
end
def unzip(list=[{_,_,_,_}|_]) do
{
list |> Enum.map(&elem(&1,0)),
list |> Enum.map(&elem(&1,1)),
list |> Enum.map(&elem(&1,2)),
list |> Enum.map(&elem(&1,3))
}
end
def unzip(list=[{_,_,_,_,_}|_]) do
{
list |> Enum.map(&elem(&1,0)),
list |> Enum.map(&elem(&1,1)),
list |> Enum.map(&elem(&1,2)),
list |> Enum.map(&elem(&1,3)),
list |> Enum.map(&elem(&1,4))
}
end
def unzip(list=[{_,_,_,_,_,_}|_]) do
{
list |> Enum.map(&elem(&1,0)),
list |> Enum.map(&elem(&1,1)),
list |> Enum.map(&elem(&1,2)),
list |> Enum.map(&elem(&1,3)),
list |> Enum.map(&elem(&1,4)),
list |> Enum.map(&elem(&1,5))
}
end
def unzip(list=[{_,_,_,_,_,_,_}|_]) do
{
list |> Enum.map(&elem(&1,0)),
list |> Enum.map(&elem(&1,1)),
list |> Enum.map(&elem(&1,2)),
list |> Enum.map(&elem(&1,3)),
list |> Enum.map(&elem(&1,4)),
list |> Enum.map(&elem(&1,5)),
list |> Enum.map(&elem(&1,6))
}
end
def unzip(list=[_|_]) do
0..tuple_size(hd(list))-1
|> Enum.reduce({},fn i,tup -> Tuple.append(tup,list |> Enum.map(&elem(&1,i))) end)
end
##
## Local functions
##
@doc """
Outputs and formats the errors that result from a call to `Chi2fit.Fit.chi2/4`
Errors are tuples of length 2 and larger: `{[min1,max1], [min2,max2], ...}`.
"""
@spec puts_errors(device :: IO.device(), errors :: tuple()) :: none()
def puts_errors(device \\ :stdio, errors) do
errors
|> Tuple.to_list
|> Enum.with_index
|> Enum.each(fn
{[mn,mx],0} -> IO.puts device, "\t\t\tchi2:\t\t#{mn}\t-\t#{mx}"
{[mn,mx],_} -> IO.puts device, "\t\t\tparameter:\t#{mn}\t-\t#{mx}"
end)
end
@doc """
Displays results of the function `Chi2fit.Fit.chi2probe/4`
"""
@spec display(device :: IO.device(), F.chi2probe() | avgsd()) :: none()
def display(device \\ :stdio, results)
def display(device,{chi2, parameters, errors, _saved}) do
IO.puts device,"Initial guess:"
IO.puts device," chi2:\t\t#{chi2}"
IO.puts device," pars:\t\t#{inspect parameters}"
IO.puts device," ranges:\t\t#{inspect errors}\n"
end
def display(device,{avg, sd, direction}) when direction in [:-, :+] do
op = case direction do
:+ -> &Kernel.+/2
:- -> &Kernel.-/2
end
IO.puts device,"50% => #{:math.ceil(avg)} units"
IO.puts device,"84% => #{:math.ceil(op.(avg,sd))} units"
IO.puts device,"97.5% => #{:math.ceil(op.(avg,2*sd))} units"
IO.puts device,"99.85% => #{:math.ceil(op.(avg,3*sd))} units"
end
def display(device, {list, direction}) when is_list(list) and direction in [:-, :+] do
sorted = case direction do
:+ -> list |> Enum.sort
:- -> list |> Enum.sort |> Enum.reverse
end
max = length(sorted)
IO.puts device,"50% => #{Enum.at(sorted,round(0.5 * max))} units"
IO.puts device,"84% => #{Enum.at(sorted,round(0.84 * max))} units"
IO.puts device,"97.5% => #{Enum.at(sorted,round(0.975 * max))} units"
IO.puts device,"99.85% => #{Enum.at(sorted,round(0.9985 * max))} units"
end
@doc """
Pretty prints subsequences.
"""
@spec display_subsequences(device :: IO.device(), trends :: list(), intervals :: [NaiveDateTime.t]) :: none()
def display_subsequences(device \\ :stdio, trends, intervals) do
trends
|> Stream.transform(1, fn arg={_,_,data}, index -> { [{arg, Enum.at(intervals,index)}], index+length(data)} end)
|> Stream.each(fn
{{chimin, [rate], subdata},date} ->
IO.puts device, "Subsequence ending @#{Timex.format!(date,~S({Mshort}, {D} {YYYY}))}"
IO.puts device, "----------------------------------"
IO.puts device, " chi2@minimum: #{Float.round(chimin,1)}"
IO.puts device, " delivery rate: #{Float.round(rate,1)}"
IO.puts device, " subsequence: #{inspect(subdata, charlists: :as_lists)}"
IO.puts device, ""
end)
|> Stream.run()
end
@doc """
Displays results of the function `Chi2fit.Fit.chi2fit/4`
"""
@spec display(device :: IO.device(),hdata :: S.ecdf(),model :: U.model(),F.chi2fit(),options :: Keyword.t) :: none()
def display(device \\ :stdio,hdata,model,{chi2, cov, parameters, errors},options) do
param_errors = cov |> Mx.diagonal |> Enum.map(fn x->x|>abs|>:math.sqrt end)
IO.puts device,"Final:"
IO.puts device," chi2:\t\t#{chi2}"
IO.puts device," Degrees of freedom:\t#{length(hdata)-D.size(model)}"
IO.puts device," gradient:\t\t#{inspect M.jacobian(parameters,&F.chi2(hdata,fn x->D.cdf(model).(x,&1) end,fn _->0.0 end,options),options)}"
IO.puts device," parameters:\t\t#{inspect parameters}"
IO.puts device," errors:\t\t#{inspect param_errors}"
IO.puts device," ranges:"
puts_errors device,errors
end
@doc """
Walks a map structure while applying the function `fun`.
"""
@spec analyze(map :: %{}, fun :: (([number],Keyword.t) -> Keyword.t), options :: Keyword.t) :: Keyword.t
def analyze(map = %{}, fun, options) do
map |> Enum.reduce(%{}, fn {k,v},acc -> Map.put(acc,k,analyze(v,fun,options)) end)
end
def analyze(data, fun, options) when is_list(data) do
cond do
Keyword.keyword?(data) ->
Keyword.merge(data, fun.(data,Keyword.put(options,:bin,data[:bin])))
true ->
analyze([throughput: data, bin: options[:bin]], fun, options)
end
end
@doc """
Pretty-prints a nested array-like structure (list or tuple) as a table.
"""
@spec as_table(rows :: [any], header :: tuple()) :: list()
def as_table(rows, header) do
map = 1..tuple_size(header) |> Enum.map(&{&1,0}) |> Enum.into(%{})
table = [header|rows] |> _to_string()
map = Enum.reduce(table, map, fn row,acc ->
row
|> Enum.with_index(1)
|> Enum.reduce(acc, fn {str,i},acc2 -> Map.update!(acc2, i, fn v -> max(v,String.length(str)) end) end)
end)
table
|> Enum.with_index()
|> Enum.each(fn
{row, 0} ->
IO.puts row |> Enum.with_index(1) |> Enum.map(fn {str,i} -> String.pad_trailing(str, map[i]) end) |> Enum.join("|")
IO.puts row |> Enum.with_index(1) |> Enum.map(fn {_,i} -> String.duplicate("-",map[i]) end) |> Enum.join("|")
{row, _} ->
IO.puts row |> Enum.with_index(1) |> Enum.map(fn {str,i} -> String.pad_trailing(str, map[i]) end) |> Enum.join("|")
end)
rows
end
defp _to_string(list) when is_list(list), do: list |> Enum.map(&_to_string/1)
defp _to_string(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list |> _to_string()
defp _to_string(string) when is_binary(string), do: string
defp _to_string(float) when is_float(float), do: "#{float}"
defp _to_string(integer) when is_integer(integer), do: "#{integer}"
@doc ~S"""
## Examples
iex> subsequences []
[]
iex> subsequences [:a, :b]
[[:a], [:a, :b]]
iex> Stream.cycle([1,2,3]) |> subsequences |> Enum.take(4)
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 1]]
"""
@spec subsequences(Enumerable.t) :: Enumerable.t
def subsequences(stream) when is_function(stream, 2) do
stream
|> Stream.transform([], fn x,acc -> {[Enum.reverse([x|acc])], [x|acc]} end)
end
def subsequences(list) do
{result, _} = list
|> Enum.reduce({[],[]}, fn x, {res,acc} -> {[Enum.reverse([x|acc])|res], [x|acc]} end)
Enum.reverse(result)
end
end