Coverage for picos/constraints/con_geomean.py: 97.80%
91 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-12 07:53 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-12 07:53 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2012-2017 Guillaume Sagnol
3# Copyright (C) 2018-2019 Maximilian Stahlberg
4#
5# This file is part of PICOS.
6#
7# PICOS is free software: you can redistribute it and/or modify it under the
8# terms of the GNU General Public License as published by the Free Software
9# Foundation, either version 3 of the License, or (at your option) any later
10# version.
11#
12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# ------------------------------------------------------------------------------
20"""Implementation of :class:`GeometricMeanConstraint`."""
22import math
23from collections import namedtuple
25from .. import glyphs
26from ..apidoc import api_end, api_start
27from .constraint import Constraint, ConstraintConversion
29_API_START = api_start(globals())
30# -------------------------------
33class GeometricMeanConstraint(Constraint):
34 """Lower bound on a geometric mean."""
36 class RSOCConversion(ConstraintConversion):
37 """Geometric mean to rotated second order cone constraint conversion."""
39 @classmethod
40 def predict(cls, subtype, options):
41 """Implement :meth:`~.constraint.ConstraintConversion.predict`."""
42 from ..expressions import RealVariable
43 from . import RSOCConstraint
45 j = subtype.argdim - 1
46 k = int(math.log(j, 2))
47 l = j + k - "{:b}".format(j - 2**k).count("1")
49 yield ("var", RealVariable.make_var_type(dim=1, bnd=0), l - 1)
50 yield ("con", RSOCConstraint.make_type(argdim=1), l)
52 # TODO: Refactor to be human readable.
53 @classmethod
54 def convert(cls, con, options):
55 """Implement :meth:`~.constraint.ConstraintConversion.convert`."""
56 from ..expressions import RealVariable
57 from ..expressions.algebra import rsoc
58 from ..modeling import Problem
60 geoMean = con.geoMean
61 lowerBound = con.lowerBound
63 P = Problem()
64 x = geoMean.x
65 m = len(x)
66 lm = [[i] for i in range(m - 1, -1, -1)]
67 K = []
68 depth = 0
69 u = {}
71 while len(lm) > 1:
72 depth += 1
73 nlm = []
74 while lm:
75 i1 = lm.pop()[-1]
76 if lm:
77 i2 = lm.pop()[0]
78 else:
79 i2 = 'x'
80 nlm.insert(0, (i2, i1))
81 k = str(depth) + ':' + str(i1) + '-' + str(i2)
82 K.append(k)
83 u[k] = RealVariable('__u[' + k + ']')
84 lm = nlm
86 root = K[-1]
87 maxd = int(K[-1].split(':')[0])
88 P.remove_variable(u[root].name) # TODO: Update for new expressions.
89 u[root] = lowerBound
91 for k in K:
92 i1 = int(k.split('-')[0].split(':')[1])
93 i2 = k.split('-')[1]
94 if i2 != 'x':
95 i2 = int(i2)
96 if k[:2] == '1:':
97 if i2 != 'x':
98 P.add_constraint((x[i1] & x[i2] & u[k]) << rsoc())
99 else:
100 P.add_constraint((x[i1] & lowerBound & u[k]) << rsoc())
101 else:
102 d = int(k.split(':')[0])
103 if i2 == 'x' and d < maxd:
104 k2pot = [ki for ki in K
105 if ki.startswith(str(d - 1) + ':')
106 and int(ki.split(':')[1].split('-')[0]) >= i1]
107 k1 = k2pot[0]
108 if len(k2pot) == 2:
109 k2 = k2pot[1]
110 P.add_constraint((u[k1] & u[k2] & u[k]) << rsoc())
111 else:
112 P.add_constraint(
113 (u[k1] & lowerBound & u[k]) << rsoc())
114 else:
115 k1 = [ki for ki in K
116 if ki.startswith(str(d - 1) + ':' + str(i1))][0]
117 k2 = [ki for ki in K if ki.startswith(str(d - 1) + ':')
118 and ki.endswith('-' + str(i2))][0]
119 P.add_constraint((u[k1] & u[k2] & u[k]) << rsoc())
121 return P
123 def __init__(self, geoMean, lowerBound):
124 """Construct a :class:`GeometricMeanConstraint`.
126 :param ~picos.expressions.GeometricMean lhs:
127 Constrained expression.
128 :param ~picos.expressions.AffineExpression lowerBound:
129 Lower bound on the expression.
130 """
131 from ..expressions import AffineExpression, GeometricMean
133 assert isinstance(geoMean, GeometricMean)
134 assert isinstance(lowerBound, AffineExpression)
135 assert len(lowerBound) == 1
137 self.geoMean = geoMean
138 self.lowerBound = lowerBound
140 super(GeometricMeanConstraint, self).__init__(geoMean._typeStr)
142 Subtype = namedtuple("Subtype", ("argdim",))
144 def _subtype(self):
145 return self.Subtype(len(self.geoMean.x))
147 @classmethod
148 def _cost(cls, subtype):
149 return subtype.argdim + 1
151 def _expression_names(self):
152 yield "geoMean"
153 yield "lowerBound"
155 def _str(self):
156 return glyphs.ge(self.geoMean.string, self.lowerBound.string)
158 def _get_slack(self):
159 return self.geoMean.safe_value - self.lowerBound.safe_value
162# --------------------------------------
163__all__ = api_end(_API_START, globals())