KAHIBARO
Discord Login Register

10.3. Custom Functions

Defining mathematical expressions

In ROOT you can define simple custom functions directly from a string expression, without writing any C++ code. This is often the fastest way to describe a mathematical formula and immediately visualize or use it in a fit.

The basic tool is TF1. A TF1 represents a one dimensional function of a single variable, usually called x. To create one from a mathematical expression you use a constructor of the form

cpp
TF1 *f = new TF1("fName", "expression", xMin, xMax);

The first argument is the internal name of the function, the second is a string containing the expression, and the last two set the range in x where the function is defined and usually drawn.

Inside the expression you can use the usual C style operators such as +, -, *, /, and pow(x, n) for powers. You can also use the standard mathematical functions that ROOT exposes such as sin, cos, tan, exp, log, and sqrt. For example, a simple exponential decay between 0 and 10 can be written as

cpp
TF1 *fexp = new TF1("fexp", "exp(-x)", 0.0, 10.0);

You can then draw the function on the current canvas with

cpp
fexp->Draw();

Many custom functions need free parameters that you later fit to data. In expression based TF1 objects you introduce parameters using the syntax [0], [1], [2], and so on. These are numbered placeholders that ROOT will treat as adjustable parameters. For instance, a straight line with slope and intercept is

cpp
TF1 *fline = new TF1("fline", "[0] + [1]*x", -5.0, 5.0);

Here [0] is the intercept and [1] is the slope. Before using the function numerically, for example to draw it or to fit data, you should set reasonable initial values:

cpp
fline->SetParameter(0, 1.0);   // intercept
fline->SetParameter(1, 0.5);   // slope

You can also set all parameters at once with SetParameters, for example

cpp
fline->SetParameters(1.0, 0.5);

The link between parameter indices in SetParameter and the placeholders in the expression is direct, so parameter index i corresponds to [i] in the formula.

You can combine parameters and built in functions in quite complex expressions. A commonly used example is a Gaussian with mean and sigma as free parameters:

cpp
TF1 *fgaus = new TF1("fgaus",
                     "[0]*exp(-0.5*((x-[1])/[2])^2)",
                     -5.0, 5.0);
fgaus->SetParameters(1.0, 0.0, 1.0);  // amplitude, mean, sigma

Note that the variable is always referenced as x in TF1 expressions. If you write y or another name, ROOT will not recognize it as the independent variable.

You can also reuse built in named functions inside expressions. For example, ROOT defines a Gaussian as "gaus" with three parameters, so you can write a sum of two Gaussians as

cpp
TF1 *fdouble = new TF1("fdouble",
                       "gaus(0) + gaus(3)",
                       -5.0, 5.0);

Here gaus(0) uses parameters [0], [1], [2] and gaus(3) uses [3], [4], [5]. This syntax is specific to ROOT and is useful when building custom models from standard pieces.

You evaluate a TF1 at a given x using its Eval method. For example,

cpp
double val = fline->Eval(2.0);

returns the numerical value of the function at x = 2. This is useful when your custom function appears inside a larger calculation or when you want to tabulate values without drawing or fitting.

If you plan to fit histograms or graphs with your custom function, the expression based TF1 integrates naturally with the fitting tools. You pass the TF1 to the Fit method of histograms or graphs and ROOT will adjust the parameters [i]. The detailed fitting workflow is covered in the fitting chapters, but the important point here is that the parameter placeholders in your expression become the quantities that the fit changes.

There are some practical limits and caveats for expression based functions. Very complicated formulas become hard to read inside a single string and are harder to debug if something is wrong. Also, expression parsing has some overhead, so if you evaluate the function many times inside tight loops, a compiled C++ function can be faster. When expressions become too long or you need more control, you should consider defining the function as C++ code instead.

Expression based TF1 functions always use x as the independent variable and [i] as parameter placeholders. Every [i] must have a corresponding parameter index set with SetParameter or SetParameters. When combining built in components such as gaus(0), check carefully which parameter indices each part uses.

Defining C++ functions

For more complex or performance critical tasks, ROOT lets you define custom functions directly as C++ code. Instead of describing your formula as a string, you write an actual function that takes double x[] and double par[] as arguments and returns a double. You then create a TF1 that uses this C++ function as its implementation.

The standard C++ signature used by TF1 is

cpp
double myfunc(double *x, double *par) {
    // ...
}

Here x is an array of independent variables and par is an array of parameters. For one dimensional TF1 objects only x[0] is used. The elements of par correspond to the adjustable parameters of the function, similar to [0], [1], etc. in expression based functions.

Inside the body you write the computation using normal C++ syntax. For example, the same straight line as before can be implemented as

cpp
double myline(double *x, double *par) {
    double xx = x[0];
    double p0 = par[0];  // intercept
    double p1 = par[1];  // slope
    return p0 + p1*xx;
}

You then construct a TF1 that points to this function:

cpp
TF1 *fline_cpp = new TF1("fline_cpp", myline, -5.0, 5.0, 2);

The last argument tells ROOT that the function has 2 parameters, par[0] and par[1]. You set initial parameter values in exactly the same way as before:

cpp
fline_cpp->SetParameters(1.0, 0.5);

From this point on, you use fline_cpp like any other function object. You can draw it, evaluate it with Eval, and use it in fits.

This C++ style definition offers several advantages. First, you can use all of C++, including if statements, loops, and calls to other C++ functions, so you are not limited to what can easily be written as a single mathematical expression. For example, you might want a piecewise function that behaves differently in different regions of x:

cpp
double piecewise(double *x, double *par) {
    double xx = x[0];
    if (xx < 0.0) {
        return par[0]*exp(xx);
    } else {
        return par[1]*xx*xx;
    }
}

Second, C++ functions can be faster than parsing expressions, which can matter when you evaluate them many times in a simulation or fit.

ROOT can interpret your function if you type it in the interactive shell or place it in a macro file that you load. For example, if you write a macro called myfunctions.C that defines double myfunc(double x, double par) and then load it in ROOT, you can immediately construct TF1 objects that use myfunc. If you need more speed you can compile the macro, which is described in detail in the ROOT macros and compilation chapters. The function signature remains the same, only the way ROOT executes it changes.

When your function has parameters you must ensure that TF1 knows how many there are. That is why the constructor for C++ based TF1 includes the last integer argument. If you specify 3, then par[0], par[1], and par[2] are valid, and you should not read or write beyond that. Their values are set with SetParameter or SetParameters, and are updated during fits.

You can also access additional information in your function if needed. For example, there is a TF1::SetNpx method that controls how many points ROOT uses when drawing the function. This does not change the function code itself, but if you draw rapidly oscillating custom functions you may want to increase this number to get a smoother curve.

C++ defined functions can be combined with other ROOT tools. You can call them yourself from C++ code, or rely on TF1::Eval for convenience. Keep in mind that TF1::Eval calls your C++ function with a small array containing the current x and the internal parameter array, so your function should not make assumptions beyond that.

If your function becomes long or has its own internal parameters that are not fit parameters, organize your code clearly with local variables and comments. This keeps the connection between par[i] and their meaning understandable when you later interpret fit results.

Custom C++ functions used with TF1 must have the exact signature double func(double x, double par) and must only use x[0] for one dimensional functions. The number of parameters in par must match the value provided in the TF1 constructor. Reading or writing beyond this range results in undefined behavior and can cause crashes.

Views: 9

Comments

Please login to add a comment.

Don't have an account? Register now!