init
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import numpy as np
|
||||
from scipy.optimize import Bounds
|
||||
|
||||
def process_bounds(bounds, lenx0):
|
||||
'''
|
||||
`bounds` can either be an object with the properties lb and ub, or a list of tuples
|
||||
indicating a lower bound and an upper bound for each variable. If the list contains
|
||||
fewer entries than the length of x0, the remaining entries will generated as -/+ infinity.
|
||||
Some examples of valid lists of tuple, assuming len(x0) == 3:
|
||||
[(0, 1), (2, 3), (4, 5)] -> returns [0, 2, 4], [1, 3, 5]
|
||||
[(0, 1), (None, 3)] -> returns [0, -inf, -inf], [1, 3, inf]
|
||||
[(0, 1), (-np.inf, 3)] -> returns [0, -inf, -inf], [1, 3, inf]
|
||||
'''
|
||||
|
||||
if bounds is None:
|
||||
lb = np.array([-np.inf]*lenx0, dtype=np.float64)
|
||||
ub = np.array([np.inf]*lenx0, dtype=np.float64)
|
||||
return lb, ub
|
||||
|
||||
if isinstance(bounds, Bounds):
|
||||
lb = np.array(bounds.lb, dtype=np.float64)
|
||||
ub = np.array(bounds.ub, dtype=np.float64)
|
||||
lb = np.concatenate((lb, -np.inf*np.ones(lenx0 - len(lb))))
|
||||
ub = np.concatenate((ub, np.inf*np.ones(lenx0 - len(ub))))
|
||||
return lb, ub
|
||||
|
||||
# If neither of the above conditions are true, we assume that bounds is a list of tuples
|
||||
lb = np.array([bound[0] if bound[0] is not None else -np.inf for bound in bounds], dtype=np.float64)
|
||||
ub = np.array([bound[1] if bound[1] is not None else np.inf for bound in bounds], dtype=np.float64)
|
||||
# If there were fewer bounds than variables, pad the rest with -/+ infinity
|
||||
lb = np.concatenate((lb, -np.inf*np.ones(lenx0 - len(lb))))
|
||||
ub = np.concatenate((ub, np.inf*np.ones(lenx0 - len(ub))))
|
||||
|
||||
return lb, ub
|
||||
@@ -0,0 +1,46 @@
|
||||
import numpy as np
|
||||
from scipy.optimize import LinearConstraint
|
||||
|
||||
|
||||
def combine_multiple_linear_constraints(constraints):
|
||||
full_A = constraints[0].A
|
||||
full_lb = constraints[0].lb
|
||||
full_ub = constraints[0].ub
|
||||
for constraint in constraints[1:]:
|
||||
full_A = np.concatenate((full_A, constraint.A), axis=0)
|
||||
full_lb = np.concatenate((full_lb, constraint.lb), axis=0)
|
||||
full_ub = np.concatenate((full_ub, constraint.ub), axis=0)
|
||||
return LinearConstraint(full_A, full_lb, full_ub)
|
||||
|
||||
|
||||
def separate_LC_into_eq_and_ineq(linear_constraint):
|
||||
# The Python interface receives linear constraints lb <= A*x <= ub, but the
|
||||
# Fortran backend of PRIMA expects that the linear constraints are specified
|
||||
# as A_eq*x = b_eq, A_ineq*x <= b_ineq.
|
||||
# As such, we must:
|
||||
# 1. for constraints with lb == ub, rewrite them as A_eq*x = lb;
|
||||
# 2. for constraints with lb < ub, rewrite them as A_ineq*x <= b_ineq.
|
||||
|
||||
# We suppose lb == ub if ub <= lb + 2*epsilon, assuming that the preprocessing
|
||||
# ensures lb <= ub.
|
||||
epsilon = np.finfo(np.float64).eps
|
||||
|
||||
eq_indices = (linear_constraint.ub <= (linear_constraint.lb + 2*epsilon))
|
||||
A_eq = linear_constraint.A[eq_indices]
|
||||
b_eq = (linear_constraint.lb[eq_indices] + linear_constraint.ub[eq_indices])/2.0
|
||||
|
||||
ineq_lb_indices = (linear_constraint.lb > -np.inf)
|
||||
A_ineq_lb = -linear_constraint.A[~eq_indices & ineq_lb_indices]
|
||||
b_ineq_lb = -linear_constraint.lb[~eq_indices & ineq_lb_indices]
|
||||
ineq_ub_indices = (linear_constraint.ub < np.inf)
|
||||
A_ineq_ub = linear_constraint.A[~eq_indices & ineq_ub_indices]
|
||||
b_ineq_ub = linear_constraint.ub[~eq_indices & ineq_ub_indices]
|
||||
A_ineq = np.concatenate((A_ineq_lb, A_ineq_ub))
|
||||
b_ineq = np.concatenate((b_ineq_lb, b_ineq_ub))
|
||||
|
||||
# Ensure dtype is float64, or set to None if empty
|
||||
A_eq = np.array(A_eq, dtype=np.float64) if len(A_eq) > 0 else None
|
||||
b_eq = np.array(b_eq, dtype=np.float64) if len(b_eq) > 0 else None
|
||||
A_ineq = np.array(A_ineq, dtype=np.float64) if len(A_ineq) > 0 else None
|
||||
b_ineq = np.array(b_ineq, dtype=np.float64) if len(b_ineq) > 0 else None
|
||||
return A_eq, b_eq, A_ineq, b_ineq
|
||||
@@ -0,0 +1,54 @@
|
||||
import numpy as np
|
||||
|
||||
def transform_constraint_function(nlc):
|
||||
'''
|
||||
The Python interfaces receives the constraints as lb <= constraint(x) <= ub,
|
||||
but the Fortran backend expects the nonlinear constraints to be constraint(x) <= 0.
|
||||
Thus a conversion is needed.
|
||||
|
||||
In addition to the conversion, we add a check to ensure that the provided lower/upper bounds
|
||||
have a shape consistent with the output of the constraint function.
|
||||
'''
|
||||
|
||||
def newconstraint(x):
|
||||
values = np.atleast_1d(np.array(nlc.fun(x), dtype=np.float64))
|
||||
|
||||
# Upgrade the lower/upper bounds to vectors if necessary
|
||||
lb = nlc.lb
|
||||
try:
|
||||
_ = len(lb)
|
||||
except TypeError:
|
||||
lb = np.array([nlc.lb]*len(values), dtype=np.float64)
|
||||
|
||||
ub = nlc.ub
|
||||
try:
|
||||
_ = len(ub)
|
||||
except TypeError:
|
||||
ub = np.array([nlc.ub]*len(values), dtype=np.float64)
|
||||
|
||||
|
||||
# Check the shapes and raise an exception if they do not match
|
||||
if len(values) != len(lb):
|
||||
raise ValueError("The number of elements in the constraint function's output does not match the number of elements in the lower bound.")
|
||||
if len(values) != len(ub):
|
||||
raise ValueError("The number of elements in the constraint function's output does not match the number of elements in the upper bound.")
|
||||
|
||||
# Combine the upper and lower bounds to transform the function into the form
|
||||
# expected by the Fortran backend.
|
||||
return np.concatenate(([lb_ii - vi for lb_ii, vi in zip(lb, values) if lb_ii > -np.inf],
|
||||
[vi - ub_ii for ub_ii, vi in zip(ub, values) if ub_ii < np.inf],
|
||||
))
|
||||
return newconstraint
|
||||
|
||||
|
||||
def process_nl_constraints(nlcs):
|
||||
functions = []
|
||||
for nlc in nlcs:
|
||||
fun_i = transform_constraint_function(nlc)
|
||||
functions.append(fun_i)
|
||||
def constraint_function(x):
|
||||
values = np.empty(0, dtype=np.float64)
|
||||
for fun in functions:
|
||||
values = np.concatenate((values, fun(x)))
|
||||
return values
|
||||
return constraint_function
|
||||
@@ -0,0 +1,173 @@
|
||||
'''
|
||||
This module provides the _project function that attempts to project the initial guess
|
||||
onto the feasible set.
|
||||
|
||||
Adapted from the corresponding function in the PDFO package (https://www.pdfo.net) by
|
||||
Tom M. Ragonneau (https://ragonneau.github.io) and Zaikun Zhang (https://www.zhangzk.net).
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
from ._linear_constraints import LinearConstraint
|
||||
from scipy.optimize import OptimizeResult
|
||||
|
||||
# All the accepted scalar types; np.generic correspond to all NumPy types.
|
||||
scalar_types = (int, float, np.generic)
|
||||
eps = np.finfo(np.float64).eps
|
||||
|
||||
def _project(x0, lb, ub, constraints):
|
||||
"""Projection of the initial guess onto the feasible set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x0: ndarray, shape (n,)
|
||||
The same as in prepdfo.
|
||||
lb: ndarray, shape (n,)
|
||||
The same as in prepdfo.
|
||||
ub: ndarray, shape (n,)
|
||||
The same as in prepdfo.
|
||||
constraints: dict
|
||||
The general constraints of the problem, defined as a dictionary with
|
||||
fields:
|
||||
linear: LinearConstraint
|
||||
The linear constraints of the problem.
|
||||
nonlinear: dict
|
||||
The nonlinear constraints of the problem. When ``_project`` is called, the nonlinear constraints are
|
||||
None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result: OptimizeResult
|
||||
The result of the projection.
|
||||
|
||||
Authors
|
||||
-------
|
||||
Tom M. RAGONNEAU (ragonneau.github.io)
|
||||
and Zaikun ZHANG (www.zhangzk.net)
|
||||
|
||||
Dedicated to the late Professor M. J. D. Powell FRS (1936--2015).
|
||||
"""
|
||||
invoker = 'prima'
|
||||
|
||||
# Validate x0.
|
||||
if isinstance(x0, scalar_types):
|
||||
x0_c = [x0]
|
||||
elif hasattr(x0, '__len__'):
|
||||
x0_c = x0
|
||||
else:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: x0 should be a vector.'.format(invoker))
|
||||
try:
|
||||
x0_c = np.asarray(x0_c, dtype=np.float64)
|
||||
except ValueError:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: x0 should contain only scalars.'.format(invoker))
|
||||
if len(x0_c.shape) != 1:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: x0 should be a vector.'.format(invoker))
|
||||
lenx0 = x0_c.size
|
||||
|
||||
# Validate lb.
|
||||
if isinstance(lb, scalar_types):
|
||||
lb_c = [lb]
|
||||
elif hasattr(lb, '__len__'):
|
||||
lb_c = lb
|
||||
else:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: lb should be a vector.'.format(invoker))
|
||||
try:
|
||||
lb_c = np.asarray(lb_c, dtype=np.float64)
|
||||
except ValueError:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: lb should contain only scalars.'.format(invoker))
|
||||
if len(lb_c.shape) != 1 or lb_c.size != lenx0:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: the size of lb is inconsistent with x0.'.format(invoker))
|
||||
|
||||
# Validate ub.
|
||||
if isinstance(ub, scalar_types):
|
||||
ub_c = [ub]
|
||||
elif hasattr(ub, '__len__'):
|
||||
ub_c = ub
|
||||
else:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: ub should be a vector.'.format(invoker))
|
||||
try:
|
||||
ub_c = np.asarray(ub_c, dtype=np.float64)
|
||||
except ValueError:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: ub should contain only scalars.'.format(invoker))
|
||||
if len(ub_c.shape) != 1 or ub_c.size != lenx0:
|
||||
raise ValueError('{}: UNEXPECTED ERROR: the size of ub is inconsistent with x0.'.format(invoker))
|
||||
|
||||
# Validate constraints.
|
||||
if not isinstance(constraints, dict) or not ({'linear', 'nonlinear'} <= set(constraints.keys())) or \
|
||||
not (isinstance(constraints['linear'], LinearConstraint) or constraints['linear'] is None):
|
||||
# the nonlinear constraints will not be taken into account in this function and are, therefore, not validated
|
||||
raise ValueError('{}: UNEXPECTED ERROR: The constraints are ill-defined.'.format(invoker))
|
||||
|
||||
max_con = 1e20 # Decide whether an inequality constraint can be ignored
|
||||
|
||||
# Project onto the feasible set.
|
||||
if constraints['linear'] is None:
|
||||
# Direct projection onto the bound constraints
|
||||
x_proj = np.nanmin((np.nanmax((x0_c, lb_c), axis=0), ub_c), axis=0)
|
||||
return OptimizeResult(x=x_proj)
|
||||
elif all(np.less_equal(np.abs(constraints['linear'].ub - constraints['linear'].lb), eps)) and \
|
||||
np.max(lb_c) <= -max_con and np.min(ub_c) >= max_con:
|
||||
# The linear constraints are all equality constraints. The projection can therefore be done by solving the
|
||||
# least-squares problem: min ||A*x - (b - A*x_0)||.
|
||||
a = constraints['linear'].A
|
||||
b = (constraints['linear'].lb + constraints['linear'].ub) / 2
|
||||
xi, _, _, _ = np.linalg.lstsq(a, b - np.dot(a, x0_c), rcond=None)
|
||||
|
||||
# The problem is not bounded. However, if the least-square solver returned values bigger in absolute value
|
||||
# than max_con, they will be reduced to this bound.
|
||||
x_proj = np.nanmin((np.nanmax((x0_c + xi, lb_c), axis=0), ub_c), axis=0)
|
||||
|
||||
return OptimizeResult(x=x_proj)
|
||||
|
||||
if constraints['linear'] is not None:
|
||||
try:
|
||||
# Project the initial guess onto the linear constraints via SciPy.
|
||||
from scipy.optimize import minimize
|
||||
from scipy.optimize import Bounds as ScipyBounds
|
||||
from scipy.optimize import LinearConstraint as ScipyLinearConstraint
|
||||
|
||||
linear = constraints['linear']
|
||||
|
||||
# To be more efficient, SciPy asks to separate the equality and the inequality constraints into two
|
||||
# different LinearConstraint structures
|
||||
pc_args_ineq, pc_args_eq = dict(), dict()
|
||||
pc_args_ineq['A'], pc_args_eq['A'] = np.asarray([[]]), np.asarray([[]])
|
||||
pc_args_ineq['A'] = pc_args_ineq['A'].reshape(0, linear.A.shape[1])
|
||||
pc_args_eq['A'] = pc_args_eq['A'].reshape(0, linear.A.shape[1])
|
||||
pc_args_ineq['lb'], pc_args_eq['lb'] = np.asarray([]), np.asarray([])
|
||||
pc_args_ineq['ub'], pc_args_eq['ub'] = np.asarray([]), np.asarray([])
|
||||
|
||||
for i in range(linear.lb.size):
|
||||
if linear.lb[i] != linear.ub[i]:
|
||||
pc_args_ineq['A'] = np.concatenate((pc_args_ineq['A'], linear.A[i:i+1, :]), axis=0)
|
||||
pc_args_ineq['lb'] = np.r_[pc_args_ineq['lb'], linear.lb[i]]
|
||||
pc_args_ineq['ub'] = np.r_[pc_args_ineq['ub'], linear.ub[i]]
|
||||
else:
|
||||
pc_args_eq['A'] = np.concatenate((pc_args_eq['A'], linear.A[i:i+1, :]), axis=0)
|
||||
pc_args_eq['lb'] = np.r_[pc_args_eq['lb'], linear.lb[i]]
|
||||
pc_args_eq['ub'] = np.r_[pc_args_eq['ub'], linear.ub[i]]
|
||||
|
||||
if pc_args_ineq['A'].size > 0 and pc_args_ineq['lb'].size > 0 and pc_args_eq['lb'].size > 0:
|
||||
project_constraints = [ScipyLinearConstraint(**pc_args_ineq), ScipyLinearConstraint(**pc_args_eq)]
|
||||
elif pc_args_ineq['A'].size > 0 and pc_args_ineq['lb'].size > 0:
|
||||
project_constraints = ScipyLinearConstraint(**pc_args_ineq)
|
||||
elif pc_args_eq['A'].size > 0:
|
||||
project_constraints = ScipyLinearConstraint(**pc_args_eq)
|
||||
else:
|
||||
project_constraints = ()
|
||||
|
||||
# Perform the actual projection.
|
||||
ax_ineq = np.dot(pc_args_ineq['A'], x0_c)
|
||||
ax_eq = np.dot(pc_args_eq['A'], x0_c)
|
||||
if np.greater(ax_ineq, pc_args_ineq['ub']).any() or np.greater(pc_args_ineq['lb'], ax_ineq).any() or \
|
||||
np.not_equal(ax_eq, pc_args_eq['lb']).any() or \
|
||||
np.greater(x0_c, ub_c).any() or np.greater(lb_c, x0_c).any():
|
||||
return minimize(lambda x: np.dot(x - x0_c, x - x0_c) / 2, x0_c, jac=lambda x: (x - x0_c),
|
||||
bounds=ScipyBounds(lb_c, ub_c), constraints=project_constraints)
|
||||
else:
|
||||
# Do not perform any projection if the initial guess is feasible.
|
||||
return OptimizeResult(x=x0_c)
|
||||
|
||||
except ImportError:
|
||||
return OptimizeResult(x=x0_c)
|
||||
|
||||
return OptimizeResult(x=x0_c)
|
||||
@@ -0,0 +1,93 @@
|
||||
'''
|
||||
This module checks whether to break out of the solver loop.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
from .infos import INFO_DEFAULT, NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, MAXFUN_REACHED
|
||||
|
||||
import numpy as np
|
||||
|
||||
def checkbreak_unc(maxfun, nf, f, ftarget, x):
|
||||
'''
|
||||
This module checks whether to break out of the solver loop in the unconstrained case.
|
||||
'''
|
||||
|
||||
# Outputs
|
||||
info = INFO_DEFAULT
|
||||
|
||||
# Local variables
|
||||
srname = "CHECKbreak_UNC"
|
||||
|
||||
# Preconditions
|
||||
assert INFO_DEFAULT not in [NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, MAXFUN_REACHED], f'NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, and MAXFUN_REACHED differ from INFO_DFT {srname}'
|
||||
# X does not contain NaN if the initial X does not contain NaN and the subroutines generating
|
||||
# trust-region/geometry steps work properly so that they never produce a step containing NaN/Inf.
|
||||
assert not any(np.isnan(x)), f'X does not contain NaN {srname}'
|
||||
# With the moderated extreme barrier, F cannot be NaN/+Inf.
|
||||
assert not (any(np.isnan(f)) or any(np.isposinf(f))), f'F is not NaN/+Inf {srname}'
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
# Although X should not contain NaN unless there is a bug, we include the following for security.
|
||||
# X can be Inf, as finite + finite can be Inf numerically.
|
||||
if any(np.isnan(x)) or any(np.isinf(x)):
|
||||
info = NAN_INF_X
|
||||
|
||||
# Although NAN_INF_F should not happen unless there is a bug, we include the following for security.
|
||||
if any(np.isnan(f)) or any(np.isposinf(f)):
|
||||
info = NAN_INF_F
|
||||
|
||||
if f <= ftarget:
|
||||
info = FTARGET_ACHIEVED
|
||||
|
||||
if nf >= maxfun:
|
||||
info = MAXFUN_REACHED
|
||||
|
||||
return info
|
||||
|
||||
def checkbreak_con(maxfun, nf, cstrv, ctol, f, ftarget, x):
|
||||
'''
|
||||
This module checks whether to break out of the solver loop in the constrained case.
|
||||
'''
|
||||
|
||||
# Outputs
|
||||
info = INFO_DEFAULT
|
||||
|
||||
# Local variables
|
||||
srname = "CHECKbreak_CON"
|
||||
|
||||
# Preconditions
|
||||
assert INFO_DEFAULT not in [NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, MAXFUN_REACHED], f'NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, and MAXFUN_REACHED differ from INFO_DFT {srname}'
|
||||
# X does not contain NaN if the initial X does not contain NaN and the subroutines generating
|
||||
# trust-region/geometry steps work properly so that they never produce a step containing NaN/Inf.
|
||||
assert not any(np.isnan(x)), f'X does not contain NaN {srname}'
|
||||
# With the moderated extreme barrier, F or CSTRV cannot be NaN/+Inf.
|
||||
assert not (np.isnan(f) or np.isposinf(f) or np.isnan(cstrv) or np.isposinf(cstrv)), f'F or CSTRV is not NaN/+Inf {srname}'
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
# Although X should not contain NaN unless there is a bug, we include the following for security.
|
||||
# X can be Inf, as finite + finite can be Inf numerically.
|
||||
if any(np.isnan(x)) or any(np.isinf(x)):
|
||||
info = NAN_INF_X
|
||||
|
||||
# Although NAN_INF_F should not happen unless there is a bug, we include the following for security.
|
||||
if np.isnan(f) or np.isposinf(f) or np.isnan(cstrv) or np.isposinf(cstrv):
|
||||
info = NAN_INF_F
|
||||
|
||||
if cstrv <= ctol and f <= ftarget:
|
||||
info = FTARGET_ACHIEVED
|
||||
|
||||
if nf >= maxfun:
|
||||
info = MAXFUN_REACHED
|
||||
|
||||
return info
|
||||
@@ -0,0 +1,47 @@
|
||||
'''
|
||||
This is a module defining some constants.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
DEBUGGING = bool(os.getenv('PRIMA_DEBUGGING'))
|
||||
|
||||
REALMIN = np.finfo(float).tiny
|
||||
REALMAX = np.finfo(float).max
|
||||
FUNCMAX = 10.0**30
|
||||
CONSTRMAX = FUNCMAX
|
||||
EPS = np.finfo(float).eps
|
||||
|
||||
# Any bound with an absolute value at least BOUNDMAX is considered as no bound.
|
||||
BOUNDMAX = REALMAX/4
|
||||
|
||||
# Some default values
|
||||
RHOBEG_DEFAULT = 1
|
||||
RHOEND_DEFAULT = 1e-6
|
||||
FTARGET_DEFAULT = -REALMAX
|
||||
CTOL_DEFAULT = np.sqrt(EPS)
|
||||
CWEIGHT_DEFAULT = 1e8
|
||||
ETA1_DEFAULT = 0.1
|
||||
ETA2_DEFAULT = 0.7
|
||||
GAMMA1_DEFAULT = 0.5
|
||||
GAMMA2_DEFAULT = 2
|
||||
IPRINT_DEFAULT = 0
|
||||
MAXFUN_DIM_DEFAULT = 500
|
||||
|
||||
PRIMA_MAX_HIST_MEM_MB = 300 # 1MB > 10^5*REAL64. 100 can be too small.
|
||||
|
||||
# Maximal amount of memory (Byte) allowed for XHIST, FHIST, CONHIST, CHIST, and the filters.
|
||||
MHM = PRIMA_MAX_HIST_MEM_MB * 10**6
|
||||
# Make sure that MAXHISTMEM does not exceed HUGE(0) to avoid overflow and memory errors.
|
||||
MAXHISTMEM = min(MHM, np.iinfo(np.int32).max)
|
||||
|
||||
# Maximal length of the filter used in constrained solvers.
|
||||
MIN_MAXFILT = 200 # Should be positive; < 200 is not recommended.
|
||||
MAXFILT_DEFAULT = 10 * MIN_MAXFILT
|
||||
@@ -0,0 +1,99 @@
|
||||
'''
|
||||
This is a module evaluating the objective/constraint function with Nan/Inf handling.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
from .consts import FUNCMAX, CONSTRMAX, REALMAX, DEBUGGING
|
||||
from .linalg import matprod, primasum
|
||||
|
||||
# This is a module evaluating the objective/constraint function with Nan/Inf handling.
|
||||
|
||||
|
||||
def moderatex(x):
|
||||
'''
|
||||
This function moderates a decision variable. It replaces NaN by 0 and Inf/-Inf by
|
||||
REALMAX/-REALMAX.
|
||||
'''
|
||||
x[np.isnan(x)] = 0
|
||||
x = np.clip(x, -REALMAX, REALMAX)
|
||||
return x
|
||||
|
||||
def moderatef(f):
|
||||
"""
|
||||
This function moderates the function value of a MINIMIZATION problem. It replaces
|
||||
NaN and any value above FUNCMAX by FUNCMAX.
|
||||
"""
|
||||
f = FUNCMAX if np.isnan(f) else f
|
||||
f = np.clip(f, -REALMAX, FUNCMAX)
|
||||
# We may moderate huge negative function values as follows, but we decide not to.
|
||||
# f = np.clip(f, -FUNCMAX, FUNCMAX)
|
||||
return f
|
||||
|
||||
|
||||
def moderatec(c):
|
||||
"""
|
||||
This function moderates the constraint value, the constraint demanding this value
|
||||
to be NONNEGATIVE. It replaces any value below -CONSTRMAX by -CONSTRMAX, and any
|
||||
NaN or value above CONSTRMAX by CONSTRMAX.
|
||||
"""
|
||||
np.nan_to_num(c, copy=False, nan=CONSTRMAX)
|
||||
c = np.clip(c, -CONSTRMAX, CONSTRMAX)
|
||||
return c
|
||||
|
||||
|
||||
def evaluate(calcfc, x, m_nlcon, amat, bvec):
|
||||
"""
|
||||
This function evaluates CALCFC at X, returning the objective function value and the
|
||||
constraint value. Nan/Inf are handled by a moderated extreme barrier.
|
||||
"""
|
||||
|
||||
# Sizes
|
||||
m_lcon = len(bvec) if bvec is not None else 0
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
# X should not contain NaN if the initial X does not contain NaN and the
|
||||
# subroutines generating # trust-region/geometry steps work properly so that
|
||||
# they never produce a step containing NaN/Inf.
|
||||
assert not any(np.isnan(x))
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
constr = np.zeros(m_lcon + m_nlcon)
|
||||
if amat is not None:
|
||||
constr[:m_lcon] = matprod(x, amat.T) - bvec
|
||||
|
||||
if any(np.isnan(x)):
|
||||
# Although this should not happen unless there is a bug, we include this case
|
||||
# for robustness.
|
||||
f = primasum(x)
|
||||
constr = np.ones(m_nlcon) * f
|
||||
else:
|
||||
f, constr[m_lcon:] = calcfc(moderatex(x))
|
||||
|
||||
# Moderated extreme barrier: replace NaN/huge objective or constraint values
|
||||
# with a large but finite value. This is naive, and better approaches surely
|
||||
# exist.
|
||||
f = moderatef(f)
|
||||
constr[m_lcon:] = moderatec(constr[m_lcon:])
|
||||
|
||||
#==================#
|
||||
# Calculation ends #
|
||||
#==================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
# With X not containing NaN, and with the moderated extreme barrier, F cannot
|
||||
# be NaN/+Inf, and CONSTR cannot be NaN/-Inf.
|
||||
assert not (np.isnan(f) or np.isposinf(f))
|
||||
assert not any(np.isnan(constr) | np.isposinf(constr))
|
||||
|
||||
return f, constr
|
||||
@@ -0,0 +1,38 @@
|
||||
'''
|
||||
This module provides subroutines that handle the X/F/C histories of the solver, taking into
|
||||
account that MAXHIST may be smaller than NF.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
def savehist(maxhist, x, xhist, f, fhist, cstrv, chist, constr, conhist):
|
||||
'''
|
||||
Save the data values to the history lists.
|
||||
|
||||
The implementation of this function is vastly different from the Fortran implementation.
|
||||
This is mostly due to the ease of creating and appending to lists in Python
|
||||
|
||||
However just like the Fortran version we should be concerned about both performance
|
||||
and memory constraints. It will probably be better to initialize an array of NaN for
|
||||
each of the histories and keep track of how many indices we have stored. Not needed for
|
||||
the moment.
|
||||
'''
|
||||
if len(xhist) < maxhist:
|
||||
xhist.append(x)
|
||||
fhist.append(f)
|
||||
chist.append(cstrv)
|
||||
conhist.append(constr)
|
||||
else:
|
||||
# This effectively accomplishes what rangehist does in the Fortran implementation
|
||||
xhist.pop(0)
|
||||
fhist.pop(0)
|
||||
chist.pop(0)
|
||||
conhist.pop(0)
|
||||
xhist.append(x)
|
||||
fhist.append(f)
|
||||
chist.append(cstrv)
|
||||
conhist.append(constr)
|
||||
@@ -0,0 +1,30 @@
|
||||
'''
|
||||
This is a module defining exit flags.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
INFO_DEFAULT = 0
|
||||
SMALL_TR_RADIUS = 0
|
||||
FTARGET_ACHIEVED = 1
|
||||
TRSUBP_FAILED = 2
|
||||
MAXFUN_REACHED = 3
|
||||
MAXTR_REACHED = 20
|
||||
NAN_INF_X = -1
|
||||
NAN_INF_F = -2
|
||||
NAN_INF_MODEL = -3
|
||||
NO_SPACE_BETWEEN_BOUNDS = 6
|
||||
DAMAGING_ROUNDING = 7
|
||||
ZERO_LINEAR_CONSTRAINT = 8
|
||||
CALLBACK_TERMINATE = 30
|
||||
|
||||
# Stop-codes.
|
||||
# The following codes are used by ERROR STOP as stop-codes, which should be default integers.
|
||||
INVALID_INPUT = 100
|
||||
ASSERTION_FAILS = 101
|
||||
VALIDATION_FAILS = 102
|
||||
MEMORY_ALLOCATION_FAILS = 103
|
||||
@@ -0,0 +1,435 @@
|
||||
'''
|
||||
This module provides some basic linear algebra procedures.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
from .consts import DEBUGGING, EPS, REALMAX, REALMIN
|
||||
from .present import present
|
||||
|
||||
|
||||
# We use naive implementations of matrix multiplication and other routines for two
|
||||
# reasons:
|
||||
# 1. When Fortran is compiled in debug mode, and Python is using these routines, we
|
||||
# can get bit for bit identical results as compared to Fortran. This is helpful
|
||||
# for comparing the two implementations. It will be particularly helpful when porting
|
||||
# the other implementations like LINCOA, etc.
|
||||
# 2. On some problems this algorithm is very sensitive to errors in finite precision
|
||||
# arithmetic. Switching to naive implementation will slow down the algorithm, but
|
||||
# may be more stable.
|
||||
USE_NAIVE_MATH = False
|
||||
|
||||
|
||||
def inprod(x, y):
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.dot(x, y)
|
||||
result = 0
|
||||
for i in range(len(x)):
|
||||
result += x[i] * y[i]
|
||||
return result
|
||||
|
||||
|
||||
def matprod12(x, y):
|
||||
result = np.zeros(y.shape[1])
|
||||
for i in range(y.shape[1]):
|
||||
result[i] = inprod(x, y[:, i])
|
||||
return result
|
||||
|
||||
|
||||
def matprod21(x, y):
|
||||
result = np.zeros(x.shape[0])
|
||||
for i in range(x.shape[1]):
|
||||
result += x[:, i] * y[i]
|
||||
return result
|
||||
|
||||
|
||||
def matprod22(x, y):
|
||||
result = np.zeros((x.shape[0], y.shape[1]))
|
||||
for i in range(y.shape[1]):
|
||||
for j in range(x.shape[1]):
|
||||
result[:, j] += x[:, i] * y[i, j]
|
||||
return result
|
||||
|
||||
|
||||
def matprod(x, y):
|
||||
if not USE_NAIVE_MATH:
|
||||
return x@y
|
||||
if len(x.shape) == 1 and len(y.shape) == 1:
|
||||
return inprod(x, y)
|
||||
elif len(x.shape) == 1 and len(y.shape) == 2:
|
||||
return matprod12(x, y)
|
||||
elif len(x.shape) == 2 and len(y.shape) == 1:
|
||||
return matprod21(x, y)
|
||||
elif len(x.shape) == 2 and len(y.shape) == 2:
|
||||
return matprod22(x, y)
|
||||
else:
|
||||
raise ValueError(f'Invalid shapes for x and y: {x.shape} and {y.shape}')
|
||||
|
||||
|
||||
def outprod(x, y):
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.outer(x, y)
|
||||
result = np.zeros((len(x), len(y)))
|
||||
for i in range(len(x)):
|
||||
result[:, i] = x * y[i]
|
||||
return result
|
||||
|
||||
|
||||
def lsqr(A, b, Q, Rdiag):
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.linalg.lstsq(A, b, rcond=None)[0]
|
||||
|
||||
m = A.shape[0]
|
||||
n = A.shape[1]
|
||||
|
||||
rank = min(m, n)
|
||||
|
||||
x = np.zeros(n)
|
||||
y = b.copy()
|
||||
|
||||
for i in range(rank - 1, -1, -1):
|
||||
yq = inprod(y, Q[:, i])
|
||||
yqa = inprod(np.abs(y), np.abs(Q[:, i]))
|
||||
if isminor(yq, yqa):
|
||||
x[i] = 0
|
||||
else:
|
||||
x[i] = yq / Rdiag[i]
|
||||
y = y - x[i] * A[:, i]
|
||||
return x
|
||||
|
||||
|
||||
def hypot(x1, x2):
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.hypot(x1, x2)
|
||||
if not np.isfinite(x1):
|
||||
r = abs(x1)
|
||||
elif not np.isfinite(x2):
|
||||
r = abs(x2)
|
||||
else:
|
||||
y = abs(np.array([x1, x2]))
|
||||
y = np.array([min(y), max(y)])
|
||||
if y[0] > np.sqrt(REALMIN) and y[1] < np.sqrt(REALMAX/2.1):
|
||||
r = np.sqrt(sum(y*y))
|
||||
elif y[1] > 0:
|
||||
r = y[1] * np.sqrt((y[0]/y[1])*(y[0]/y[1]) + 1)
|
||||
else:
|
||||
r = 0
|
||||
return r
|
||||
|
||||
|
||||
def norm(x):
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.linalg.norm(x)
|
||||
# NOTE: Avoid np.pow! And exponentiation in general!
|
||||
# It appears that in Fortran, x*x and x**2 are the same, but in Python they are not!
|
||||
# Try it with x = 5 - 1e-15
|
||||
result = np.sqrt(sum([xi*xi for xi in x]))
|
||||
return result
|
||||
|
||||
|
||||
def istril(A, tol=0):
|
||||
return primasum(abs(A) - np.tril(abs(A))) <= tol
|
||||
|
||||
def istriu(A, tol=0):
|
||||
return primasum(abs(A) - np.triu(abs(A))) <= tol
|
||||
|
||||
|
||||
def inv(A):
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.linalg.inv(A)
|
||||
A = A.copy()
|
||||
n = A.shape[0]
|
||||
if istril(A):
|
||||
# This case is invoked in COBYLA.
|
||||
R = A.T
|
||||
B = np.zeros((n, n))
|
||||
for i in range(n):
|
||||
B[i, i] = 1 / R[i, i]
|
||||
B[:i, i] = -matprod(B[:i, :i], R[:i, i]) / R[i, i]
|
||||
return B.T
|
||||
elif istriu(A):
|
||||
B = np.zeros((n, n))
|
||||
for i in range(n):
|
||||
B[i, i] = 1 / A[i, i]
|
||||
B[:i, i] = -matprod(B[:i, :i], A[:i, i]) / A[i, i]
|
||||
else:
|
||||
# This is NOT the best algorithm for the inverse, but since the QR subroutine is available ...
|
||||
Q, R, P = qr(A)
|
||||
R = R.T
|
||||
B = np.zeros((n, n))
|
||||
for i in range(n - 1, -1, -1):
|
||||
B[:, i] = (Q[:, i] - matprod(B[:, i + 1:n], R[i + 1:n, i])) / R[i, i]
|
||||
InvP = np.zeros(n, dtype=int)
|
||||
InvP[P] = np.linspace(0, n-1, n)
|
||||
B = B[:, InvP].T
|
||||
return B
|
||||
|
||||
|
||||
def qr(A):
|
||||
m = A.shape[0]
|
||||
n = A.shape[1]
|
||||
|
||||
Q = np.eye(m)
|
||||
T = A.T
|
||||
P = np.linspace(0, n-1, n, dtype=int)
|
||||
|
||||
for j in range(n):
|
||||
k = np.argmax(primasum(primapow2(T[j:n+1, j:m+1]), axis=1), axis=0)
|
||||
if k > 0 and k <= n - j - 1:
|
||||
k += j
|
||||
P[j], P[k] = P[k], P[j]
|
||||
T[[j, k], :] = T[[k, j], :]
|
||||
for i in range(m-1, j, -1):
|
||||
G = planerot(T[j, [j, i]]).T
|
||||
T[j, [j, i]] = np.append(hypot(T[j, j], T[j, i]), 0)
|
||||
T[j + 1:n + 1, [j, i]] = matprod(T[j + 1:n + 1, [j, i]], G)
|
||||
Q[:, [j, i]] = matprod(Q[:, [j, i]], G)
|
||||
|
||||
R = T.T
|
||||
|
||||
return Q, R, P
|
||||
|
||||
|
||||
def primasum(x, axis=None):
|
||||
'''
|
||||
According to its documentation, np.sum will sometimes do partial pairwise summation.
|
||||
For our purposes, when comparing, we want don't want to do anything fancy, and we
|
||||
just want to add things up one at a time.
|
||||
'''
|
||||
if not USE_NAIVE_MATH:
|
||||
return np.sum(x, axis=axis)
|
||||
if axis is None:
|
||||
if x.ndim == 2:
|
||||
# Sum columns first, then sum the result
|
||||
return sum(primasum(x, axis=0))
|
||||
else:
|
||||
return sum(x)
|
||||
elif axis == 0:
|
||||
result = np.zeros(x.shape[1])
|
||||
for i in range(x.shape[1]):
|
||||
result[i] = sum(x[:, i])
|
||||
return result
|
||||
elif axis == 1:
|
||||
result = np.zeros(x.shape[0])
|
||||
for i in range(x.shape[0]):
|
||||
result[i] = sum(x[i, :])
|
||||
return result
|
||||
|
||||
|
||||
def primapow2(x):
|
||||
'''
|
||||
Believe it or now, x**2 is not always the same as x*x in Python. In Fortran they
|
||||
appear to be identical. Here's a quick one-line to find an example on your system
|
||||
(well, two liner after importing numpy):
|
||||
list(filter(lambda x: x[1], [(x:=np.random.random(), x**2 - x*x != 0) for _ in range(10000)]))
|
||||
'''
|
||||
return x*x
|
||||
|
||||
|
||||
def planerot(x):
|
||||
'''
|
||||
As in MATLAB, planerot(x) returns a 2x2 Givens matrix G for x in R2 so that Y=G@x has Y[1] = 0.
|
||||
Roughly speaking, G = np.array([[x[0]/R, x[1]/R], [-x[1]/R, x[0]/R]]), where R = np.linalg.norm(x).
|
||||
0. We need to take care of the possibilities of R=0, Inf, NaN, and over/underflow.
|
||||
1. The G defined above is continuous with respect to X except at 0. Following this definition,
|
||||
G = np.array([[np.sign(x[0]), 0], [0, np.sign(x[0])]]) if x[1] == 0,
|
||||
G = np.array([[0, np.sign(x[1])], [np.sign(x[1]), 0]]) if x[0] == 0
|
||||
Yet some implementations ignore the signs, leading to discontinuity and numerical instability.
|
||||
2. Difference from MATLAB: if x contains NaN of consists of only Inf, MATLAB returns a NaN matrix,
|
||||
but we return an identity matrix or a matrix of +/-np.sqrt(2). We intend to keep G always orthogonal.
|
||||
'''
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert len(x) == 2, "x must be a 2-vector"
|
||||
|
||||
# ==================
|
||||
# Calculation starts
|
||||
# ==================
|
||||
|
||||
# Define C = X(1) / R and S = X(2) / R with R = HYPOT(X(1), X(2)). Handle Inf/NaN, over/underflow.
|
||||
if (any(np.isnan(x))):
|
||||
# In this case, MATLAB sets G to NaN(2, 2). We refrain from doing so to keep G orthogonal.
|
||||
c = 1
|
||||
s = 0
|
||||
elif (all(np.isinf(x))):
|
||||
# In this case, MATLAB sets G to NaN(2, 2). We refrain from doing so to keep G orthogonal.
|
||||
c = 1 / np.sqrt(2) * np.sign(x[0])
|
||||
s = 1 / np.sqrt(2) * np.sign(x[1])
|
||||
elif (abs(x[0]) <= 0 and abs(x[1]) <= 0): # X(1) == 0 == X(2).
|
||||
c = 1
|
||||
s = 0
|
||||
elif (abs(x[1]) <= EPS * abs(x[0])):
|
||||
# N.B.:
|
||||
# 0. With <= instead of <, this case covers X(1) == 0 == X(2), which is treated above separately
|
||||
# to avoid the confusing SIGN(., 0) (see 1).
|
||||
# 1. SIGN(A, 0) = ABS(A) in Fortran but sign(0) = 0 in MATLAB, Python, Julia, and R#
|
||||
# 2. Taking SIGN(X(1)) into account ensures the continuity of G with respect to X except at 0.
|
||||
c = np.sign(x[0])
|
||||
s = 0
|
||||
elif (abs(x[0]) <= EPS * abs(x[1])):
|
||||
# N.B.: SIGN(A, X) = ABS(A) * sign of X /= A * sign of X # Therefore, it is WRONG to define G
|
||||
# as SIGN(RESHAPE([ZERO, -ONE, ONE, ZERO], [2, 2]), X(2)). This mistake was committed on
|
||||
# 20211206 and took a whole day to debug! NEVER use SIGN on arrays unless you are really sure.
|
||||
c = 0
|
||||
s = np.sign(x[1])
|
||||
else:
|
||||
# Here is the normal case. It implements the Givens rotation in a stable & continuous way as in:
|
||||
# Bindel, D., Demmel, J., Kahan, W., and Marques, O. (2002). On computing Givens rotations
|
||||
# reliably and efficiently. ACM Transactions on Mathematical Software (TOMS), 28(2), 206-238.
|
||||
# N.B.: 1. Modern compilers compute SQRT(REALMIN) and SQRT(REALMAX/2.1) at compilation time.
|
||||
# 2. The direct calculation without involving T and U seems to work better; use it if possible.
|
||||
if (all(np.logical_and(np.sqrt(REALMIN) < np.abs(x), np.abs(x) < np.sqrt(REALMAX / 2.1)))):
|
||||
# Do NOT use HYPOTENUSE here; the best implementation for one may be suboptimal for the other
|
||||
r = norm(x)
|
||||
c = x[0] / r
|
||||
s = x[1] / r
|
||||
elif (abs(x[0]) > abs(x[1])):
|
||||
t = x[1] / x[0]
|
||||
u = max(1, abs(t), np.sqrt(1 + t*t)) # MAXVAL: precaution against rounding error.
|
||||
u *= np.sign(x[0]) ##MATLAB: u = sign(x(1))*sqrt(1 + t**2)
|
||||
c = 1 / u
|
||||
s = t / u
|
||||
else:
|
||||
t = x[0] / x[1]
|
||||
u = max([1, abs(t), np.sqrt(1 + t*t)]) # MAXVAL: precaution against rounding error.
|
||||
u *= np.sign(x[1]) ##MATLAB: u = sign(x(2))*sqrt(1 + t**2)
|
||||
c = t / u
|
||||
s = 1 / u
|
||||
|
||||
G = np.array([[c, s], [-s, c]]) # MATLAB: G = [c, s; -s, c]
|
||||
|
||||
#====================#
|
||||
# Calculation ends #
|
||||
#====================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
assert G.shape == (2,2)
|
||||
assert np.all(np.isfinite(G))
|
||||
assert abs(G[0, 0] - G[1, 1]) + abs(G[0, 1] + G[1, 0]) <= 0
|
||||
tol = np.maximum(1.0E-10, np.minimum(1.0E-1, 1.0E6 * EPS))
|
||||
assert isorth(G, tol)
|
||||
if all(np.logical_and(np.isfinite(x), np.abs(x) < np.sqrt(REALMAX / 2.1))):
|
||||
r = np.linalg.norm(x)
|
||||
assert max(abs(G@x - [r, 0])) <= max(tol, tol * r), 'G @ X = [||X||, 0]'
|
||||
|
||||
return G
|
||||
|
||||
|
||||
def isminor(x, ref):
|
||||
'''
|
||||
This function tests whether x is minor compared to ref. It is used by Powell, e.g., in COBYLA.
|
||||
In precise arithmetic, isminor(x, ref) is true if and only if x == 0; in floating point
|
||||
arithmetic, isminor(x, ref) is true if x is 0 or its nonzero value can be attributed to
|
||||
computer rounding errors according to ref.
|
||||
Larger sensitivity means the function is more strict/precise, the value 0.1 being due to Powell.
|
||||
|
||||
For example:
|
||||
isminor(1e-20, 1e300) -> True, because in floating point arithmetic 1e-20 cannot be added to
|
||||
1e300 without being rounded to 1e300.
|
||||
isminor(1e300, 1e-20) -> False, because in floating point arithmetic adding 1e300 to 1e-20
|
||||
dominates the latter number.
|
||||
isminor(3, 4) -> False, because 3 can be added to 4 without being rounded off
|
||||
'''
|
||||
|
||||
sensitivity = 0.1
|
||||
refa = abs(ref) + sensitivity * abs(x)
|
||||
refb = abs(ref) + 2 * sensitivity * abs(x)
|
||||
return np.logical_or(abs(ref) >= refa, refa >= refb)
|
||||
|
||||
|
||||
def isinv(A, B, tol=None):
|
||||
'''
|
||||
This procedure tests whether A = B^{-1} up to the tolerance TOL.
|
||||
'''
|
||||
|
||||
# Sizes
|
||||
n = np.size(A, 0)
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert np.size(A, 0) == np.size(A, 1)
|
||||
assert np.size(B, 0) == np.size(B, 1)
|
||||
assert np.size(A, 0) == np.size(B, 0)
|
||||
if present(tol):
|
||||
assert tol >= 0
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
tol = tol if present(tol) else np.minimum(1e-3, 1e2 * EPS * np.maximum(np.size(A, 0), np.size(A, 1)))
|
||||
tol = np.max([tol, tol * np.max(abs(A)), tol * np.max(abs(B))])
|
||||
is_inv = ((abs(matprod(A, B)) - np.eye(n)) <= tol).all() or ((abs(matprod(B, A) - np.eye(n))) <= tol).all()
|
||||
|
||||
#===================#
|
||||
# Calculation ends #
|
||||
#===================#
|
||||
return is_inv
|
||||
|
||||
|
||||
def isorth(A, tol=None):
|
||||
'''
|
||||
This function tests whether the matrix A has orthonormal columns up to the tolerance TOL.
|
||||
'''
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
if present(tol):
|
||||
assert tol >= 0
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
num_vars = np.size(A, 1)
|
||||
|
||||
if num_vars > np.size(A, 0):
|
||||
is_orth = False
|
||||
elif (np.isnan(primasum(abs(A)))):
|
||||
is_orth = False
|
||||
else:
|
||||
if present(tol):
|
||||
is_orth = (abs(matprod(A.T, A) - np.eye(num_vars)) <= np.maximum(tol, tol * np.max(abs(A)))).all()
|
||||
else:
|
||||
is_orth = (abs(matprod(A.T, A) - np.eye(num_vars)) <= 0).all()
|
||||
|
||||
#====================#
|
||||
# Calculation ends #
|
||||
#====================#
|
||||
return is_orth
|
||||
|
||||
|
||||
def get_arrays_tol(*arrays):
|
||||
"""
|
||||
Get a relative tolerance for a set of arrays. Borrowed from COBYQA
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*arrays: tuple
|
||||
Set of `numpy.ndarray` to get the tolerance for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Relative tolerance for the set of arrays.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If no array is provided.
|
||||
"""
|
||||
if len(arrays) == 0:
|
||||
raise ValueError("At least one array must be provided.")
|
||||
size = max(array.size for array in arrays)
|
||||
weight = max(
|
||||
np.max(np.abs(array[np.isfinite(array)]), initial=1.0)
|
||||
for array in arrays
|
||||
)
|
||||
return 10.0 * EPS * max(size, 1.0) * weight
|
||||
@@ -0,0 +1,290 @@
|
||||
'''
|
||||
This module provides some functions that print messages to terminal/files.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
|
||||
N.B.:
|
||||
1. In case parallelism is desirable (especially during initialization), the functions may
|
||||
have to be modified or disabled due to the IO operations.
|
||||
2. IPRINT indicates the level of verbosity, which increases with the absolute value of IPRINT.
|
||||
IPRINT = +/-3 can be expensive due to high IO operations.
|
||||
'''
|
||||
|
||||
from .consts import DEBUGGING
|
||||
from .infos import FTARGET_ACHIEVED, MAXFUN_REACHED, MAXTR_REACHED, \
|
||||
SMALL_TR_RADIUS, TRSUBP_FAILED, NAN_INF_F, NAN_INF_X, NAN_INF_MODEL, DAMAGING_ROUNDING, \
|
||||
NO_SPACE_BETWEEN_BOUNDS, ZERO_LINEAR_CONSTRAINT, CALLBACK_TERMINATE
|
||||
from .present import present
|
||||
import numpy as np
|
||||
|
||||
spaces = ' '
|
||||
|
||||
|
||||
def get_info_string(solver, info):
|
||||
if info == FTARGET_ACHIEVED:
|
||||
reason = 'the target function value is achieved.'
|
||||
elif info == MAXFUN_REACHED:
|
||||
reason = 'the objective function has been evaluated MAXFUN times.'
|
||||
elif info == MAXTR_REACHED:
|
||||
reason = 'the maximal number of trust region iterations has been reached.'
|
||||
elif info == SMALL_TR_RADIUS:
|
||||
reason = 'the trust region radius reaches its lower bound.'
|
||||
elif info == TRSUBP_FAILED:
|
||||
reason = 'a trust region step has failed to reduce the quadratic model.'
|
||||
elif info == NAN_INF_X:
|
||||
reason = 'NaN or Inf occurs in x.'
|
||||
elif info == NAN_INF_F:
|
||||
reason = 'the objective function returns NaN/+Inf.'
|
||||
elif info == NAN_INF_MODEL:
|
||||
reason = 'NaN or Inf occurs in the models.'
|
||||
elif info == DAMAGING_ROUNDING:
|
||||
reason = 'rounding errors are becoming damaging.'
|
||||
elif info == NO_SPACE_BETWEEN_BOUNDS:
|
||||
reason = 'there is no space between the lower and upper bounds of variable.'
|
||||
elif info == ZERO_LINEAR_CONSTRAINT:
|
||||
reason = 'one of the linear constraints has a zero gradient'
|
||||
elif info == CALLBACK_TERMINATE:
|
||||
reason = 'the callback function requested termination'
|
||||
else:
|
||||
reason = 'UNKNOWN EXIT FLAG'
|
||||
ret_message = f'Return from {solver} because {reason.strip()}'
|
||||
return ret_message
|
||||
|
||||
|
||||
def retmsg(solver, info, iprint, nf, f, x, cstrv=None, constr=None):
|
||||
'''
|
||||
This function prints messages at return.
|
||||
'''
|
||||
# Local variables
|
||||
valid_exit_codes = [FTARGET_ACHIEVED, MAXFUN_REACHED, MAXTR_REACHED,
|
||||
SMALL_TR_RADIUS, TRSUBP_FAILED, NAN_INF_F, NAN_INF_X, NAN_INF_MODEL, DAMAGING_ROUNDING,
|
||||
NO_SPACE_BETWEEN_BOUNDS, ZERO_LINEAR_CONSTRAINT, CALLBACK_TERMINATE]
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert info in valid_exit_codes
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
if abs(iprint) < 1: # No printing (iprint == 0)
|
||||
return
|
||||
elif iprint > 0: # Print the message to the standard out.
|
||||
fname = ''
|
||||
else: # Print the message to a file named FNAME.
|
||||
fname = f'{solver}_output.txt'
|
||||
|
||||
# Decide whether the problem is truly constrained.
|
||||
if present(constr):
|
||||
is_constrained = (np.size(constr) > 0)
|
||||
else:
|
||||
is_constrained = present(cstrv)
|
||||
|
||||
# Decide the constraint violation.
|
||||
if present(cstrv):
|
||||
cstrv_loc = cstrv
|
||||
elif present(constr):
|
||||
cstrv_loc = np.max(np.append(0, -constr)) # N.B.: We assume that the constraint is CONSTR >= 0.
|
||||
else:
|
||||
cstrv_loc = 0
|
||||
|
||||
# Decide the return message.
|
||||
ret_message = get_info_string(solver, info)
|
||||
|
||||
if np.size(x) <= 2:
|
||||
x_message = f'\nThe corresponding X is: {x}' # Printed in one line
|
||||
else:
|
||||
x_message = f'\nThe corresponding X is:\n{x}'
|
||||
|
||||
if is_constrained:
|
||||
nf_message = (f'\nNumber of function values = {nf}{spaces}'
|
||||
f'Least value of F = {f}{spaces}Constraint violation = {cstrv_loc}')
|
||||
else:
|
||||
nf_message = f'\nNumber of function values = {nf}{spaces}Least value of F = {f}'
|
||||
|
||||
if is_constrained and present(constr):
|
||||
if np.size(constr) <= 2:
|
||||
constr_message = f'\nThe constraint value is: {constr}' # Printed in one line
|
||||
else:
|
||||
constr_message = f'\nThe constraint value is:\n{constr}'
|
||||
else:
|
||||
constr_message = ''
|
||||
|
||||
# Print the message.
|
||||
if abs(iprint) >= 2:
|
||||
message = f'\n{ret_message}{nf_message}{x_message}{constr_message}\n'
|
||||
else:
|
||||
message = f'{ret_message}{nf_message}{x_message}{constr_message}\n'
|
||||
if len(fname) > 0:
|
||||
with open(fname, 'a') as f: f.write(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
|
||||
def rhomsg(solver, iprint, nf, delta, f, rho, x, cstrv=None, constr=None, cpen=None):
|
||||
'''
|
||||
This function prints messages when RHO is updated.
|
||||
'''
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
if abs(iprint) < 2: # No printing
|
||||
return
|
||||
elif iprint > 0: # Print the message to the standard out.
|
||||
fname = ''
|
||||
else: # Print the message to a file named FNAME.
|
||||
fname = f'{solver.strip()}_output.txt'
|
||||
|
||||
# Decide whether the problem is truly constrained.
|
||||
if present(constr):
|
||||
is_constrained = (np.size(constr) > 0)
|
||||
else:
|
||||
is_constrained = present(cstrv)
|
||||
|
||||
# Decide the constraint violation.
|
||||
if present(cstrv):
|
||||
cstrv_loc = cstrv
|
||||
elif present(constr):
|
||||
cstrv_loc = np.max(np.append(0, -constr)) # N.B.: We assume that the constraint is CONSTR >= 0.
|
||||
else:
|
||||
cstrv_loc = 0
|
||||
|
||||
if present(cpen):
|
||||
rho_message = (f'\nNew RHO = {rho}{spaces}Delta = {delta}{spaces}'
|
||||
f'CPEN = {cpen}')
|
||||
else:
|
||||
rho_message = f'\nNew RHO = {rho}{spaces}Delta = {delta}'
|
||||
|
||||
if np.size(x) <= 2:
|
||||
x_message = f'\nThe corresponding X is: {x}' # Printed in one line
|
||||
else:
|
||||
x_message = f'\nThe corresponding X is:\n{x}'
|
||||
|
||||
if is_constrained:
|
||||
nf_message = (f'\nNumber of function values = {nf}{spaces}'
|
||||
f'Least value of F = {f}{spaces}Constraint violation = {cstrv_loc}')
|
||||
else:
|
||||
nf_message = f'\nNumber of function values = {nf}{spaces}Least value of F = {f}'
|
||||
|
||||
if is_constrained and present(constr):
|
||||
if np.size(constr) <= 2:
|
||||
constr_message = f'\nThe constraint value is: {constr}' # Printed in one line
|
||||
else:
|
||||
constr_message = f'\nThe constraint value is:\n{constr}'
|
||||
else:
|
||||
constr_message = ''
|
||||
|
||||
# Print the message.
|
||||
if abs(iprint) >= 3:
|
||||
message = f'\n{rho_message}{nf_message}{x_message}{constr_message}'
|
||||
else:
|
||||
message = f'{rho_message}{nf_message}{x_message}{constr_message}'
|
||||
if len(fname) > 0:
|
||||
with open(fname, 'a') as f: f.write(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
#====================#
|
||||
# Calculation ends #
|
||||
#====================#
|
||||
|
||||
|
||||
def cpenmsg(solver, iprint, cpen):
|
||||
'''
|
||||
This function prints a message when CPEN is updated.
|
||||
'''
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
if abs(iprint) < 2: # No printing
|
||||
return
|
||||
elif iprint > 0: # Print the message to the standard out.
|
||||
fname = ''
|
||||
else: # Print the message to a file named FNAME.
|
||||
fname = f'{solver.strip()}_output.txt'
|
||||
|
||||
# Print the message.
|
||||
if abs(iprint) >= 3:
|
||||
message = f'\nSet CPEN to {cpen}'
|
||||
else:
|
||||
message = f'\n\nSet CPEN to {cpen}'
|
||||
if len(fname) > 0:
|
||||
with open(fname, 'a') as f: f.write(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
#====================#
|
||||
# Calculation ends #
|
||||
#====================#
|
||||
|
||||
|
||||
def fmsg(solver, state, iprint, nf, delta, f, x, cstrv=None, constr=None):
|
||||
'''
|
||||
This subroutine prints messages for each evaluation of the objective function.
|
||||
'''
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
if abs(iprint) < 2: # No printing
|
||||
return
|
||||
elif iprint > 0: # Print the message to the standard out.
|
||||
fname = ''
|
||||
else: # Print the message to a file named FNAME.
|
||||
fname = f'{solver.strip()}_output.txt'
|
||||
|
||||
# Decide whether the problem is truly constrained.
|
||||
if present(constr):
|
||||
is_constrained = (np.size(constr) > 0)
|
||||
else:
|
||||
is_constrained = present(cstrv)
|
||||
|
||||
# Decide the constraint violation.
|
||||
if present(cstrv):
|
||||
cstrv_loc = cstrv
|
||||
elif present(constr):
|
||||
cstrv_loc = np.max(np.append(0, -constr)) # N.B.: We assume that the constraint is CONSTR >= 0.
|
||||
else:
|
||||
cstrv_loc = 0
|
||||
|
||||
delta_message = f'\n{state} step with radius = {delta}'
|
||||
|
||||
if is_constrained:
|
||||
nf_message = (f'\nNumber of function values = {nf}{spaces}'
|
||||
f'Least value of F = {f}{spaces}Constraint violation = {cstrv_loc}')
|
||||
else:
|
||||
nf_message = f'\nNumber of function values = {nf}{spaces}Least value of F = {f}'
|
||||
|
||||
if np.size(x) <= 2:
|
||||
x_message = f'\nThe corresponding X is: {x}' # Printed in one line
|
||||
else:
|
||||
x_message = f'\nThe corresponding X is:\n{x}'
|
||||
|
||||
if is_constrained and present(constr):
|
||||
if np.size(constr) <= 2:
|
||||
constr_message = f'\nThe constraint value is: {constr}' # Printed in one line
|
||||
else:
|
||||
constr_message = f'\nThe constraint value is:\n{constr}'
|
||||
else:
|
||||
constr_message = ''
|
||||
|
||||
# Print the message.
|
||||
message = f'{delta_message}{nf_message}{x_message}{constr_message}'
|
||||
if len(fname) > 0:
|
||||
with open(fname, 'a') as f: f.write(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
#====================#
|
||||
# Calculation ends #
|
||||
#====================#
|
||||
@@ -0,0 +1,131 @@
|
||||
'''
|
||||
This module provides some Powell-style linear algebra procedures.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
from .linalg import isminor, planerot, matprod, inprod, hypot
|
||||
from .consts import DEBUGGING, EPS
|
||||
|
||||
|
||||
def qradd_Rdiag(c, Q, Rdiag, n):
|
||||
'''
|
||||
This function updates the QR factorization of an MxN matrix A of full column rank, attempting to
|
||||
add a new column C to this matrix as the LAST column while maintaining the full-rankness.
|
||||
Case 1. If C is not in range(A) (theoretically, it implies N < M), then the new matrix is np.hstack([A, C])
|
||||
Case 2. If C is in range(A), then the new matrix is np.hstack([A[:, :n-1], C])
|
||||
N.B.:
|
||||
0. Instead of R, this subroutine updates Rdiag, which is np.diag(R), with a size at most M and at
|
||||
least min(m, n+1). The number is min(m, n+1) rather than min(m, n) as n may be augmented by 1 in
|
||||
the function.
|
||||
1. With the two cases specified as above, this function does not need A as an input.
|
||||
2. The function changes only Q[:, nsave:m] (nsave is the original value of n) and
|
||||
R[:, n-1] (n takes the updated value)
|
||||
3. Indeed, when C is in range(A), Powell wrote in comments that "set iOUT to the index of the
|
||||
constraint (here, column of A --- Zaikun) to be deleted, but branch if no suitable index can be
|
||||
found". The idea is to replace a column of A by C so that the new matrix still has full rank
|
||||
(such a column must exist unless C = 0). But his code essentially sets iout=n always. Maybe he
|
||||
found this worked well enough in practice. Meanwhile, Powell's code includes a snippet that can
|
||||
never be reached, which was probably intended to deal with the case that IOUT != n
|
||||
'''
|
||||
m = Q.shape[1]
|
||||
nsave = n # Needed for debugging (only)
|
||||
|
||||
# As in Powell's COBYLA, CQ is set to 0 at the positions with CQ being negligible as per ISMINOR.
|
||||
# This may not be the best choice if the subroutine is used in other contexts, e.g. LINCOA.
|
||||
cq = matprod(c, Q)
|
||||
cqa = matprod(abs(c), abs(Q))
|
||||
# The line below basically makes an element of cq 0 if adding it to the corresponding element of
|
||||
# cqa does not change the latter.
|
||||
cq = np.array([0 if isminor(cqi, cqai) else cqi for cqi, cqai in zip(cq, cqa)])
|
||||
|
||||
# Update Q so that the columns of Q[:, n+1:m] are orthogonal to C. This is done by applying a 2D
|
||||
# Givens rotation to Q[:, [k, k+1]] from the right to zero C' @ Q[:, k+1] out for K=n+1, ... m-1.
|
||||
# Nothing will be done if n >= m-1
|
||||
for k in range(m-2, n-1, -1):
|
||||
if abs(cq[k+1]) > 0:
|
||||
# Powell wrote cq[k+1] != 0 instead of abs. The two differ if cq[k+1] is NaN.
|
||||
# If we apply the rotation below when cq[k+1] = 0, then cq[k] will get updated to |cq[k]|.
|
||||
G = planerot(cq[k:k+2])
|
||||
Q[:, [k, k+1]] = matprod(Q[:, [k, k+1]], G.T)
|
||||
cq[k] = hypot(*cq[k:k+2])
|
||||
|
||||
# Augment n by 1 if C is not in range(A)
|
||||
if n < m:
|
||||
# Powell's condition for the following if: cq[n+1] != 0
|
||||
if abs(cq[n]) > EPS**2 and not isminor(cq[n], cqa[n]):
|
||||
n += 1
|
||||
|
||||
# Update Rdiag so that Rdiag[n] = cq[n] = np.dot(c, q[:, n]). Note that N may be been augmented.
|
||||
if n - 1 >= 0 and n - 1 < m: # n >= m should not happen unless the input is wrong
|
||||
Rdiag[n - 1] = cq[n - 1]
|
||||
|
||||
if DEBUGGING:
|
||||
assert nsave <= n <= min(nsave + 1, m)
|
||||
assert n <= len(Rdiag) <= m
|
||||
assert Q.shape == (m, m)
|
||||
|
||||
return Q, Rdiag, n
|
||||
|
||||
|
||||
def qrexc_Rdiag(A, Q, Rdiag, i): # Used in COBYLA
|
||||
'''
|
||||
This function updates the QR factorization for an MxN matrix A=Q@R so that the updated Q and
|
||||
R form a QR factorization of [A_0, ..., A_{I-1}, A_{I+1}, ..., A_{N-1}, A_I] which is the matrix
|
||||
obtained by rearranging columns [I, I+1, ... N-1] of A to [I+1, ..., N-1, I]. Here A is ASSUMED TO
|
||||
BE OF FULL COLUMN RANK, Q is a matrix whose columns are orthogonal, and R, which is not present,
|
||||
is an upper triangular matrix whose diagonal entries are nonzero. Q and R need not be square.
|
||||
N.B.:
|
||||
0. Instead of R, this function updates Rdiag, which is np.diag(R), the size being n.
|
||||
1. With L = Q.shape[1] = R.shape[0], we have M >= L >= N. Most often L = M or N.
|
||||
2. This function changes only Q[:, i:] and Rdiag[i:]
|
||||
3. (NDB 20230919) In Python, i is either icon or nact - 2, whereas in FORTRAN it is either icon or nact - 1.
|
||||
'''
|
||||
|
||||
# Sizes
|
||||
m, n = A.shape
|
||||
|
||||
# Preconditions
|
||||
assert n >= 1 and n <= m
|
||||
assert i >= 0 and i < n
|
||||
assert len(Rdiag) == n
|
||||
assert Q.shape[0] == m and Q.shape[1] >= n and Q.shape[1] <= m
|
||||
# tol = max(1.0E-8, min(1.0E-1, 1.0E8 * EPS * m + 1))
|
||||
# assert isorth(Q, tol) # Costly!
|
||||
|
||||
|
||||
if i < 0 or i >= n:
|
||||
return Q, Rdiag
|
||||
|
||||
# Let R be the upper triangular matrix in the QR factorization, namely R = Q.T@A.
|
||||
# For each k, find the Givens rotation G with G@(R[k:k+2, :]) = [hypt, 0], and update Q[:, k:k+2]
|
||||
# to Q[:, k:k+2]@(G.T). Then R = Q.T@A is an upper triangular matrix as long as A[:, [k, k+1]] is
|
||||
# updated to A[:, [k+1, k]]. Indeed, this new upper triangular matrix can be obtained by first
|
||||
# updating R[[k, k+1], :] to G@(R[[k, k+1], :]) and then exchanging its columns K and K+1; at the same
|
||||
# time, entries k and k+1 of R's diagonal Rdiag become [hypt, -(Rdiag[k+1]/hypt)*RDiag[k]].
|
||||
# After this is done for each k = 0, ..., n-2, we obtain the QR factorization of the matrix that
|
||||
# rearranges columns [i, i+1, ... n-1] of A as [i+1, ..., n-1, i].
|
||||
# Powell's code, however, is slightly different: before everything, he first exchanged columns k and
|
||||
# k+1 of Q (as well as rows k and k+1 of R). This makes sure that the entries of the update Rdiag
|
||||
# are all positive if it is the case for the original Rdiag.
|
||||
for k in range(i, n-1):
|
||||
G = planerot([Rdiag[k+1], inprod(Q[:, k], A[:, k+1])])
|
||||
Q[:, [k, k+1]] = matprod(Q[:, [k+1, k]], (G.T))
|
||||
# Powell's code updates Rdiag in the following way:
|
||||
# hypt = np.sqrt(Rdiag[k+1]**2 + np.dot(Q[:, k], A[:, k+1])**2)
|
||||
# Rdiag[[k, k+1]] = [hypt, (Rdiag[k+1]/hypt)*Rdiag[k]]
|
||||
# Note that Rdiag[n-1] inherits all rounding in Rdiag[i:n-1] and Q[:, i:n-1] and hence contains
|
||||
# significant errors. Thus we may modify Powell's code to set only Rdiag[k] = hypt here and then
|
||||
# calculate Rdiag[n] by an inner product after the loop. Nevertheless, we simple calculate RDiag
|
||||
# from scratch below.
|
||||
|
||||
# Calculate Rdiag(i:n) from scratch
|
||||
Rdiag[i:n-1] = [inprod(Q[:, k], A[:, k+1]) for k in range(i, n-1)]
|
||||
Rdiag[n-1] = inprod(Q[:, n-1], A[:, i])
|
||||
|
||||
return Q, Rdiag
|
||||
@@ -0,0 +1,277 @@
|
||||
'''
|
||||
This is a module that preprocesses the inputs.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
from .consts import DEBUGGING, EPS, IPRINT_DEFAULT, FTARGET_DEFAULT, \
|
||||
MIN_MAXFILT, MAXFILT_DEFAULT, MAXHISTMEM, ETA1_DEFAULT, ETA2_DEFAULT, \
|
||||
GAMMA1_DEFAULT, GAMMA2_DEFAULT, RHOBEG_DEFAULT, RHOEND_DEFAULT, \
|
||||
CTOL_DEFAULT, CWEIGHT_DEFAULT
|
||||
from .present import present
|
||||
from warnings import warn
|
||||
import numpy as np
|
||||
|
||||
|
||||
def preproc(solver, num_vars, iprint, maxfun, maxhist, ftarget, rhobeg, rhoend,
|
||||
num_constraints=None, npt=None, maxfilt=None, ctol=None, cweight=None,
|
||||
eta1=None, eta2=None, gamma1=None, gamma2=None, is_constrained=None, has_rhobeg=None,
|
||||
honour_x0=None, xl=None, xu=None, x0=None):
|
||||
'''
|
||||
This subroutine preprocesses the inputs. It does nothing to the inputs that are valid.
|
||||
'''
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert num_vars >= 1
|
||||
if present(num_constraints):
|
||||
assert num_constraints >= 0
|
||||
assert num_constraints == 0 or solver.lower() == 'cobyla'
|
||||
if solver.lower() == 'cobyla' and present(num_constraints) and present(is_constrained):
|
||||
assert num_constraints == 0 or is_constrained
|
||||
if solver.lower() == 'bobyqa':
|
||||
assert present(xl) and present(xu)
|
||||
assert all(xu - xl >= 2 * EPS)
|
||||
if present(honour_x0):
|
||||
assert solver.lower() == 'bobyqa' and present(has_rhobeg) and present(xl) and present(xu) and present(x0)
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
# Read num_constraints, if necessary
|
||||
num_constraints = num_constraints if (present(num_constraints) and solver.lower() == 'cobyla') else 0
|
||||
|
||||
# Decide whether the problem is truly constrained
|
||||
is_constrained = is_constrained if (present(is_constrained)) else num_constraints > 0
|
||||
|
||||
# Validate IPRINT
|
||||
if np.abs(iprint) > 3:
|
||||
iprint = IPRINT_DEFAULT
|
||||
warn(f'{solver}: Invalid IPRINT; it should be 0, 1, -1, 2, -2, 3, or -3; it is set to {iprint}')
|
||||
|
||||
# Validate MAXFUN
|
||||
if solver.lower() == 'uobyqa':
|
||||
min_maxfun = (num_vars + 1) * (num_vars + 2) / 2 + 1
|
||||
min_maxfun_str = '(N+1)(N+2)/2 + 1'
|
||||
elif solver.lower() == 'cobyla':
|
||||
min_maxfun = num_vars + 2
|
||||
min_maxfun_str = 'num_vars + 2'
|
||||
else: # CASE ('NEWUOA', 'BOBYQA', 'LINCOA')
|
||||
min_maxfun = num_vars + 3
|
||||
min_maxfun_str = 'num_vars + 3'
|
||||
if maxfun < min_maxfun:
|
||||
maxfun = min_maxfun
|
||||
warn(f'{solver}: Invalid MAXFUN; it should be at least {min_maxfun_str}; it is set to {maxfun}')
|
||||
|
||||
# Validate MAXHIST
|
||||
if maxhist < 0:
|
||||
maxhist = maxfun
|
||||
warn(f'{solver}: Invalid MAXHIST; it should be a nonnegative integer; it is set to {maxhist}')
|
||||
maxhist = min(maxhist, maxfun) # MAXHIST > MAXFUN is never needed.
|
||||
|
||||
# Validate FTARGET
|
||||
if np.isnan(ftarget):
|
||||
ftarget = FTARGET_DEFAULT
|
||||
warn(f'{solver}: Invalid FTARGET; it should be a real number; it is set to {ftarget}')
|
||||
|
||||
# Validate NPT
|
||||
if (solver.lower() == 'newuoa' or solver.lower() == 'bobyqa' or solver.lower() == 'lincoa') and present(npt):
|
||||
if (npt < num_vars + 2 or npt > min(maxfun - 1, ((num_vars + 2) * (num_vars + 1)) / 2)):
|
||||
npt = int(min(maxfun - 1, 2 * num_vars + 1))
|
||||
warn(f'{solver}: Invalid NPT; it should be an integer in the interval [N+2, (N+1)(N+2)/2] and less than MAXFUN; it is set to {npt}')
|
||||
|
||||
# Validate MAXFILT
|
||||
if present(maxfilt) and (solver.lower() == 'lincoa' or solver.lower() == 'cobyla'):
|
||||
maxfilt_in = maxfilt
|
||||
if maxfilt < 1:
|
||||
maxfilt = MAXFILT_DEFAULT # The inputted MAXFILT is obviously wrong.
|
||||
else:
|
||||
maxfilt = max(MIN_MAXFILT, maxfilt) # The inputted MAXFILT is too small.
|
||||
# Further revise MAXFILT according to MAXHISTMEM.
|
||||
if solver.lower() == 'lincoa':
|
||||
unit_memo = (num_vars + 2) * np.dtype(float).itemsize
|
||||
elif solver.lower() == 'cobyla':
|
||||
unit_memo = (num_constraints + num_vars + 2) * np.dtype(float).itemsize
|
||||
else:
|
||||
unit_memo = 1
|
||||
# We cannot simply set MAXFILT = MIN(MAXFILT, MAXHISTMEM/...), as they may not have
|
||||
# the same kind, and compilers may complain. We may convert them, but overflow may occur.
|
||||
if maxfilt > MAXHISTMEM / unit_memo:
|
||||
maxfilt = int(MAXHISTMEM / unit_memo) # Integer division.
|
||||
maxfilt = min(maxfun, max(MIN_MAXFILT, maxfilt))
|
||||
if is_constrained:
|
||||
if maxfilt_in < 1:
|
||||
warn(f'{solver}: Invalid MAXFILT; it should be a positive integer; it is set to {maxfilt}')
|
||||
elif maxfilt_in < min(maxfun, MIN_MAXFILT):
|
||||
warn(f'{solver}: MAXFILT is too small; it is set to {maxfilt}')
|
||||
elif maxfilt < min(maxfilt_in, maxfun):
|
||||
warn(f'{solver}: MAXFILT is set to {maxfilt} due to memory limit')
|
||||
|
||||
# Validate ETA1 and ETA2
|
||||
eta1_local = eta1 if present(eta1) else ETA1_DEFAULT
|
||||
eta2_local = eta2 if present(eta2) else ETA2_DEFAULT
|
||||
|
||||
# When the difference between ETA1 and ETA2 is tiny, we force them to equal.
|
||||
# See the explanation around RHOBEG and RHOEND for the reason.
|
||||
if present(eta1) and present(eta2):
|
||||
if np.abs(eta1 - eta2) < 1.0E2 * EPS * max(np.abs(eta1), 1):
|
||||
eta2 = eta1
|
||||
|
||||
if present(eta1):
|
||||
if np.isnan(eta1):
|
||||
# In this case, we take the value hard coded in Powell's original code
|
||||
# without any warning. It is useful when interfacing with MATLAB/Python.
|
||||
eta1 = ETA1_DEFAULT
|
||||
elif eta1 < 0 or eta1 >= 1:
|
||||
# Take ETA2 into account if it has a valid value.
|
||||
if present(eta2) and eta2_local > 0 and eta2_local <= 1:
|
||||
eta1 = max(EPS, eta2 / 7.0)
|
||||
else:
|
||||
eta1 = ETA1_DEFAULT
|
||||
warn(f'{solver}: Invalid ETA1; it should be in the interval [0, 1) and not more than ETA2; it is set to {eta1}')
|
||||
|
||||
if present(eta2):
|
||||
if np.isnan(eta2):
|
||||
# In this case, we take the value hard coded in Powell's original code
|
||||
# without any warning. It is useful when interfacing with MATLAB/Python.
|
||||
eta2 = ETA2_DEFAULT
|
||||
elif present(eta1) and (eta2 < eta1_local or eta2 > 1):
|
||||
eta2 = (eta1 + 2) / 3.0
|
||||
warn(f'{solver}: Invalid ETA2; it should be in the interval [0, 1) and not less than ETA1; it is set to {eta2}')
|
||||
|
||||
# Validate GAMMA1 and GAMMA2
|
||||
if present(gamma1):
|
||||
if np.isnan(gamma1):
|
||||
# In this case, we take the value hard coded in Powell's original code
|
||||
# without any warning. It is useful when interfacing with MATLAB/Python.
|
||||
gamma1 = GAMMA1_DEFAULT
|
||||
elif gamma1 <= 0 or gamma1 >= 1:
|
||||
gamma1 = GAMMA1_DEFAULT
|
||||
warn(f'{solver}: Invalid GAMMA1; it should in the interval (0, 1); it is set to {gamma1}')
|
||||
|
||||
if present(gamma2):
|
||||
if np.isnan(gamma2):
|
||||
# In this case, we take the value hard coded in Powell's original code
|
||||
# without any warning. It is useful when interfacing with MATLAB/Python.
|
||||
gamma2 = GAMMA2_DEFAULT
|
||||
elif gamma2 < 1 or np.isinf(gamma2):
|
||||
gamma2 = GAMMA2_DEFAULT
|
||||
warn(f'{solver}: Invalid GAMMA2; it should be a real number not less than 1; it is set to {gamma2}')
|
||||
|
||||
# Validate RHOBEG and RHOEND
|
||||
|
||||
if np.abs(rhobeg - rhoend) < 1.0e2 * EPS * np.maximum(np.abs(rhobeg), 1):
|
||||
# When the data is passed from the interfaces (e.g., MEX) to the Fortran code, RHOBEG, and RHOEND
|
||||
# may change a bit. It was observed in a MATLAB test that MEX passed 1 to Fortran as
|
||||
# 0.99999999999999978. Therefore, if we set RHOEND = RHOBEG in the interfaces, then it may happen
|
||||
# that RHOEND > RHOBEG, which is considered as an invalid input. To avoid this situation, we
|
||||
# force RHOBEG and RHOEND to equal when the difference is tiny.
|
||||
rhoend = rhobeg
|
||||
|
||||
# Revise the default values for RHOBEG/RHOEND according to the solver.
|
||||
if solver.lower() == 'bobyqa':
|
||||
rhobeg_default = np.maximum(EPS, np.min(RHOBEG_DEFAULT, np.min(xu - xl) / 4.0))
|
||||
rhoend_default = np.maximum(EPS, np.min(0.1 * rhobeg_default, RHOEND_DEFAULT))
|
||||
else:
|
||||
rhobeg_default = RHOBEG_DEFAULT
|
||||
rhoend_default = RHOEND_DEFAULT
|
||||
|
||||
if solver.lower() == 'bobyqa':
|
||||
# Do NOT merge the IF below into the ELIF above! Otherwise, XU and XL may be accessed even if
|
||||
# the solver is not BOBYQA, because the logical evaluation is not short-circuit.
|
||||
if rhobeg > np.min(xu - xl) / 2:
|
||||
# Do NOT make this revision if RHOBEG not positive or not finite, because otherwise RHOBEG
|
||||
# will get a huge value when XU or XL contains huge values that indicate unbounded variables.
|
||||
rhobeg = np.min(xu - xl) / 4.0 # Here, we do not take RHOBEG_DEFAULT.
|
||||
warn(f'{solver}: Invalid RHOBEG; {solver} requires 0 < RHOBEG <= np.min(XU-XL)/2; it is set to np.min(XU-XL)/4')
|
||||
if rhobeg <= 0 or np.isnan(rhobeg) or np.isinf(rhobeg):
|
||||
# Take RHOEND into account if it has a valid value. We do not do this if the solver is BOBYQA,
|
||||
# which requires that RHOBEG <= (XU-XL)/2.
|
||||
if np.isfinite(rhoend) and rhoend > 0 and solver.lower() != 'bobyqa':
|
||||
rhobeg = max(10 * rhoend, rhobeg_default)
|
||||
else:
|
||||
rhobeg = rhobeg_default
|
||||
warn(f'{solver}: Invalid RHOBEG; it should be a positive number; it is set to {rhobeg}')
|
||||
|
||||
if rhoend <= 0 or rhobeg < rhoend or np.isnan(rhoend) or np.isinf(rhoend):
|
||||
rhoend = max(EPS, min(0.1 * rhobeg, rhoend_default))
|
||||
warn(f'{solver}: Invalid RHOEND; it should be a positive number and RHOEND <= RHOBEG; it is set to {rhoend}')
|
||||
|
||||
# For BOBYQA, revise X0 or RHOBEG so that the distance between X0 and the inactive bounds is at
|
||||
# least RHOBEG. If HONOUR_X0 == TRUE, revise RHOBEG if needed; otherwise, revise HONOUR_X0 if needed.
|
||||
if present(honour_x0):
|
||||
if honour_x0:
|
||||
rhobeg_old = rhobeg;
|
||||
lbx = np.isfinite(xl) & (x0 - xl <= EPS * np.maximum(1, np.abs(xl))) # X0 essentially equals XL
|
||||
ubx = np.isfinite(xu) & (x0 - xu >= -EPS * np.maximum(1, np.abs(xu))) # X0 essentially equals XU
|
||||
x0[lbx] = xl[lbx]
|
||||
x0[ubx] = xu[ubx]
|
||||
rhobeg = max(EPS, np.min([rhobeg, x0[~lbx] - xl[~lbx], xu[~ubx] - x0[~ubx]]))
|
||||
if rhobeg_old - rhobeg > EPS * max(1, rhobeg_old):
|
||||
rhoend = max(EPS, min(0.1 * rhobeg, rhoend)) # We do not revise RHOEND unless RHOBEG is truly revised.
|
||||
if has_rhobeg:
|
||||
warn(f'{solver}: RHOBEG is revised to {rhobeg} and RHOEND to at most 0.1*RHOBEG so that the distance between X0 and the inactive bounds is at least RHOBEG')
|
||||
else:
|
||||
rhoend = np.minimum(rhoend, rhobeg) # This may update RHOEND slightly.
|
||||
else:
|
||||
x0_old = x0 # Recorded to see whether X0 is really revised.
|
||||
# N.B.: The following revision is valid only if XL <= X0 <= XU and RHOBEG <= MINVAL(XU-XL)/2,
|
||||
# which should hold at this point due to the revision of RHOBEG and moderation of X0.
|
||||
# The cases below are mutually exclusive in precise arithmetic as MINVAL(XU-XL) >= 2*RHOBEG.
|
||||
lbx = x0 <= xl + 0.5 * rhobeg
|
||||
lbx_plus = (x0 > xl + 0.5 * rhobeg) & (x0 < xl + rhobeg)
|
||||
ubx = x0 >= xu - 0.5 * rhobeg
|
||||
ubx_minus = (x0 < xu - 0.5 * rhobeg) & (x0 > xu - rhobeg)
|
||||
x0[lbx] = xl[lbx]
|
||||
x0[lbx_plus] = xl[lbx_plus] + rhobeg
|
||||
x0[ubx] = xu[ubx]
|
||||
x0[ubx_minus] = xu[ubx_minus] - rhobeg
|
||||
|
||||
if (any(np.abs(x0_old - x0) > 0)):
|
||||
warn(f'{solver}: X0 is revised so that the distance between X0 and the inactive bounds is at least RHOBEG set HONOUR_X0 to .TRUE. if you prefer to keep X0 unchanged')
|
||||
|
||||
# Validate CTOL (it can be 0)
|
||||
if (present(ctol)):
|
||||
if (np.isnan(ctol) or ctol < 0):
|
||||
ctol = CTOL_DEFAULT
|
||||
if (is_constrained):
|
||||
warn(f'{solver}: Invalid CTOL; it should be a nonnegative number; it is set to {ctol}')
|
||||
|
||||
# Validate CWEIGHT (it can be +Inf)
|
||||
if (present(cweight)):
|
||||
if (np.isnan(cweight) or cweight < 0):
|
||||
cweight = CWEIGHT_DEFAULT
|
||||
if (is_constrained):
|
||||
warn(f'{solver}: Invalid CWEIGHT; it should be a nonnegative number; it is set to {cweight}')
|
||||
|
||||
#====================#
|
||||
# Calculation ends #
|
||||
#====================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
assert abs(iprint) <= 3
|
||||
assert maxhist >= 0 and maxhist <= maxfun
|
||||
if present(npt):
|
||||
assert maxfun >= npt + 1
|
||||
assert npt >= 3
|
||||
if present(maxfilt):
|
||||
assert maxfilt >= np.minimum(MIN_MAXFILT, maxfun) and maxfilt <= maxfun
|
||||
if present(eta1) and present(eta2):
|
||||
assert eta1 >= 0 and eta1 <= eta2 and eta2 < 1
|
||||
if present(gamma1) and present(gamma2):
|
||||
assert gamma1 > 0 and gamma1 < 1 and gamma2 > 1
|
||||
assert rhobeg >= rhoend and rhoend > 0
|
||||
if solver.lower() == 'bobyqa':
|
||||
assert all(rhobeg <= (xu - xl) / 2)
|
||||
assert all(np.isfinite(x0))
|
||||
assert all(x0 >= xl and (x0 <= xl or x0 >= xl + rhobeg))
|
||||
assert all(x0 <= xu and (x0 >= xu or x0 <= xu - rhobeg))
|
||||
if present(ctol):
|
||||
assert ctol >= 0
|
||||
|
||||
return iprint, maxfun, maxhist, ftarget, rhobeg, rhoend, npt, maxfilt, ctol, cweight, eta1, eta2, gamma1, gamma2, x0
|
||||
@@ -0,0 +1,5 @@
|
||||
def present(x):
|
||||
'''
|
||||
This is a Python equivalent of the Fortran 'present' function for optional arguments.
|
||||
'''
|
||||
return x is not None
|
||||
@@ -0,0 +1,54 @@
|
||||
'''
|
||||
This module calculates the reduction ratio for trust-region methods.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
from .consts import DEBUGGING, REALMAX
|
||||
import numpy as np
|
||||
|
||||
def redrat(ared, pred, rshrink):
|
||||
'''
|
||||
This function evaluates the reduction ratio of a trust-region step, handling inf/nan properly.
|
||||
'''
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert rshrink >= 0
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
if np.isnan(ared):
|
||||
# This should not happen in unconstrained problems due to the moderated extreme barrier.
|
||||
ratio = -REALMAX
|
||||
elif np.isnan(pred) or pred <= 0:
|
||||
# The trust-region subproblem solver fails in this rare case. Instead of terminating as Powell's
|
||||
# original code does, we set ratio as follows so that the solver may continue to progress.
|
||||
if ared > 0:
|
||||
# The trial point will be accepted, but the trust-region radius will be shrunk if rshrink>0
|
||||
ratio = rshrink/2
|
||||
else:
|
||||
# Set the ration to a large negative number to signify a bad trust-region step, so that the
|
||||
# solver will check whether to take a geometry step or reduce rho.
|
||||
ratio = -REALMAX
|
||||
elif np.isposinf(pred) and np.isposinf(ared):
|
||||
ratio = 1 # ared/pred = NaN if calculated directly
|
||||
elif np.isposinf(pred) and np.isneginf(ared):
|
||||
ratio = -REALMAX # ared/pred = NaN if calculated directly
|
||||
else:
|
||||
ratio = ared/pred
|
||||
|
||||
#==================#
|
||||
# Calculation ends #
|
||||
#==================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
assert not np.isnan(ratio)
|
||||
return ratio
|
||||
@@ -0,0 +1,47 @@
|
||||
'''
|
||||
This module provides a function that calculates RHO when it needs to be reduced.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
from .consts import DEBUGGING
|
||||
import numpy as np
|
||||
|
||||
def redrho(rho_in, rhoend):
|
||||
'''
|
||||
This function calculates RHO when it needs to be reduced.
|
||||
The scheme is shared by UOBYQA, NEWUOA, BOBYQA, LINCOA. For COBYLA, Powell's code reduces RHO by
|
||||
'RHO *= 0.5; if RHO <= 1.5 * RHOEND: RHO = RHOEND' as specified in (11) of the COBYLA
|
||||
paper. However, this scheme seems to work better, especially after we introduce DELTA.
|
||||
'''
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert rho_in > rhoend > 0
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
rho_ratio = rho_in / rhoend
|
||||
|
||||
if rho_ratio > 250:
|
||||
rho = 0.1 * rho_in
|
||||
elif rho_ratio <= 16:
|
||||
rho = rhoend
|
||||
else:
|
||||
rho = np.sqrt(rho_ratio) * rhoend # rho = np.sqrt(rho * rhoend)
|
||||
|
||||
#==================#
|
||||
# Calculation ends #
|
||||
#==================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
assert rho_in > rho >= rhoend
|
||||
|
||||
return rho
|
||||
@@ -0,0 +1,296 @@
|
||||
'''
|
||||
This module provides subroutines that ensure the returned X is optimal among all the calculated
|
||||
points in the sense that no other point achieves both lower function value and lower constraint
|
||||
violation at the same time. This module is needed only in the constrained case.
|
||||
|
||||
Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
|
||||
|
||||
Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
|
||||
|
||||
Python translation by Nickolai Belakovski.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from .consts import DEBUGGING, EPS, CONSTRMAX, REALMAX, FUNCMAX
|
||||
from .present import present
|
||||
|
||||
def isbetter(f1: float, c1: float, f2: float, c2: float, ctol: float) -> bool:
|
||||
'''
|
||||
This function compares whether FC1 = (F1, C1) is (strictly) better than FC2 = (F2, C2), which
|
||||
basically means that (F1 < F2 and C1 <= C2) or (F1 <= F2 and C1 < C2).
|
||||
It takes care of the cases where some of these values are NaN or Inf, even though some cases
|
||||
should never happen due to the moderated extreme barrier.
|
||||
At return, BETTER = TRUE if and only if (F1, C1) is better than (F2, C2).
|
||||
Here, C means constraint violation, which is a nonnegative number.
|
||||
'''
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert not any(np.isnan([f1, c1]) | np.isposinf([f2, c2]))
|
||||
assert not any(np.isnan([f2, c2]) | np.isposinf([f2, c2]))
|
||||
assert c1 >= 0 and c2 >= 0
|
||||
assert ctol >= 0
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
is_better = False
|
||||
# Even though NaN/+Inf should not occur in FC1 or FC2 due to the moderated extreme barrier, for
|
||||
# security and robustness, the code below does not make this assumption.
|
||||
is_better = is_better or (any(np.isnan([f1, c1]) | np.isposinf([f1, c1])) and not any(np.isnan([f2, c2]) | np.isposinf([f2, c2])))
|
||||
|
||||
is_better = is_better or (f1 < f2 and c1 <= c2)
|
||||
is_better = is_better or (f1 <= f2 and c1 < c2)
|
||||
|
||||
# If C1 <= CTOL and C2 is significantly larger/worse than CTOL, i.e., C2 > MAX(CTOL,CREF),
|
||||
# then FC1 is better than FC2 as long as F1 < REALMAX. Normally CREF >= CTOL so MAX(CTOL, CREF)
|
||||
# is indeed CREF. However, this may not be true if CTOL > 1E-1*CONSTRMAX.
|
||||
cref = 10 * max(EPS, min(ctol, 1.0E-2 * CONSTRMAX)) # The MIN avoids overflow.
|
||||
is_better = is_better or (f1 < REALMAX and c1 <= ctol and (c2 > max(ctol, cref) or np.isnan(c2)))
|
||||
|
||||
#==================#
|
||||
# Calculation ends #
|
||||
#==================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
assert not (is_better and f1 >= f2 and c1 >= c2)
|
||||
assert is_better or not (f1 <= f2 and c1 < c2)
|
||||
assert is_better or not (f1 < f2 and c1 <= c2)
|
||||
|
||||
return is_better
|
||||
|
||||
|
||||
def savefilt(cstrv, ctol, cweight, f, x, nfilt, cfilt, ffilt, xfilt, constr=None, confilt=None):
|
||||
'''
|
||||
This subroutine saves X, F, and CSTRV in XFILT, FFILT, and CFILT (and CONSTR in CONFILT
|
||||
if they are present), unless a vector in XFILT[:, :NFILT] is better than X.
|
||||
If X is better than some vectors in XFILT[:, :NFILT] then these vectors will be
|
||||
removed. If X is not better than any of XFILT[:, :NFILT], but NFILT == MAXFILT,
|
||||
then we remove a column from XFILT according to the merit function
|
||||
PHI = FFILT + CWEIGHT * max(CFILT - CTOL, 0)
|
||||
N.B.:
|
||||
1. Only XFILT[:, :NFILT] and FFILT[:, :NFILT] etc contains valid information,
|
||||
while XFILT[:, NFILT+1:MAXFILT] and FFILT[:, NFILT+1:MAXFILT] etc are not
|
||||
initialized yet.
|
||||
2. We decide whether and X is better than another by the ISBETTER function
|
||||
'''
|
||||
|
||||
# Sizes
|
||||
if present(constr):
|
||||
num_constraints = len(constr)
|
||||
else:
|
||||
num_constraints = 0
|
||||
num_vars = len(x)
|
||||
maxfilt = len(ffilt)
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
# Check the size of X.
|
||||
assert num_vars >= 1
|
||||
# Check CWEIGHT and CTOL
|
||||
assert cweight >= 0
|
||||
assert ctol >= 0
|
||||
# Check NFILT
|
||||
assert nfilt >= 0 and nfilt <= maxfilt
|
||||
# Check the sizes of XFILT, FFILT, CFILT.
|
||||
assert maxfilt >= 1
|
||||
assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
|
||||
assert np.size(cfilt) == maxfilt
|
||||
# Check the values of XFILT, FFILT, CFILT.
|
||||
assert not (np.isnan(xfilt[:, :nfilt])).any()
|
||||
assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
|
||||
assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
|
||||
# Check the values of X, F, CSTRV.
|
||||
# X does not contain NaN if X0 does not and the trust-region/geometry steps are proper.
|
||||
assert not any(np.isnan(x))
|
||||
# F cannot be NaN/+Inf due to the moderated extreme barrier.
|
||||
assert not (np.isnan(f) | np.isposinf(f))
|
||||
# CSTRV cannot be NaN/+Inf due to the moderated extreme barrier.
|
||||
assert not (cstrv < 0 | np.isnan(cstrv) | np.isposinf(cstrv))
|
||||
# Check CONSTR and CONFILT.
|
||||
assert present(constr) == present(confilt)
|
||||
if present(constr):
|
||||
# CONSTR cannot contain NaN/-Inf due to the moderated extreme barrier.
|
||||
assert not any(np.isnan(constr) | np.isneginf(constr))
|
||||
assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
|
||||
assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
# Return immediately if any column of XFILT is better than X.
|
||||
if any((isbetter(ffilt_i, cfilt_i, f, cstrv, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt]))) or \
|
||||
any(np.logical_and(ffilt[:nfilt] <= f, cfilt[:nfilt] <= cstrv)):
|
||||
return nfilt, cfilt, ffilt, xfilt, confilt
|
||||
|
||||
# Decide which columns of XFILT to keep.
|
||||
keep = np.logical_not([isbetter(f, cstrv, ffilt_i, cfilt_i, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
|
||||
|
||||
# If NFILT == MAXFILT and X is not better than any column of XFILT, then we remove the worst column
|
||||
# of XFILT according to the merit function PHI = FFILT + CWEIGHT * MAX(CFILT - CTOL, ZERO).
|
||||
if sum(keep) == maxfilt: # In this case, NFILT = SIZE(KEEP) = COUNT(KEEP) = MAXFILT > 0.
|
||||
cfilt_shifted = np.maximum(cfilt - ctol, 0)
|
||||
if cweight <= 0:
|
||||
phi = ffilt
|
||||
elif np.isposinf(cweight):
|
||||
phi = cfilt_shifted
|
||||
# We should not use CFILT here; if MAX(CFILT_SHIFTED) is attained at multiple indices, then
|
||||
# we will check FFILT to exhaust the remaining degree of freedom.
|
||||
else:
|
||||
phi = np.maximum(ffilt, -REALMAX)
|
||||
phi = np.nan_to_num(phi, nan=-REALMAX) # Replace NaN with -REALMAX and +/- inf with large numbers
|
||||
phi += cweight * cfilt_shifted
|
||||
# We select X to maximize PHI. In case there are multiple maximizers, we take the one with the
|
||||
# largest CSTRV_SHIFTED; if there are more than one choices, we take the one with the largest F;
|
||||
# if there are several candidates, we take the one with the largest CSTRV; if the last comparison
|
||||
# still leads to more than one possibilities, then they are equally bad and we choose the first.
|
||||
# N.B.:
|
||||
# 1. This process is the opposite of selecting KOPT in SELECTX.
|
||||
# 2. In finite-precision arithmetic, PHI_1 == PHI_2 and CSTRV_SHIFTED_1 == CSTRV_SHIFTED_2 do
|
||||
# not ensure that F_1 == F_2!
|
||||
phimax = max(phi)
|
||||
cref = max(cfilt_shifted[phi >= phimax])
|
||||
fref = max(ffilt[cfilt_shifted >= cref])
|
||||
kworst = np.ma.array(cfilt, mask=(ffilt > fref)).argmax()
|
||||
if kworst < 0 or kworst >= len(keep): # For security. Should not happen.
|
||||
kworst = 0
|
||||
keep[kworst] = False
|
||||
|
||||
# Keep the good xfilt values and remove all the ones that are strictly worse than the new x.
|
||||
nfilt = sum(keep)
|
||||
index_to_keep = np.where(keep)[0]
|
||||
xfilt[:, :nfilt] = xfilt[:, index_to_keep]
|
||||
ffilt[:nfilt] = ffilt[index_to_keep]
|
||||
cfilt[:nfilt] = cfilt[index_to_keep]
|
||||
if confilt is not None and constr is not None:
|
||||
confilt[:, :nfilt] = confilt[:, index_to_keep]
|
||||
|
||||
# Once we have removed all the vectors that are strictly worse than x,
|
||||
# we add x to the filter.
|
||||
xfilt[:, nfilt] = x
|
||||
ffilt[nfilt] = f
|
||||
cfilt[nfilt] = cstrv
|
||||
if confilt is not None and constr is not None:
|
||||
confilt[:, nfilt] = constr
|
||||
nfilt += 1 # In Python we need to increment the index afterwards
|
||||
|
||||
#==================#
|
||||
# Calculation ends #
|
||||
#==================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
# Check NFILT and the sizes of XFILT, FFILT, CFILT.
|
||||
assert nfilt >= 1 and nfilt <= maxfilt
|
||||
assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
|
||||
assert np.size(ffilt) == maxfilt
|
||||
assert np.size(cfilt) == maxfilt
|
||||
# Check the values of XFILT, FFILT, CFILT.
|
||||
assert not (np.isnan(xfilt[:, :nfilt])).any()
|
||||
assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
|
||||
assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
|
||||
# Check that no point in the filter is better than X, and X is better than no point.
|
||||
assert not any([isbetter(ffilt_i, cfilt_i, f, cstrv, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
|
||||
assert not any([isbetter(f, cstrv, ffilt_i, cfilt_i, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
|
||||
# Check CONFILT.
|
||||
if present(confilt):
|
||||
assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
|
||||
assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
|
||||
|
||||
|
||||
return nfilt, cfilt, ffilt, xfilt, confilt
|
||||
|
||||
|
||||
def selectx(fhist: npt.NDArray, chist: npt.NDArray, cweight: float, ctol: float):
|
||||
'''
|
||||
This subroutine selects X according to FHIST and CHIST, which represents (a part of) history
|
||||
of F and CSTRV. Normally, FHIST and CHIST are not the full history but only a filter, e.g. ffilt
|
||||
and CFILT generated by SAVEFILT. However, we name them as FHIST and CHIST because the [F, CSTRV]
|
||||
in a filter should not dominate each other, but this subroutine does NOT assume such a property.
|
||||
N.B.: CTOL is the tolerance of the constraint violation (CSTRV). A point is considered feasible if
|
||||
its constraint violation is at most CTOL. Not that CTOL is absolute, not relative.
|
||||
'''
|
||||
|
||||
# Sizes
|
||||
nhist = len(fhist)
|
||||
|
||||
# Preconditions
|
||||
if DEBUGGING:
|
||||
assert nhist >= 1
|
||||
assert np.size(chist) == nhist
|
||||
assert not any(np.isnan(fhist) | np.isposinf(fhist))
|
||||
assert not any(chist < 0 | np.isnan(chist) | np.isposinf(chist))
|
||||
assert cweight >= 0
|
||||
assert ctol >= 0
|
||||
|
||||
#====================#
|
||||
# Calculation starts #
|
||||
#====================#
|
||||
|
||||
# We select X among the points with F < FREF and CSTRV < CREF.
|
||||
# Do NOT use F <= FREF, because F == FREF (FUNCMAX or REALMAX) may mean F == INF in practice!
|
||||
if any(np.logical_and(fhist < FUNCMAX, chist < CONSTRMAX)):
|
||||
fref = FUNCMAX
|
||||
cref = CONSTRMAX
|
||||
elif any(np.logical_and(fhist < REALMAX, chist < CONSTRMAX)):
|
||||
fref = REALMAX
|
||||
cref = CONSTRMAX
|
||||
elif any(np.logical_and(fhist < FUNCMAX, chist < REALMAX)):
|
||||
fref = FUNCMAX
|
||||
cref = REALMAX
|
||||
else:
|
||||
fref = REALMAX
|
||||
cref = REALMAX
|
||||
|
||||
if not any(np.logical_and(fhist < fref, chist < cref)):
|
||||
kopt = nhist - 1
|
||||
else:
|
||||
# Shift the constraint violations by ctol, so that cstrv <= ctol is regarded as no violation.
|
||||
chist_shifted = np.maximum(chist - ctol, 0)
|
||||
# cmin is the minimal shift constraint violation attained in the history.
|
||||
cmin = np.min(chist_shifted[fhist < fref])
|
||||
# We consider only the points whose shifted constraint violations are at most the cref below.
|
||||
# N.B.: Without taking np.maximum(EPS, .), cref would be 0 if cmin = 0. In that case, asking for
|
||||
# cstrv_shift < cref would be WRONG!
|
||||
cref = np.maximum(EPS, 2*cmin)
|
||||
# We use the following phi as our merit function to select X.
|
||||
if cweight <= 0:
|
||||
phi = fhist
|
||||
elif np.isposinf(cweight):
|
||||
phi = chist_shifted
|
||||
# We should not use chist here; if np.minimum(chist_shifted) is attained at multiple indices, then
|
||||
# we will check fhist to exhaust the remaining degree of freedom.
|
||||
else:
|
||||
phi = np.maximum(fhist, -REALMAX) + cweight * chist_shifted
|
||||
# np.maximum(fhist, -REALMAX) makes sure that phi will not contain NaN (unless there is a bug).
|
||||
|
||||
# We select X to minimize phi subject to f < fref and cstrv_shift <= cref (see the comments
|
||||
# above for the reason of taking "<" and "<=" in these two constraints). In case there are
|
||||
# multiple minimizers, we take the one with the least cstrv_shift; if there is more than one
|
||||
# choice, we take the one with the least f; if there are several candidates, we take the one
|
||||
# with the least cstrv; if the last comparison still leads to more than one possibility, then
|
||||
# they are equally good and we choose the first.
|
||||
# N.B.:
|
||||
# 1. This process is the opposite of selecting kworst in savefilt
|
||||
# 2. In finite-precision arithmetic, phi_2 == phi_2 and cstrv_shift_1 == cstrv_shifted_2 do
|
||||
# not ensure thatn f_1 == f_2!
|
||||
phimin = np.min(phi[np.logical_and(fhist < fref, chist_shifted <= cref)])
|
||||
cref = np.min(chist_shifted[np.logical_and(fhist < fref, phi <= phimin)])
|
||||
fref = np.min(fhist[chist_shifted <= cref])
|
||||
# Can't use argmin here because using it with a mask throws off the index
|
||||
kopt = np.ma.array(chist, mask=(fhist > fref)).argmin()
|
||||
|
||||
#==================#
|
||||
# Calculation ends #
|
||||
#==================#
|
||||
|
||||
# Postconditions
|
||||
if DEBUGGING:
|
||||
assert kopt >= 0 and kopt < nhist
|
||||
assert not any([isbetter(fhisti, chisti, fhist[kopt], chist[kopt], ctol) for fhisti, chisti in zip(fhist[:nhist], chist[:nhist])])
|
||||
|
||||
return kopt
|
||||
Reference in New Issue
Block a user