123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- """Equality-constrained quadratic programming solvers."""
- from scipy.sparse import (linalg, bmat, csc_matrix)
- from math import copysign
- import numpy as np
- from numpy.linalg import norm
- __all__ = [
- 'eqp_kktfact',
- 'sphere_intersections',
- 'box_intersections',
- 'box_sphere_intersections',
- 'inside_box_boundaries',
- 'modified_dogleg',
- 'projected_cg'
- ]
- # For comparison with the projected CG
- def eqp_kktfact(H, c, A, b):
- """Solve equality-constrained quadratic programming (EQP) problem.
- Solve ``min 1/2 x.T H x + x.t c`` subject to ``A x + b = 0``
- using direct factorization of the KKT system.
- Parameters
- ----------
- H : sparse matrix, shape (n, n)
- Hessian matrix of the EQP problem.
- c : array_like, shape (n,)
- Gradient of the quadratic objective function.
- A : sparse matrix
- Jacobian matrix of the EQP problem.
- b : array_like, shape (m,)
- Right-hand side of the constraint equation.
- Returns
- -------
- x : array_like, shape (n,)
- Solution of the KKT problem.
- lagrange_multipliers : ndarray, shape (m,)
- Lagrange multipliers of the KKT problem.
- """
- n, = np.shape(c) # Number of parameters
- m, = np.shape(b) # Number of constraints
- # Karush-Kuhn-Tucker matrix of coefficients.
- # Defined as in Nocedal/Wright "Numerical
- # Optimization" p.452 in Eq. (16.4).
- kkt_matrix = csc_matrix(bmat([[H, A.T], [A, None]]))
- # Vector of coefficients.
- kkt_vec = np.hstack([-c, -b])
- # TODO: Use a symmetric indefinite factorization
- # to solve the system twice as fast (because
- # of the symmetry).
- lu = linalg.splu(kkt_matrix)
- kkt_sol = lu.solve(kkt_vec)
- x = kkt_sol[:n]
- lagrange_multipliers = -kkt_sol[n:n+m]
- return x, lagrange_multipliers
- def sphere_intersections(z, d, trust_radius,
- entire_line=False):
- """Find the intersection between segment (or line) and spherical constraints.
- Find the intersection between the segment (or line) defined by the
- parametric equation ``x(t) = z + t*d`` and the ball
- ``||x|| <= trust_radius``.
- Parameters
- ----------
- z : array_like, shape (n,)
- Initial point.
- d : array_like, shape (n,)
- Direction.
- trust_radius : float
- Ball radius.
- entire_line : bool, optional
- When ``True``, the function returns the intersection between the line
- ``x(t) = z + t*d`` (``t`` can assume any value) and the ball
- ``||x|| <= trust_radius``. When ``False``, the function returns the intersection
- between the segment ``x(t) = z + t*d``, ``0 <= t <= 1``, and the ball.
- Returns
- -------
- ta, tb : float
- The line/segment ``x(t) = z + t*d`` is inside the ball for
- for ``ta <= t <= tb``.
- intersect : bool
- When ``True``, there is a intersection between the line/segment
- and the sphere. On the other hand, when ``False``, there is no
- intersection.
- """
- # Special case when d=0
- if norm(d) == 0:
- return 0, 0, False
- # Check for inf trust_radius
- if np.isinf(trust_radius):
- if entire_line:
- ta = -np.inf
- tb = np.inf
- else:
- ta = 0
- tb = 1
- intersect = True
- return ta, tb, intersect
- a = np.dot(d, d)
- b = 2 * np.dot(z, d)
- c = np.dot(z, z) - trust_radius**2
- discriminant = b*b - 4*a*c
- if discriminant < 0:
- intersect = False
- return 0, 0, intersect
- sqrt_discriminant = np.sqrt(discriminant)
- # The following calculation is mathematically
- # equivalent to:
- # ta = (-b - sqrt_discriminant) / (2*a)
- # tb = (-b + sqrt_discriminant) / (2*a)
- # but produce smaller round off errors.
- # Look at Matrix Computation p.97
- # for a better justification.
- aux = b + copysign(sqrt_discriminant, b)
- ta = -aux / (2*a)
- tb = -2*c / aux
- ta, tb = sorted([ta, tb])
- if entire_line:
- intersect = True
- else:
- # Checks to see if intersection happens
- # within vectors length.
- if tb < 0 or ta > 1:
- intersect = False
- ta = 0
- tb = 0
- else:
- intersect = True
- # Restrict intersection interval
- # between 0 and 1.
- ta = max(0, ta)
- tb = min(1, tb)
- return ta, tb, intersect
- def box_intersections(z, d, lb, ub,
- entire_line=False):
- """Find the intersection between segment (or line) and box constraints.
- Find the intersection between the segment (or line) defined by the
- parametric equation ``x(t) = z + t*d`` and the rectangular box
- ``lb <= x <= ub``.
- Parameters
- ----------
- z : array_like, shape (n,)
- Initial point.
- d : array_like, shape (n,)
- Direction.
- lb : array_like, shape (n,)
- Lower bounds to each one of the components of ``x``. Used
- to delimit the rectangular box.
- ub : array_like, shape (n, )
- Upper bounds to each one of the components of ``x``. Used
- to delimit the rectangular box.
- entire_line : bool, optional
- When ``True``, the function returns the intersection between the line
- ``x(t) = z + t*d`` (``t`` can assume any value) and the rectangular
- box. When ``False``, the function returns the intersection between the segment
- ``x(t) = z + t*d``, ``0 <= t <= 1``, and the rectangular box.
- Returns
- -------
- ta, tb : float
- The line/segment ``x(t) = z + t*d`` is inside the box for
- for ``ta <= t <= tb``.
- intersect : bool
- When ``True``, there is a intersection between the line (or segment)
- and the rectangular box. On the other hand, when ``False``, there is no
- intersection.
- """
- # Make sure it is a numpy array
- z = np.asarray(z)
- d = np.asarray(d)
- lb = np.asarray(lb)
- ub = np.asarray(ub)
- # Special case when d=0
- if norm(d) == 0:
- return 0, 0, False
- # Get values for which d==0
- zero_d = (d == 0)
- # If the boundaries are not satisfied for some coordinate
- # for which "d" is zero, there is no box-line intersection.
- if (z[zero_d] < lb[zero_d]).any() or (z[zero_d] > ub[zero_d]).any():
- intersect = False
- return 0, 0, intersect
- # Remove values for which d is zero
- not_zero_d = np.logical_not(zero_d)
- z = z[not_zero_d]
- d = d[not_zero_d]
- lb = lb[not_zero_d]
- ub = ub[not_zero_d]
- # Find a series of intervals (t_lb[i], t_ub[i]).
- t_lb = (lb-z) / d
- t_ub = (ub-z) / d
- # Get the intersection of all those intervals.
- ta = max(np.minimum(t_lb, t_ub))
- tb = min(np.maximum(t_lb, t_ub))
- # Check if intersection is feasible
- if ta <= tb:
- intersect = True
- else:
- intersect = False
- # Checks to see if intersection happens within vectors length.
- if not entire_line:
- if tb < 0 or ta > 1:
- intersect = False
- ta = 0
- tb = 0
- else:
- # Restrict intersection interval between 0 and 1.
- ta = max(0, ta)
- tb = min(1, tb)
- return ta, tb, intersect
- def box_sphere_intersections(z, d, lb, ub, trust_radius,
- entire_line=False,
- extra_info=False):
- """Find the intersection between segment (or line) and box/sphere constraints.
- Find the intersection between the segment (or line) defined by the
- parametric equation ``x(t) = z + t*d``, the rectangular box
- ``lb <= x <= ub`` and the ball ``||x|| <= trust_radius``.
- Parameters
- ----------
- z : array_like, shape (n,)
- Initial point.
- d : array_like, shape (n,)
- Direction.
- lb : array_like, shape (n,)
- Lower bounds to each one of the components of ``x``. Used
- to delimit the rectangular box.
- ub : array_like, shape (n, )
- Upper bounds to each one of the components of ``x``. Used
- to delimit the rectangular box.
- trust_radius : float
- Ball radius.
- entire_line : bool, optional
- When ``True``, the function returns the intersection between the line
- ``x(t) = z + t*d`` (``t`` can assume any value) and the constraints.
- When ``False``, the function returns the intersection between the segment
- ``x(t) = z + t*d``, ``0 <= t <= 1`` and the constraints.
- extra_info : bool, optional
- When ``True``, the function returns ``intersect_sphere`` and ``intersect_box``.
- Returns
- -------
- ta, tb : float
- The line/segment ``x(t) = z + t*d`` is inside the rectangular box and
- inside the ball for ``ta <= t <= tb``.
- intersect : bool
- When ``True``, there is a intersection between the line (or segment)
- and both constraints. On the other hand, when ``False``, there is no
- intersection.
- sphere_info : dict, optional
- Dictionary ``{ta, tb, intersect}`` containing the interval ``[ta, tb]``
- for which the line intercepts the ball. And a boolean value indicating
- whether the sphere is intersected by the line.
- box_info : dict, optional
- Dictionary ``{ta, tb, intersect}`` containing the interval ``[ta, tb]``
- for which the line intercepts the box. And a boolean value indicating
- whether the box is intersected by the line.
- """
- ta_b, tb_b, intersect_b = box_intersections(z, d, lb, ub,
- entire_line)
- ta_s, tb_s, intersect_s = sphere_intersections(z, d,
- trust_radius,
- entire_line)
- ta = np.maximum(ta_b, ta_s)
- tb = np.minimum(tb_b, tb_s)
- if intersect_b and intersect_s and ta <= tb:
- intersect = True
- else:
- intersect = False
- if extra_info:
- sphere_info = {'ta': ta_s, 'tb': tb_s, 'intersect': intersect_s}
- box_info = {'ta': ta_b, 'tb': tb_b, 'intersect': intersect_b}
- return ta, tb, intersect, sphere_info, box_info
- else:
- return ta, tb, intersect
- def inside_box_boundaries(x, lb, ub):
- """Check if lb <= x <= ub."""
- return (lb <= x).all() and (x <= ub).all()
- def reinforce_box_boundaries(x, lb, ub):
- """Return clipped value of x"""
- return np.minimum(np.maximum(x, lb), ub)
- def modified_dogleg(A, Y, b, trust_radius, lb, ub):
- """Approximately minimize ``1/2*|| A x + b ||^2`` inside trust-region.
- Approximately solve the problem of minimizing ``1/2*|| A x + b ||^2``
- subject to ``||x|| < Delta`` and ``lb <= x <= ub`` using a modification
- of the classical dogleg approach.
- Parameters
- ----------
- A : LinearOperator (or sparse matrix or ndarray), shape (m, n)
- Matrix ``A`` in the minimization problem. It should have
- dimension ``(m, n)`` such that ``m < n``.
- Y : LinearOperator (or sparse matrix or ndarray), shape (n, m)
- LinearOperator that apply the projection matrix
- ``Q = A.T inv(A A.T)`` to the vector. The obtained vector
- ``y = Q x`` being the minimum norm solution of ``A y = x``.
- b : array_like, shape (m,)
- Vector ``b``in the minimization problem.
- trust_radius: float
- Trust radius to be considered. Delimits a sphere boundary
- to the problem.
- lb : array_like, shape (n,)
- Lower bounds to each one of the components of ``x``.
- It is expected that ``lb <= 0``, otherwise the algorithm
- may fail. If ``lb[i] = -Inf``, the lower
- bound for the ith component is just ignored.
- ub : array_like, shape (n, )
- Upper bounds to each one of the components of ``x``.
- It is expected that ``ub >= 0``, otherwise the algorithm
- may fail. If ``ub[i] = Inf``, the upper bound for the ith
- component is just ignored.
- Returns
- -------
- x : array_like, shape (n,)
- Solution to the problem.
- Notes
- -----
- Based on implementations described in pp. 885-886 from [1]_.
- References
- ----------
- .. [1] Byrd, Richard H., Mary E. Hribar, and Jorge Nocedal.
- "An interior point algorithm for large-scale nonlinear
- programming." SIAM Journal on Optimization 9.4 (1999): 877-900.
- """
- # Compute minimum norm minimizer of 1/2*|| A x + b ||^2.
- newton_point = -Y.dot(b)
- # Check for interior point
- if inside_box_boundaries(newton_point, lb, ub) \
- and norm(newton_point) <= trust_radius:
- x = newton_point
- return x
- # Compute gradient vector ``g = A.T b``
- g = A.T.dot(b)
- # Compute Cauchy point
- # `cauchy_point = g.T g / (g.T A.T A g)``.
- A_g = A.dot(g)
- cauchy_point = -np.dot(g, g) / np.dot(A_g, A_g) * g
- # Origin
- origin_point = np.zeros_like(cauchy_point)
- # Check the segment between cauchy_point and newton_point
- # for a possible solution.
- z = cauchy_point
- p = newton_point - cauchy_point
- _, alpha, intersect = box_sphere_intersections(z, p, lb, ub,
- trust_radius)
- if intersect:
- x1 = z + alpha*p
- else:
- # Check the segment between the origin and cauchy_point
- # for a possible solution.
- z = origin_point
- p = cauchy_point
- _, alpha, _ = box_sphere_intersections(z, p, lb, ub,
- trust_radius)
- x1 = z + alpha*p
- # Check the segment between origin and newton_point
- # for a possible solution.
- z = origin_point
- p = newton_point
- _, alpha, _ = box_sphere_intersections(z, p, lb, ub,
- trust_radius)
- x2 = z + alpha*p
- # Return the best solution among x1 and x2.
- if norm(A.dot(x1) + b) < norm(A.dot(x2) + b):
- return x1
- else:
- return x2
- def projected_cg(H, c, Z, Y, b, trust_radius=np.inf,
- lb=None, ub=None, tol=None,
- max_iter=None, max_infeasible_iter=None,
- return_all=False):
- """Solve EQP problem with projected CG method.
- Solve equality-constrained quadratic programming problem
- ``min 1/2 x.T H x + x.t c`` subject to ``A x + b = 0`` and,
- possibly, to trust region constraints ``||x|| < trust_radius``
- and box constraints ``lb <= x <= ub``.
- Parameters
- ----------
- H : LinearOperator (or sparse matrix or ndarray), shape (n, n)
- Operator for computing ``H v``.
- c : array_like, shape (n,)
- Gradient of the quadratic objective function.
- Z : LinearOperator (or sparse matrix or ndarray), shape (n, n)
- Operator for projecting ``x`` into the null space of A.
- Y : LinearOperator, sparse matrix, ndarray, shape (n, m)
- Operator that, for a given a vector ``b``, compute smallest
- norm solution of ``A x + b = 0``.
- b : array_like, shape (m,)
- Right-hand side of the constraint equation.
- trust_radius : float, optional
- Trust radius to be considered. By default, uses ``trust_radius=inf``,
- which means no trust radius at all.
- lb : array_like, shape (n,), optional
- Lower bounds to each one of the components of ``x``.
- If ``lb[i] = -Inf`` the lower bound for the i-th
- component is just ignored (default).
- ub : array_like, shape (n, ), optional
- Upper bounds to each one of the components of ``x``.
- If ``ub[i] = Inf`` the upper bound for the i-th
- component is just ignored (default).
- tol : float, optional
- Tolerance used to interrupt the algorithm.
- max_iter : int, optional
- Maximum algorithm iterations. Where ``max_inter <= n-m``.
- By default, uses ``max_iter = n-m``.
- max_infeasible_iter : int, optional
- Maximum infeasible (regarding box constraints) iterations the
- algorithm is allowed to take.
- By default, uses ``max_infeasible_iter = n-m``.
- return_all : bool, optional
- When ``true``, return the list of all vectors through the iterations.
- Returns
- -------
- x : array_like, shape (n,)
- Solution of the EQP problem.
- info : Dict
- Dictionary containing the following:
- - niter : Number of iterations.
- - stop_cond : Reason for algorithm termination:
- 1. Iteration limit was reached;
- 2. Reached the trust-region boundary;
- 3. Negative curvature detected;
- 4. Tolerance was satisfied.
- - allvecs : List containing all intermediary vectors (optional).
- - hits_boundary : True if the proposed step is on the boundary
- of the trust region.
- Notes
- -----
- Implementation of Algorithm 6.2 on [1]_.
- In the absence of spherical and box constraints, for sufficient
- iterations, the method returns a truly optimal result.
- In the presence of those constraints, the value returned is only
- a inexpensive approximation of the optimal value.
- References
- ----------
- .. [1] Gould, Nicholas IM, Mary E. Hribar, and Jorge Nocedal.
- "On the solution of equality constrained quadratic
- programming problems arising in optimization."
- SIAM Journal on Scientific Computing 23.4 (2001): 1376-1395.
- """
- CLOSE_TO_ZERO = 1e-25
- n, = np.shape(c) # Number of parameters
- m, = np.shape(b) # Number of constraints
- # Initial Values
- x = Y.dot(-b)
- r = Z.dot(H.dot(x) + c)
- g = Z.dot(r)
- p = -g
- # Store ``x`` value
- if return_all:
- allvecs = [x]
- # Values for the first iteration
- H_p = H.dot(p)
- rt_g = norm(g)**2 # g.T g = r.T Z g = r.T g (ref [1]_ p.1389)
- # If x > trust-region the problem does not have a solution.
- tr_distance = trust_radius - norm(x)
- if tr_distance < 0:
- raise ValueError("Trust region problem does not have a solution.")
- # If x == trust_radius, then x is the solution
- # to the optimization problem, since x is the
- # minimum norm solution to Ax=b.
- elif tr_distance < CLOSE_TO_ZERO:
- info = {'niter': 0, 'stop_cond': 2, 'hits_boundary': True}
- if return_all:
- allvecs.append(x)
- info['allvecs'] = allvecs
- return x, info
- # Set default tolerance
- if tol is None:
- tol = max(min(0.01 * np.sqrt(rt_g), 0.1 * rt_g), CLOSE_TO_ZERO)
- # Set default lower and upper bounds
- if lb is None:
- lb = np.full(n, -np.inf)
- if ub is None:
- ub = np.full(n, np.inf)
- # Set maximum iterations
- if max_iter is None:
- max_iter = n-m
- max_iter = min(max_iter, n-m)
- # Set maximum infeasible iterations
- if max_infeasible_iter is None:
- max_infeasible_iter = n-m
- hits_boundary = False
- stop_cond = 1
- counter = 0
- last_feasible_x = np.zeros_like(x)
- k = 0
- for i in range(max_iter):
- # Stop criteria - Tolerance : r.T g < tol
- if rt_g < tol:
- stop_cond = 4
- break
- k += 1
- # Compute curvature
- pt_H_p = H_p.dot(p)
- # Stop criteria - Negative curvature
- if pt_H_p <= 0:
- if np.isinf(trust_radius):
- raise ValueError("Negative curvature not allowed "
- "for unrestricted problems.")
- else:
- # Find intersection with constraints
- _, alpha, intersect = box_sphere_intersections(
- x, p, lb, ub, trust_radius, entire_line=True)
- # Update solution
- if intersect:
- x = x + alpha*p
- # Reinforce variables are inside box constraints.
- # This is only necessary because of roundoff errors.
- x = reinforce_box_boundaries(x, lb, ub)
- # Attribute information
- stop_cond = 3
- hits_boundary = True
- break
- # Get next step
- alpha = rt_g / pt_H_p
- x_next = x + alpha*p
- # Stop criteria - Hits boundary
- if np.linalg.norm(x_next) >= trust_radius:
- # Find intersection with box constraints
- _, theta, intersect = box_sphere_intersections(x, alpha*p, lb, ub,
- trust_radius)
- # Update solution
- if intersect:
- x = x + theta*alpha*p
- # Reinforce variables are inside box constraints.
- # This is only necessary because of roundoff errors.
- x = reinforce_box_boundaries(x, lb, ub)
- # Attribute information
- stop_cond = 2
- hits_boundary = True
- break
- # Check if ``x`` is inside the box and start counter if it is not.
- if inside_box_boundaries(x_next, lb, ub):
- counter = 0
- else:
- counter += 1
- # Whenever outside box constraints keep looking for intersections.
- if counter > 0:
- _, theta, intersect = box_sphere_intersections(x, alpha*p, lb, ub,
- trust_radius)
- if intersect:
- last_feasible_x = x + theta*alpha*p
- # Reinforce variables are inside box constraints.
- # This is only necessary because of roundoff errors.
- last_feasible_x = reinforce_box_boundaries(last_feasible_x,
- lb, ub)
- counter = 0
- # Stop after too many infeasible (regarding box constraints) iteration.
- if counter > max_infeasible_iter:
- break
- # Store ``x_next`` value
- if return_all:
- allvecs.append(x_next)
- # Update residual
- r_next = r + alpha*H_p
- # Project residual g+ = Z r+
- g_next = Z.dot(r_next)
- # Compute conjugate direction step d
- rt_g_next = norm(g_next)**2 # g.T g = r.T g (ref [1]_ p.1389)
- beta = rt_g_next / rt_g
- p = - g_next + beta*p
- # Prepare for next iteration
- x = x_next
- g = g_next
- r = g_next
- rt_g = norm(g)**2 # g.T g = r.T Z g = r.T g (ref [1]_ p.1389)
- H_p = H.dot(p)
- if not inside_box_boundaries(x, lb, ub):
- x = last_feasible_x
- hits_boundary = True
- info = {'niter': k, 'stop_cond': stop_cond,
- 'hits_boundary': hits_boundary}
- if return_all:
- info['allvecs'] = allvecs
- return x, info
|