pepflow 0.1.0__py3-none-any.whl → 0.1.3a1__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 CHANGED
@@ -0,0 +1,50 @@
1
+ # Copyright: 2025 The PEPFlow Developers
2
+ #
3
+ # Licensed to the Apache Software Foundation (ASF) under one
4
+ # or more contributor license agreements. See the NOTICE file
5
+ # distributed with this work for additional information
6
+ # regarding copyright ownership. The ASF licenses this file
7
+ # to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance
9
+ # with the License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing,
14
+ # software distributed under the License is distributed on an
15
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ # KIND, either express or implied. See the License for the
17
+ # specific language governing permissions and limitations
18
+ # under the License.
19
+
20
+ # isort: skip_file
21
+ from .constants import PSD_CONSTRAINT as PSD_CONSTRAINT
22
+ from .constraint import Constraint as Constraint
23
+ from .expression_manager import ExpressionManager as ExpressionManager
24
+
25
+ # interactive_constraint
26
+ from .interactive_constraint import launch as launch
27
+
28
+ # pep
29
+ from .pep import PEPBuilder as PEPBuilder
30
+ from .pep import PEPResult as PEPResult
31
+ from .pep_context import PEPContext as PEPContext
32
+ from .pep_context import get_current_context as get_current_context
33
+ from .pep_context import set_current_context as set_current_context
34
+
35
+ # Function, Point, Scalar
36
+ from .function import Function as Function
37
+ from .function import SmoothConvexFunction as SmoothConvexFunction
38
+ from .function import Triplet as Triplet
39
+ from .point import EvaluatedPoint as EvaluatedPoint
40
+ from .point import Point as Point
41
+ from .scalar import EvaluatedScalar as EvaluatedScalar
42
+ from .scalar import Scalar as Scalar
43
+
44
+ # Solver
45
+ from .solver import CVXSolver as CVXSolver
46
+ from .solver import DualVariableManager as DualVariableManager
47
+
48
+ # Others
49
+ from .utils import SOP as SOP
50
+ from .utils import SOP_self as SOP_self
pepflow/constants.py ADDED
@@ -0,0 +1,20 @@
1
+ # Copyright: 2025 The PEPFlow Developers
2
+ #
3
+ # Licensed to the Apache Software Foundation (ASF) under one
4
+ # or more contributor license agreements. See the NOTICE file
5
+ # distributed with this work for additional information
6
+ # regarding copyright ownership. The ASF licenses this file
7
+ # to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance
9
+ # with the License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing,
14
+ # software distributed under the License is distributed on an
15
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ # KIND, either express or implied. See the License for the
17
+ # specific language governing permissions and limitations
18
+ # under the License.
19
+
20
+ PSD_CONSTRAINT = "PSD of Grammian Matrix"
pepflow/constraint.py CHANGED
@@ -1,3 +1,22 @@
1
+ # Copyright: 2025 The PEPFlow Developers
2
+ #
3
+ # Licensed to the Apache Software Foundation (ASF) under one
4
+ # or more contributor license agreements. See the NOTICE file
5
+ # distributed with this work for additional information
6
+ # regarding copyright ownership. The ASF licenses this file
7
+ # to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance
9
+ # with the License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing,
14
+ # software distributed under the License is distributed on an
15
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ # KIND, either express or implied. See the License for the
17
+ # specific language governing permissions and limitations
18
+ # under the License.
19
+
1
20
  from __future__ import annotations
2
21
 
3
22
  from typing import TYPE_CHECKING
@@ -1,3 +1,22 @@
1
+ # Copyright: 2025 The PEPFlow Developers
2
+ #
3
+ # Licensed to the Apache Software Foundation (ASF) under one
4
+ # or more contributor license agreements. See the NOTICE file
5
+ # distributed with this work for additional information
6
+ # regarding copyright ownership. The ASF licenses this file
7
+ # to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance
9
+ # with the License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing,
14
+ # software distributed under the License is distributed on an
15
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ # KIND, either express or implied. See the License for the
17
+ # specific language governing permissions and limitations
18
+ # under the License.
19
+
1
20
  import functools
2
21
 
3
22
  import numpy as np
