using IStation.Numerics.LinearAlgebra;
using System;
using System.Linq;
namespace IStation.Numerics.Optimization
{
public abstract class NonlinearMinimizerBase
{
///
/// The stopping threshold for the function value or L2 norm of the residuals.
///
public static double FunctionTolerance { get; set; }
///
/// The stopping threshold for L2 norm of the change of the parameters.
///
public static double StepTolerance { get; set; }
///
/// The stopping threshold for infinity norm of the gradient.
///
public static double GradientTolerance { get; set; }
///
/// The maximum number of iterations.
///
public static int MaximumIterations { get; set; }
///
/// The lower bound of the parameters.
///
public static Vector LowerBound { get; private set; }
///
/// The upper bound of the parameters.
///
public static Vector UpperBound { get; private set; }
///
/// The scale factors for the parameters.
///
public static Vector Scales { get; private set; }
private static bool IsBounded => LowerBound != null || UpperBound != null || Scales != null;
protected NonlinearMinimizerBase(double gradientTolerance = 1E-18, double stepTolerance = 1E-18, double functionTolerance = 1E-18, int maximumIterations = -1)
{
GradientTolerance = gradientTolerance;
StepTolerance = stepTolerance;
FunctionTolerance = functionTolerance;
MaximumIterations = maximumIterations;
}
protected static void ValidateBounds(Vector parameters, Vector lowerBound = null, Vector upperBound = null, Vector scales = null)
{
if (parameters == null)
{
throw new ArgumentNullException("parameters");
}
if (lowerBound != null && lowerBound.Count(x => double.IsInfinity(x) || double.IsNaN(x)) > 0)
{
throw new ArgumentException("The lower bounds must be finite.");
}
if (lowerBound != null && lowerBound.Count != parameters.Count)
{
throw new ArgumentException("The lower bounds can't have different size from the parameters.");
}
LowerBound = lowerBound;
if (upperBound != null && upperBound.Count(x => double.IsInfinity(x) || double.IsNaN(x)) > 0)
{
throw new ArgumentException("The upper bounds must be finite.");
}
if (upperBound != null && upperBound.Count != parameters.Count)
{
throw new ArgumentException("The upper bounds can't have different size from the parameetrs.");
}
UpperBound = upperBound;
if (scales != null && scales.Count(x => double.IsInfinity(x) || double.IsNaN(x) || x == 0) > 0)
{
throw new ArgumentException("The scales must be finite.");
}
if (scales != null && scales.Count != parameters.Count)
{
throw new ArgumentException("The scales can't have different size from the parameters.");
}
if (scales != null && scales.Count(x => x < 0) > 0)
{
scales.PointwiseAbs();
}
Scales = scales;
}
protected static double EvaluateFunction(IObjectiveModel objective, Vector Pint)
{
var Pext = ProjectToExternalParameters(Pint);
objective.EvaluateAt(Pext);
return objective.Value;
}
protected static Tuple, Matrix> EvaluateJacobian(IObjectiveModel objective, Vector Pint)
{
var gradient = objective.Gradient;
var hessian = objective.Hessian;
if (IsBounded)
{
var scaleFactors = ScaleFactorsOfJacobian(Pint); // the parameters argument is always internal.
for (int i = 0; i < gradient.Count; i++)
{
gradient[i] = gradient[i] * scaleFactors[i];
}
for (int i = 0; i < hessian.RowCount; i++)
{
for (int j = 0; j < hessian.ColumnCount; j++)
{
hessian[i, j] = hessian[i, j] * scaleFactors[i] * scaleFactors[j];
}
}
}
return new Tuple, Matrix>(gradient, hessian);
}
#region Projection of Parameters
// To handle the box constrained minimization as the unconstrained minimization,
// the parameters are mapping by the following rules,
// which are modified the rules shown in the ref[1] in order to introduce scales.
//
// 1. lower < Pext < upper
// Pint = asin(2 * (Pext - lower) / (upper - lower) - 1)
// Pext = lower + (sin(Pint) + 1) * (upper - lower) / 2
// dPext/dPint = (upper - lower) / 2 * cos(Pint)
//
// 2. lower < Pext
// Pint = sqrt((Pext/scale - lower/scale + 1)^2 - 1)
// Pext = lower + scale * (sqrt(Pint^2 + 1) - 1)
// dPext/dPint = scale * Pint / sqrt(Pint^2 + 1)
//
// 3. Pext < upper
// Pint = sqrt((upper / scale - Pext / scale + 1)^2 - 1)
// Pext = upper + scale - scale * sqrt(Pint^2 + 1)
// dPext/dPint = - scale * Pint / sqrt(Pint^2 + 1)
//
// 4. no bounds, but scales
// Pint = Pext / scale
// Pext = Pint * scale
// dPext/dPint = scale
//
// The rules are applied in ProjectParametersToInternal, ProjectParametersToExternal, and ScaleFactorsOfJacobian methods.
//
// References:
// [1] https://lmfit.github.io/lmfit-py/bounds.html
// [2] MINUIT User's Guide, https://root.cern.ch/download/minuit.pdf
//
// Except when it is initial guess, the parameters argument is always internal parameter.
// So, first map the parameters argument to the external parameters in order to calculate function values.
protected static Vector ProjectToInternalParameters(Vector Pext)
{
var Pint = Pext.Clone();
if (LowerBound != null && UpperBound != null)
{
for (int i = 0; i < Pext.Count; i++)
{
Pint[i] = Math.Asin((2.0 * (Pext[i] - LowerBound[i]) / (UpperBound[i] - LowerBound[i])) - 1.0);
}
return Pint;
}
else if (LowerBound != null && UpperBound == null)
{
for (int i = 0; i < Pext.Count; i++)
{
Pint[i] = (Scales == null)
? Math.Sqrt(Math.Pow(Pext[i] - LowerBound[i] + 1.0, 2) - 1.0)
: Math.Sqrt(Math.Pow((Pext[i] - LowerBound[i]) / Scales[i] + 1.0, 2) - 1.0);
}
return Pint;
}
else if (LowerBound == null && UpperBound != null)
{
for (int i = 0; i < Pext.Count; i++)
{
Pint[i] = (Scales == null)
? Math.Sqrt(Math.Pow(UpperBound[i] - Pext[i] + 1.0, 2) - 1.0)
: Math.Sqrt(Math.Pow((UpperBound[i] - Pext[i]) / Scales[i] + 1.0, 2) - 1.0);
}
return Pint;
}
else if (Scales != null)
{
for (int i = 0; i < Pext.Count; i++)
{
Pint[i] = Pext[i] / Scales[i];
}
return Pint;
}
return Pint;
}
protected static Vector ProjectToExternalParameters(Vector Pint)
{
var Pext = Pint.Clone();
if (LowerBound != null && UpperBound != null)
{
for (int i = 0; i < Pint.Count; i++)
{
Pext[i] = LowerBound[i] + (UpperBound[i] / 2.0 - LowerBound[i] / 2.0) * (Math.Sin(Pint[i]) + 1.0);
}
return Pext;
}
else if (LowerBound != null && UpperBound == null)
{
for (int i = 0; i < Pint.Count; i++)
{
Pext[i] = (Scales == null)
? LowerBound[i] + Math.Sqrt(Pint[i] * Pint[i] + 1.0) - 1.0
: LowerBound[i] + Scales[i] * (Math.Sqrt(Pint[i] * Pint[i] + 1.0) - 1.0);
}
return Pext;
}
else if (LowerBound == null && UpperBound != null)
{
for (int i = 0; i < Pint.Count; i++)
{
Pext[i] = (Scales == null)
? UpperBound[i] - Math.Sqrt(Pint[i] * Pint[i] + 1.0) + 1.0
: UpperBound[i] - Scales[i] * (Math.Sqrt(Pint[i] * Pint[i] + 1.0) - 1.0);
}
return Pext;
}
else if (Scales != null)
{
for (int i = 0; i < Pint.Count; i++)
{
Pext[i] = Pint[i] * Scales[i];
}
return Pext;
}
return Pext;
}
protected static Vector ScaleFactorsOfJacobian(Vector Pint)
{
var scale = Vector.Build.Dense(Pint.Count, 1.0);
if (LowerBound != null && UpperBound != null)
{
for (int i = 0; i < Pint.Count; i++)
{
scale[i] = (UpperBound[i] - LowerBound[i]) / 2.0 * Math.Cos(Pint[i]);
}
return scale;
}
else if (LowerBound != null && UpperBound == null)
{
for (int i = 0; i < Pint.Count; i++)
{
scale[i] = (Scales == null)
? Pint[i] / Math.Sqrt(Pint[i] * Pint[i] + 1.0)
: Scales[i] * Pint[i] / Math.Sqrt(Pint[i] * Pint[i] + 1.0);
}
return scale;
}
else if (LowerBound == null && UpperBound != null)
{
for (int i = 0; i < Pint.Count; i++)
{
scale[i] = (Scales == null)
? -Pint[i] / Math.Sqrt(Pint[i] * Pint[i] + 1.0)
: -Scales[i] * Pint[i] / Math.Sqrt(Pint[i] * Pint[i] + 1.0);
}
return scale;
}
else if (Scales != null)
{
return Scales;
}
return scale;
}
#endregion Projection of Parameters
}
}