Source code for Irene.relaxations

r"""
This module is responsible for conversion of a given symbolic optimization problem into semidefinite optimization
problems.
The main classes included in this module are:

    + `SDPRelaxations`
    + `SDRelaxSol`
    + `Mom`
"""

#  from __future__ import print_function
from .base import base
from .sdp import sdp

from numpy import array, float64, ndarray, sqrt, zeros, abs, linalg, trim_zeros, where, random, dot
from numpy import zeros as npzeros
from numpy.random import uniform
from numpy.linalg import cholesky, LinAlgError
from sympy import Function, Symbol, QQ, groebner, Poly, zeros, reduced, sympify, Matrix, expand, latex, lambdify, Abs
from sympy.core.relational import Equality, GreaterThan, LessThan, StrictGreaterThan, StrictLessThan
from sympy.polys.polyerrors import PolynomialError
from sympy.polys.matrices import DomainMatrix
from scipy import optimize as opt
from scipy.linalg import eigvals
from scipy import linalg as spla
from math import ceil
from functools import reduce
from Irene.program import OptimizationProblem
from itertools import product
from operator import mul
from time import time
import multiprocessing as mp
from copy import copy
from pickle import load, loads, dump, dumps


[docs] def Calpha_(expn, Mmnt): r""" Given an exponent `expn`, this function finds the corresponding :math:`C_{expn}` matrix which can be used for parallel processing. """ r = Mmnt.shape[0] C = zeros(r, r) for i in range(r): for j in range(i, r): entity = Mmnt[i, j] if expn in entity: C[i, j] = entity[expn] C[j, i] = C[i, j] return array(C.tolist()).astype(float64)
[docs] def Calpha__(expn, Mmnt, ii, q): r""" Given an exponent `expn`, this function finds the corresponding :math:`C_{expn}` matrix which can be used for parallel processing. """ r = Mmnt.shape[0] C = zeros(r, r) for i in range(r): for j in range(i, r): entity = Mmnt[i, j].as_dict() if expn in entity: C[i, j] = entity[expn] C[j, i] = C[i, j] q.put([ii, array(C.tolist()).astype(float64)])
[docs] class SDPRelaxations(base): r""" This class defines a function space by taking a family of sympy symbolic functions and relations among them. Simply, it initiates a commutative free real algebra on the symbolic functions and defines the function space as the quotient of the free algebra by the ideal generated by the given relations. It takes three arguments: - `gens` which is a list of ``sympy`` symbols and function symbols, - `relations` which is a set of ``sympy`` expressions in terms of `gens` that defines an ideal. - `name` is a given name which is used to save the state of the instant at break. """ GensError = r"""The `gens` must be a list of sympy functions or symbols""" RelsError = r"""The `relations` must be a list of relation among generators""" MonoOrdError = r"""`ord` must be one of 'lex', 'grlex', 'grevlex', 'ilex', 'igrlex', 'igrevlex'""" MmntOrdError = r"""The order of moments must be a positive integer""" SDPInpTypError = r"""The input of the SDP solver must be either a numpy matrix or ndarray""" # Monomial order: "lex", "grlex", "grevlex", "ilex", "igrlex", "igrevlex" MonomialOrder = 'lex' SDPSolver = 'cvxopt' Info = {} ErrorTolerance = 1e-6 AvailableSolvers = [] PSDMoment = True Probability = True Parallel = True def __init__(self, gens, relations=(), name="SDPRlx"): assert type(gens) is list, self.GensError assert type(gens) is list, self.RelsError super(SDPRelaxations, self).__init__() self.NumCores = mp.cpu_count() self.EQ = Equality self.GEQ = GreaterThan self.LEQ = LessThan self.GT = StrictGreaterThan self.LT = StrictLessThan self.ExpTypes = [Equality, GreaterThan, LessThan, StrictGreaterThan, StrictLessThan] self.Field = QQ self.Generators = [] self.SymDict = {} self.RevSymDict = {} self.AuxSyms = [] self.NumGenerators = 0 self.FreeRelations = [] self.Groebner = [] self.MmntOrd = 0 self.ReducedBases = {} # self.Constraints = [] self.OrgConst = [] self.MomConst = [] self.OrgMomConst = [] self.ObjDeg = 0 self.ObjHalfDeg = 0 self.CnsDegs = [] self.CnsHalfDegs = [] self.MmntCnsDeg = 0 self.Blck = [] self.C_ = [] self.InitIdx = 0 self.LastIdxVal = 0 self.Stage = None self.PrevStage = None self.Name = name self.SDP = sdp() self.MatSize = [] self.InitTime = 0 self.Solution = None self.f_min = 0 # check generators for f in gens: if isinstance(f, Function) or isinstance(f, Symbol): self.Generators.append(f) self.NumGenerators += 1 t_sym = Symbol('X%d' % self.NumGenerators) self.SymDict[f] = t_sym self.RevSymDict[t_sym] = f self.AuxSyms.append(t_sym) else: raise TypeError(self.GensError) self.Objective = Poly(0, *self.Generators) self.RedObjective = Poly(0, *self.AuxSyms) # check the relations # TBI for r in relations: t_rel = r.subs(self.SymDict) self.FreeRelations.append(t_rel) if self.FreeRelations: self.Groebner = groebner( self.FreeRelations, domain=self.Field, order=self.MonomialOrder) self.AvailableSolvers = self.AvailableSDPSolvers()
[docs] def SetMonoOrd(self, ordr): r""" Changes the default monomial order to `ord` which mustbe among `lex`, `grlex`, `grevlex`, `ilex`, `igrlex`, `igrevlex`. """ assert ordr in ['lex', 'grlex', 'grevlex', 'ilex', 'igrlex', 'igrevlex'], self.MonoOrdError self.MonomialOrder = ordr if self.FreeRelations: self.Groebner = groebner( self.FreeRelations, domain=self.Field, order=self.MonomialOrder)
[docs] @classmethod def from_problem(cls, optim_prob: OptimizationProblem, name="SDPRlx"): """ Creates an SDPRelaxations instance from an OptimizationProblem. This method acts as an alternative constructor to bridge compatibility with problems defined using SemigroupAlgebra. Args: optim_prob (OptimizationProblem): The optimization problem defined with SemigroupAlgebra. name (str): A name for the relaxation instance. Returns: An instance of SDPRelaxations. """ sga = optim_prob.sga gen_names = sga.gens sympy_gens = [Symbol(g) for g in gen_names] sym_map = {name: sym for name, sym in zip(gen_names, sympy_gens)} # Convert relations if they exist relations = [optim_prob.to_sympy(rel, sym_map) for rel in optim_prob.relations] if optim_prob.relations else [] rlx = cls(sympy_gens, relations, name) rlx.SetObjective(optim_prob.to_sympy(optim_prob.objective, sym_map)) for const in optim_prob.constraints: rlx.AddConstraint(optim_prob.to_sympy(const, sym_map) >= 0) return rlx
[docs] def SetNumCores(self, num): r""" Sets the maximum number of workers which cannot be bigger than number of available cores. """ assert (num > 0) and type( num) is int, "Number of cores must be a positive integer." self.NumCores = min(self.NumCores, num)
[docs] def SetSDPSolver(self, solver): r""" Sets the default SDP solver. The followings are currently supported: - CVXOPT - DSDP - SDPA - CSDP The selected solver must be installed otherwise it cannot be called. The default solver is `CVXOPT` which has an interface for Python. `DSDP` is called through the CVXOPT's interface. `SDPA` and `CSDP` are called independently. """ assert solver.upper() in ['CVXOPT', 'DSDP', 'SDPA', 'CSDP'], "'%s' sdp solver is not supported" % solver self.SDPSolver = solver
[docs] def ReduceExp(self, expr): r""" Takes an expression `expr`, either in terms of internal free symbolic variables or generating functions and returns the reduced expression in terms of internal symbolic variables, if a relation among generators is present, otherwise it just substitutes generating functions with their corresponding internal symbols. """ try: T = expr.subs(self.SymDict) except: T = Poly(expr, *self.AuxSyms) if self.Groebner: return reduced(T, self.Groebner)[1] else: return T
[docs] def SetObjective(self, obj): r""" Takes the objective function `obj` as an algebraic combination of the generating symbolic functions, replace the symbolic functions with corresponding auxiliary symbols and reduce them according to the given relations. """ self.Objective = sympify(obj) self.RedObjective = self.ReduceExp(sympify(obj)) # self.CheckVars(obj) tot_deg = Poly(self.RedObjective, *self.AuxSyms).total_degree() self.ObjDeg = tot_deg self.ObjHalfDeg = int(ceil(tot_deg / 2.))
[docs] def AddConstraint(self, cnst): r""" Takes an (in)equality as an algebraic combination of the generating functions that defines the feasibility region. It reduces the defining (in)equalities according to the given relations. """ self.OrgConst.append(cnst) if isinstance(cnst, (self.GEQ, self.GT)): non_red_exp = cnst.lhs - cnst.rhs expr = self.ReduceExp(non_red_exp) self.Constraints.append(expr) tot_deg = Poly(expr, *self.AuxSyms).total_degree() self.CnsDegs.append(tot_deg) self.CnsHalfDegs.append(int(ceil(tot_deg / 2.))) elif isinstance(cnst, (self.LEQ, self.LT)): non_red_exp = cnst.rhs - cnst.lhs expr = self.ReduceExp(non_red_exp) self.Constraints.append(expr) tot_deg = Poly(expr, *self.AuxSyms).total_degree() self.CnsDegs.append(tot_deg) self.CnsHalfDegs.append(int(ceil(tot_deg / 2.))) elif isinstance(cnst, self.EQ): non_red_exp = cnst.lhs - cnst.rhs expr = self.ReduceExp(non_red_exp) self.Constraints.append(self.ErrorTolerance + expr) self.Constraints.append(self.ErrorTolerance - expr) tot_deg = Poly(expr, *self.AuxSyms).total_degree() # add twice self.CnsDegs.append(tot_deg) self.CnsDegs.append(tot_deg) self.CnsHalfDegs.append(int(ceil(tot_deg / 2.))) self.CnsHalfDegs.append(int(ceil(tot_deg / 2.)))
[docs] def MomentConstraint(self, cnst): r""" Takes constraints on the moments. The input must be an instance of `Mom` class. """ assert isinstance( cnst, Mom), "The argument must be of moment type 'Mom'" self.OrgMomConst.append(cnst) CnsTyp = cnst.TYPE if CnsTyp in ['ge', 'gt']: expr = self.ReduceExp(cnst.Content) tot_deg = Poly(expr, *self.AuxSyms).total_degree() self.MmntCnsDeg = max(int(ceil(tot_deg / 2.)), self.MmntCnsDeg) self.MomConst.append([expr, cnst.rhs]) elif CnsTyp in ['le', 'lt']: expr = self.ReduceExp(-cnst.Content) tot_deg = Poly(expr, *self.AuxSyms).total_degree() self.MmntCnsDeg = max(int(ceil(tot_deg / 2.)), self.MmntCnsDeg) self.MomConst.append([expr, -cnst.rhs]) elif CnsTyp == 'eq': non_red_exp = cnst.Content - cnst.rhs expr = self.ReduceExp(cnst.Content) tot_deg = Poly(expr, *self.AuxSyms).total_degree() self.MmntCnsDeg = max(int(ceil(tot_deg / 2.)), self.MmntCnsDeg) self.MomConst.append([expr, cnst.rhs - self.ErrorTolerance]) self.MomConst.append([-expr, -cnst.rhs - self.ErrorTolerance])
[docs] def ReducedMonomialBase(self, deg): r""" Returns a reduce monomial basis up to degree `d`. """ if deg in self.ReducedBases: return self.ReducedBases[deg] all_monos = product(range(deg + 1), repeat=self.NumGenerators) req_monos = filter(lambda x: sum(x) <= deg, all_monos) monos = [reduce(mul, [self.AuxSyms[i] ** expn[i] for i in range(self.NumGenerators)], 1) for expn in req_monos] RBase = [] for expr in monos: rexpr = self.ReduceExp(expr) expr_monos = Poly(rexpr, *self.AuxSyms).as_dict() for mono_exp in expr_monos: t_mono = reduce(mul, [self.AuxSyms[i] ** mono_exp[i] for i in range(self.NumGenerators)], 1) if t_mono not in RBase: RBase.append(t_mono) self.ReducedBases[deg] = RBase return RBase
[docs] def ExponentsVec(self, deg): r""" Returns all the exponents that appear in the reduced basis of all monomials of the auxiliary symbols of degree at most `deg`. """ basis = self.ReducedMonomialBase(deg) exponents = [] for elmnt in basis: rbp = Poly(elmnt, *self.AuxSyms).as_dict() for expnt in rbp: if expnt not in exponents: exponents.append(expnt) return exponents
[docs] def MomentsOrd(self, ordr): r""" Sets the order of moments to be considered. """ # from types import IntType # assert (type(ordr) is IntType) and (ordr > 0), self.MmntOrdError assert (type(ordr) is int) and (ordr > 0), self.MmntOrdError self.MmntOrd = ordr
[docs] def RelaxationDeg(self): r""" Finds the minimum required order of moments according to user's request, objective function and constraints. """ if not self.CnsHalfDegs: CHD = 0 else: CHD = max(self.CnsHalfDegs) RlxDeg = max([CHD, self.ObjHalfDeg, self.MmntOrd, self.MmntCnsDeg]) self.MmntOrd = RlxDeg return RlxDeg
[docs] def PolyCoefFullVec(self): r""" return the vector of coefficient of the reduced objective function as an element of the vector space of elements of degree up to the order of moments. """ c = [] fmono = Poly(self.RedObjective, *self.AuxSyms).as_dict() exponents = self.ExponentsVec(2 * self.MmntOrd) for expn in exponents: if expn in fmono: c.append(fmono[expn]) else: c.append(0) return c
def _poly_total_degree_or_raise(self, expr, context): try: return Poly(expr, *self.AuxSyms).total_degree() except PolynomialError as exc: raise ValueError("Unable to determine polynomial degree for %s" % context) from exc
[docs] def LocalizedMoment(self, p): r""" Computes the reduced symbolic moment generating matrix localized at `p`. """ tot_deg = self._poly_total_degree_or_raise(p, 'localized moment') half_deg = int(ceil(tot_deg / 2.)) mmntord = self.MmntOrd - half_deg m = Matrix(self.ReducedMonomialBase(mmntord)) LMmnt = expand(p * m * m.T) LrMmnt = zeros(*LMmnt.shape) for i in range(LMmnt.shape[0]): for j in range(i, LMmnt.shape[1]): LrMmnt[i, j] = self.ReduceExp(LMmnt[i, j]) LrMmnt[j, i] = LrMmnt[i, j] return LrMmnt
[docs] def LocalizedMoment_(self, p): r""" Computes the reduced symbolic moment generating matrix localized at `p`. """ from sympy.polys.polymatrix import PolyMatrix tot_deg = self._poly_total_degree_or_raise(p, 'localized moment') half_deg = int(ceil(tot_deg / 2.)) mmntord = self.MmntOrd - half_deg m = Matrix(self.ReducedMonomialBase(mmntord)) LMmnt = expand(p * m * m.T) # LrMmnt = zeros(*LMmnt.shape) tmp = [[0.*self.AuxSyms[0] for _ in range(LMmnt.shape[1])] for __ in range(LMmnt.shape[0])] LrMmnt = tmp # PolyMatrix(tmp) for i in range(LMmnt.shape[0]): for j in range(i, LMmnt.shape[1]): #LrMmnt[i, j] = Poly(self.ReduceExp( #LMmnt[i, j]), *self.AuxSyms).as_dict() #LrMmnt[j, i] = LrMmnt[i, j] LrMmnt[i][j] = Poly(self.ReduceExp( LMmnt[i, j]), *self.AuxSyms) LrMmnt[j][i] = LrMmnt[i][j] return PolyMatrix(LrMmnt)
[docs] def MomentMat(self): r""" Returns the numerical moment matrix resulted from solving the SDP. """ assert 'moments' in self.Info, "The sdp has not been (successfully) solved (yet)." Mmnt = self.LocalizedMoment(1.) for i in range(Mmnt.shape[0]): for j in range(Mmnt.shape[1]): t_monos = Poly(Mmnt[i, j], *self.AuxSyms).as_dict() t_mmnt = 0 for expn in t_monos: mono = reduce(mul, [self.AuxSyms[k] ** expn[k] for k in range(self.NumGenerators)], 1) t_mmnt += t_monos[expn] * self.Info['moments'][mono] Mmnt[i, j] = t_mmnt Mmnt[j, i] = Mmnt[i, j] return array(Mmnt.tolist()).astype(float64)
[docs] def Calpha(self, expn, Mmnt): r""" Given an exponent `expn`, this method finds the corresponding :math:`C_{expn}` matrix. """ r = Mmnt.shape[0] C = zeros(r, r) for i in range(r): for j in range(i, r): entity = Mmnt[i, j] entity_monos = Poly(entity, *self.AuxSyms).as_dict() if expn in entity_monos: C[i, j] = entity_monos[expn] C[j, i] = C[i, j] return array(C.tolist()).astype(float64)
[docs] def sInitSDP(self): r""" Initializes the semidefinite program (SDP), in serial mode, whose solution is a lower bound for the minimum of the program. """ start = time() self.SDP = sdp(self.SDPSolver) self.RelaxationDeg() N = len(self.ReducedMonomialBase(2 * self.MmntOrd)) self.MatSize = [len(self.ReducedMonomialBase(self.MmntOrd)), N] Blck = [[] for _ in range(N)] C = [] # Number of constraints NumCns = len(self.CnsDegs) # Number of moment constraints NumMomCns = len(self.MomConst) # Reduced vector of monomials of the given order ExpVec = self.ExponentsVec(2 * self.MmntOrd) # The localized moment matrices should be psd ## for idx in range(NumCns): d = len(self.ReducedMonomialBase( self.MmntOrd - self.CnsHalfDegs[idx])) # Corresponding C block is 0 h = zeros(d, d) C.append(array(h.tolist()).astype(float64)) Mmnt = self.LocalizedMoment(self.Constraints[idx]) for i in range(N): Blck[i].append(self.Calpha(ExpVec[i], Mmnt)) # Moment matrix should be psd ## if self.PSDMoment: d = len(self.ReducedMonomialBase(self.MmntOrd)) # Corresponding C block is 0 h = zeros(d, d) C.append(array(h.tolist()).astype(float64)) Mmnt = self.LocalizedMoment(1.) for i in range(N): Blck[i].append(self.Calpha(ExpVec[i], Mmnt)) # L(1) = 1 # if self.Probability: for i in range(N): Blck[i].append(array( zeros(1, 1).tolist()).astype(float64)) Blck[i].append(array( zeros(1, 1).tolist()).astype(float64)) # Blck[0][NumCns + 1][0] = 1 # Blck[0][NumCns + 2][0] = -1 Blck[0][-2][0] = 1 Blck[0][-1][0] = -1 C.append(array(Matrix([1]).tolist()).astype(float64)) C.append(array(Matrix([-1]).tolist()).astype(float64)) # Moment constraints for idx in range(NumMomCns): MomCns = Matrix([self.MomConst[idx][0]]) for i in range(N): Blck[i].append(self.Calpha(ExpVec[i], MomCns)) C.append(array( Matrix([self.MomConst[idx][1]]).tolist()).astype(float64)) self.SDP.C = C self.SDP.b = self.PolyCoefFullVec() self.SDP.A = Blck elapsed = (time() - start) self.InitTime = elapsed
[docs] def Commit(self, blk, c, idx): r""" Sets the latest computed values for the final SDP and saves the current state. """ self.Blck = copy(blk) self.C_ = copy(c) self.InitIdx = idx
def _commit_stage_state(self, blk, c, idx): r""" Commit stage state, preserving current interrupt semantics. """ try: self.Commit(blk, c, idx) except: self.Commit(blk, c, idx) raise KeyboardInterrupt def _parallel_calpha_results(self, expvec, mmnt): r""" Computes ``Calpha__`` results in parallel and cleans up worker processes deterministically. """ queue = mp.Queue(self.NumCores) started_procs = [] results = [None for _ in range(len(expvec))] try: for idx, expn in enumerate(expvec): proc = mp.Process(target=Calpha__, args=(expn, mmnt, idx, queue)) proc.start() started_procs.append(proc) for _ in range(len(expvec)): tmp = queue.get() results[tmp[0]] = tmp[1] return results except BaseException: for proc in started_procs: if proc.is_alive(): proc.terminate() raise finally: for proc in started_procs: proc.join() if hasattr(queue, 'close'): queue.close() if hasattr(queue, 'join_thread'): queue.join_thread()
[docs] def pInitSDP(self): r""" Initializes the semidefinite program (SDP), in parallel, whose solution is a lower bound for the minimum of the program. """ start = time() self.SDP = sdp(self.SDPSolver, solver_path=self.Path) self.RelaxationDeg() N = len(self.ReducedMonomialBase(2 * self.MmntOrd)) self.MatSize = [len(self.ReducedMonomialBase(self.MmntOrd)), N] if not self.Blck: self.Blck = [[] for _ in range(N)] # Number of constraints NumCns = len(self.CnsDegs) # Number of moment constraints NumMomCns = len(self.MomConst) # Reduced vector of monomials of the given order ExpVec = self.ExponentsVec(2 * self.MmntOrd) # The localized moment matrices should be psd ## if (self.PrevStage is None) or (self.PrevStage == "PSDLocMom"): self.Stage = "PSDLocMom" self.PrevStage = None idx = self.LastIdxVal while idx < NumCns: d = len(self.ReducedMonomialBase( self.MmntOrd - self.CnsHalfDegs[idx])) Mmnt = self.LocalizedMoment_(self.Constraints[idx]) results = self._parallel_calpha_results(ExpVec, Mmnt) # stash changes tBlck = copy(self.Blck) tC_ = copy(self.C_) for i in range(N): tBlck[i].append(results[i]) # Corresponding self.C_ block is 0 h = zeros(d, d) tC_.append(array(h.tolist()).astype(float64)) # increase loop counter idx += 1 # commit changes self._commit_stage_state(tBlck, tC_, idx) # self.InitIdx = idx self.LastIdxVal = 0 # Moment matrix should be psd ## if (self.PrevStage is None) or (self.PrevStage == "PSDMom"): self.Stage = "PSDMom" self.PrevStage = None if self.PSDMoment: d = len(self.ReducedMonomialBase(self.MmntOrd)) Mmnt = self.LocalizedMoment_(1.) results = self._parallel_calpha_results(ExpVec, Mmnt) # stash changes tBlck = copy(self.Blck) tC_ = copy(self.C_) for i in range(N): tBlck[i].append(results[i]) # Corresponding self.C_ block is 0 h = zeros(d, d) tC_.append(array(h.tolist()).astype(float64)) # commit changes self._commit_stage_state(tBlck, tC_, 0) # self.Blck = copy(tBlck) # self.C_ = copy(tC_) # L(1) = 1 ## if (self.PrevStage is None) or (self.PrevStage == "L(1)=1"): self.Stage = "L(1)=1" self.PrevStage = None if self.Probability: # stash changes tBlck = copy(self.Blck) tC_ = copy(self.C_) for i in range(N): tBlck[i].append(array( zeros(1, 1).tolist()).astype(float64)) tBlck[i].append(array( zeros(1, 1).tolist()).astype(float64)) # Blck[0][NumCns + 1][0] = 1 # Blck[0][NumCns + 2][0] = -1 tBlck[0][-2][0] = 1 tBlck[0][-1][0] = -1 tC_.append(array(Matrix([1]).tolist()).astype(float64)) tC_.append( array(Matrix([-1]).tolist()).astype(float64)) # commit changes self._commit_stage_state(tBlck, tC_, 0) # self.Blck = copy(tBlck) # self.C_ = copy(tC_) # Moment constraints if (self.PrevStage is None) or (self.PrevStage == "MomConst"): self.Stage = "MomConst" self.PrevStage = None idx = self.LastIdxVal while idx < NumMomCns: MomCns = Matrix([self.MomConst[idx][0]]) # stash changes tBlck = copy(self.Blck) tC_ = copy(self.C_) for i in range(N): tBlck[i].append(self.Calpha(ExpVec[i], MomCns)) tC_.append(array( Matrix([self.MomConst[idx][1]]).tolist()).astype(float64)) # increase loop counter idx += 1 # commit changes self._commit_stage_state(tBlck, tC_, idx) # self.Blck = copy(tBlck) # self.C_ = copy(tC_) # self.InitIdx = idx self.SDP.C = self.C_ self.SDP.b = self.PolyCoefFullVec() self.SDP.A = self.Blck elapsed = (time() - start) self.InitTime = elapsed
[docs] def InitSDP(self): r""" Initializes the SDP based on the value of ``self.Parallel``. If it is ``True``, it runs in parallel mode, otherwise in serial. """ if self.Parallel: try: self.pInitSDP() except KeyboardInterrupt: with open(self.Name + '.rlx', 'wb') as obj_file: dump(self, obj_file) print("\n...::: The program is saved in '" + self.Name + ".rlx' :::...") raise KeyboardInterrupt else: self.sInitSDP()
[docs] def Minimize(self): r""" Finds the minimum of the truncated moment problem which provides a lower bound for the actual minimum. """ self.SDP.solve() self.Solution = SDRelaxSol( self.AuxSyms, symdict=self.SymDict, err_tol=self.ErrorTolerance) self.Info = {} self.Solution.Status = self.SDP.Info['Status'] if self.SDP.Info['Status'] == 'Optimal': self.f_min = min(self.SDP.Info['PObj'], self.SDP.Info['DObj']) self.Solution.Primal = self.SDP.Info['PObj'] self.Solution.Dual = self.SDP.Info['DObj'] self.Info = {"min": self.f_min, "CPU": self.SDP.Info[ 'CPU'], 'InitTime': self.InitTime} self.Solution.RunTime = self.SDP.Info['CPU'] self.Solution.InitTime = self.InitTime self.Info['status'] = 'Optimal' self.Info[ 'Message'] = 'Feasible solution for moments of order ' + str(self.MmntOrd) self.Solution.Message = self.Info['Message'] self.Info['tms'] = self.SDP.Info['y'] FullMonVec = self.ReducedMonomialBase(2 * self.MmntOrd) self.Info['moments'] = {FullMonVec[i]: self.Info[ 'tms'][i] for i in range(len(FullMonVec))} self.Info['solver'] = self.SDP.solver for idx in self.Info['moments']: self.Solution.TruncatedMmntSeq[idx.subs(self.RevSymDict)] = self.Info[ 'moments'][idx] self.Solution.MomentMatrix = self.MomentMat() self.Solution.MonoBase = self.ReducedMonomialBase(self.MmntOrd) self.Solution.Solver = self.SDP.solver self.Solution.NumGenerators = self.NumGenerators else: self.f_min = None self.Info['min'] = self.f_min self.Info['status'] = 'Infeasible' self.Info['Message'] = 'No feasible solution for moments of order ' + \ str(self.MmntOrd) + ' were found' self.Solution.Status = 'Infeasible' self.Solution.Message = self.Info['Message'] self.Solution.Solver = self.SDP.solver self.Info["Size"] = self.MatSize return self.f_min
[docs] def Decompose(self): r""" Returns a dictionary that associates a list to every constraint, :math:`g_i\ge0` for :math:`i=0,\dots,m`, where :math:`g_0=1`. Each list consists of elements of algebra whose sums of squares is equal to :math:`\sigma_i` and :math:`f-f_*=\sum_{i=0}^m\sigma_ig_i`. Here, :math:`f_*` is the output of the ``SDPRelaxation.Minimize()``. """ SOSCoefs = {} blks = [] NumCns = len(self.CnsDegs) for M in self.SDP.Info['X']: blks.append(Matrix(cholesky(M))) for idx in range(NumCns): SOSCoefs[idx + 1] = [] v = Matrix(self.ReducedMonomialBase( self.MmntOrd - self.CnsHalfDegs[idx])).T decomp = v * blks[idx] for p in decomp: SOSCoefs[idx + 1].append(p.subs(self.RevSymDict)) v = Matrix(self.ReducedMonomialBase(self.MmntOrd)).T SOSCoefs[0] = [] decomp = v * blks[NumCns] for p in decomp: SOSCoefs[0].append(p.subs(self.RevSymDict)) return SOSCoefs
[docs] def getObjective(self): r""" Returns the objective function of the problem after reduction modulo the relations, if given. """ return self.RedObjective.subs(self.RevSymDict)
[docs] def getConstraint(self, idx): r""" Returns the constraint number `idx` of the problem after reduction modulo the relations, if given. """ assert idx < len(self.Constraints), "Index out of range." return self.Constraints[idx].subs(self.RevSymDict) >= 0
[docs] def getMomentConstraint(self, idx): r""" Returns the moment constraint number `idx` of the problem after reduction modulo the relations, if given. """ assert idx < len(self.MomConst), "Index out of range." return self.MomConst[idx][0].subs(self.RevSymDict) >= sympify(self.MomConst[idx][1]).subs(self.RevSymDict)
[docs] def Resume(self): r""" Resumes the process of a previously saved program. """ with open(self.Name + '.rlx', 'rb') as obj_file: return load(obj_file)
[docs] def SaveState(self): r""" Saves the current state of the relaxation object to the file `self.Name+'.rlx'`. """ with open(self.Name + '.rlx', 'wb') as obj_file: dump(self, obj_file)
[docs] def State(self): r""" Returns the latest state of the object at last break and save. """ with open(self.Name + '.rlx', 'rb') as obj_file: ser_inst = load(obj_file) return ser_inst.PrevStage, ser_inst.LastIdxVal
def __str__(self): r""" String output. """ out_txt = "=" * 70 + "\n" out_txt += "Minimize\t" out_txt += str(self.RedObjective.subs(self.RevSymDict)) + "\n" out_txt += "Subject to\n" for cns in self.Constraints: out_txt += "\t\t" + str(cns.subs(self.RevSymDict) >= 0) + "\n" out_txt += "And\n" for cns in self.MomConst: out_txt += "\t\tMoment " + \ str(cns[0].subs(self.RevSymDict) >= sympify( cns[1]).subs(self.RevSymDict)) + "\n" out_txt += "=" * 70 + "\n" return out_txt def __getstate__(self): r""" Pickling process. """ self.PrevStage = self.Stage self.LastIdxVal = self.InitIdx # self.SDP = None exceptions = ['RevSymDict', 'Generators', 'Objective', 'SymDict', 'Solution', 'OrgConst'] cur_inst = self.__dict__ ser_inst = {} for kw in cur_inst: if kw in exceptions: ser_inst[kw] = str(cur_inst[kw]) else: ser_inst[kw] = dumps(cur_inst[kw]) return dumps(ser_inst) def __setstate__(self, state): r""" Loading pickles """ exceptions = ['RevSymDict', 'Generators', 'Objective', 'SymDict', 'Solution', 'OrgConst'] ser_inst = loads(state) for kw in ser_inst: if kw in exceptions: if kw not in ['Solution']: self.__dict__[kw] = sympify(ser_inst[kw]) else: self.__dict__[kw] = loads(ser_inst[kw]) def __latex__(self): r""" Generates LaTeX code of the optimization problem. """ latexcode = "\\left\\lbrace\n" latexcode += "\\begin{array}{ll}\n" latexcode += "\t\\min & " + latex(self.Objective) + "\\\\\n" latexcode += "\t\\textrm{subject to} & \\\\\n" for cns in self.OrgConst: latexcode += "\t\t & " + latex(cns) + "\\\\\n" latexcode += "\t\\textrm{where} & \\\\\n" for cns in self.OrgMomConst: latexcode += "\t\t" + cns.__latex__(True) + "\\\\\n" latexcode += "\\end{array}" latexcode += "\\right." return latexcode
####################################################################### # Solution of the Semidefinite Relaxation
[docs] class SDRelaxSol(object): r""" Instances of this class carry information on the solution of the semidefinite relaxation associated to an optimization problem. It includes various pieces of information: - ``SDRelaxSol.TruncatedMmntSeq`` a dictionary of resulted moments - ``SDRelaxSol.MomentMatrix`` the resulted moment matrix - ``SDRelaxSol.Primal`` the value of the SDP in primal form - ``SDRelaxSol.Dual`` the value of the SDP in dual form - ``SDRelaxSol.RunTime`` the run time of the sdp solver - ``SDRelaxSol.InitTime`` the total time consumed for initialization of the sdp - ``SDRelaxSol.Solver`` the name of sdp solver - ``SDRelaxSol.Status`` final status of the sdp solver - ``SDRelaxSol.RelaxationOrd`` order of relaxation - ``SDRelaxSol.Message`` the message that maybe returned by the sdp solver - ``SDRelaxSol.ScipySolver`` the scipy solver to extract solutions - ``SDRelaxSol.err_tol`` the minimum value which is considered to be nonzero - ``SDRelaxSol.Support`` the support of discrete measure resulted from ``SDPRelaxation.Minimize()`` - ``SDRelaxSol.Weights`` corresponding weights for the Dirac measures """ def __init__(self, X, symdict={}, err_tol=10e-6): self.TruncatedMmntSeq = {} self.MomentMatrix = None self.Primal = None self.Dual = None self.RunTime = None self.InitTime = None self.Solver = None self.Status = None self.RelaxationOrd = None self.Message = None self.MonoBase = None self.NumGenerators = None # SDPRelaxations auxiliary symbols self.X = X self.Xij = None self.SymDict = symdict self.RevSymDict = {} for v in self.SymDict: self.RevSymDict[self.SymDict[v]] = v self.err_tol = err_tol self.ScipySolver = 'lm' self.Support = None self.Weights = None self.weight = [] def __str__(self): r""" Generate the output for print. """ out_str = "Solution of a Semidefinite Program:\n" out_str += " Solver: " + self.Solver + "\n" out_str += " Status: " + self.Status + "\n" out_str += " Initialization Time: " + \ str(self.InitTime) + " seconds\n" out_str += " Run Time: " + \ str(self.RunTime) + " seconds\n" out_str += "Primal Objective Value: " + str(self.Primal) + "\n" out_str += " Dual Objective Value: " + str(self.Dual) + "\n" if self.Support is not None: out_str += " Support:\n" for p in self.Support: out_str += "\t\t" + str(p) + "\n" out_str += " Support solver: " + self.ScipySolver + "\n" out_str += self.Message + "\n" return out_str def __getitem__(self, idx): r""" Returns the moment corresponding to the index ``idx`` if exists, otherwise, returns ``None``. """ if idx in self.TruncatedMmntSeq: return self.TruncatedMmntSeq[idx] else: return None def __len__(self): r""" Returns the length of the moment sequence. """ return len(self.TruncatedMmntSeq) def __iter__(self): return iter(self.TruncatedMmntSeq)
[docs] def SetScipySolver(self, solver): r""" Sets the ``scipy.optimize.root`` solver to `solver`. """ assert solver.lower() in ['hybr', 'lm', 'broyden1', 'broyden2', 'anderson', 'linearmixing', 'diagbroyden', 'excitingmixing', 'krylov', 'df-sane'], "Unrecognized solver. The solver must be among 'hybr', 'lm', 'broyden1', 'broyden2', 'anderson', 'linearmixing', 'diagbroyden', 'excitingmixing', 'krylov', 'df-sane'" self.ScipySolver = solver.lower()
[docs] def Pivot(self, arr): r""" Get the leading term of each column. """ if arr.ndim == 1: idxs, = arr.nonzero() elif arr.ndim == 2: assert arr.shape[0] == 1 idxs = zip(*arr.nonzero()) else: raise Exception("Array of unexpected size: " + arr.ndim) for idx in idxs: elem = arr[idx] if abs(elem) > self.err_tol: if arr.ndim == 1: return idx, elem elif arr.ndim == 2: return idx[1], elem return 0, arr[0]
[docs] def StblRedEch(self, A): r""" Compute the stabilized row reduced echelon form. """ A = array(A) m, n = A.shape Q = [] R = npzeros((min(m, n), n)) # Rows for i, ai in enumerate(A.T): # Remove any contribution from previous rows for j, qj in enumerate(Q): R[j, i] = ai.dot(qj) ai -= ai.dot(qj) * qj li = sqrt((ai ** 2).sum()) if li > self.err_tol: assert len(Q) < min(m, n) # Add a new column to Q Q.append(ai / li) # And write down the contribution R[len(Q) - 1, i] = li # Convert to reduced row echelon form nrows, _ = R.shape for i in range(nrows - 1, 0, -1): k, v = self.Pivot(R[i, :]) if v > self.err_tol: for j in range(i): R[j, :] -= R[i, :] * R[j, k] / R[i, k] else: R[i, :] = 0 # row_normalize for r in R: li = sqrt((r ** 2).sum()) if li < self.err_tol: r[:] = 0 else: r /= li return array(Q).T, R
[docs] def NumericalRank(self): r""" Finds the rank of the moment matrix based on the size of its eigenvalues. It considers those with absolute value less than ``self.err_tol`` to be zero. """ num_rnk = 0 eignvls = eigvals(self.MomentMatrix) for ev in eignvls: if abs(ev) >= self.err_tol: num_rnk += 1 return num_rnk
[docs] def Term2Mmnt(self, trm, rnk, X): r""" Converts a moment object into an algebraic equation. """ num_vars = len(X) expr = 0 for i in range(rnk): expr += self.weight[i] * \ trm.subs({X[j]: self.Xij[i][j] for j in range(num_vars)}) return expr
[docs] def ExtractSolutionScipy(self, card=0): r""" This method tries to extract the corresponding values for generators of the ``SDPRelaxation`` class. Number of points is the rank of the moment matrix which is computed numerically according to the size of its eigenvalues. Then the points are extracted as solutions of a system of polynomial equations using a `scipy` solver. The following solvers are currently acceptable by ``scipy``: - ``hybr``, - ``lm`` (default), - ``broyden1``, - ``broyden2``, - ``anderson``, - ``linearmixing``, - ``diagbroyden``, - ``excitingmixing``, - ``krylov``, - ``df-sane``. """ if card > 0: rnk = min(self.NumericalRank(), card) else: rnk = self.NumericalRank() self.weight = [Symbol('w%d' % i, real=True) for i in range(1, rnk + 1)] self.Xij = [[Symbol('X%d%d' % (i, j), real=True) for i in range(1, self.NumGenerators + 1)] for j in range(1, rnk + 1)] syms = [s for row in self.Xij for s in row] for ri in self.weight: syms.append(ri) req = sum(self.weight) - 1 algeqs = {idx.subs(self.SymDict): self.TruncatedMmntSeq[ idx] for idx in self.TruncatedMmntSeq} included_sysms = set(self.weight) EQS = [req] hold = [] for i in range(len(algeqs)): trm = list(algeqs.keys())[i] if trm != 1: strm = self.Term2Mmnt(trm, rnk, self.X) - algeqs[trm] strm_syms = strm.free_symbols if not strm_syms.issubset(included_sysms): # EQS.append(strm) EQS.append(strm.subs({ri: Abs(ri) for ri in self.weight})) included_sysms = included_sysms.union(strm_syms) else: # hold.append(strm) hold.append(strm.subs({ri: Abs(ri) for ri in self.weight})) idx = 0 while len(EQS) < len(syms): if len(hold) > idx: EQS.append(hold[idx]) idx += 1 else: break if (included_sysms != set(syms)) or (len(EQS) != len(syms)): raise Exception("Unable to find the support.") f_ = [lambdify(syms, eq, 'numpy') for eq in EQS] def f(x): z = tuple(float(x.item(i)) for i in range(len(syms))) return [fn(*z) for fn in f_] init_point = array(tuple(uniform(-2 * self.err_tol, 2 * self.err_tol) for _ in range(len(syms)))) sol = opt.root(f, init_point, method=self.ScipySolver) if sol['success']: self.Support = [] self.Weights = [] idx = 0 while idx < len(syms) - rnk: minimizer = [] for i in range(self.NumGenerators): # minimizer.append(sol['x'][idx]) minimizer.append({self.RevSymDict[self.X[i]]: sol['x'][idx]}) idx += 1 self.Support.append(tuple(minimizer)) while idx < len(syms): self.Weights.append(sol['x'][idx]) idx += 1
[docs] def ExtractSolutionLH(self, card=0): r""" Extract solutions based on Lasserre--Henrion's method. """ M = self.MomentMatrix try: Us, Sigma, Vs = linalg.svd(M) except LinAlgError: print("Failed to find any minimizers.") return Kmax = self.NumericalRank() if card > 0: count = min(Kmax, sum(Sigma > self.err_tol), card) else: count = min(Kmax, sum(Sigma > self.err_tol)) sols = {} T, Ut = self.StblRedEch(Vs[0:count, :]) # normalize for r in Ut: lead = trim_zeros(r)[0] r /= lead couldbes = where(Ut > 0.9) ind_leadones = npzeros(Ut.shape[0], dtype=int) for j in reversed(range(len(couldbes[0]))): ind_leadones[couldbes[0][j]] = couldbes[1][j] basis = [self.MonoBase[i] for i in ind_leadones] RowMonos = {} for i, mono in enumerate(self.MonoBase): RowMonos[mono] = i Ns = {} bl = len(basis) # create multiplication matrix for each variable for x in self.X: Nx = npzeros((bl, bl)) for i, b in enumerate(basis): if x * b in RowMonos: Nx[:, i] = Ut[:, RowMonos[x * b]] Ns[x] = Nx N = npzeros((bl, bl)) for x in Ns: N += Ns[x] * random.randn() T, Q = spla.schur(N) quadf = lambda A, x: dot(x, dot(A, x)) for x in self.X: sols[x] = array([quadf(Ns[x], Q[:, j]) for j in range(bl)]) self.Support = [] for idx in range(count): pnt = [] for x in sols: pnt.append({self.RevSymDict[x]: sols[x][idx]}) pnt = tuple(pnt) if pnt not in self.Support: self.Support.append(pnt)
[docs] def ExtractSolution(self, mthd='LH', card=0): r""" Extract support of the solution measure from ``SDPRelaxations``: -``mthd`` should be either 'LH' or 'Scipy', where 'LH' stands for 'Lasserre-Henrion' and 'Scipy' employs a Scipy solver to find points matching the moments, -``card`` restricts the number of points of the support. """ if mthd.lower() == 'lh': self.ExtractSolutionLH(card) self.ScipySolver = "Lasserre--Henrion" elif mthd.lower() == 'scipy': self.ExtractSolutionScipy(card) else: raise Exception("Unsupported solver.")
def __latex__(self): r""" Generates LaTeX code for the moment matrix. """ a = self.MomentMatrix lines = str(a).replace('[', '').replace(']', '').splitlines() rv = [r'\begin{bmatrix}'] rv += [' ' + ' & '.join(l.split()) + r'\\' for l in lines] rv += [r'\end{bmatrix}'] return '\n'.join(rv)
####################################################################### # A Symbolic object to handle moment constraints
[docs] class Mom(object): r""" This is a simple interface to define moment constraints to be used via `SDPRelaxations.MomentConstraint`. It takes a sympy expression as input and initiates an object which can be used to force particular constraints on the moment sequence. **Example:** Force the moment of :math:`x^2f(x) + f(x)^2` to be at least `.5`:: Mom(x**2 * f + f**2) >= .5 # OR Mom(x**2 * f) + Mom(f**2) >= .5 """ def __init__(self, expr): # from types import IntType, LongType, FloatType # self.NumericTypes = [IntType, LongType, FloatType] self.NumericTypes = [int, float] self.Content = sympify(expr) self.rhs = 0 self.TYPE = None def __add__(self, x): if isinstance(x, Mom): self.Content += x.Content else: self.Content += x return self def __sub__(self, x): if isinstance(x, Mom): self.Content -= x.Content else: self.Content -= x return self def __neg__(self): self.Content = -self.Content return self def __mul__(self, x): if type(x) in self.NumericTypes: self.Content = x * self.Content else: raise Exception("Operation not supported") return self def __rmul__(self, x): if type(x) in self.NumericTypes: self.Content = x * self.Content else: raise Exception("Operation not supported") return self def __ge__(self, x): if isinstance(x, Mom): self.rhs = 0 self.Content -= x.Content elif type(x) in self.NumericTypes: self.rhs = x self.TYPE = 'ge' return self def __gt__(self, x): if isinstance(x, Mom): self.rhs = 0 self.Content -= x.Content elif type(x) in self.NumericTypes: self.rhs = x self.TYPE = 'gt' return self def __le__(self, x): if isinstance(x, Mom): self.rhs = 0 self.Content -= x.Content elif type(x) in self.NumericTypes: self.rhs = x self.TYPE = 'le' return self def __lt__(self, x): if isinstance(x, Mom): self.rhs = 0 self.Content -= x.Content elif type(x) in self.NumericTypes: self.rhs = x self.TYPE = 'lt' return self def __eq__(self, x): if isinstance(x, Mom): self.rhs = 0 self.Content -= x.Content elif type(x) in self.NumericTypes: self.rhs = x else: self.rhs += x self.TYPE = 'eq' return self def __str__(self): symbs = {'lt': '<', 'le': '<=', 'gt': '>', 'ge': '>=', 'eq': '=='} strng = str(self.Content) if self.TYPE is not None: strng += " " + symbs[self.TYPE] strng += " " + str(self.rhs) return strng def __getstate__(self): r""" Pickling process. """ ser_inst = {'NumericTypes': dumps(self.NumericTypes), 'Content': str(self.Content), 'rhs': dumps(self.rhs), 'TYPE': dumps(self.TYPE)} return dumps(ser_inst) def __setstate__(self, state): r""" Loading pickles """ ser_inst = loads(state) self.__dict__['NumericTypes'] = loads(ser_inst['NumericTypes']) self.__dict__['Content'] = sympify(ser_inst['Content']) self.__dict__['rhs'] = loads(ser_inst['rhs']) self.__dict__['TYPE'] = loads(ser_inst['TYPE']) def __latex__(self, external=False): r""" Generates LaTeX code for the moment term. """ symbs = {'lt': '<', 'le': '\\leq', 'gt': '>', 'ge': '\\geq', 'eq': '='} latexcode = "\\textrm{Moment of }" if external: latexcode += " & " latexcode += latex(self.Content) latexcode += symbs[self.TYPE] + latex(self.rhs) return latexcode