2019-10-05 07:54:26 -07:00
|
|
|
/*
|
|
|
|
* @file pid_functions.h
|
|
|
|
*
|
|
|
|
* Functions used by PID tuner
|
|
|
|
*
|
|
|
|
* @date Sep 27, 2019
|
|
|
|
* @author andreika, (c) 2019
|
|
|
|
*/
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
#include "global.h"
|
|
|
|
|
|
|
|
#include "levenberg_marquardt_solver.h"
|
|
|
|
#include "pid_avg_buf.h"
|
|
|
|
|
|
|
|
// This helps to differentiate with respect to 'delay' axis (while finding the Hessian matrix)
|
|
|
|
#define INTERPOLATED_STEP_FUNCTION
|
|
|
|
|
|
|
|
// Generic Step model params:
|
|
|
|
enum {
|
|
|
|
PARAM_K = 0, // K = Gain
|
|
|
|
PARAM_T, // T = Time constant
|
|
|
|
PARAM_L, // L = Delay (dead time)
|
|
|
|
|
|
|
|
// 2nd order params
|
|
|
|
PARAM_T2, // T2 = Time constant
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2019-10-11 11:14:56 -07:00
|
|
|
// PID model params:
|
|
|
|
enum {
|
|
|
|
PARAM_Kp = 0,
|
|
|
|
PARAM_Ki,
|
|
|
|
PARAM_Kd,
|
|
|
|
};
|
|
|
|
const int numParamsForPid = 3;
|
|
|
|
|
2019-10-09 10:11:18 -07:00
|
|
|
// the limit used for K and L params
|
2019-10-23 07:45:12 -07:00
|
|
|
static const double_t minParamK = 0.001;
|
|
|
|
static const double_t minParamL = 0.1;
|
2019-10-09 10:11:18 -07:00
|
|
|
|
|
|
|
// T (or T2) has a separate limit, because exp(-1/T) is zero for small T values
|
|
|
|
// (and we need to compare between them to find a direction for optimization method).
|
2019-10-23 07:45:12 -07:00
|
|
|
static const double_t minParamT = 0.01;
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
class InputFunction {
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
InputFunction(double_t timeScale_) : timeScale(timeScale_) {
|
2019-10-07 11:13:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// i = index, d = delay time (in seconds?)
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t getValue(double_t i, double_t d) const = 0;
|
2019-10-07 11:13:16 -07:00
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t getTimeScale() const {
|
2019-10-07 11:13:16 -07:00
|
|
|
return timeScale;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
// time scale needed to synchronize between the virtual step function and the real measured data
|
|
|
|
// timeScale=100 means 100 points per second.
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t timeScale;
|
2019-10-05 07:54:26 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// Heaviside step function interpolated between 'min' and 'max' values with 'stepPoint' time offset
|
|
|
|
class StepFunction : public InputFunction
|
|
|
|
{
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
StepFunction(double_t minValue_, double_t maxValue_, double_t stepPoint_, double_t timeScale_) :
|
2019-10-19 05:56:17 -07:00
|
|
|
minValue(minValue_), maxValue(maxValue_), stepPoint(stepPoint_), InputFunction(timeScale_) {
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t getValue(double_t i, double_t d) const {
|
2019-10-07 11:13:16 -07:00
|
|
|
// delayed index
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t id = i - d * timeScale;
|
2019-10-05 07:54:26 -07:00
|
|
|
#ifdef INTERPOLATED_STEP_FUNCTION
|
|
|
|
// the delay parameter L may not be integer, so we have to interpolate between the closest input values (near and far in the past)
|
2019-10-07 11:13:16 -07:00
|
|
|
int I = (int)id;
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t fract = id - I; // 0 = choose near value, 1 = choose far value
|
2019-10-05 07:54:26 -07:00
|
|
|
// find two closest input values for the given delay
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t vNear = (I < stepPoint) ? minValue : maxValue;
|
|
|
|
double_t vFar = (I + 1 < stepPoint) ? minValue : maxValue;
|
2019-10-05 07:54:26 -07:00
|
|
|
// interpolate
|
2019-10-19 05:56:17 -07:00
|
|
|
return vFar * fract + vNear * (1.0f - fract);
|
2019-10-05 07:54:26 -07:00
|
|
|
#else
|
2019-10-19 05:56:17 -07:00
|
|
|
return ((id < stepPoint) ? minValue : maxValue);
|
2019-10-05 07:54:26 -07:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t minValue, maxValue;
|
|
|
|
// stepPoint is not integer because we have AveragingDataBuffer, and the time axis may be scaled
|
|
|
|
double_t stepPoint;
|
2019-10-05 07:54:26 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
template <int numPoints>
|
|
|
|
class StoredDataInputFunction : public InputFunction {
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
StoredDataInputFunction(double_t timeScale_) : InputFunction(timeScale_) {
|
2019-10-11 11:14:56 -07:00
|
|
|
reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
void reset() {
|
2019-10-07 11:13:16 -07:00
|
|
|
inputData.init();
|
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
void addDataPoint(float_t v) {
|
2019-10-05 07:54:26 -07:00
|
|
|
// todo: support data scaling
|
|
|
|
assert(inputData.getNumDataPoints() <= numPoints);
|
|
|
|
inputData.addDataPoint(v);
|
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t getValue(double_t i, double_t d) const {
|
|
|
|
return inputData.getValue((float_t)(i - d * timeScale));
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
AveragingDataBuffer<numPoints> inputData;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Abstract indirect transfer function used for step response analytic simulation
|
|
|
|
template <int numParams>
|
|
|
|
class AbstractDelayLineFunction : public LMSFunction<numParams> {
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
AbstractDelayLineFunction(const InputFunction *input, const float_t *measuredOutput, int numDataPoints, double_t modelBias) {
|
2019-10-05 07:54:26 -07:00
|
|
|
dataPoints = measuredOutput;
|
|
|
|
inputFunc = input;
|
|
|
|
numPoints = numDataPoints;
|
2019-10-19 05:56:17 -07:00
|
|
|
this->modelBias = modelBias;
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t getResidual(int i, const double_t *params) const {
|
|
|
|
return dataPoints[i] - this->getEstimatedValueAtPoint(i, params);
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t calcEstimatedValuesAtPoint(int i, const double_t *params) const = 0;
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// Get the total number of data points
|
2019-10-11 11:14:56 -07:00
|
|
|
virtual int getNumPoints() const {
|
2019-10-05 07:54:26 -07:00
|
|
|
return numPoints;
|
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
float_t getDataPoint(int i) const {
|
2019-10-09 10:11:18 -07:00
|
|
|
return dataPoints[i];
|
|
|
|
}
|
|
|
|
|
2019-10-05 07:54:26 -07:00
|
|
|
protected:
|
|
|
|
const InputFunction *inputFunc;
|
2019-10-23 07:45:12 -07:00
|
|
|
const float_t *dataPoints;
|
2019-10-05 07:54:26 -07:00
|
|
|
int numPoints;
|
2019-10-19 05:56:17 -07:00
|
|
|
// needed to match the "ideal" curve and the real plant data; it doesn't affect the params but helps to fit the curve.
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t modelBias;
|
2019-10-05 07:54:26 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// FODPT indirect transfer function used for step response analytic simulation.
|
2019-10-09 10:11:18 -07:00
|
|
|
// Used mostly as an approximate model for chemical or thermal processes
|
2019-10-05 07:54:26 -07:00
|
|
|
// The Laplace representation is: K * exp(-L*s) / (T*s + 1)
|
|
|
|
class FirstOrderPlusDelayLineFunction : public AbstractDelayLineFunction<3> {
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
FirstOrderPlusDelayLineFunction(const InputFunction *input, const float_t *measuredOutput, int numDataPoints, double_t modelBias) :
|
2019-10-19 05:56:17 -07:00
|
|
|
AbstractDelayLineFunction(input, measuredOutput, numDataPoints, modelBias) {
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual void justifyParams(double_t *params) const {
|
2019-10-19 05:56:17 -07:00
|
|
|
params[PARAM_L] = fmax(params[PARAM_L], minParamL);
|
2019-10-09 10:11:18 -07:00
|
|
|
params[PARAM_T] = fmax(params[PARAM_T], minParamT);
|
2019-10-19 05:56:17 -07:00
|
|
|
params[PARAM_K] = (fabs(params[PARAM_K]) < minParamK) ? minParamK : params[PARAM_K];
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Creating a state-space representation using Rosenbrock system matrix
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t calcEstimatedValuesAtPoint(int i, const double_t *params) const {
|
2019-10-05 07:54:26 -07:00
|
|
|
// only positive values allowed (todo: choose the limits)
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t pL = fmax(params[PARAM_L], minParamL);
|
|
|
|
double_t pT = fmax(params[PARAM_T], minParamT);
|
|
|
|
double_t pK = (fabs(params[PARAM_K]) < minParamK) ? minParamK : params[PARAM_K];
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// state-space params
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t lambda = exp(-1.0 / (pT * inputFunc->getTimeScale()));
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// todo: find better initial value?
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t y = inputFunc->getValue(0, 0) * pK;
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// The FO response function is indirect, so we need to iterate all previous values to find the current one
|
|
|
|
for (int j = 0; j <= i; j++) {
|
|
|
|
// delayed input
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t inp = inputFunc->getValue((double_t)j, pL);
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// indirect model response in Controllable Canonical Form (1st order CCF)
|
2019-10-09 10:11:18 -07:00
|
|
|
y = lambda * y + pK * (1.0 - lambda) * inp;
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
2019-10-19 05:56:17 -07:00
|
|
|
|
|
|
|
// the output can be biased
|
|
|
|
return y + modelBias;
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// "Overdamped" SODPT indirect transfer function used for step response analytic simulation (xi > 1)
|
2019-10-09 10:11:18 -07:00
|
|
|
// Used mostly as an approximate model for electro-mechanical processes (e.g. manometer
|
2019-10-05 07:54:26 -07:00
|
|
|
// The Laplace representation is: K * exp(-L * s) / ((T1*T2)*s^2 + (T1+T2)*s + 1)
|
|
|
|
class SecondOrderPlusDelayLineOverdampedFunction : public AbstractDelayLineFunction<4> {
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
SecondOrderPlusDelayLineOverdampedFunction(const InputFunction *input, const float_t *measuredOutput, int numDataPoints, double_t modelBias) :
|
2019-10-19 05:56:17 -07:00
|
|
|
AbstractDelayLineFunction(input, measuredOutput, numDataPoints, modelBias) {
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual void justifyParams(double_t *params) const {
|
2019-10-19 05:56:17 -07:00
|
|
|
params[PARAM_L] = fmax(params[PARAM_L], minParamL);
|
2019-10-09 10:11:18 -07:00
|
|
|
params[PARAM_T] = fmax(params[PARAM_T], minParamT);
|
|
|
|
params[PARAM_T2] = fmax(params[PARAM_T2], minParamT);
|
2019-10-19 05:56:17 -07:00
|
|
|
params[PARAM_K] = (fabs(params[PARAM_K]) < minParamK) ? minParamK : params[PARAM_K];
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Creating a state-space representation using Rosenbrock system matrix
|
2019-10-23 07:45:12 -07:00
|
|
|
virtual double_t calcEstimatedValuesAtPoint(int i, const double_t *params) const {
|
2019-10-05 07:54:26 -07:00
|
|
|
// only positive values allowed (todo: choose the limits)
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t pL = fmax(params[PARAM_L], minParamL);
|
|
|
|
double_t pT = fmax(params[PARAM_T], minParamT);
|
|
|
|
double_t pT2 = fmax(params[PARAM_T2], minParamT);
|
|
|
|
double_t pK = (fabs(params[PARAM_K]) < minParamK) ? minParamK : params[PARAM_K];
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// state-space params
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t lambda = exp(-1.0 / (pT * inputFunc->getTimeScale()));
|
|
|
|
double_t lambda2 = exp(-1.0 / (pT2 * inputFunc->getTimeScale()));
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// todo: find better initial values?
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t x = inputFunc->getValue(0, 0) * pK;
|
|
|
|
double_t y = inputFunc->getValue(0, 0) * pK;
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// The SO response function is indirect, so we need to iterate all previous values to find the current one
|
|
|
|
for (int j = 0; j <= i; j++) {
|
|
|
|
// delayed input
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t inp = inputFunc->getValue((double_t)j, pL);
|
2019-10-05 07:54:26 -07:00
|
|
|
|
|
|
|
// indirect model response in Controllable Canonical Form (2nd order CCF)
|
|
|
|
y = lambda2 * y + (1.0 - lambda2) * x;
|
2019-10-09 10:11:18 -07:00
|
|
|
x = lambda * x + pK * (1.0 - lambda) * inp;
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
2019-10-19 05:56:17 -07:00
|
|
|
|
|
|
|
// the output can be biased
|
|
|
|
return y + modelBias;
|
2019-10-05 07:54:26 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-10-07 11:13:16 -07:00
|
|
|
// Harriot's relation function (based on the graph)
|
|
|
|
// Used to approximate initial parameters for SOPDT model
|
|
|
|
// See: findSecondOrderInitialParamsHarriot() and "Harriot P. Process control (1964). McGraw-Hill. USA."
|
|
|
|
class HarriotFunction {
|
|
|
|
public:
|
2019-10-23 07:45:12 -07:00
|
|
|
double_t getValue(double_t x) const {
|
|
|
|
return buf.getValue((float_t)((x - 2.8 / 719.0 - 0.26) * 719.0 / 2.8));
|
2019-10-07 11:13:16 -07:00
|
|
|
}
|
|
|
|
|
2019-10-23 07:45:12 -07:00
|
|
|
static constexpr double_t minX = 2.8 / 719.0 - 0.26;
|
|
|
|
static constexpr double_t maxX = 33 / 719.0 * 2.8 + minX;
|
2019-10-09 10:11:18 -07:00
|
|
|
|
2019-10-07 11:13:16 -07:00
|
|
|
private:
|
|
|
|
const AveragingDataBuffer<34> buf = { {
|
2019-10-09 10:11:18 -07:00
|
|
|
0.500000000f, 0.589560440f, 0.624725275f, 0.652747253f, 0.675274725f, 0.694505495f, 0.712637363f, 0.729120879f,
|
|
|
|
0.743406593f, 0.757142857f, 0.769780220f, 0.781318681f, 0.793956044f, 0.804395604f, 0.814285714f, 0.824725275f,
|
|
|
|
0.834065934f, 0.844505495f, 0.853296703f, 0.862637363f, 0.870879121f, 0.880219780f, 0.889010989f, 0.897802198f,
|
|
|
|
0.906593407f, 0.915384615f, 0.924175824f, 0.933516484f, 0.942857143f, 0.953296703f, 0.963736264f, 0.975274725f,
|
|
|
|
0.986813187f, 1.000000000f
|
2019-10-07 11:13:16 -07:00
|
|
|
}, 34, 0 };
|
|
|
|
};
|