| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- '''
- This module contains subroutines concerning the update of the interpolation set.
- 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 ..common.consts import DEBUGGING
- from ..common.infos import DAMAGING_ROUNDING, INFO_DEFAULT
- from ..common.linalg import isinv, matprod, outprod, inprod, inv, primasum
- import numpy as np
- def updatexfc(jdrop, constr, cpen, cstrv, d, f, conmat, cval, fval, sim, simi):
- '''
- This function revises the simplex by updating the elements of SIM, SIMI, FVAL, CONMAT, and CVAL
- '''
- # Local variables
- itol = 1
- # Sizes
- num_constraints = np.size(constr)
- num_vars = np.size(sim, 0)
- # Preconditions
- if DEBUGGING:
- assert num_constraints >= 0
- assert num_vars >= 1
- assert jdrop >= 0 and jdrop <= num_vars + 1
- assert not any(np.isnan(constr) | np.isneginf(constr))
- assert not (np.isnan(cstrv) | np.isposinf(cstrv))
- assert np.size(d) == num_vars and all(np.isfinite(d))
- assert not (np.isnan(f) | np.isposinf(f))
- assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
- assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
- assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
- assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
- assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
- assert np.isfinite(sim).all()
- assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
- assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
- assert np.isfinite(simi).all()
- assert isinv(sim[:, :num_vars], simi, itol)
- #====================#
- # Calculation starts #
- #====================#
- # Do nothing when JDROP is None. This can only happen after a trust-region step.
- if jdrop is None: # JDROP is None is impossible if the input is correct.
- return conmat, cval, fval, sim, simi, INFO_DEFAULT
- sim_old = sim
- simi_old = simi
- if jdrop < num_vars:
- sim[:, jdrop] = d
- simi_jdrop = simi[jdrop, :] / inprod(simi[jdrop, :], d)
- simi -= outprod(matprod(simi, d), simi_jdrop)
- simi[jdrop, :] = simi_jdrop
- else: # jdrop == num_vars
- sim[:, num_vars] += d
- sim[:, :num_vars] -= np.tile(d, (num_vars, 1)).T
- simid = matprod(simi, d)
- sum_simi = primasum(simi, axis=0)
- simi += outprod(simid, sum_simi / (1 - sum(simid)))
- # Check whether SIMI is a poor approximation to the inverse of SIM[:, :NUM_VARS]
- # Calculate SIMI from scratch if the current one is damaged by rounding errors.
- itol = 1
- erri = np.max(abs(matprod(simi, sim[:, :num_vars]) - np.eye(num_vars))) # np.max returns NaN if any input is NaN
- if erri > 0.1 * itol or np.isnan(erri):
- simi_test = inv(sim[:, :num_vars])
- erri_test = np.max(abs(matprod(simi_test, sim[:, :num_vars]) - np.eye(num_vars)))
- if erri_test < erri or (np.isnan(erri) and not np.isnan(erri_test)):
- simi = simi_test
- erri = erri_test
- # If SIMI is satisfactory, then update FVAL, CONMAT, CVAL, and the pole position. Otherwise restore
- # SIM and SIMI, and return with INFO = DAMAGING_ROUNDING.
- if erri <= itol:
- fval[jdrop] = f
- conmat[:, jdrop] = constr
- cval[jdrop] = cstrv
- # Switch the best vertex to the pole position SIM[:, NUM_VARS] if it is not there already
- conmat, cval, fval, sim, simi, info = updatepole(cpen, conmat, cval, fval, sim, simi)
- else:
- info = DAMAGING_ROUNDING
- sim = sim_old
- simi = simi_old
- #==================#
- # Calculation ends #
- #==================#
- # Postconditions
- if DEBUGGING:
- assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
- assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
- assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
- assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
- assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
- assert np.isfinite(sim).all()
- assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
- assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
- assert np.isfinite(simi).all()
- assert isinv(sim[:, :num_vars], simi, itol) or info == DAMAGING_ROUNDING
- return sim, simi, fval, conmat, cval, info
- def findpole(cpen, cval, fval):
- '''
- This subroutine identifies the best vertex of the current simplex with respect to the merit
- function PHI = F + CPEN * CSTRV.
- '''
- # Size
- num_vars = np.size(fval) - 1
- # Preconditions
- if DEBUGGING:
- assert cpen > 0
- assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
- assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
- #====================#
- # Calculation starts #
- #====================#
- # Identify the optimal vertex of the current simplex
- jopt = np.size(fval) - 1
- phi = fval + cpen * cval
- phimin = min(phi)
- # Essentially jopt = np.argmin(phi). However, we keep jopt = num_vars unless there
- # is a strictly better choice. When there are multiple choices, we choose the jopt
- # with the smallest value of cval.
- if phimin < phi[jopt] or any((cval < cval[jopt]) & (phi <= phi[jopt])):
- # While we could use argmin(phi), there may be two places where phi achieves
- # phimin, and in that case we should choose the one with the smallest cval.
- jopt = np.ma.array(cval, mask=(phi > phimin)).argmin()
- #==================#
- # Calculation ends #
- #==================#
- # Postconditions
- if DEBUGGING:
- assert jopt >= 0 and jopt < num_vars + 1
- assert jopt == num_vars or phi[jopt] < phi[num_vars] or (phi[jopt] <= phi[num_vars] and cval[jopt] < cval[num_vars])
- return jopt
- def updatepole(cpen, conmat, cval, fval, sim, simi):
- #--------------------------------------------------------------------------------------------------!
- # This subroutine identifies the best vertex of the current simplex with respect to the merit
- # function PHI = F + CPEN * CSTRV, and then switch this vertex to SIM[:, NUM_VARS], which Powell called
- # the "pole position" in his comments. CONMAT, CVAL, FVAL, and SIMI are updated accordingly.
- #
- # N.B. 1: In precise arithmetic, the following two procedures produce the same results:
- # 1) apply UPDATEPOLE to SIM twice, first with CPEN = CPEN1 and then with CPEN = CPEN2;
- # 2) apply UPDATEPOLE to SIM with CPEN = CPEN2.
- # In finite-precision arithmetic, however, they may produce different results unless CPEN1 = CPEN2.
- #
- # N.B. 2: When JOPT == N+1, the best vertex is already at the pole position, so there is nothing to
- # switch. However, as in Powell's code, the code below will check whether SIMI is good enough to
- # work as the inverse of SIM(:, 1:N) or not. If not, Powell's code would invoke an error return of
- # COBYLB; our implementation, however, will try calculating SIMI from scratch; if the recalculated
- # SIMI is still of poor quality, then UPDATEPOLE will return with INFO = DAMAGING_ROUNDING,
- # informing COBYLB that SIMI is poor due to damaging rounding errors.
- #
- # N.B. 3: UPDATEPOLE should be called when and only when FINDPOLE can potentially returns a value
- # other than N+1. The value of FINDPOLE is determined by CPEN, CVAL, and FVAL, the latter two being
- # decided by SIM. Thus UPDATEPOLE should be called after CPEN or SIM changes. COBYLA updates CPEN at
- # only two places: the beginning of each trust-region iteration, and when REDRHO is called;
- # SIM is updated only by UPDATEXFC, which itself calls UPDATEPOLE internally. Therefore, we only
- # need to call UPDATEPOLE after updating CPEN at the beginning of each trust-region iteration and
- # after each invocation of REDRHO.
- # Local variables
- itol = 1
- # Sizes
- num_constraints = conmat.shape[0]
- num_vars = sim.shape[0]
- # Preconditions
- if DEBUGGING:
- assert num_constraints >= 0
- assert num_vars >= 1
- assert cpen > 0
- assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
- assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
- assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
- assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
- assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
- assert np.isfinite(sim).all()
- assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
- assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
- assert np.isfinite(simi).all()
- assert isinv(sim[:, :num_vars], simi, itol)
- #====================#
- # Calculation starts #
- #====================#
- # INFO must be set, as it is an output.
- info = INFO_DEFAULT
- # Identify the optimal vertex of the current simplex.
- jopt = findpole(cpen, cval, fval)
- # Switch the best vertex to the pole position SIM[:, NUM_VARS] if it is not there already and update
- # SIMI. Before the update, save a copy of SIM and SIMI. If the update is unsuccessful due to
- # damaging rounding errors, we restore them and return with INFO = DAMAGING_ROUNDING.
- sim_old = sim.copy()
- simi_old = simi.copy()
- if 0 <= jopt < num_vars:
- # Unless there is a bug in FINDPOLE it is guaranteed that JOPT >= 0
- # When JOPT == NUM_VARS, there is nothing to switch; in addition SIMI[JOPT, :] will be illegal.
- # fval[[jopt, -1]] = fval[[-1, jopt]]
- # conmat[:, [jopt, -1]] = conmat[:, [-1, jopt]] # Exchange CONMAT[:, JOPT] AND CONMAT[:, -1]
- # cval[[jopt, -1]] = cval[[-1, jopt]]
- sim[:, num_vars] += sim[:, jopt]
- sim_jopt = sim[:, jopt].copy()
- sim[:, jopt] = 0 # np.zeros(num_constraints)?
- sim[:, :num_vars] -= np.tile(sim_jopt, (num_vars, 1)).T
- # The above update is equivalent to multiplying SIM[:, :NUM_VARS] from the right side by a matrix whose
- # JOPT-th row is [-1, -1, ..., -1], while all the other rows are the same as those of the
- # identity matrix. It is easy to check that the inverse of this matrix is itself. Therefore,
- # SIMI should be updated by a multiplication with this matrix (i.e. its inverse) from the left
- # side, as is done in the following line. The JOPT-th row of the updated SIMI is minus the sum
- # of all rows of the original SIMI, whereas all the other rows remain unchanged.
- # NDB 20250114: In testing the cutest problem 'SYNTHES2' between the Python implementation and
- # the Fortran bindings, I saw a difference between the following for loop and the
- # np.sum command. The differences were small, on the order of 1e-16, i.e. epsilon.
- # According to numpy documentation, np.sum sometimes uses partial pairwise summation,
- # depending on the memory layout of the array and the axis specified.
- # for i in range(simi.shape[1]):
- # simi[jopt, i] = -sum(simi[:, i])
- simi[jopt, :] = -primasum(simi, axis=0)
- # Check whether SIMI is a poor approximation to the inverse of SIM[:, :NUM_VARS]
- # Calculate SIMI from scratch if the current one is damaged by rounding errors.
- erri = np.max(abs(matprod(simi, sim[:, :num_vars]) - np.eye(num_vars))) # np.max returns NaN if any input is NaN
- itol = 1
- if erri > 0.1 * itol or np.isnan(erri):
- simi_test = inv(sim[:, :num_vars])
- erri_test = np.max(abs(matprod(simi_test, sim[:, :num_vars]) - np.eye(num_vars)))
- if erri_test < erri or (np.isnan(erri) and not np.isnan(erri_test)):
- simi = simi_test
- erri = erri_test
- # If SIMI is satisfactory, then update FVAL, CONMAT, and CVAL. Otherwise restore SIM and SIMI, and
- # return with INFO = DAMAGING_ROUNDING.
- if erri <= itol:
- if 0 <= jopt < num_vars:
- fval[[jopt, num_vars]] = fval[[num_vars, jopt]]
- conmat[:, [jopt, num_vars]] = conmat[:, [num_vars, jopt]]
- cval[[jopt, num_vars]] = cval[[num_vars, jopt]]
- else: # erri > itol or erri is NaN
- info = DAMAGING_ROUNDING
- sim = sim_old
- simi = simi_old
- #==================#
- # Calculation ends #
- #==================#
- # Postconditions
- if DEBUGGING:
- assert findpole(cpen, cval, fval) == num_vars or info == DAMAGING_ROUNDING
- assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
- assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
- assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
- assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
- assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
- assert np.isfinite(sim).all()
- assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
- assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
- assert np.isfinite(simi).all()
- # Do not check SIMI = SIM[:, :num_vars]^{-1}, as it may not be true due to damaging rounding.
- assert isinv(sim[:, :num_vars], simi, itol) or info == DAMAGING_ROUNDING
- return conmat, cval, fval, sim, simi, info
|