fpex0.fpex0

  1import sys
  2import traceback
  3
  4import numpy as np
  5from scipy import optimize
  6import time
  7
  8
  9def simulate(FPEX0setup, pvec, odeoptions={}, method="BDF"):
 10    """
 11    Simulates Fokker-Planck with specified parameters for FP drift, diffusion, and initial function.
 12
 13    ## Takes
 14    **FPEX0setup**
 15    <br> An FPEX0 setup configuration (`fpex0.setup.Setup`).
 16
 17    **p_all**
 18    <br> Vector of parameters, coupled to FPEX0setup.
 19    <br> It must have the same scheme as FPEX0setup.Parameters.p_0, 
 20    then simulate() knows how to extract the parameters correctly.
 21    Usually this is ensured by the optimizer, who got the initial parameters and changes them through
 22    optimization steps.
 23
 24    **odeoptions**
 25    <br> kwargs passed to the scipy solve_ivp ODE solver 
 26    (https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html). 
 27
 28    **method**
 29    <br> The method scipy solve_ivp should use.
 30    <br> Default is BDF, an an implicit multistep method.
 31
 32
 33    ## Returns
 34    **solution**
 35    <br> A scipy solve_ivp bunch object. Important parameters are
 36    - t: time points
 37    - y: corresponding values
 38    - sol: A (here) callable solution
 39
 40    See link above for details.
 41
 42    ### Comments
 43    The function will not let you set dense_output = False in odeoptions.
 44    """
 45    # extract parameters
 46    p_FPdrift     = FPEX0setup.Parameters.extract_p_FPdrift(pvec)
 47    p_FPdiffusion = FPEX0setup.Parameters.extract_p_FPdiffusion(pvec)
 48    p_IC          = FPEX0setup.Parameters.extract_p_iniDist(pvec)
 49
 50    # evaluate initial distribution
 51    gridT         = FPEX0setup.Grid.gridT
 52    u0            = FPEX0setup.IniDistFcn(gridT, p_IC)
 53
 54    # retrieve "time" horizon for integrator
 55    t0tf          = FPEX0setup.Grid.gridTdot[[0, -1]]
 56    
 57    # generate right hand side, jacobian
 58    FPrhs         = FPEX0setup.make_rhsFcn(p_FPdrift, p_FPdiffusion)
 59    FPjac         = FPEX0setup.make_jacFcn(p_FPdrift, p_FPdiffusion)
 60
 61    # setup integrator and update options, jacobian therein
 62    integrator    = FPEX0setup.Integration.integrator    
 63    odeoptions = FPEX0setup.Integration.updateOptions(odeoptions)
 64    odeoptions["dense_output"] = True   # dense_output for evaluable solution
 65    odeoptions = FPEX0setup.Integration.updateJacobian(FPjac)
 66    
 67
 68    # start integration
 69    try:
 70        sTime = time.time()
 71        solution = integrator(FPrhs, t0tf, u0, method,**odeoptions)
 72        duration = time.time() - sTime
 73        print(f"Simulate: {duration:.3f}s")
 74    except:
 75        exception = sys.exc_info()
 76        traceback.print_exception(*exception)
 77        print('Integration failed!')
 78
 79        duration = None
 80        solution = None
 81    
 82    return solution
 83
 84
 85def fit(FPEX0setup, optimizer='lsq'):
 86    """
 87    Fits the Fokker-Planck simulation to the given measurements as an optimization of the parameters
 88    for drift, diffusion and the initial distribution.
 89
 90    ## Takes
 91    **FPEX0setup**
 92    <br> An FPEX0 setup configuration (`fpex0.setup.Setup`).
 93
 94    **optimizer**
 95    <br> The optimizer that should be used. So far only least squares is implemented.
 96
 97
 98    ## Returns
 99    **result** 
100    <br> The optimized parameters as a vector.
101
102    """
103    # retrieve parameter values, bounds, indices
104    p_0  = FPEX0setup.Parameters.p0
105    p_lb = FPEX0setup.Parameters.p_lb
106    p_ub = FPEX0setup.Parameters.p_ub
107    
108    # set function that computes the residual vector
109    resvecfun = lambda p: residual(FPEX0setup, p)
110
111    # optimization
112    print(f'Running {optimizer}.\n')
113    
114    if optimizer.lower() == 'lsq':
115        lsq_opts = {}
116        # set options
117        lsq_opts["jac"] = '3-point'
118        lsq_opts["max_nfev"]                    = 100000    # max function evaluations
119                                                            # tolerances
120        lsq_opts["xtol"]                        = 1e-6      # x-step
121        lsq_opts["ftol"]                        = 1e-10     # function-step
122        lsq_opts["gtol"]                        = 1.0       # norm of gradient, quite high, but okay for FD
123
124        lsq_opts["x_scale"]                     = 'jac'     # let set scipy set scale with jacobian
125        lsq_opts["verbose"]                     = 2         # give detailed progress information
126        result = optimize.least_squares(resvecfun, p_0, bounds=(p_lb, p_ub), **lsq_opts)
127        
128        return result
129
130    else:
131        raise ValueError("Your specified optimizer is not yet implemented. \nYou are welcomed to contact us by mail if interested in contributing!")
132
133
134def residual(FPEX0setup, p_all):
135    """
136    Calculates the residual vector of measurements and simulation values, i.e. measVals - simVals
137    at suitable points.
138    
139    
140    ## Takes
141    **FPEX0setup**
142    <br> An FPEX0 setup configuration (`fpex0.setup.Setup`).
143
144    **p_all**
145    <br> Vector of parameters, coupled to FPEX0setup.
146    <br> It must have the same scheme as FPEX0setup.Parameters.p_0, 
147    then residual() knows how to use the parameters correctly.
148    Usually this is ensured by the optimizer, who got the initial parameters and changes them through
149    optimization steps.
150
151    ## Returns
152    **resvec**
153    <br> Residual vector as described above.
154
155    """
156    measurements = FPEX0setup.Measurements
157    meas_count   = len(measurements.rates)
158    meas_values  = measurements.values
159    meas_T       = measurements.temperatures
160    meas_rates   = measurements.rates
161
162    grid_T       = FPEX0setup.Grid.gridT
163
164    # simulate and store the FP solution
165    sol = simulate(FPEX0setup, p_all)
166
167    # evaluate at measurement rates
168    simdata = sol.sol(meas_rates)
169
170    resvec = np.empty(1)
171    for k in range(meas_count):
172        # select grid points matching to measurements
173        _, idxGrid, idxMeas = np.intersect1d(grid_T, meas_T[k], assume_unique=True, return_indices=True)
174        if len(idxGrid) != len(meas_T[k]):
175            raise ValueError("Grid does not fit.")
176        # get corresponding measurement and simulation data
177        measVals    = meas_values[k][idxMeas]
178        simVals     = simdata[idxGrid, k]
179        resvec   = np.append(resvec, measVals - simVals)
180
181    return resvec
def simulate(FPEX0setup, pvec, odeoptions={}, method='BDF'):
10def simulate(FPEX0setup, pvec, odeoptions={}, method="BDF"):
11    """
12    Simulates Fokker-Planck with specified parameters for FP drift, diffusion, and initial function.
13
14    ## Takes
15    **FPEX0setup**
16    <br> An FPEX0 setup configuration (`fpex0.setup.Setup`).
17
18    **p_all**
19    <br> Vector of parameters, coupled to FPEX0setup.
20    <br> It must have the same scheme as FPEX0setup.Parameters.p_0, 
21    then simulate() knows how to extract the parameters correctly.
22    Usually this is ensured by the optimizer, who got the initial parameters and changes them through
23    optimization steps.
24
25    **odeoptions**
26    <br> kwargs passed to the scipy solve_ivp ODE solver 
27    (https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html). 
28
29    **method**
30    <br> The method scipy solve_ivp should use.
31    <br> Default is BDF, an an implicit multistep method.
32
33
34    ## Returns
35    **solution**
36    <br> A scipy solve_ivp bunch object. Important parameters are
37    - t: time points
38    - y: corresponding values
39    - sol: A (here) callable solution
40
41    See link above for details.
42
43    ### Comments
44    The function will not let you set dense_output = False in odeoptions.
45    """
46    # extract parameters
47    p_FPdrift     = FPEX0setup.Parameters.extract_p_FPdrift(pvec)
48    p_FPdiffusion = FPEX0setup.Parameters.extract_p_FPdiffusion(pvec)
49    p_IC          = FPEX0setup.Parameters.extract_p_iniDist(pvec)
50
51    # evaluate initial distribution
52    gridT         = FPEX0setup.Grid.gridT
53    u0            = FPEX0setup.IniDistFcn(gridT, p_IC)
54
55    # retrieve "time" horizon for integrator
56    t0tf          = FPEX0setup.Grid.gridTdot[[0, -1]]
57    
58    # generate right hand side, jacobian
59    FPrhs         = FPEX0setup.make_rhsFcn(p_FPdrift, p_FPdiffusion)
60    FPjac         = FPEX0setup.make_jacFcn(p_FPdrift, p_FPdiffusion)
61
62    # setup integrator and update options, jacobian therein
63    integrator    = FPEX0setup.Integration.integrator    
64    odeoptions = FPEX0setup.Integration.updateOptions(odeoptions)
65    odeoptions["dense_output"] = True   # dense_output for evaluable solution
66    odeoptions = FPEX0setup.Integration.updateJacobian(FPjac)
67    
68
69    # start integration
70    try:
71        sTime = time.time()
72        solution = integrator(FPrhs, t0tf, u0, method,**odeoptions)
73        duration = time.time() - sTime
74        print(f"Simulate: {duration:.3f}s")
75    except:
76        exception = sys.exc_info()
77        traceback.print_exception(*exception)
78        print('Integration failed!')
79
80        duration = None
81        solution = None
82    
83    return solution

