Custom components

Besides the Built-in components, the user may define any number of custom components. The latter are structures satisfying the following constraints:

  • They should inherit from GModelFit.AbstractComponent;

  • The component parameters (if present) must be defined as fields with type Parameter, e.g.:

    struct MyComponent <: AbstractComponent
        param1::Parameter
        param2::Parameter
        ...
    end

    Alternatively, the parameters may be specified as a single field of type OrderedDict{Symbol, Parameter} (see the Polynomial component for an example). The structure may also contain further fields of any type;

  • The GModelFit.evaluate! function should be extended with a dedicated method to evaluate the component, as shown below.

Optionally, the user may choose to extend also the following functions:

  • GModelFit.dependencies: to specify the list of the component dependencies. If not defined, the fallback method returns Symbol[];
  • GModelFit.prepare!: to pre-compute quantities depending on the evaluation domain. If not defined, the fallback method does nothing;
  • GModelFit.result_length: to specify the length of the output vector. This is used to pre-allocate the evaluation buffer. If not defined, the fallback method returns the length of the domain.

The following example shows how to define a custom component and implement all of the above methods:

using GModelFit
import GModelFit: prepare!, result_length, dependencies, evaluate!

struct MyComponent <: GModelFit.AbstractComponent
    param1::GModelFit.Parameter

    function MyComponent(param1)
        println(" -> call to MyComponent constructor;")
        new(GModelFit.Parameter(param1))
     end
end

function dependencies(comp::MyComponent)
    println(" -> call to dependencies()")
    return Symbol[]
end

function prepare!(comp::MyComponent, domain::AbstractDomain)
    println(" -> call to prepare!()")
end

function result_length(comp::MyComponent, domain::AbstractDomain)
    println(" -> call to result_length()")
    return length(domain)
end

function evaluate!(comp::MyComponent, domain::AbstractDomain, output::Vector, param1)
    println(" -> call to evaluate!() with parameter value: ", param1)
    output .= param1
end
Note

In the vast majority of cases there is no need to extend the dependencies, prepare! and result_length functions. The only mandatory implementaion is the one for evaluate!.

Life cycle of a component

The life cycle of a component is as follows:

  1. The component is created by invoking its constructor;

  2. In order to prepare the data structures for component evaluation the following functions are invoked:

    • GModelFit.dependencies;
    • GModelFit.prepare!;
    • GModelFit.result_length;
  3. Finally the GModelFit.evaluate! method is invoked to actually evaluate the component.

A quick way to evaluate a component on a Domain is as follows:

# Create a domain and a component
dom = Domain(1:4)
comp = MyComponent(1)

# Evaluate component on the domain
comp(dom)
 -> call to MyComponent constructor;
 -> call to dependencies()
 -> call to prepare!()
 -> call to result_length()
 -> call to evaluate!() with parameter value: 1.0

It is also possible to evaluate the component specifying a custom value for the component:

comp(dom, param1=4.5)
 -> call to dependencies()
 -> call to prepare!()
 -> call to result_length()
 -> call to evaluate!() with parameter value: 4.5

A similar life cycle is observed when the componens is evaluated from within a Model:

# Create a domain and a model
dom = Domain(1:4)
model = Model(:mycomp => MyComponent(1))

# Evaluate model on the domain
model(dom)
 -> call to MyComponent constructor;
 -> call to prepare!()
 -> call to result_length()
 -> call to dependencies()
 -> call to dependencies()
 -> call to dependencies()
 -> call to dependencies()
 -> call to evaluate!() with parameter value: 1.0

It is possible to modify the param1 and re-evaluate with:

model[:mycomp].param1.val = 4.5
model(dom)
 -> call to prepare!()
 -> call to result_length()
 -> call to dependencies()
 -> call to dependencies()
 -> call to dependencies()
 -> call to dependencies()
 -> call to evaluate!() with parameter value: 4.5

Finally, when fitting a model the prepare! and result_length are invoked only once. The dependencies function is invoked a number of times to create the internal data structures, while evaluate! is invoked each time the solver needs to probe the model on a specific set of parameter values:

bestfit, fsumm = fit(model, Measures(dom, [9., 9., 9., 9.], 1.))
 -> call to prepare!()
 -> call to result_length()
 -> call to dependencies()
 -> call to dependencies()
 -> call to dependencies()
 -> call to dependencies()
 -> call to evaluate!() with parameter value: 4.5
 -> call to evaluate!() with parameter value: 4.5000272495450355
 -> call to evaluate!() with parameter value: 4.4999727504549645
 -> call to evaluate!() with parameter value: 4.5
 -> call to evaluate!() with parameter value: 4.9090909090946075
 -> call to evaluate!() with parameter value: 4.909120635871011
 -> call to evaluate!() with parameter value: 4.909061182318204
 -> call to evaluate!() with parameter value: 6.954545454517334
 -> call to evaluate!() with parameter value: 6.954587567450571
 -> call to evaluate!() with parameter value: 6.954503341584098
 -> call to evaluate!() with parameter value: 8.81404958678716
 -> call to evaluate!() with parameter value: 8.814102959862973
 -> call to evaluate!() with parameter value: 8.813996213711347
 -> call to evaluate!() with parameter value: 8.998158906802335
 -> call to evaluate!() with parameter value: 8.99821339474375
 -> call to evaluate!() with parameter value: 8.998104418860919
 -> call to evaluate!() with parameter value: 8.99999816074605
 -> call to evaluate!() with parameter value: 9.000052659824984
 -> call to evaluate!() with parameter value: 8.999943661667118
 -> call to evaluate!() with parameter value: 8.999999999816094
 -> call to evaluate!() with parameter value: 9.000054498906165
 -> call to evaluate!() with parameter value: 8.999945500726023
 -> call to evaluate!() with parameter value: 8.999999999999998
 -> call to evaluate!() with parameter value: 9.00005449909007
 -> call to evaluate!() with parameter value: 8.999945500909927
 -> call to evaluate!() with parameter value: 8.999999999999998
 -> call to dependencies()

