lib/utilities.ex

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