Simulates Fokker-Planck with specified parameters for FP drift, diffusion, and initial function.

Takes

FPEX0setup
An FPEX0 setup configuration (fpex0.setup.Setup).

p_all
Vector of parameters, coupled to FPEX0setup.
It must have the same scheme as FPEX0setup.Parameters.p_0, then simulate() knows how to extract the parameters correctly. Usually this is ensured by the optimizer, who got the initial parameters and changes them through optimization steps.

odeoptions
kwargs passed to the scipy solve_ivp ODE solver (https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html).

method
The method scipy solve_ivp should use.
Default is BDF, an an implicit multistep method.

Returns

solution
A scipy solve_ivp bunch object. Important parameters are

  • t: time points
  • y: corresponding values
  • sol: A (here) callable solution

See link above for details.

Comments

The function will not let you set dense_output = False in odeoptions.

def fit(FPEX0setup, optimizer='lsq'):
 86def fit(FPEX0setup, optimizer='lsq'):
 87    """
 88    Fits the Fokker-Planck simulation to the given measurements as an optimization of the parameters
 89    for drift, diffusion and the initial distribution.
 90
 91    ## Takes
 92    **FPEX0setup**
 93    <br> An FPEX0 setup configuration (`fpex0.setup.Setup`).
 94
 95    **optimizer**
 96    <br> The optimizer that should be used. So far only least squares is implemented.
 97
 98
 99    ## Returns
100    **result** 
101    <br> The optimized parameters as a vector.
102
103    """
104    # retrieve parameter values, bounds, indices
105    p_0  = FPEX0setup.Parameters.p0
106    p_lb = FPEX0setup.Parameters.p_lb
107    p_ub = FPEX0setup.Parameters.p_ub
108    
109    # set function that computes the residual vector
110    resvecfun = lambda p: residual(FPEX0setup, p)
111
112    # optimization
113    print(f'Running {optimizer}.\n')
114    
115    if optimizer.lower() == 'lsq':
116        lsq_opts = {}
117        # set options
118        lsq_opts["jac"] = '3-point'
119        lsq_opts["max_nfev"]                    = 100000    # max function evaluations
120                                                            # tolerances
121        lsq_opts["xtol"]                        = 1e-6      # x-step
122        lsq_opts["ftol"]                        = 1e-10     # function-step
123        lsq_opts["gtol"]                        = 1.0       # norm of gradient, quite high, but okay for FD
124
125        lsq_opts["x_scale"]                     = 'jac'     # let set scipy set scale with jacobian
126        lsq_opts["verbose"]                     = 2         # give detailed progress information
127        result = optimize.least_squares(resvecfun, p_0, bounds=(p_lb, p_ub), **lsq_opts)
128        
129        return result
130
131    else:
132        raise ValueError("Your specified optimizer is not yet implemented. \nYou are welcomed to contact us by mail if interested in contributing!")

