defmodule Chi2fit.Fit do
# Copyright 2012-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 """
Implements fitting a distribution function to sample data. It minimizes the liklihood function.
## Asymmetric Errors
To handle asymmetric errors the module provides three ways of determining the contribution to the likelihood function:
`simple` - value difference of the observable and model divided by the averaged error lower and upper bounds;
`asimple` - value difference of the observable and model divided by the difference between upper/lower bound and the observed
value depending on whether the model is larger or smaller than the observed value;
`linear` - value difference of the observable and model divided by a linear tranformation (See below).
### 'linear': Linear transformation
Linear transformation that:
- is continuous in u=0,
- passes through the point sigma+ at u=1,
- asymptotically reaches 1-y at u->infinity
- pass through the point -sigma- at u=-1,
- asymptotically reaches -y at u->-infinity
## References
[1] See https://arxiv.org/pdf/physics/0401042v1.pdf
"""
require Logger
alias Chi2fit.Distribution, as: D
alias Chi2fit.Matrix, as: M
alias Chi2fit.Math, as: Ma
alias Chi2fit.Statistics, as: S
alias Chi2fit.Utilities, as: U
@typedoc "Observation with symmetric errors 'dy'."
@type observable_symm :: {x :: float, y :: float, dy :: float}
@typedoc "Observation with asymmetric bounds 'y1 < y < y2'."
@type observable_asym :: {x :: float, y :: float, y1 :: float, y2 :: float}
@type observable :: observable_symm | observable_asym
@type observables :: [observable]
@typedoc "Cumulative distribution mapping 'x' and parameters to a float in the range [0,1]."
@type distribution :: ((x::float,[parameter::float])->float)
@typedoc "Tuple describing the parameter values and the distribution function."
@type model :: {[float], distribution}
@typedoc "Chi-squared statistic"
@type chi2 :: float
@typedoc "Covariance matrix"
@type cov :: M.matrix()
@typedoc "List of parameter ranges"
@type params :: [{float,float}]
@typedoc "Tuple with chi-squared, parameter values, and parameter errors at the found minimum (see `chi2probe/4`)"
@type chi2probe_simple :: {chi2(),[parameters::float],{[float],[float]}}
@typedoc "Tuple with chi-squared, parameter values, parameter errors, and list of intermediate fit results (see `chi2probe/4`)"
@type chi2probe_saved :: {chi2(),[parameters::float],{[float],[float]},[{float,[float]}]}
@typedoc "Result of chi-squared probe (see &chi2probe/4)"
@type chi2probe :: chi2probe_simple() | chi2probe_saved()
@typedoc "Tuple holding chi-squared value, covariance matrix, parameter values, and parameter errors at the minimum chi2fit(see `chi2fit/4`)"
@type chi2fit :: {chi2(), cov(), parameters :: [float], errors :: [float]}
@arithmic_penalty 1_000_000_000
@extreme_punishment 1_000_000
def nopenalties(_,_), do: 0.0
@cutoff 0.0001
defp dchi2_simple(y, y1, y2,f), do: (f-y)/abs(y-(y1+y2)/2)
defp dchi2_asimple(y, y1,_y2,f) when f<y, do: (y-f)/(y-y1)
defp dchi2_asimple(y,_y1,_y2,f) when y==f, do: 0.0
defp dchi2_asimple(y,_y1, y2,f), do: (f-y)/(y2-y)
defp dchi2_linear(y,y1,y2,f) do
delta = f-y
splus = y2-y
smin = y-y1
cond do
# Special cases
f==1.0 and y2==1.0 -> 1.0
f==0.0 and y1==0.0 -> 1.0
f==y -> 0.0
#
f<@cutoff and f<y1 -> dchi2_linear(y,y1,y2,min(@cutoff,y1))
f>(1-@cutoff) and f>y2 -> dchi2_linear(y,y1,y2,max(1-@cutoff,y2))
# Extreme punishment
f==1.0 -> @extreme_punishment
f==0.0 -> @extreme_punishment
delta>0 -> (1.0-y2)/(1.0-f) * delta/splus
true -> y1/f * delta/smin
end
end
defp likelihood_contrib(:linear, y,y1,y2,f), do: dchi2_linear y,y1,y2,f
defp likelihood_contrib(:simple, y,y1,y2,f), do: dchi2_simple y,y1,y2,f
defp likelihood_contrib(:asimple, y,y1,y2,f), do: dchi2_asimple y,y1,y2,f
@doc """
Calculates the Chi-squared function for a list of observables.
The `observables` are given as a list. Each observation has an error associated with it. The errors can be either
symmetric or asymmetric.
A 'penalties'-function is used to assign penalties and these contribute to the chi-squared function. It may be used
to 'forbid' certain parameter, x combinations.
## Options
`model` - Required. Determines the contribution to chi-squared taking the asymmetric errors into account.
Vaid values are `:linear`, `:simple`, and `:asimple`. See Errors below
## Errors
`simple` - Use for asymmetric errors when the sigma+ and sigma- are close to each other
`asimple` - Use for asymmetric errors when y-values are not bound.
`linear` - Use this model when the y-values ar bound between 0 and 1. Linear transformation that:
- is continuous in u=0,
- passes through the point sigma+ at u=1,
- asymptotically reaches 1-y at u->infinity
- pass through the point -sigma- at u=-1,
- asymptotically reaches -y at u->-infinity
## Examples
iex> fun = &(&1)
...> chi2 [{0,3,1}], fun
9.0
iex> fun = &(&1)
...> chi2 [{0,3,1},{1,7,1},{2,3,1}], fun
46.0
iex> fun = &(&1)
...> chi2 [{0,3,3},{1,7,1},{2,3,1}], fun
38.0
iex> fun = &(&1-2)
...> chi2 [{0,3,1}], fun
25.0
end
"""
@spec chi2(observables, ((float)->float), ((float)->float), Keyword.t) :: float
def chi2(observables, fun, penalties \\ fn (_)->0.0 end, options \\ [])
def chi2(observables, fun, penalties, []), do: chi2(observables, fun, penalties, [model: :simple])
def chi2(observables, fun, penalties, options) do
observables
|> Stream.map(
fn
({x,y,dy}) ->
# Symmetric errors
tmp = (y-fun.(x))/dy
tmp*tmp + penalties.(x)
({x,y,y1,y2}) ->
## Carefully handle asymmetric errors
## See Bohm (DESY), formula (8.5)
try do
tmp = likelihood_contrib options[:model], y,y1,y2,fun.(x)
tmp*tmp + penalties.(x)
rescue
ArithmeticError -> @arithmic_penalty
ArgumentError -> @arithmic_penalty
end
end)
|> Enum.sum
end
defp gamma(observables, {parameters, fun, penalties, options}) do
gammafun = &(gamma(&1,observables, {parameters, fun, penalties, options}))
Enum.reduce(length(parameters)..1, [], fn (k,acc)->[gammafun.(k)|acc] end)
end
@spec gamma(pos_integer, observables, model) :: float
defp gamma(k, observables, {parameters, fun, penalties, options}) when k>0 and k<=length(parameters) do
smoothing? = options[:smoothing] || false
params_k = parameters |> derive_par(k)
-0.5*Ma.der params_k, fn (pars)->chi2smooth(observables, pars, {fun,penalties},smoothing?,options) end, options
end
defp alpha(observables, {parameters, fun, penalties, options}) do
alphafun = &(alpha({&1,&2}, observables, {parameters, fun, penalties,options}))
Enum.reduce(length(parameters)..1, [], fn
(k,acc) -> [
Enum.reduce(length(parameters)..1, [], fn
(j,acc)->[alphafun.(k,j)|acc] end)
|acc]
end)
end
defp derive_par(list, index) do
list |> List.update_at(index-1, fn
(val) when is_number(val) ->
{val,1}
({val,n}) ->
{val,n+1}
end)
end
@spec alpha({pos_integer,pos_integer}, observables, model) :: float
defp alpha({k,j}, observables, {parameters, fun, penalties, options}) when k>0 and k<=length(parameters) and j>0 and j<=length(parameters) do
smoothing? = options[:smoothing] || false
params_kj = parameters |> derive_par(k) |> derive_par(j)
0.5*Ma.der params_kj,fn (pars)->chi2smooth(observables, pars, {fun,penalties},smoothing?,options) end, options
end
#######################################################################################################
## Chi squared fit
##
defp chi2smooth(observables,parameters,{fun,penalties},true,options) do
rx = 5.0e-4
ry = 5.0e-3
n = 1
(for dx<- -n..n, dy<- -n..n, do: {rx*dx,ry*dy})
|> Stream.map(fn ({dx,dy})-> [p1,p2]=parameters; [p1+dx,p2+dy] end)
|> Stream.map(fn (pars)-> chi2(observables, &(fun.(&1,pars)), &(penalties.(&1,pars)), options)/(2*n+1)/(2*n+1) end)
|> Enum.sum
end
defp chi2smooth(observables,parameters,{fun,penalties},false,options) do
chi2(observables, &(fun.(&1,parameters)), &(penalties.(&1,parameters)), options)
end
defp sample(list) do
list |> Enum.map(fn
({low,high})->low + :rand.uniform()*(high-low)
(x)->x
end)
end
@doc """
Probes the chi-squared surface within a certain range of the parameters.
It does so by randomly selecting parameter value combinations and calculate the chi-squared for the list
of observations based on the selected parameter values. This routine is used to roughly probe the chi-squared
surface and perform more detailed and expensive calculations to precisely determine the minimum by `chi2fit/4`.
Returns the minimum chi-squared found, the parameter values, and all probes that resulted in chi-squared difference
less than 1 with the minimum. The parameter values found in this set correspond with the errors in determining
the parameters.
## Options
`num` or `probes` - the number of points to calculate,
`mark` - progress indicator: a keyword list with keys `m`, `c`, `x`, and `*`; the value must be a call back
function taking zero arguments. These are called when 1000, 100, 10, probes have been done. The value of
key `*` is called when a new chi-squared minimum has been found,
`smoothing` - boolean value indicating whether the chi-squared is smoothened using a Gauss distribution. This
is used in case the surface is rough because of numerical instabilities to smoothen the surface,
`model` - See `chi2/3` and `chi2/4`
"""
@spec chi2probe(observables, [float], (...->any), Keyword.t) :: chi2probe()
def chi2probe(observables, parranges, fun_penalties, options) do
chi2probe(observables, parranges, fun_penalties, options[:num] || options[:probes], nil, options)
end
defp chi2probe(_observables, _parranges, {_fun,_penalties}, 0, best, options) do
## Refactor this!!!!!
{chi2,parameters,saved} = best
{_chis,plists} = saved |> Enum.unzip
result = {chi2,parameters,
plists
|> Enum.map(&List.to_tuple/1)
|> U.unzip
|> Tuple.to_list
|> Enum.map(fn plist -> [ Enum.min(plist),Enum.max(plist) ] end)
|> List.to_tuple}
if options[:saved?], do: Tuple.append(result,saved), else: result
end
defp chi2probe(observables, parranges, {fun,penalties}, num, best, options) do
if options[:progress] do
cond do
rem(num,1000) == 0 -> options[:mark][:m].()
rem(num,100) == 0 -> options[:mark][:c].()
rem(num,10) == 0 -> options[:mark][:x].()
true -> :ok
end
end
try do
smoothing = options[:smoothing] || false
parameters = parranges |> sample
chi2 = chi2smooth observables,parameters,{fun,penalties},smoothing,options
if options[:surfacefile], do: IO.puts options[:surfacefile], "#{Enum.join(parameters,",")},#{chi2}"
if options[:save], do: options[:save].(parameters,chi2)
chi2probe(observables, parranges, {fun,penalties}, num-1,
case best do
nil ->
{chi2,parameters,[{chi2,parameters}]}
{oldchi2,_,saved} when chi2<oldchi2 ->
is_function(options[:mark][:*]) && options[:mark][:*].()
{chi2,parameters,[{chi2,parameters}|Enum.filter(saved,fn ({x,_})-> x < chi2+1.0 end)]}
{oldchi2,oldpars,saved} when chi2<oldchi2+1.0 ->
{oldchi2,oldpars,[{chi2,parameters}|saved]}
_else ->
best
end,
options)
rescue
ArithmeticError ->
chi2probe(observables, parranges, {fun,penalties}, num-1, best, options)
err ->
reraise err, __STACKTRACE__
end
end
defp filter_param(parameters, flags) do
parameters |> Enum.zip(flags) |> Enum.map(fn {par, true}->par; {_par, false}->0.0 end)
end
defp vary_params(parameters, flags, _first, _second, num_variations) when is_list(parameters) do
-1..length(parameters) |> Enum.to_list
|> Stream.map(&(List.duplicate(&1,num_variations)))
|> Stream.concat
|> Stream.flat_map(
fn
(-1) ->
[
List.duplicate(:rand.uniform(),length(parameters))|>filter_param(flags),
List.duplicate(:rand.uniform()/100,length(parameters))|>filter_param(flags)
]
(0) ->
[
List.duplicate(0.0,length(parameters)) |> Enum.map(fn (_)-> 2* :rand.uniform() end)|>filter_param(flags),
List.duplicate(0.0,length(parameters)) |> Enum.map(fn (_)-> :rand.uniform() end)|>filter_param(flags),
List.duplicate(0.0,length(parameters)) |> Enum.map(fn (_)-> - :rand.uniform() end)|>filter_param(flags)
]
(n) when is_integer(n) and n>0 ->
[
List.duplicate(0.0,length(parameters)) |> List.replace_at(n-1, :rand.uniform())|>filter_param(flags),
List.duplicate(0.0,length(parameters)) |> List.replace_at(n-1, :rand.uniform()/100)|>filter_param(flags),
List.duplicate(0.0,length(parameters)) |> List.replace_at(n-1, :rand.uniform()/1000)|>filter_param(flags),
List.duplicate(0.0,length(parameters)) |> List.replace_at(n-1, :rand.uniform()/10000)|>filter_param(flags)
]
end)
|> Stream.filter(fn list -> Enum.any?(list, fn x->x != 0.0 end) end)
end
@doc """
Fits observables to a known model.
Returns the found minimum chi-squared value, covariance matrix, gradient at the minimum, and the corresponding parameter values including
error estimates.
For a good fit check the following:
`chi2 per degree of freedom` - this should be about 1 or less,
`gradient` - at the minimum the gradient should be zero at all directions.
For asymmetric errors use the option `model` equal to `linear`.
Rough chi-squared surfaces or if numerically unstable, use the option `smoothing` set to `true`.
## Arguments
`observables` - list of measurements including errors,
`model` - `{parameters, fun}`: set of initial parameter values and a function to fit against the measurements
## Options
`onstep` - call back function; it is called with a map with keys `delta`, `chi2`, and `params`,
`smoothing` - boolean value indicating whether the chi-squared is smoothened using a Gauss distribution. This
is used in case the surface is rough because of numerical instabilities to smoothen the surface,
`model` - The same values as in `chi2/3` and `chi2/4`
`grid?` - Performs a grid search: per step, tries to fit only one parameter and keeps the others fixed; selects the parameter in
a round-robin fashion
`probes` -- a list of tuples containing the result of the `chi2probe/4` function. Each tuple contains the chi2 value and parameter list.
Defaults to the empty list.
"""
@spec chi2fit(observables, model, iterations::pos_integer, options::Keyword.t) :: chi2fit()
def chi2fit(observables, model, max \\ 100, options \\ []) do
probes = options[:probes] || []
chi2fit observables, model, max, {nil,probes}, options
end
defp chi2fit(observables, {parameters, fun}, max, data, options), do: chi2fit observables, {parameters, fun, &nopenalties/2}, max, data, options
defp chi2fit(observables, {parameters, fun, penalties}, 0, {{cov,_error}, params}, options) do
chi = chi2(observables, &(fun.(&1,parameters)), &(penalties.(&1,parameters)), options)
ranges = params
|> Enum.flat_map(fn {c,p}-> if(c < chi+1.0, do: [List.to_tuple([c|p])], else: []) end)
|> Chi2fit.Utilities.unzip
|> Tuple.to_list
|> Enum.map(fn x->[Enum.min(x),Enum.max(x)] end)
|> List.to_tuple
{chi, cov, parameters, ranges}
end
defp chi2fit observables, {parameters, fun, penalties}, 0, {nil,ranges}, options do
chi2 = chi2(observables, &(fun.(&1,parameters)), &(penalties.(&1,parameters)), options)
alpha = alpha(observables, {parameters, fun, penalties, options})
{:ok,cov} = try do
alpha |> M.inverse
rescue
ArithmeticError ->
throw {:inverse_error, ArithmeticError, chi2, parameters}
catch
{:impossible_inverse,error} ->
throw {:inverse_error, error, chi2, parameters}
{:failed_to_reach_tolerance,_pars,error} ->
throw {:failed_to_reach_tolerance, error, chi2, parameters}
end
error = cov |> M.diagonal
chi2fit observables, {parameters, fun, penalties}, 0, {{cov,error},[{chi2,parameters}|ranges]}, options
end
defp chi2fit(observables, {parameters, fun, penalties}, max, {preverror,ranges}, options) when max>0 do
grid? = options[:grid?] || false
parmax = options[:pariter] || 10
vecg = gamma(observables, {parameters, fun, penalties, options})
chi2 = chi2(observables, &(fun.(&1,parameters)), &(penalties.(&1,parameters)),options)
alpha = alpha(observables, {parameters, fun, penalties,options})
ranges = [{chi2,parameters}|ranges]
try do
{:ok,cov} = alpha |> M.inverse
error = cov |> M.diagonal
flags = if grid? do
List.duplicate(false, length(parameters))
|> List.replace_at(rem(max,length(parameters)),true) ## grid search
else
List.duplicate(true, length(parameters))
end
delta = cov |> Enum.map(&(M.dotproduct(&1,vecg)))
options[:onbegin] && options[:onbegin].(%{step: max, chi: chi2, derivatives: Enum.zip(vecg,M.diagonal(alpha))})
{params,_chi2,_,ranges} = parameters
|> vary_params(flags,vecg,M.diagonal(alpha),parmax)
|> Enum.reduce_while({parameters,chi2,{nil,nil},ranges},
fn
(factor,{pars,oldchi,minimum,data}) ->
dvec = factor |> M.from_diagonal |> Enum.map(&M.dotproduct(&1,delta))
vec = M.add(parameters,dvec)
try do
smoothing? = options[:smoothing] || false
newchi = chi2smooth observables,vec,{fun,penalties},smoothing?,options
data = [{newchi,vec}|data]
new_minimum = case minimum do
{nil,nil} ->
current_par = pars |> filter_param(flags) |> Enum.sum
par = vec |> filter_param(flags) |> Enum.sum
cond do
newchi < oldchi and par <= current_par ->
{nil,{pars,oldchi}}
newchi < oldchi and par > current_par ->
{{pars,oldchi},nil}
newchi >= oldchi and par <= current_par ->
{{vec,newchi},nil}
newchi >= oldchi and par > current_par ->
{nil,{vec,newchi}}
end
{left={left_pars,_},nil} ->
current_par = pars |> filter_param(flags) |> Enum.sum
left_par = left_pars |> filter_param(flags) |> Enum.sum
par = vec |> filter_param(flags) |> Enum.sum
cond do
newchi < oldchi and par < current_par and par > left_par ->
{left,{pars,oldchi}}
newchi < oldchi and par > current_par ->
{{pars,oldchi},nil}
newchi > oldchi and par < current_par and par < left_par ->
{{vec,newchi},nil}
newchi > oldchi and par > current_par ->
{left,{vec,newchi}}
true ->
{left,nil}
end
{nil,right={right_pars,_}} ->
current_par = pars |> filter_param(flags) |> Enum.sum
right_par = right_pars |> filter_param(flags) |> Enum.sum
par = vec |> filter_param(flags) |> Enum.sum
cond do
newchi < oldchi and par > current_par and par < right_par ->
{{pars,oldchi},right}
newchi < oldchi and par < current_par ->
{nil,{pars,oldchi}}
newchi > oldchi and par > current_par and par < right_par ->
{nil,{vec,newchi}}
newchi > oldchi and par < current_par ->
{{vec,newchi},right}
true ->
{nil,right}
end
local -> local
end
case new_minimum do
{{left_pars,_lchi2},{right_pars,_rchi2}} ->
left_par = left_pars |> filter_param(flags) |> Enum.sum
right_par = right_pars |> filter_param(flags) |> Enum.sum
try do
smoothing? = options[:smoothing] || false
{_, {left,right},{_vleft,_vright}} = Ma.newton(left_par, right_par, fn x->
nvec = parameters |> List.replace_at(rem(max,length(parameters)), {x,1})
Ma.der(nvec, fn lp -> chi2smooth(observables,lp,{fun,penalties},smoothing?,options) end,options)
end, parmax, options)
nvec = pars |> List.replace_at(rem(max,length(parameters)), (left+right)/2)
newchi = chi2smooth observables,nvec,{fun,penalties},smoothing?,options
data = [{newchi,nvec}|data]
if newchi<oldchi do
options[:onstep] && options[:onstep].(%{delta: dvec, chi2: newchi, params: vec})
{:halt, {nvec,newchi,:done,data}}
else
{:cont, {pars,oldchi,minimum,data}}
end
rescue
ArgumentError -> {:cont, {pars,oldchi,minimum,data}}
ArithmeticError -> {:cont, {pars,oldchi,minimum,data}}
end
_otherwise ->
cond do
newchi < oldchi ->
options[:onstep] && options[:onstep].(%{delta: dvec, chi2: newchi, params: vec})
{:cont, {vec,newchi,new_minimum,data}}
true ->
{:cont, {pars,oldchi,minimum,data}}
end
end
rescue
ArithmeticError ->
Logger.warn "chi2fit: arithmetic error [#{inspect vec}] [#{inspect __STACKTRACE__}]"
{:cont, {pars,oldchi,minimum,data}}
end
end)
cond do
Enum.all?(delta, &(&1 == 0)) ->
chi2fit observables, {params,fun,penalties}, 0, {{cov,error},ranges}, options
true ->
chi2fit observables, {params,fun,penalties}, max-1, {{cov,error},ranges}, options
end
rescue
ArithmeticError ->
Logger.warn "chi2: arithmetic error"
IO.puts "#{inspect __STACKTRACE__}"
chi2fit observables, {parameters,fun,penalties}, 0, {preverror,ranges}, options
catch
{:impossible_inverse,error} ->
Logger.warn "chi2: impossible inverse: #{error}"
chi2fit observables, {parameters,fun,penalties}, 0, {preverror,ranges}, options
{:failed_to_reach_tolerance,_pars,error} ->
Logger.warn "chi2: failed to reach tolerance: #{error}"
chi2fit observables, {parameters,fun,penalties}, 0, {preverror,ranges}, options
end
end
defp _find_change(enumerable, fun) when is_function(enumerable,2), do: enumerable |> U.subsequences |> Stream.map(fun)
defp _find_change(enumerable, fun), do: enumerable |> U.subsequences |> Enum.map(fun)
defp probe_seq(seq, binsize, initial, cdf, options) do
seq
|> S.to_bins({binsize,0})
|> chi2probe(initial, {cdf, &nopenalties/2}, options)
|> Tuple.to_list
|> Enum.take(2)
|> List.to_tuple
end
@doc """
Finds the point in the data where the chi-squared has a jump when fitting the model
"""
@spec find_change(list :: [number()],options :: Keyword.t) :: [{chi :: float,[float]}]
def find_change(list, options) do
binsize = options[:bin] || 1
initial = options[:init]
model = options[:fitmodel]
cdf = D.cdf(model)
list |> _find_change(& probe_seq(&1,binsize,initial,cdf,options))
end
@doc """
Partitions the data list in segments with similar chi-squared values when fitting the model
"""
@spec find_all(nil | [number],options :: Keyword.t) :: [[float()]]
def find_all(data,options), do: find_all(data,[],options)
defp find_all(data, acc, options)
defp find_all(nil, acc, _options), do: Enum.reverse(acc)
defp find_all(data, _acc, options) do
threshold = options[:threshold] || 10
binsize = options[:bin] || 1
initial = options[:init]
model = options[:fitmodel]
tolerance = options[:tolerance] || 10
cdf = D.cdf(model)
data
|> Stream.chunk_while({0,nil,[]},
fn
dat, {0,nil,[]} ->
{chi2, params} = probe_seq([dat],binsize,initial,cdf,options)
{:cont, {chi2,params,[dat]}}
dat, {lastchi2, lastparams, saved} ->
{chi2, params} = probe_seq([dat|saved],binsize,initial,cdf,options)
if chi2<tolerance*lastchi2 or chi2<threshold do
{:cont, {chi2, params, [dat|saved]}}
else
{newchi2, newparams} = probe_seq([dat],binsize,initial,cdf,options)
{:cont, {lastchi2, lastparams, Enum.reverse(saved)}, {newchi2, newparams, [dat]}}
end
end,
fn
{chi,params,saved} -> {:cont,{chi,params,Enum.reverse(saved)},[]}
end)
|> (& if is_function(data, 2), do: &1, else: Enum.into(&1, [])).()
end
end