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/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 point, scalar, utils
27
+ from pepflow import pep_context as pc
28
+ from pepflow import point, scalar
26
29
 
27
30
 
28
- def test_scalar_add_tag():
29
- pep_builder = pep.PEPBuilder()
30
- with pep_builder.make_context("test"):
31
- s1 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s1"])
32
- s2 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s2"])
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
- s_add = s1 + 0.1
38
- assert s_add.tag == "s1+0.1"
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
- s_radd = 0.1 + s1
41
- assert s_radd.tag == "0.1+s1"
43
+ s_add = s1 + s2
44
+ assert s_add.tag == "s1+s2"
42
45
 
43
- s_sub = s1 - s2
44
- assert s_sub.tag == "s1-s2"
46
+ s_add = s1 + 0.1
47
+ assert s_add.tag == "s1+0.1"
45
48
 
46
- s_sub = s1 - (s2 + s1)
47
- assert s_sub.tag == "s1-(s2+s1)"
49
+ s_radd = 0.1 + s1
50
+ assert s_radd.tag == "0.1+s1"
48
51
 
49
- s_sub = s1 - (s2 - s1)
50
- assert s_sub.tag == "s1-(s2-s1)"
52
+ s_sub = s1 - s2
53
+ assert s_sub.tag == "s1-s2"
51
54
 
52
- s_sub = s1 - 0.1
53
- assert s_sub.tag == "s1-0.1"
55
+ s_sub = s1 - (s2 + s1)
56
+ assert s_sub.tag == "s1-(s2+s1)"
54
57
 
55
- s_rsub = 0.1 - s1
56
- assert s_rsub.tag == "0.1-s1"
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
- def test_scalar_mul_tag():
60
- pep_builder = pep.PEPBuilder()
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
- s_rmul = 0.1 * s
68
- assert s_rmul.tag == "0.1*s"
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
- s_neg = -s
71
- assert s_neg.tag == "-s"
71
+ s_mul = s * 0.1
72
+ assert s_mul.tag == "s*0.1"
72
73
 
73
- s_truediv = s / 0.1
74
- assert s_truediv.tag == "1/0.1*s"
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
- def test_scalar_add_and_mul_tag():
78
- pep_builder = pep.PEPBuilder()
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
- s_add_mul = s1 + s2 * 0.1
87
- assert s_add_mul.tag == "s1+s2*0.1"
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
- s_neg_add = -(s1 + s2)
90
- assert s_neg_add.tag == "-(s1+s2)"
88
+ s_add_mul = (s1 + s2) * 0.1
89
+ assert s_add_mul.tag == "(s1+s2)*0.1"
91
90
 
92
- s_rmul_add = 0.1 * (s1 + s2)
93
- assert s_rmul_add.tag == "0.1*(s1+s2)"
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
- def test_scalar_hash_different():
97
- pep_builder = pep.PEPBuilder()
98
- with pep_builder.make_context("test"):
99
- s1 = scalar.Scalar(is_basis=True, eval_expression=None)
100
- s2 = scalar.Scalar(is_basis=True, eval_expression=None)
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
- pep_builder = pep.PEPBuilder()
106
- with pep_builder.make_context("test"):
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
- pep_builder = pep.PEPBuilder()
115
- with pep_builder.make_context("test"):
116
- s1 = scalar.Scalar(is_basis=True, tags=["s1"])
117
- print(s1) # it should be fine without tag
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
- pep_builder = pep.PEPBuilder()
124
- with pep_builder.make_context("test"):
125
- s1 = scalar.Scalar(is_basis=True, eval_expression=None)
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
- pep_builder = pep.PEPBuilder()
134
- with pep_builder.make_context("test") as ctx:
135
- s1 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s1"])
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
- np.testing.assert_allclose(pm.eval_scalar(s1).vector, np.array([1, 0]))
140
- np.testing.assert_allclose(pm.eval_scalar(s2).vector, np.array([0, 1]))
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
- s3 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s3"]) # noqa: F841
143
- pm = exm.ExpressionManager(ctx)
137
+ s3 = scalar.Scalar(is_basis=True, eval_expression=None, tags=["s3"]) # noqa: F841
138
+ pm = exm.ExpressionManager(pep_context)
144
139
 
145
- np.testing.assert_allclose(pm.eval_scalar(s1).vector, np.array([1, 0, 0]))
146
- np.testing.assert_allclose(pm.eval_scalar(s2).vector, np.array([0, 1, 0]))
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
- pep_builder = pep.PEPBuilder()
151
- with pep_builder.make_context("test") as ctx:
152
- s1 = scalar.Scalar(is_basis=True, tags=["s1"])
153
- s2 = scalar.Scalar(is_basis=True, tags=["s2"])
154
- s3 = 2 * s1 + s2 / 4 + 5
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
- p1 = point.Point(is_basis=True, tags=["p1"])
159
- p2 = point.Point(is_basis=True, tags=["p2"])
160
- s6 = p1 * p2
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
- p3 = point.Point(is_basis=True, tags=["p3"])
163
- p4 = point.Point(is_basis=True, tags=["p4"])
164
- s7 = 5 * p3 * p4
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
- s8 = s6 + s7
159
+ s8 = s6 + s7
167
160
 
168
- pm = exm.ExpressionManager(ctx)
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 test_constraint():
218
- pep_builder = pep.PEPBuilder()
219
- with pep_builder.make_context("test") as ctx:
220
- s1 = scalar.Scalar(is_basis=True, tags=["s1"])
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
- np.testing.assert_allclose(pm.eval_scalar(c4.scalar).vector, np.array([2, 0.25]))
245
- np.testing.assert_allclose(pm.eval_scalar(c4.scalar).constant, 0)
246
- assert c4.comparator == utils.Comparator.GT
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
- np.testing.assert_allclose(pm.eval_scalar(c5.scalar).vector, np.array([2, 0.25]))
249
- np.testing.assert_allclose(pm.eval_scalar(c5.scalar).constant, 0)
250
- assert c5.comparator == utils.Comparator.EQ
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 CVXSolver:
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(self) -> cvxpy.Problem:
87
- em = exm.ExpressionManager(self.context)
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.CVXSolver(
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.CVXSolver(
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
- def SOP(v, w):
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
- return 1 / 2 * (np.outer(v, w) + np.outer(w, v))
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 val.eval_expression.op == Op.ADD or val.eval_expression.op == Op.SUB:
64
- tmp_tag = f"({val.tag})"
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}$"