This commit is contained in:
2026-05-06 19:47:31 +07:00
parent 94d8682530
commit 12dbb7731b
9963 changed files with 2747894 additions and 0 deletions
@@ -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