pepflow/function.py CHANGED
@@ -1,140 +1,303 @@
1
+ # Copyright: 2025 The PEPFlow Developers
2
+ #
3
+ # Licensed to the Apache Software Foundation (ASF) under one
4
+ # or more contributor license agreements. See the NOTICE file
5
+ # distributed with this work for additional information
6
+ # regarding copyright ownership. The ASF licenses this file
7
+ # to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance
9
+ # with the License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing,
14
+ # software distributed under the License is distributed on an
15
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ # KIND, either express or implied. See the License for the
17
+ # specific language governing permissions and limitations
18
+ # under the License.
19
+
20
+ from __future__ import annotations
21
+
1
22
  import uuid
2
23
 
3
24
  import attrs
4
25
 
26
+ from pepflow import pep_context as pc
5
27
  from pepflow import point as pt
6
28
  from pepflow import scalar as sc
7
29
  from pepflow import utils
8
30
 
9
31
 
32
+ @attrs.frozen
33
+ class Triplet:
34
+ point: pt.Point
35
+ function_value: sc.Scalar
36
+ gradient: pt.Point
37
+ name: str | None
38
+ uid: uuid.UUID = attrs.field(factory=uuid.uuid4, init=False)
39
+
40
+
41
+ @attrs.frozen
42
+ class AddedFunc:
43
+ """Represents left_func + right_func."""
44
+
45
+ left_func: Function
46
+ right_func: Function
47
+
48
+
49
+ @attrs.frozen
50
+ class ScaledFunc:
51
+ """Represents scale * base_func."""
52
+
53
+ scale: float
54
+ base_func: Function
55
+
56
+
57
+ @attrs.mutable
10
58
  class Function:
11
- def __init__(
12
- self,
13
- is_basis: bool,
14
- reuse_gradient: bool,
15
- composition: dict | None = None,
16
- tag: str | None = None,
17
- ):
18
- self.is_basis = is_basis
19
- self.reuse_gradient = reuse_gradient
20
- self.tag = tag
21
- self.uid = attrs.field(factory=uuid.uuid4, init=False)
22
- self.triplets = [] #: list[("point", "scalar", "point")] = []
23
- self.constraints = [] #: list["constraint"] = []
24
-
25
- if is_basis:
26
- assert composition is None
27
- self.composition = {self: 1}
59
+ is_basis: bool
60
+ reuse_gradient: bool
61
+
62
+ composition: AddedFunc | ScaledFunc | None = None
63
+
64
+ # Human tagged value for the function
65
+ tags: list[str] = attrs.field(factory=list)
66
+
67
+ # Generate an automatic id
68
+ uid: uuid.UUID = attrs.field(factory=uuid.uuid4, init=False)
69
+
70
+ def __attrs_post_init__(self):
71
+ if self.is_basis:
72
+ assert self.composition is None
28
73
  else:
29
- assert isinstance(composition, dict)
30
- self.composition = composition #: dict[{"function": float})] = []
74
+ assert self.composition is not None
75
+
76
+ @property
77
+ def tag(self):
78
+ if len(self.tags) == 0:
79
+ raise ValueError("Function should have a name.")
80
+ return self.tags[-1]
31
81
 
32
82
  def add_tag(self, tag: str) -> None:
33
- self.tag = tag
34
- return None
83
+ self.tags.append(tag)
84
+
85
+ def __repr__(self):
86
+ if self.tags:
87
+ return self.tag
88
+ return super().__repr__()
35
89
 
36
90
  def get_interpolation_constraints(self):
37
91
  raise NotImplementedError(
38
92
  "This method should be implemented in the children class."
39
93
  )
40
94
 