Complete example

A common case is to compare empirical data with a numerically evaluated theoretical model, possibly defined on a different grid with respect to the empirical one. An interpolation is therefore required in order to compare the model to the data.

Let's assume the theoretical model is defined as follows:

theory_x = 0.:10
theory_y = [0, 0.841, 0.909, 0.141, -0.757, -0.959, -0.279, 0.657, 0.989, 0.412, -0.544]

while the empirical data are:

obs_x = [0.500, 2.071, 3.642, 5.212, 6.783, 8.354, 9.925]
obs_y = [2.048, 3.481, 1.060, 0.515, 3.220, 4.398, 1.808]

The following example shows how to implement a component which interpolates a theoretical model onto a specific empirical domain, with the only parameter being a global scaling factor:

using GModelFit, Interpolations
import GModelFit: prepare!, result_length, evaluate!

# Define the component structure and constructor
struct Interpolator <: GModelFit.AbstractComponent
    theory_x::Vector{Float64}
    theory_y::Vector{Float64}
    interp_y::Vector{Float64}  # will contain the interpolated values
    scale::GModelFit.Parameter

    function Interpolator(theory_x, theory_y)
        scale = GModelFit.Parameter(1)
        scale.low = 0                  # ensure scale parameter is positive
        interp_y = Vector{Float64}()   # this will be populated in prepare!()
        return new(theory_x, theory_y, interp_y, scale)
    end
end

# Component preparation: invoked only once to precompute quantities
# and allocate evaluation buffer
function prepare!(comp::Interpolator, domain::AbstractDomain{1})
    # Pre-compute interpolation on the empirical domain
    itp = linear_interpolation(comp.theory_x, comp.theory_y)
    append!(comp.interp_y, itp(coords(domain)))
end

result_length(comp::Interpolator, domain::AbstractDomain{1}) = length(domain)

# Component evaluation (apply scaling factor)
function evaluate!(comp::Interpolator, ::AbstractDomain{1}, output,
                   scale)
    output .= scale .* comp.interp_y
end

The following code shows how to prepare a Model including the interpolated theoretical model, and to take into account the possible background introduced by the detector used to obtain empirical data:

model = Model(:theory => Interpolator(theory_x, theory_y),
              :background => GModelFit.OffsetSlope(1., 0., 0.2),
              :main => SumReducer(:theory, :background))

data = Measures(Domain(obs_x), obs_y, 0.2)
bestfit, stats = fit(model, data)
(Components:
───────────────┬───────────────────────────────────────────┬───────┬─────────────┬───────────┬───────────┬───────────┬─────────╮
 Component      Type                                       #Free  Eval. count  Min        Max        Mean       NaN/Inf 
├───────────────┼───────────────────────────────────────────┼───────┼─────────────┼───────────┼───────────┼───────────┼─────────┤
 main           SumReducer                                        59              0.4323      4.278      2.361  0       
 ├─╴theory      Main.Interpolator  1      34              -1.694      1.777     0.2349  0       
 └─╴background  OffsetSlope_1D                             2      43               1.346      2.907      2.127  0       
╰───────────────┴───────────────────────────────────────────┴───────┴─────────────┴───────────┴───────────┴───────────┴─────────╯

Parameters:
────────────┬───────────────────────────────────────────┬────────┬──────────┬───────────┬───────────┬────────┬───────╮
 Component   Type                                       Param.  Range     Value      Uncert.    Actual  Patch 
├────────────┼───────────────────────────────────────────┼────────┼──────────┼───────────┼───────────┼────────┼───────┤
 theory      Main.Interpolator  scale   0:Inf         2.079    0.09647                
├────────────┼───────────────────────────────────────────┼────────┼──────────┼───────────┼───────────┼────────┼───────┤
 background  OffsetSlope_1D                             offset  -Inf:Inf      1.263     0.1182                
                                                        x0      -Inf:Inf          0   (fixed)                 
                                                        slope   -Inf:Inf     0.1656    0.01913                
╰────────────┴───────────────────────────────────────────┴────────┴──────────┴───────────┴───────────┴────────┴───────╯
, Fit summary: #data: 7, #free pars: 3, red. fit stat.: 0.60105, status: OK      
)