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 +50 -0
- pepflow/constants.py +20 -0
- pepflow/constraint.py +19 -0
- pepflow/expression_manager.py +19 -0
- pepflow/function.py +273 -90
- pepflow/function_test.py +134 -0
- pepflow/interactive_constraint.py +264 -0
- pepflow/pep.py +63 -7
- pepflow/pep_context.py +107 -6
- pepflow/pep_context_test.py +102 -0
- pepflow/pep_test.py +19 -0
- pepflow/point.py +42 -1
- pepflow/point_test.py +67 -30
- pepflow/scalar.py +43 -2
- pepflow/solver.py +28 -3
- pepflow/solver_test.py +19 -0
- pepflow/utils.py +19 -18
- {pepflow-0.1.0.dist-info → pepflow-0.1.3a1.dist-info}/METADATA +7 -2
- pepflow-0.1.3a1.dist-info/RECORD +22 -0
- pepflow-0.1.0.dist-info/RECORD +0 -18
- {pepflow-0.1.0.dist-info → pepflow-0.1.3a1.dist-info}/WHEEL +0 -0
- {pepflow-0.1.0.dist-info → pepflow-0.1.3a1.dist-info}/licenses/LICENSE +0 -0
- {pepflow-0.1.0.dist-info → pepflow-0.1.3a1.dist-info}/top_level.txt +0 -0
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
|
pepflow/expression_manager.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
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
self.
|
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
|
30
|
-
|
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
|
34
|
-
|
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
|
42
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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,
|
236
|
+
return Triplet(point, function_value, gradient, name=None)
|
81
237
|
|
82
238
|
def gradient(self, point: pt.Point) -> pt.Point:
|
83
|
-
|
84
|
-
return
|
239
|
+
triplet = self.generate_triplet(point)
|
240
|
+
return triplet.gradient
|
85
241
|
|
86
242
|
def subgradient(self, point: pt.Point) -> pt.Point:
|
87
|
-
|
88
|
-
return
|
243
|
+
triplet = self.generate_triplet(point)
|
244
|
+
return triplet.gradient
|
89
245
|
|
90
246
|
def function_value(self, point: pt.Point) -> sc.Scalar:
|
91
|
-
|
92
|
-
return
|
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=
|
102
|
-
tag
|
255
|
+
composition=AddedFunc(self, other),
|
256
|
+
tags=[f"{self.tag}+{other.tag}"],
|
103
257
|
)
|
104
258
|
|
105
259
|
def __sub__(self, other):
|
106
|
-
|
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
|
-
|
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=
|
121
|
-
tag
|
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
|
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=
|
137
|
-
tag
|
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
|
158
|
-
|
159
|
-
|
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
|
-
|
175
|
-
|
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
|