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 } }