User-Defined Function

For situations when the fit equation cannot be expressed in analytical form by using the arithmetic operations and functions that are supported, the user can create a custom function that is compiled as a DLL. This custom DLL must export four functions with the following names:

  • GetFunctionName

  • GetFunctionValue

  • GetNumParameters

  • GetNumVariables

How to define these four functions is best shown with examples.

C++ Example (1D)

A user-defined function is created in C++ in order to fit the Roszman1 dataset that is provided by NIST. This fit equation requires the arctan function, which is not one of the built-in functions that are currently supported by the Delphi software (but could be upon request).

The header file is

// Roszman1.h
#define EXPORT __declspec(dllexport)

#define pi 3.141592653589793238462643383279

extern "C" {
    EXPORT void GetFunctionName(char* name);
    EXPORT void GetFunctionValue(double* x, double* a, double* y);
    EXPORT void GetNumParameters(int* n);
    EXPORT void GetNumVariables(int* n);
}

and the source file is

/*
* Roszman1.cpp
*
* User-defined function for the Roszman1 dataset that is provided by NIST
* 
* https://www.itl.nist.gov/div898/strd/nls/data/LINKS/DATA/Roszman1.dat
*/
#include <math.h>  // atan
#include <string.h>  // strcpy_s
#include "Roszman1.h"

void GetFunctionName(char* name) {
    // The name must begin with f followed by a positive integer followed by a colon.
    // The remainder of the string is for information for the user.
    strcpy_s(name, 255, "f1: Roszman1 f1=a1-a2*x-arctan(a3/(x-a4))/pi");
}

void GetFunctionValue(double* x, double* a, double* y) {
    // Receives the x value, the fit parameters and a pointer to the y value.
    // C++ array indices are zero based (i.e., a1=a[0] and x=x[0])
    *y = a[0] - a[1] * x[0] - atan(a[2] / (x[0] - a[3])) / pi;
}

void GetNumParameters(int* n) {
    // There are 4 parameters: a1, a2, a3, a4
    *n = 4;
}

void GetNumVariables(int* n) {
    // There is only 1 independent variable: x
    *n = 1;
}

To compile the C++ source code to a DLL, one could use Visual Studio C++,

cl.exe /LD Roszman1.cpp

C++ Example (2D)

A user-defined function is created in C++ in order to fit the Nelson dataset that is provided by NIST. This fit equation, a1-a2*x1*exp(-a3*x2), could have been passed directly to a Model since all arithmetic operations and functions are supported; however, this example illustrates how to handle situations when there are multiple \(x\) variables

The header file is

// Nelson.h
#define EXPORT __declspec(dllexport)

extern "C" {
    EXPORT void GetFunctionName(char* name);
    EXPORT void GetFunctionValue(double* x, double* a, double* y);
    EXPORT void GetNumParameters(int* n);
    EXPORT void GetNumVariables(int* n);
}

and the source file is

/*
* Nelson.cpp
*
* User-defined function for the Nelson dataset that is provided by NIST
*
* https://www.itl.nist.gov/div898/strd/nls/data/LINKS/DATA/Nelson.dat
*/
#include <math.h>  // exp
#include <string.h>  // strcpy_s
#include "Nelson.h"

void GetFunctionName(char* name) {
    // The name must begin with f followed by a positive integer followed by a colon.
    // The remainder of the string is for information for the user.
    strcpy_s(name, 255, "f2: Nelson log(f2)=a1-a2*x1*exp(-a3*x2)");
}

void GetFunctionValue(double* x, double* a, double* y) {
    // Receives the x value, the fit parameters and a pointer to the y value.
    // C++ array indices are zero based (i.e., a1=a[0], a2=a[1], a3=a[2], x1=x[0], x2=x[1])
    *y = a[0] - a[1] * x[0] * exp(-a[2] * x[1]);
}

void GetNumParameters(int* n) {
    // There are 3 parameters: a1, a2, a3
    *n = 3;
}

void GetNumVariables(int* n) {
    // There are 2 independent variables: x1, x2
    *n = 2;
}

To compile the C++ source code to a DLL, one could use Visual Studio C++,

cl.exe /LD Nelson.cpp

Delphi Example

A user-defined function is created in Delphi Pascal for the Beta Distribution.

library BetaDLL;

uses
    SysUtils,
    Classes,
    sfGamma in '..\..\Maths Functions\sfgamma.pas',
    AMath in '..\..\Maths Functions\amath.pas',
    sfBasic in '..\..\Maths Functions\sfbasic.pas',
    sfZeta in '..\..\Maths Functions\sfzeta.pas',
    sfExpInt in '..\..\Maths Functions\sfexpint.pas',
    sfHyperG in '..\..\Maths Functions\sfhyperg.pas',
    sfPoly in '..\..\Maths Functions\sfpoly.pas',
    sfEllInt in '..\..\Maths Functions\sfellint.pas',
    sfMisc in '..\..\Maths Functions\sfmisc.pas',
    sfBessel in '..\..\Maths Functions\sfbessel.pas',
    sfErf in '..\..\Maths Functions\sferf.pas';

type
    PArray=^TArray;
    TArray=array[1..100] of Double;

function Gamma(X:Double):Double;
begin
    Gamma:=sfc_gamma(X);
end;

procedure GetFunctionName(var Name:PAnsiChar); cdecl;
begin
    {The name must begin with f followed by a positive integer followed by a colon.
    The remainder of the string is for information for the user.}
    StrCopy(Name, 'f3: Beta Distribution (f3=a3/(a5-a4)*Gamma(a1+a2)/(Gamma(a1)*Gamma(a2))*((x-a4)/(a5-a4))^(a1-1)*((a5-x)/(a5-a4))^(a2-1))');
end;

procedure GetFunctionValue(x,a:PArray; var y:Double); cdecl;
{Returns the value of the user-defined function in the y-variable based on the
input x array and a array, where a is the parameter array.}
var
    p1,p2,g1,g2:Double;
begin
    if (a[5]<=a[4]) or (x[1]<=a[4]) or (x[1]>=a[5]) then
        y:=0
    else
    begin
        p1:=Power((x[1]-a[4])/(a[5]-a[4]),a[1]-1);
        p2:=Power((a[5]-x[1])/(a[5]-a[4]),a[2]-1);
        g1:=Gamma(a[1]);
        g2:=Gamma(a[2]);
        if (g1=0) or (g1=PosInf_x) or (g2=0) or (g2=PosInf_x) then
            y:=0
        else
            y:=a[3]/(a[5]-a[4])*Gamma(a[1]+a[2])/(g1*g2)*p1*p2;
    end;
end;

procedure GetNumParameters(var NumParameters:Integer); cdecl;
begin
    {There are 5 parameters: a1, a2, a3, a4, a5}
    NumParameters:=5;
end;

procedure GetNumVariables(var NumVariables:Integer); cdecl;
begin
    {There is only 1 independent variable: x}
    NumVariables:=1;
end;

exports
    GetFunctionName index 1,
    GetFunctionValue index 2,
    GetNumParameters index 3,
    GetNumVariables index 4;

begin
end.

Using the Function

To use a custom function, the first parameter passed when defining a Model must be the first part of the name, up to the colon, defined in GetFunctionName, and, optionally, specify the directory where the custom DLL is located as a user_dir keyword argument. If you are also using the Delphi GUI, the directory that has been set in the GUI for the user-defined functions will be used as the default user_dir value. Otherwise, the current working directory is used as the default user_dir value if a directory is not explicitly specified.

Below, the C++ function, f1, is used as the custom function

from msl.nlf import Model

model = Model('f1', user_dir='./tests/user_defined')