Fits the Fokker-Planck simulation to the given measurements as an optimization of the parameters for drift, diffusion and the initial distribution.

Takes

FPEX0setup
An FPEX0 setup configuration (fpex0.setup.Setup).

optimizer
The optimizer that should be used. So far only least squares is implemented.

Returns

result
The optimized parameters as a vector.

def residual(FPEX0setup, p_all):
135def residual(FPEX0setup, p_all):
136    """
137    Calculates the residual vector of measurements and simulation values, i.e. measVals - simVals
138    at suitable points.
139    
140    
141    ## Takes
142    **FPEX0setup**
143    <br> An FPEX0 setup configuration (`fpex0.setup.Setup`).
144
145    **p_all**
146    <br> Vector of parameters, coupled to FPEX0setup.
147    <br> It must have the same scheme as FPEX0setup.Parameters.p_0, 
148    then residual() knows how to use the parameters correctly.
149    Usually this is ensured by the optimizer, who got the initial parameters and changes them through
150    optimization steps.
151
152    ## Returns
153    **resvec**
154    <br> Residual vector as described above.
155
156    """
157    measurements = FPEX0setup.Measurements
158    meas_count   = len(measurements.rates)
159    meas_values  = measurements.values
160    meas_T       = measurements.temperatures
161    meas_rates   = measurements.rates
162
163    grid_T       = FPEX0setup.Grid.gridT
164
165    # simulate and store the FP solution
166    sol = simulate(FPEX0setup, p_all)
167
168    # evaluate at measurement rates
169    simdata = sol.sol(meas_rates)
170
171    resvec = np.empty(1)
172    for k in range(meas_count):
173        # select grid points matching to measurements
174        _, idxGrid, idxMeas = np.intersect1d(grid_T, meas_T[k], assume_unique=True, return_indices=True)
175        if len(idxGrid) != len(meas_T[k]):
176            raise ValueError("Grid does not fit.")
177        # get corresponding measurement and simulation data
178        measVals    = meas_values[k][idxMeas]
179        simVals     = simdata[idxGrid, k]
180        resvec   = np.append(resvec, measVals - simVals)
181
182    return resvec

Calculates the residual vector of measurements and simulation values, i.e. measVals - simVals at suitable points.

Takes

FPEX0setup
An FPEX0 setup configuration (fpex0.setup.Setup).

p_all
Vector of parameters, coupled to FPEX0setup.
It must have the same scheme as FPEX0setup.Parameters.p_0, then residual() knows how to use the parameters correctly. Usually this is ensured by the optimizer, who got the initial parameters and changes them through optimization steps.

Returns

resvec
Residual vector as described above.