41
- def add_triplet(self, triplet: tuple) -> None:
42
- return NotImplemented
95
+ def add_triplet_to_func(self, triplet: Triplet) -> None:
96
+ pep_context = pc.get_current_context()
97
+ if pep_context is None:
98
+ raise RuntimeError("Did you forget to create a context?")
99
+ pep_context.triplets[self].append(triplet)
100
+
101
+ def add_point_with_grad_restriction(
102
+ self, point: pt.Point, desired_grad: pt.Point
103
+ ) -> Triplet:
104
+ # todo find a better tagging approach.
105
+ if self.is_basis:
106
+ function_value = sc.Scalar(is_basis=True)
107
+ function_value.add_tag(f"{self.tag}({point.tag})")
108
+ triplet = Triplet(
109
+ point,
110
+ function_value,
111
+ desired_grad,
112
+ name=f"{point.tag}_{function_value.tag}_{desired_grad.tag}",
113
+ )
114
+ self.add_triplet_to_func(triplet)
115
+ else:
116
+ if isinstance(self.composition, AddedFunc):
117
+ left_triplet = self.composition.left_func.generate_triplet(point)
118
+ next_desired_grad = desired_grad - left_triplet.gradient
119
+ next_desired_grad.add_tag(
120
+ f"gradient_{self.composition.right_func.tag}({point.tag})"
121
+ )
122
+ right_triplet = (
123
+ self.composition.right_func.add_point_with_grad_restriction(
124
+ point, next_desired_grad
125
+ )
126
+ )
127
+ triplet = Triplet(
128
+ point,
129
+ left_triplet.function_value + right_triplet.function_value,
130
+ desired_grad,
131
+ name=f"{point.tag}_{self.tag}_{desired_grad.tag}",
132
+ )
133
+ elif isinstance(self.composition, ScaledFunc):
134
+ next_desired_grad = desired_grad / self.composition.scale
135
+ next_desired_grad.add_tag(
136
+ f"gradient_{self.composition.base_func.tag}({point.tag})"
137
+ )
138
+ base_triplet = (
139
+ self.composition.base_func.add_point_with_grad_restriction(
140
+ point, next_desired_grad
141
+ )
142
+ )
143
+ triplet = Triplet(
144
+ point,
145
+ base_triplet.function_value * self.composition.scale,
146
+ desired_grad,
147
+ name=f"{point.tag}_{self.tag}_{desired_grad.tag}",
148
+ )
149
+ else:
150
+ raise ValueError(
151
+ f"Unknown composition of functions: {self.composition}"
152
+ )
153
+ return triplet
43
154
 
44
- def add_stationary_point(self) -> pt.Point:
155
+ def add_stationary_point(self, name: str) -> pt.Point:
156
+ # assert we can only add one stationary point?
45
157
  point = pt.Point(is_basis=True)
46
- _, _, grad = self.generate_triplet(point)
47
- self.constraints.append(
48
- (grad**2).eq(0, name=str(self.__hash__) + " stationary point")
49
- )
158
+ point.add_tag(name)
159
+ desired_grad = 0 * point
160
+ desired_grad.add_tag(f"gradient_{self.tag}({name})")
161
+ self.add_point_with_grad_restriction(point, desired_grad)
50
162
  return point
51
163
 
52
- def generate_triplet(self, point: pt.Point) -> tuple:
53
- func_value = 0
54
- grad = 0
164
+ # The following the old gradient(opt) = 0 constraint style.
165
+ # It is mathamatically correct but hard for solver so we abandon it.
166
+ #
167
+ # triplet = self.generate_triplet(point)
168
+ # pep_context = pc.get_current_context()
169
+ # pep_context.add_opt_condition(
170
+ # self,
171
+ # ((triplet.gradient) ** 2).eq(
172
+ # 0, name=f"{self.tags[0]}({point.tags[0]}) optimality condition"
173
+ # ),
174
+ # )
175
+ # return point
176
+
177
+ def generate_triplet(self, point: pt.Point) -> Triplet:
178
+ pep_context = pc.get_current_context()
179
+ if pep_context is None:
180
+ raise RuntimeError("Did you forget to create a context?")
55
181
 
56
182
  if self.is_basis:
57
183
  generate_new_basis = True
58
- for triplet in self.triplets:
59
- if triplet[0].uid == point.uid and self.reuse_gradient:
60
- func_value = triplet[1]
61
- grad = triplet[2]
184
+ instances_of_point = 0
185
+ for triplet in pep_context.triplets[self]:
186
+ if triplet.point.uid == point.uid:
187
+ instances_of_point += 1
62
188
  generate_new_basis = False
63
- break
64
- elif triplet[0].uid == point.uid and not self.reuse_gradient:
65
- func_value = triplet[1]
66
- grad = pt.Point(is_basis=True)
67
- generate_new_basis = False
68
- self.triplets.append((point, func_value, grad))
69
- break
189
+ previous_triplet = triplet
190
+
70
191
  if generate_new_basis:
