pepflow 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pepflow/__init__.py +6 -1
- pepflow/constraint.py +58 -1
- pepflow/constraint_test.py +71 -0
- pepflow/e2e_test.py +83 -4
- pepflow/expression_manager.py +329 -44
- pepflow/expression_manager_test.py +150 -0
- pepflow/function.py +294 -52
- pepflow/function_test.py +180 -114
- pepflow/interactive_constraint.py +165 -75
- pepflow/parameter.py +187 -0
- pepflow/parameter_test.py +128 -0
- pepflow/pep.py +263 -16
- pepflow/pep_context.py +122 -6
- pepflow/pep_context_test.py +25 -0
- pepflow/pep_test.py +8 -0
- pepflow/point.py +155 -49
- pepflow/point_test.py +40 -188
- pepflow/scalar.py +260 -47
- pepflow/scalar_test.py +102 -130
- pepflow/solver.py +170 -3
- pepflow/solver_test.py +50 -2
- pepflow/utils.py +39 -7
- {pepflow-0.1.4.dist-info → pepflow-0.1.5.dist-info}/METADATA +24 -5
- pepflow-0.1.5.dist-info/RECORD +28 -0
- pepflow-0.1.4.dist-info/RECORD +0 -24
- {pepflow-0.1.4.dist-info → pepflow-0.1.5.dist-info}/WHEEL +0 -0
- {pepflow-0.1.4.dist-info → pepflow-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {pepflow-0.1.4.dist-info → pepflow-0.1.5.dist-info}/top_level.txt +0 -0
pepflow/scalar_test.py
CHANGED
@@ -17,155 +17,148 @@
|
|
17
17
|
# specific language governing permissions and limitations
|
18
18
|
# under the License.
|
19
19
|
|
20
|
+
from typing import Iterator
|
20
21
|
|
21
22
|
import numpy as np
|
23
|
+
import pytest
|
22
24
|
|
23
25
|
from pepflow import expression_manager as exm
|
24
26
|
from pepflow import pep as pep
|
25
|
-
from pepflow import
|
27
|
+
from pepflow import pep_context as pc
|
28
|
+
from pepflow import point, scalar
|
26
29
|
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
@pytest.fixture
|
32
|
+
def pep_context() -> Iterator[pc.PEPContext]:
|
33
|
+
"""Prepare the pep context and reset the context to None at the end."""
|
34
|
+
ctx = pc.PEPContext("test").set_as_current()
|
35
|
+
yield ctx
|
36
|
+
pc.set_current_context(None)
|
33
37
|
|
34
|
-
s_add = s1 + s2
|
35
|
-
assert s_add.tag == "s1+s2"
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
+
def test_scalar_add_tag(pep_context: pc.PEPContext):
|
40
|
+
s1 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s1"])
|
41
|
+
s2 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s2"])
|
39
42
|
|
40
|
-
|
41
|
-
|
43
|
+
s_add = s1 + s2
|
44
|
+
assert s_add.tag == "s1+s2"
|
42
45
|
|
43
|
-
|
44
|
-
|
46
|
+
s_add = s1 + 0.1
|
47
|
+
assert s_add.tag == "s1+0.1"
|
45
48
|
|
46
|
-
|
47
|
-
|
49
|
+
s_radd = 0.1 + s1
|
50
|
+
assert s_radd.tag == "0.1+s1"
|
48
51
|
|
49
|
-
|
50
|
-
|
52
|
+
s_sub = s1 - s2
|
53
|
+
assert s_sub.tag == "s1-s2"
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
s_sub = s1 - (s2 + s1)
|
56
|
+
assert s_sub.tag == "s1-(s2+s1)"
|
54
57
|
|
55
|
-
|
56
|
-
|
58
|
+
s_sub = s1 - (s2 - s1)
|
59
|
+
assert s_sub.tag == "s1-(s2-s1)"
|
57
60
|
|
61
|
+
s_sub = s1 - 0.1
|
62
|
+
assert s_sub.tag == "s1-0.1"
|
58
63
|
|
59
|
-
|
60
|
-
|
61
|
-
with pep_builder.make_context("test"):
|
62
|
-
s = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s"])
|
64
|
+
s_rsub = 0.1 - s1
|
65
|
+
assert s_rsub.tag == "0.1-s1"
|
63
66
|
|
64
|
-
s_mul = s * 0.1
|
65
|
-
assert s_mul.tag == "s*0.1"
|
66
67
|
|
67
|
-
|
68
|
-
|
68
|
+
def test_scalar_mul_tag(pep_context: pc.PEPContext):
|
69
|
+
s = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s"])
|
69
70
|
|
70
|
-
|
71
|
-
|
71
|
+
s_mul = s * 0.1
|
72
|
+
assert s_mul.tag == "s*0.1"
|
72
73
|
|
73
|
-
|
74
|
-
|
74
|
+
s_rmul = 0.1 * s
|
75
|
+
assert s_rmul.tag == "0.1*s"
|
75
76
|
|
77
|
+
s_neg = -s
|
78
|
+
assert s_neg.tag == "-s"
|
76
79
|
|
77
|
-
|
78
|
-
|
79
|
-
with pep_builder.make_context("test"):
|
80
|
-
s1 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s1"])
|
81
|
-
s2 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s2"])
|
80
|
+
s_truediv = s / 0.1
|
81
|
+
assert s_truediv.tag == "1/0.1*s"
|
82
82
|
|
83
|
-
s_add_mul = (s1 + s2) * 0.1
|
84
|
-
assert s_add_mul.tag == "(s1+s2)*0.1"
|
85
83
|
|
86
|
-
|
87
|
-
|
84
|
+
def test_scalar_add_and_mul_tag(pep_context: pc.PEPContext):
|
85
|
+
s1 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s1"])
|
86
|
+
s2 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s2"])
|
88
87
|
|
89
|
-
|
90
|
-
|
88
|
+
s_add_mul = (s1 + s2) * 0.1
|
89
|
+
assert s_add_mul.tag == "(s1+s2)*0.1"
|
91
90
|
|
92
|
-
|
93
|
-
|
91
|
+
s_add_mul = s1 + s2 * 0.1
|
92
|
+
assert s_add_mul.tag == "s1+s2*0.1"
|
94
93
|
|
94
|
+
s_neg_add = -(s1 + s2)
|
95
|
+
assert s_neg_add.tag == "-(s1+s2)"
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
97
|
+
s_rmul_add = 0.1 * (s1 + s2)
|
98
|
+
assert s_rmul_add.tag == "0.1*(s1+s2)"
|
99
|
+
|
100
|
+
|
101
|
+
def test_scalar_hash_different(pep_context: pc.PEPContext):
|
102
|
+
s1 = scalar.Scalar(is_basis=True, eval_expression=None)
|
103
|
+
s2 = scalar.Scalar(is_basis=True, eval_expression=None)
|
101
104
|
assert s1.uid != s2.uid
|
102
105
|
|
103
106
|
|
104
|
-
def test_scalar_tag():
|
105
|
-
|
106
|
-
|
107
|
-
s1 = scalar.Scalar(is_basis=True, eval_expression=None)
|
108
|
-
s1.add_tag(tag="my_tag")
|
107
|
+
def test_scalar_tag(pep_context: pc.PEPContext):
|
108
|
+
s1 = scalar.Scalar(is_basis=True, eval_expression=None)
|
109
|
+
s1.add_tag(tag="my_tag")
|
109
110
|
assert s1.tags == ["my_tag"]
|
110
111
|
assert s1.tag == "my_tag"
|
111
112
|
|
112
113
|
|
113
|
-
def test_scalar_repr():
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
s1.add_tag("my_tag")
|
119
|
-
assert str(s1) == "my_tag"
|
114
|
+
def test_scalar_repr(pep_context: pc.PEPContext):
|
115
|
+
s1 = scalar.Scalar(is_basis=True, tags=["s1"])
|
116
|
+
print(s1) # it should be fine without tag
|
117
|
+
s1.add_tag("my_tag")
|
118
|
+
assert str(s1) == "my_tag"
|
120
119
|
|
121
120
|
|
122
|
-
def test_scalar_in_a_list():
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
s2 = scalar.Scalar(is_basis=True, eval_expression=None)
|
127
|
-
s3 = scalar.Scalar(is_basis=True, eval_expression=None)
|
121
|
+
def test_scalar_in_a_list(pep_context: pc.PEPContext):
|
122
|
+
s1 = scalar.Scalar(is_basis=True, eval_expression=None)
|
123
|
+
s2 = scalar.Scalar(is_basis=True, eval_expression=None)
|
124
|
+
s3 = scalar.Scalar(is_basis=True, eval_expression=None)
|
128
125
|
assert s1 in [s1, s2]
|
129
126
|
assert s3 not in [s1, s2]
|
130
127
|
|
131
128
|
|
132
|
-
def test_expression_manager_on_basis_scalar():
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
s2 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s2"])
|
137
|
-
pm = exm.ExpressionManager(ctx)
|
129
|
+
def test_expression_manager_on_basis_scalar(pep_context: pc.PEPContext):
|
130
|
+
s1 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s1"])
|
131
|
+
s2 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s2"])
|
132
|
+
pm = exm.ExpressionManager(pep_context)
|
138
133
|
|
139
|
-
|
140
|
-
|
134
|
+
np.testing.assert_allclose(pm.eval_scalar(s1).vector, np.array([1, 0]))
|
135
|
+
np.testing.assert_allclose(pm.eval_scalar(s2).vector, np.array([0, 1]))
|
141
136
|
|
142
|
-
|
143
|
-
|
137
|
+
s3 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s3"]) # noqa: F841
|
138
|
+
pm = exm.ExpressionManager(pep_context)
|
144
139
|
|
145
|
-
|
146
|
-
|
140
|
+
np.testing.assert_allclose(pm.eval_scalar(s1).vector, np.array([1, 0, 0]))
|
141
|
+
np.testing.assert_allclose(pm.eval_scalar(s2).vector, np.array([0, 1, 0]))
|
147
142
|
|
148
143
|
|
149
|
-
def test_expression_manager_eval_scalar():
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
s4 = s3 + s1
|
156
|
-
s5 = s4 + 5
|
144
|
+
def test_expression_manager_eval_scalar(pep_context: pc.PEPContext):
|
145
|
+
s1 = scalar.Scalar(is_basis=True, tags=["s1"])
|
146
|
+
s2 = scalar.Scalar(is_basis=True, tags=["s2"])
|
147
|
+
s3 = 2 * s1 + s2 / 4 + 5
|
148
|
+
s4 = s3 + s1
|
149
|
+
s5 = s4 + 5
|
157
150
|
|
158
|
-
|
159
|
-
|
160
|
-
|
151
|
+
p1 = point.Point(is_basis=True, tags=["p1"])
|
152
|
+
p2 = point.Point(is_basis=True, tags=["p2"])
|
153
|
+
s6 = p1 * p2
|
161
154
|
|
162
|
-
|
163
|
-
|
164
|
-
|
155
|
+
p3 = point.Point(is_basis=True, tags=["p3"])
|
156
|
+
p4 = point.Point(is_basis=True, tags=["p4"])
|
157
|
+
s7 = 5 * p3 * p4
|
165
158
|
|
166
|
-
|
159
|
+
s8 = s6 + s7
|
167
160
|
|
168
|
-
|
161
|
+
pm = exm.ExpressionManager(pep_context)
|
169
162
|
|
170
163
|
np.testing.assert_allclose(pm.eval_scalar(s3).vector, np.array([2, 0.25]))
|
171
164
|
np.testing.assert_allclose(pm.eval_scalar(s3).constant, 5)
|
@@ -214,37 +207,16 @@ def test_expression_manager_eval_scalar():
|
|
214
207
|
)
|
215
208
|
|
216
209
|
|
217
|
-
def
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
s2 = scalar.Scalar(is_basis=True, tags=["s2"])
|
222
|
-
s3 = 2 * s1 + s2 / 4 + 5
|
223
|
-
|
224
|
-
c1 = s3.le(5, name="c1")
|
225
|
-
c2 = s3.lt(5, name="c2")
|
226
|
-
c3 = s3.ge(5, name="c3")
|
227
|
-
c4 = s3.gt(5, name="c4")
|
228
|
-
c5 = s3.eq(5, name="c5")
|
229
|
-
|
230
|
-
pm = exm.ExpressionManager(ctx)
|
231
|
-
|
232
|
-
np.testing.assert_allclose(pm.eval_scalar(c1.scalar).vector, np.array([2, 0.25]))
|
233
|
-
np.testing.assert_allclose(pm.eval_scalar(c1.scalar).constant, 0)
|
234
|
-
assert c1.comparator == utils.Comparator.LT
|
235
|
-
|
236
|
-
np.testing.assert_allclose(pm.eval_scalar(c2.scalar).vector, np.array([2, 0.25]))
|
237
|
-
np.testing.assert_allclose(pm.eval_scalar(c2.scalar).constant, 0)
|
238
|
-
assert c2.comparator == utils.Comparator.LT
|
239
|
-
|
240
|
-
np.testing.assert_allclose(pm.eval_scalar(c3.scalar).vector, np.array([2, 0.25]))
|
241
|
-
np.testing.assert_allclose(pm.eval_scalar(c3.scalar).constant, 0)
|
242
|
-
assert c3.comparator == utils.Comparator.GT
|
210
|
+
def test_zero_scalar(pep_context):
|
211
|
+
_ = scalar.Scalar(is_basis=True, tags=["s1"])
|
212
|
+
_ = point.Point(is_basis=True, tags=["p1"])
|
213
|
+
s0 = scalar.Scalar.zero()
|
243
214
|
|
244
|
-
|
245
|
-
np.testing.assert_allclose(pm.eval_scalar(
|
246
|
-
|
215
|
+
pm = exm.ExpressionManager(pep_context)
|
216
|
+
np.testing.assert_allclose(pm.eval_scalar(s0).vector, np.array([0]))
|
217
|
+
np.testing.assert_allclose(pm.eval_scalar(s0).matrix, np.array([[0]]))
|
247
218
|
|
248
|
-
|
249
|
-
|
250
|
-
|
219
|
+
_ = point.Point(is_basis=True, tags=["p2"])
|
220
|
+
pm = exm.ExpressionManager(pep_context)
|
221
|
+
np.testing.assert_allclose(pm.eval_scalar(s0).vector, np.array([0]))
|
222
|
+
np.testing.assert_allclose(pm.eval_scalar(s0).matrix, np.array([[0, 0], [0, 0]]))
|
pepflow/solver.py
CHANGED
@@ -46,6 +46,14 @@ def evaled_scalar_to_cvx_express(
|
|
46
46
|
|
47
47
|
|
48
48
|
class DualVariableManager:
|
49
|
+
"""
|
50
|
+
A class to access the dual variables associated with the constraints
|
51
|
+
of the Primal PEP. Should not be instantiated directly. Automatically
|
52
|
+
generated as a member variable of the :class:`PEPResult` object
|
53
|
+
returned when calling :py:func:`pepflow.PEPBuilder.solve_primal`.
|
54
|
+
"""
|
55
|
+
|
56
|
+
# It is used in the primal PEP to get the dual variables.
|
49
57
|
def __init__(self, named_constraints: list[tuple[str, cvxpy.Constraint]]):
|
50
58
|
self.named_constraints = {}
|
51
59
|
for name, c in named_constraints:
|
@@ -63,6 +71,19 @@ class DualVariableManager:
|
|
63
71
|
self.named_constraints[name] = constraint
|
64
72
|
|
65
73
|
def dual_value(self, name: str) -> float | None:
|
74
|
+
"""
|
75
|
+
Given the name of a :class:`Constraint` object that represents a
|
76
|
+
constraint in Primal PEP, return the value of its corresponding
|
77
|
+
dual variable.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
name (str): The name of the :class:`Constraint` object whose
|
81
|
+
associated dual variable we want to retrieve.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
float: The value of the dual variable corresponding to the
|
85
|
+
:class:`Constraint` object associated with the `name` argument.
|
86
|
+
"""
|
66
87
|
if name not in self.named_constraints:
|
67
88
|
return None # Is this good choice?
|
68
89
|
dual_value = self.named_constraints[name].dual_value
|
@@ -71,7 +92,65 @@ class DualVariableManager:
|
|
71
92
|
return dual_value
|
72
93
|
|
73
94
|
|
74
|
-
class
|
95
|
+
class PrimalVariableManager:
|
96
|
+
"""
|
97
|
+
A class to access the primal variables of the Dual PEP. The primal
|
98
|
+
variables of the Dual PEP are the dual variables associated with the
|
99
|
+
constraints of the Primal PEP. Should not be instantiated directly.
|
100
|
+
Automatically generated as a member variable of the :class:`PEPResult`
|
101
|
+
object returned when calling :py:func:`pepflow.PEPBuilder.solve_dual`.
|
102
|
+
"""
|
103
|
+
|
104
|
+
# It is used in the dual PEP to get the primal variables.
|
105
|
+
def __init__(self, named_variables: list[tuple[str, cvxpy.Variable]]):
|
106
|
+
self.named_variables = {}
|
107
|
+
for name, v in named_variables:
|
108
|
+
self.add_variable(name, v)
|
109
|
+
|
110
|
+
def cvx_variables(self) -> list[cvxpy.Variable]:
|
111
|
+
return list(self.named_variables.values())
|
112
|
+
|
113
|
+
def clear(self) -> None:
|
114
|
+
self.named_variables.clear()
|
115
|
+
|
116
|
+
def add_variable(self, name: str, variable: cvxpy.Variable) -> None:
|
117
|
+
if name in self.named_variables:
|
118
|
+
raise KeyError(f"There is already a variable named {name}")
|
119
|
+
self.named_variables[name] = variable
|
120
|
+
|
121
|
+
def get_variable(self, name: str) -> cvxpy.Variable:
|
122
|
+
if name not in self.named_variables:
|
123
|
+
raise KeyError(f"Cannot find a variable named {name}")
|
124
|
+
return self.named_variables[name]
|
125
|
+
|
126
|
+
def primal_value(self, name: str) -> float | None:
|
127
|
+
"""
|
128
|
+
The primal variables of the Dual PEP are the dual variables associated
|
129
|
+
with the constraints of the Primal PEP.
|
130
|
+
|
131
|
+
Given the name of a :class:`Constraint` object that represents a
|
132
|
+
constraint in the Primal PEP, return the value of the corresponding
|
133
|
+
primal variable of the Dual PEP.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
name (str): The name of the :class:`Constraint` object whose
|
137
|
+
corresponding primal variable of the Dual PEP we want to
|
138
|
+
retrieve.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
float: The value of the primal variable of the Dual PEP
|
142
|
+
corresponding to the :class:`Constraint` object associated with
|
143
|
+
the `name` argument.
|
144
|
+
"""
|
145
|
+
if name not in self.named_variables:
|
146
|
+
return None # Is this good choice?
|
147
|
+
primal_value = self.named_variables[name].value
|
148
|
+
if primal_value is None:
|
149
|
+
return None
|
150
|
+
return primal_value
|
151
|
+
|
152
|
+
|
153
|
+
class CVXPrimalSolver:
|
75
154
|
def __init__(
|
76
155
|
self,
|
77
156
|
perf_metric: sc.Scalar,
|
@@ -83,8 +162,10 @@ class CVXSolver:
|
|
83
162
|
self.dual_var_manager = DualVariableManager([])
|
84
163
|
self.context = context
|
85
164
|
|
86
|
-
def build_problem(
|
87
|
-
|
165
|
+
def build_problem(
|
166
|
+
self, resolve_parameters: dict[str, utils.NUMERICAL_TYPE] | None = None
|
167
|
+
) -> cvxpy.Problem:
|
168
|
+
em = exm.ExpressionManager(self.context, resolve_parameters=resolve_parameters)
|
88
169
|
f_var = cvxpy.Variable(em._num_basis_scalars)
|
89
170
|
g_var = cvxpy.Variable(
|
90
171
|
(em._num_basis_points, em._num_basis_points), symmetric=True
|
@@ -121,3 +202,89 @@ class CVXSolver:
|
|
121
202
|
problem = self.build_problem()
|
122
203
|
result = problem.solve(**kwargs)
|
123
204
|
return result
|
205
|
+
|
206
|
+
|
207
|
+
class CVXDualSolver:
|
208
|
+
def __init__(
|
209
|
+
self,
|
210
|
+
perf_metric: sc.Scalar,
|
211
|
+
constraints: list[ctr.Constraint],
|
212
|
+
context: pc.PEPContext,
|
213
|
+
):
|
214
|
+
self.perf_metric = perf_metric
|
215
|
+
self.constraints = constraints
|
216
|
+
self.primal_var_manager = PrimalVariableManager([])
|
217
|
+
self.context = context
|
218
|
+
|
219
|
+
def build_problem(
|
220
|
+
self, resolve_parameters: dict[str, utils.NUMERICAL_TYPE] | None = None
|
221
|
+
) -> cvxpy.Problem:
|
222
|
+
# The primal problem is always the following form:
|
223
|
+
#
|
224
|
+
# max_{F, G}: <perf.vec, F> + Tr(G perf.Mat) + perf.const
|
225
|
+
# s.t. <constraint.vec, F> + Tr(G constraint.Mat) + constraint.const <= 0
|
226
|
+
# G >= 0
|
227
|
+
# Caveat: we use max instead of min in primal problem.
|
228
|
+
#
|
229
|
+
# Dual prob = min_{l, S} [max_{F, G} (<perf.vec, F> + Tr(G perf.Mat) - l * (constraint) + Tr(S*G))]
|
230
|
+
# Note the sign above.
|
231
|
+
# Becaus F is unbounded and the Lagrangian w.r.t. F is linear, the coefficients of F must be 0.
|
232
|
+
# Similarly, the Lagrangian w.r.t. G is linear and G is PSD, the coefficients of G must << 0.
|
233
|
+
dual_constraints = []
|
234
|
+
lambd_constraints = []
|
235
|
+
em = exm.ExpressionManager(self.context, resolve_parameters=resolve_parameters)
|
236
|
+
# The one corresponding to G >= 0
|
237
|
+
S = cvxpy.Variable((em._num_basis_points, em._num_basis_points), PSD=True)
|
238
|
+
evaled_perf_metric_scalar = em.eval_scalar(self.perf_metric)
|
239
|
+
|
240
|
+
extra_constraints = []
|
241
|
+
obj = evaled_perf_metric_scalar.constant
|
242
|
+
F_coef_vec = 0
|
243
|
+
G_coef_mat = 0
|
244
|
+
# l * (Tr(G*eval_s.Matrix) + <F, eval_s.vec> + eval_s.const)
|
245
|
+
for c in self.constraints:
|
246
|
+
lambd = cvxpy.Variable()
|
247
|
+
self.primal_var_manager.add_variable(c.name, lambd)
|
248
|
+
evaled_scalar = em.eval_scalar(c.scalar)
|
249
|
+
if c.comparator == utils.Comparator.GT:
|
250
|
+
sign = 1
|
251
|
+
lambd_constraints.append(lambd >= 0)
|
252
|
+
elif c.comparator == utils.Comparator.LT:
|
253
|
+
sign = -1 # We flip f(x) <=0 into -f(x) >= 0
|
254
|
+
lambd_constraints.append(lambd >= 0)
|
255
|
+
elif c.comparator == utils.Comparator.EQ:
|
256
|
+
sign = 1
|
257
|
+
else:
|
258
|
+
raise RuntimeError(
|
259
|
+
f"Unknown comparator in constraint {c.anme}: get {c.comparator=}"
|
260
|
+
)
|
261
|
+
G_coef_mat += sign * lambd * evaled_scalar.matrix
|
262
|
+
F_coef_vec += sign * lambd * evaled_scalar.vector
|
263
|
+
obj += sign * lambd * evaled_scalar.constant
|
264
|
+
|
265
|
+
# We can add extra constraints to directly manipulate the primal variable in dual PEP.
|
266
|
+
for comparator, val in c.associated_dual_var_constraints:
|
267
|
+
if c.comparator == utils.Comparator.GT:
|
268
|
+
extra_constraints.append(lambd >= val)
|
269
|
+
elif c.comparator == utils.Comparator.LT:
|
270
|
+
extra_constraints.append(lambd <= val)
|
271
|
+
elif c.comparator == utils.Comparator.EQ:
|
272
|
+
extra_constraints.append(lambd == val)
|
273
|
+
else:
|
274
|
+
raise RuntimeError(
|
275
|
+
f"Unknown comparator in constraint {c.anme} associated dual one:"
|
276
|
+
f"get {c.comparator=}"
|
277
|
+
)
|
278
|
+
|
279
|
+
dual_constraints.append(F_coef_vec + evaled_perf_metric_scalar.vector == 0)
|
280
|
+
dual_constraints.append(S + evaled_perf_metric_scalar.matrix + G_coef_mat == 0)
|
281
|
+
|
282
|
+
return cvxpy.Problem(
|
283
|
+
cvxpy.Minimize(obj),
|
284
|
+
dual_constraints + lambd_constraints + extra_constraints,
|
285
|
+
)
|
286
|
+
|
287
|
+
def solve(self, **kwargs):
|
288
|
+
problem = self.build_problem()
|
289
|
+
result = problem.solve(**kwargs)
|
290
|
+
return result
|
pepflow/solver_test.py
CHANGED
@@ -32,7 +32,7 @@ def test_cvx_solver_case1():
|
|
32
32
|
s2 = -(1 + p1 * p1)
|
33
33
|
constraints = [(p1 * p1).gt(1, name="x^2 >= 1"), s1.gt(0, name="s1 > 0")]
|
34
34
|
|
35
|
-
solver = ps.
|
35
|
+
solver = ps.CVXPrimalSolver(
|
36
36
|
perf_metric=s2,
|
37
37
|
constraints=constraints,
|
38
38
|
context=pep_builder.get_context("test"),
|
@@ -55,7 +55,7 @@ def test_cvx_solver_case2():
|
|
55
55
|
s2 = -p1 * p1 + 2
|
56
56
|
constraints = [(p1 * p1).lt(1, name="x^2 <= 1"), s1.gt(0, name="s1 > 0")]
|
57
57
|
|
58
|
-
solver = ps.
|
58
|
+
solver = ps.CVXPrimalSolver(
|
59
59
|
perf_metric=s2,
|
60
60
|
constraints=constraints,
|
61
61
|
context=pep_builder.get_context("test"),
|
@@ -68,3 +68,51 @@ def test_cvx_solver_case2():
|
|
68
68
|
|
69
69
|
assert np.isclose(solver.dual_var_manager.dual_value("x^2 <= 1"), 0)
|
70
70
|
assert solver.dual_var_manager.dual_value("s1 > 0") == 0
|
71
|
+
|
72
|
+
|
73
|
+
def test_cvx_dual_solver_case1():
|
74
|
+
pep_builder = pep.PEPBuilder()
|
75
|
+
with pep_builder.make_context("test"):
|
76
|
+
p1 = pp.Point(is_basis=True, tags=["p1"])
|
77
|
+
s1 = pp.Scalar(is_basis=True, tags=["s1"])
|
78
|
+
s2 = -(1 + p1 * p1)
|
79
|
+
constraints = [(p1 * p1).gt(1, name="x^2 >= 1"), s1.gt(0, name="s1 > 0")]
|
80
|
+
|
81
|
+
dual_solver = ps.CVXDualSolver(
|
82
|
+
perf_metric=s2,
|
83
|
+
constraints=constraints,
|
84
|
+
context=pep_builder.get_context("test"),
|
85
|
+
)
|
86
|
+
problem = dual_solver.build_problem()
|
87
|
+
result = problem.solve()
|
88
|
+
assert abs(-result - 2) < 1e-6
|
89
|
+
|
90
|
+
assert np.isclose(dual_solver.primal_var_manager.primal_value("x^2 >= 1"), 1)
|
91
|
+
assert np.isclose(dual_solver.primal_var_manager.primal_value("s1 > 0"), 0)
|
92
|
+
|
93
|
+
|
94
|
+
def test_cvx_dual_solver_case2():
|
95
|
+
pep_builder = pep.PEPBuilder()
|
96
|
+
with pep_builder.make_context("test"):
|
97
|
+
p1 = pp.Point(is_basis=True, tags=["p1"])
|
98
|
+
s1 = pp.Scalar(is_basis=True, tags=["s1"])
|
99
|
+
s2 = -p1 * p1 + 2
|
100
|
+
constraints = [(p1 * p1).lt(1, name="x^2 <= 1"), s1.gt(0, name="s1 > 0")]
|
101
|
+
|
102
|
+
dual_solver = ps.CVXDualSolver(
|
103
|
+
perf_metric=s2,
|
104
|
+
constraints=constraints,
|
105
|
+
context=pep_builder.get_context("test"),
|
106
|
+
)
|
107
|
+
|
108
|
+
# It is a simple `min_x x^2-2; s.t. x^2 <= 1` problem.
|
109
|
+
problem = dual_solver.build_problem()
|
110
|
+
result = problem.solve()
|
111
|
+
assert abs(-result + 2) < 1e-6
|
112
|
+
|
113
|
+
assert np.isclose(
|
114
|
+
dual_solver.primal_var_manager.primal_value("x^2 <= 1"), 0, atol=1e-7
|
115
|
+
)
|
116
|
+
assert np.isclose(
|
117
|
+
dual_solver.primal_var_manager.primal_value("s1 > 0"), 0, atol=1e-7
|
118
|
+
)
|
pepflow/utils.py
CHANGED
@@ -24,6 +24,7 @@ import numbers
|
|
24
24
|
from typing import TYPE_CHECKING, Any
|
25
25
|
|
26
26
|
import numpy as np
|
27
|
+
import sympy as sp
|
27
28
|
|
28
29
|
if TYPE_CHECKING:
|
29
30
|
from pepflow.function import Function
|
@@ -31,13 +32,17 @@ if TYPE_CHECKING:
|
|
31
32
|
from pepflow.scalar import Scalar
|
32
33
|
|
33
34
|
|
34
|
-
|
35
|
+
NUMERICAL_TYPE = numbers.Number | sp.Rational
|
36
|
+
|
37
|
+
|
38
|
+
def SOP(v, w, sympy_mode: bool = False) -> np.ndarray:
|
35
39
|
"""Symmetric Outer Product."""
|
36
|
-
|
40
|
+
coef = sp.S(1) / 2 if sympy_mode else 1 / 2
|
41
|
+
return coef * (np.outer(v, w) + np.outer(w, v))
|
37
42
|
|
38
43
|
|
39
|
-
def SOP_self(v):
|
40
|
-
return SOP(v, v)
|
44
|
+
def SOP_self(v, sympy_mode: bool = False) -> np.ndarray:
|
45
|
+
return SOP(v, v, sympy_mode=sympy_mode)
|
41
46
|
|
42
47
|
|
43
48
|
class Op(enum.Enum):
|
@@ -54,12 +59,39 @@ class Comparator(enum.Enum):
|
|
54
59
|
|
55
60
|
|
56
61
|
def is_numerical(val: Any) -> bool:
|
57
|
-
return isinstance(val, numbers.Number)
|
62
|
+
return isinstance(val, numbers.Number) or isinstance(val, sp.Rational)
|
63
|
+
|
64
|
+
|
65
|
+
def is_numerical_or_parameter(val: Any) -> bool:
|
66
|
+
from pepflow import parameter as param
|
67
|
+
|
68
|
+
return is_numerical(val) or isinstance(val, param.Parameter)
|
69
|
+
|
70
|
+
|
71
|
+
def numerical_str(val: Any) -> str:
|
72
|
+
from pepflow import parameter as param
|
73
|
+
|
74
|
+
if not is_numerical_or_parameter(val):
|
75
|
+
raise ValueError(
|
76
|
+
"Cannot call numerical_str for {val} since it is not numerical."
|
77
|
+
)
|
78
|
+
if isinstance(val, param.Parameter):
|
79
|
+
return str(val)
|
80
|
+
return str(val) if isinstance(val, sp.Rational) else f"{val:.4g}"
|
58
81
|
|
59
82
|
|
60
83
|
def parenthesize_tag(val: Point | Scalar | Function) -> str:
|
61
84
|
tmp_tag = val.tag
|
62
85
|
if not val.is_basis:
|
63
|
-
if
|
64
|
-
|
86
|
+
if op := getattr(val.eval_expression, "op", None):
|
87
|
+
if op in (Op.ADD, Op.SUB):
|
88
|
+
tmp_tag = f"({val.tag})"
|
65
89
|
return tmp_tag
|
90
|
+
|
91
|
+
|
92
|
+
def str_to_latex(s: str) -> str:
|
93
|
+
"""Convert string into latex style."""
|
94
|
+
s = s.replace("star", r"\star")
|
95
|
+
s = s.replace("gradient_", r"\nabla ")
|
96
|
+
s = s.replace("|", r"\|")
|
97
|
+
return rf"$\displaystyle {s}$"
|