71
- func_value = sc.Scalar(is_basis=True)
72
- grad = pt.Point(is_basis=True)
73
- self.triplets.append((point, func_value, grad))
192
+ function_value = sc.Scalar(is_basis=True)
193
+ function_value.add_tag(f"{self.tag}({point.tag})")
194
+ gradient = pt.Point(is_basis=True)
195
+ gradient.add_tag(f"gradient_{self.tag}({point.tag})")
196
+
197
+ new_triplet = Triplet(
198
+ point,
199
+ function_value,
200
+ gradient,
201
+ name=f"{point.tag}_{function_value.tag}_{gradient.tag}",
202
+ )
203
+ self.add_triplet_to_func(new_triplet)
204
+ elif not generate_new_basis and self.reuse_gradient:
205
+ function_value = previous_triplet.function_value
206
+ gradient = previous_triplet.gradient
207
+ elif not generate_new_basis and not self.reuse_gradient:
208
+ function_value = previous_triplet.function_value
209
+ gradient = pt.Point(is_basis=True)
210
+ gradient.add_tag(f"gradient_{self.tag}({point.tag})")
211
+
212
+ new_triplet = Triplet(
213
+ point,
214
+ previous_triplet.function_value,
215
+ gradient,
216
+ name=f"{point.tag}_{function_value.tag}_{gradient.tag}_{instances_of_point}",
217
+ )
218
+ self.add_triplet_to_func(new_triplet)
74
219
  else:
75
- for function, weights in self.composition.items():
76
- _, func_value_slice, grad_slice = function.generate_triplet(point)
77
- func_value += weights * func_value_slice
78
- grad += weights * grad_slice
220
+ if isinstance(self.composition, AddedFunc):
221
+ left_triplet = self.composition.left_func.generate_triplet(point)
222
+ right_triplet = self.composition.right_func.generate_triplet(point)
223
+ function_value = (
224
+ left_triplet.function_value + right_triplet.function_value
225
+ )
226
+ gradient = left_triplet.gradient + right_triplet.gradient
227
+ elif isinstance(self.composition, ScaledFunc):
228
+ base_triplet = self.composition.base_func.generate_triplet(point)
229
+ function_value = self.composition.scale * base_triplet.function_value
230
+ gradient = self.composition.scale * base_triplet.gradient
231
+ else:
232
+ raise ValueError(
233
+ f"Unknown composition of functions: {self.composition}"
234
+ )
79
235
 
80
- return (point, func_value, grad)
236
+ return Triplet(point, function_value, gradient, name=None)
81
237
 
82
238
  def gradient(self, point: pt.Point) -> pt.Point:
83
- _, _, grad = self.generate_triplet(point)
84
- return grad
239
+ triplet = self.generate_triplet(point)
240
+ return triplet.gradient
85
241
 
86
242
  def subgradient(self, point: pt.Point) -> pt.Point:
87
- _, _, subgrad = self.generate_triplet(point)
88
- return subgrad
243
+ triplet = self.generate_triplet(point)
244
+ return triplet.gradient
89
245
 
90
246
  def function_value(self, point: pt.Point) -> sc.Scalar:
91
- _, func_value, _ = self.generate_triplet(point)
92
- return func_value
247
+ triplet = self.generate_triplet(point)
248
+ return triplet.function_value
93
249
 
94
250
  def __add__(self, other):
95
251
  assert isinstance(other, Function)
96
- merged_composition = utils.merge_dict(self.composition, other.composition)
97
- pruned_composition = utils.prune_dict(merged_composition)
98
252
  return Function(
99
253
  is_basis=False,
100
254
  reuse_gradient=self.reuse_gradient and other.reuse_gradient,
101
- composition=pruned_composition,
102
- tag=None,
255
+ composition=AddedFunc(self, other),
256
+ tags=[f"{self.tag}+{other.tag}"],
103
257
  )
104
258
 
105
259
  def __sub__(self, other):
106
- return self.__add__(-other)
260
+ assert isinstance(other, Function)
261
+ return Function(
262
+ is_basis=False,
263
+ reuse_gradient=self.reuse_gradient and other.reuse_gradient,
264
+ composition=AddedFunc(self, -other),
265
+ tags=[f"{self.tag}-{other.tag}"],
266
+ )
107
267
 
108
268
  def __mul__(self, other):
109
- return self.__rmul__(other=other)
269
+ assert utils.is_numerical(other)
270
+ return Function(
271
+ is_basis=False,
272
+ reuse_gradient=self.reuse_gradient,
273
+ composition=ScaledFunc(scale=other, base_func=self),
274
+ tags=[f"{other:.4g}*{self.tag}"],
275
+ )
110
276
 
111
277
  def __rmul__(self, other):
112
278
  assert utils.is_numerical(other)
113
- scaled_composition = dict()
114
- for key, value in self.composition.items():
115
- scaled_composition[key] = value * other
116
- pruned_composition = utils.prune_dict(scaled_composition)
117
279
  return Function(
118
280
  is_basis=False,
119
281
  reuse_gradient=self.reuse_gradient,
120
- composition=pruned_composition,
121
- tag=None,
282
+ composition=ScaledFunc(scale=other, base_func=self),
283
+ tags=[f"{other:.4g}*{self.tag}"],
122
284
  )
123
285
 
124
286
  def __neg__(self):
125
- return self.__mul__(other=-1)
287
+ return Function(
288
+ is_basis=False,
289
+ reuse_gradient=self.reuse_gradient,
290
+ composition=ScaledFunc(scale=-1, base_func=self),
291
+ tags=[f"-{self.tag}"],
292
+ )
126
293
 
127
294
  def __truediv__(self, other):
128
295
  assert utils.is_numerical(other)
129
- scaled_composition = dict()
130
- for key, value in self.composition.items():
131
- scaled_composition[key] = value / other
132
- pruned_composition = utils.prune_dict(scaled_composition)
133
296
  return Function(
134
297
  is_basis=False,
135
298
  reuse_gradient=self.reuse_gradient,
136
- composition=pruned_composition,
137
- tag=None,
299
+ composition=ScaledFunc(scale=1 / other, base_func=self),
300
+ tags=[f"1/{other:.4g}*{self.tag}"],
138
301
  )
139
302
 
140
303
  def __hash__(self):
@@ -154,30 +317,50 @@ class SmoothConvexFunction(Function):
154
317
  self.L = L
155
318
 
156
319
  def smooth_convex_interpolability_constraints(self, triplet_i, triplet_j):
157
- point_i, func_value_i, grad_i = triplet_i
158
- point_j, func_value_j, grad_j = triplet_j
159
- func_diff = func_value_j - func_value_i
320
+ point_i = triplet_i.point
321
+ function_value_i = triplet_i.function_value
322
+ grad_i = triplet_i.gradient
323
+
324
+ point_j = triplet_j.point
325
+ function_value_j = triplet_j.function_value
326
+ grad_j = triplet_j.gradient
327
+
328
+ func_diff = function_value_j - function_value_i
160
329
  cross_term = grad_j * (point_i - point_j)
161
330
  quad_term = 1 / (2 * self.L) * (grad_i - grad_j) ** 2
162
331
 
163
332
  return (func_diff + cross_term + quad_term).le(
164
- 0,
165
- name=str(self.__hash__())
166
- + ":"
167
- + str(point_i.__hash__())
168
- + ","
169
- + str(point_j.__hash__()),
333
+ 0, name=f"{self.tag}:{point_i.tag},{point_j.tag}"
170
334
  )
171
335
 
172
- def get_interpolation_constraints(self):
336
+ def get_interpolation_constraints(self, pep_context: pc.PEPContext | None = None):
173
337
  interpolation_constraints = []
174
- for i in range(len(self.triplets)):
175
- for j in range(len(self.triplets)):
338
+ if pep_context is None:
339
+ pep_context = pc.get_current_context()
340
+ if pep_context is None:
341
+ raise RuntimeError("Did you forget to create a context?")
342
+ for i in pep_context.triplets[self]:
343
+ for j in pep_context.triplets[self]:
176
344
  if i == j:
177
345
  continue
178
346
  interpolation_constraints.append(
179
- self.smooth_convex_interpolability_constraints(
180
- self.triplets[i], self.triplets[j]
181
- )
347
+ self.smooth_convex_interpolability_constraints(i, j)
182
348
  )
183
349
  return interpolation_constraints
350
+
351
+ def interpolate_ineq(
352
+ self, p1_tag: str, p2_tag: str, pep_context: pc.PEPContext | None = None
353
+ ) -> pt.Scalar:
354
+ """Generate the interpolation inequality scalar by tags."""
355
+ if pep_context is None:
356
+ pep_context = pc.get_current_context()
357
+ if pep_context is None:
358
+ raise RuntimeError("Did you forget to specify a context?")
359
+ # TODO: we definitely need a more robust tag system
360
+ x1 = pep_context.get_by_tag(p1_tag)
361
+ x2 = pep_context.get_by_tag(p2_tag)
362
+ f1 = pep_context.get_by_tag(f"{self.tag}({p1_tag})")
363
+ f2 = pep_context.get_by_tag(f"{self.tag}({p2_tag})")
364
+ g1 = pep_context.get_by_tag(f"gradient_{self.tag}({p1_tag})")
365
+ g2 = pep_context.get_by_tag(f"gradient_{self.tag}({p2_tag})")
366
+ return f2 - f1 + g2 * (x1 - x2) + 1 / 2 * (g1 - g2) ** 2