pepflow 0.1.0__tar.gz → 0.1.3a1__tar.gz
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-0.1.0 → pepflow-0.1.3a1}/PKG-INFO +7 -2
- pepflow-0.1.3a1/pepflow/__init__.py +50 -0
- pepflow-0.1.3a1/pepflow/constants.py +20 -0
- pepflow-0.1.3a1/pepflow/constraint.py +38 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/expression_manager.py +19 -0
- pepflow-0.1.3a1/pepflow/function.py +366 -0
- pepflow-0.1.3a1/pepflow/function_test.py +134 -0
- pepflow-0.1.3a1/pepflow/interactive_constraint.py +264 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/pep.py +63 -7
- pepflow-0.1.3a1/pepflow/pep_context.py +131 -0
- pepflow-0.1.3a1/pepflow/pep_context_test.py +102 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/pep_test.py +19 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/point.py +42 -1
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/point_test.py +67 -30
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/scalar.py +43 -2
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/solver.py +28 -3
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow/solver_test.py +19 -0
- pepflow-0.1.3a1/pepflow/utils.py +52 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow.egg-info/PKG-INFO +7 -2
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow.egg-info/SOURCES.txt +4 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow.egg-info/requires.txt +5 -0
- pepflow-0.1.3a1/pyproject.toml +30 -0
- pepflow-0.1.0/pepflow/__init__.py +0 -0
- pepflow-0.1.0/pepflow/constraint.py +0 -19
- pepflow-0.1.0/pepflow/function.py +0 -183
- pepflow-0.1.0/pepflow/pep_context.py +0 -30
- pepflow-0.1.0/pepflow/utils.py +0 -51
- pepflow-0.1.0/pyproject.toml +0 -16
- {pepflow-0.1.0 → pepflow-0.1.3a1}/LICENSE +0 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/README.md +0 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow.egg-info/dependency_links.txt +0 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/pepflow.egg-info/top_level.txt +0 -0
- {pepflow-0.1.0 → pepflow-0.1.3a1}/setup.cfg +0 -0
@@ -1,15 +1,20 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pepflow
|
3
|
-
Version: 0.1.
|
4
|
-
Summary:
|
3
|
+
Version: 0.1.3a1
|
4
|
+
Summary: PEPFlow: A framework for Performance Estimation Problem (PEP) Workflow
|
5
5
|
Requires-Python: >=3.9
|
6
6
|
Description-Content-Type: text/markdown
|
7
7
|
License-File: LICENSE
|
8
8
|
Requires-Dist: attrs>=25.3.0
|
9
9
|
Requires-Dist: cvxpy>=1.7.1
|
10
|
+
Requires-Dist: dash>=3.2.0
|
11
|
+
Requires-Dist: dash-bootstrap-components>=2.0.3
|
10
12
|
Requires-Dist: isort>=6.0.1
|
11
13
|
Requires-Dist: matplotlib>=3.9.4
|
14
|
+
Requires-Dist: natsort>=8.4.0
|
12
15
|
Requires-Dist: numpy>=2.0.2
|
16
|
+
Requires-Dist: pandas>=2.3.1
|
17
|
+
Requires-Dist: plotly>=6.3.0
|
13
18
|
Requires-Dist: pytest>=8.4.1
|
14
19
|
Requires-Dist: ruff>=0.12.4
|
15
20
|
Requires-Dist: sympy>=1.14.0
|
@@ -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
|
@@ -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"
|
@@ -0,0 +1,38 @@
|
|
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
|
+
|
22
|
+
from typing import TYPE_CHECKING
|
23
|
+
|
24
|
+
import attrs
|
25
|
+
|
26
|
+
if TYPE_CHECKING:
|
27
|
+
from pepflow.point import Scalar
|
28
|
+
|
29
|
+
from pepflow import utils
|
30
|
+
|
31
|
+
|
32
|
+
@attrs.frozen
|
33
|
+
class Constraint:
|
34
|
+
"""It represents `expression relation 0`."""
|
35
|
+
|
36
|
+
scalar: Scalar | float
|
37
|
+
comparator: utils.Comparator
|
38
|
+
name: str
|
@@ -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
|
@@ -0,0 +1,366 @@
|
|
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
|
+
|
22
|
+
import uuid
|
23
|
+
|
24
|
+
import attrs
|
25
|
+
|
26
|
+
from pepflow import pep_context as pc
|
27
|
+
from pepflow import point as pt
|
28
|
+
from pepflow import scalar as sc
|
29
|
+
from pepflow import utils
|
30
|
+
|
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
|
58
|
+
class Function:
|
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
|
73
|
+
else:
|
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]
|
81
|
+
|
82
|
+
def add_tag(self, tag: str) -> None:
|
83
|
+
self.tags.append(tag)
|
84
|
+
|
85
|
+
def __repr__(self):
|
86
|
+
if self.tags:
|
87
|
+
return self.tag
|
88
|
+
return super().__repr__()
|
89
|
+
|
90
|
+
def get_interpolation_constraints(self):
|
91
|
+
raise NotImplementedError(
|
92
|
+
"This method should be implemented in the children class."
|
93
|
+
)
|
94
|
+
|
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
|
154
|
+
|
155
|
+
def add_stationary_point(self, name: str) -> pt.Point:
|
156
|
+
# assert we can only add one stationary point?
|
157
|
+
point = pt.Point(is_basis=True)
|
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)
|
162
|
+
return point
|
163
|
+
|
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?")
|
181
|
+
|
182
|
+
if self.is_basis:
|
183
|
+
generate_new_basis = True
|
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
|
188
|
+
generate_new_basis = False
|
189
|
+
previous_triplet = triplet
|
190
|
+
|
191
|
+
if generate_new_basis:
|
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)
|
219
|
+
else:
|
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
|
+
)
|
235
|
+
|
236
|
+
return Triplet(point, function_value, gradient, name=None)
|
237
|
+
|
238
|
+
def gradient(self, point: pt.Point) -> pt.Point:
|
239
|
+
triplet = self.generate_triplet(point)
|
240
|
+
return triplet.gradient
|
241
|
+
|
242
|
+
def subgradient(self, point: pt.Point) -> pt.Point:
|
243
|
+
triplet = self.generate_triplet(point)
|
244
|
+
return triplet.gradient
|
245
|
+
|
246
|
+
def function_value(self, point: pt.Point) -> sc.Scalar:
|
247
|
+
triplet = self.generate_triplet(point)
|
248
|
+
return triplet.function_value
|
249
|
+
|
250
|
+
def __add__(self, other):
|
251
|
+
assert isinstance(other, Function)
|
252
|
+
return Function(
|
253
|
+
is_basis=False,
|
254
|
+
reuse_gradient=self.reuse_gradient and other.reuse_gradient,
|
255
|
+
composition=AddedFunc(self, other),
|
256
|
+
tags=[f"{self.tag}+{other.tag}"],
|
257
|
+
)
|
258
|
+
|
259
|
+
def __sub__(self, 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
|
+
)
|
267
|
+
|
268
|
+
def __mul__(self, 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
|
+
)
|
276
|
+
|
277
|
+
def __rmul__(self, other):
|
278
|
+
assert utils.is_numerical(other)
|
279
|
+
return Function(
|
280
|
+
is_basis=False,
|
281
|
+
reuse_gradient=self.reuse_gradient,
|
282
|
+
composition=ScaledFunc(scale=other, base_func=self),
|
283
|
+
tags=[f"{other:.4g}*{self.tag}"],
|
284
|
+
)
|
285
|
+
|
286
|
+
def __neg__(self):
|
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
|
+
)
|
293
|
+
|
294
|
+
def __truediv__(self, other):
|
295
|
+
assert utils.is_numerical(other)
|
296
|
+
return Function(
|
297
|
+
is_basis=False,
|
298
|
+
reuse_gradient=self.reuse_gradient,
|
299
|
+
composition=ScaledFunc(scale=1 / other, base_func=self),
|
300
|
+
tags=[f"1/{other:.4g}*{self.tag}"],
|
301
|
+
)
|
302
|
+
|
303
|
+
def __hash__(self):
|
304
|
+
return hash(self.uid)
|
305
|
+
|
306
|
+
def __eq__(self, other):
|
307
|
+
if not isinstance(other, Function):
|
308
|
+
return NotImplemented
|
309
|
+
return self.uid == other.uid
|
310
|
+
|
311
|
+
|
312
|
+
class SmoothConvexFunction(Function):
|
313
|
+
def __init__(self, L, is_basis=True, composition=None, reuse_gradient=True):
|
314
|
+
super().__init__(
|
315
|
+
is_basis=is_basis, composition=composition, reuse_gradient=reuse_gradient
|
316
|
+
)
|
317
|
+
self.L = L
|
318
|
+
|
319
|
+
def smooth_convex_interpolability_constraints(self, triplet_i, triplet_j):
|
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
|
329
|
+
cross_term = grad_j * (point_i - point_j)
|
330
|
+
quad_term = 1 / (2 * self.L) * (grad_i - grad_j) ** 2
|
331
|
+
|
332
|
+
return (func_diff + cross_term + quad_term).le(
|
333
|
+
0, name=f"{self.tag}:{point_i.tag},{point_j.tag}"
|
334
|
+
)
|
335
|
+
|
336
|
+
def get_interpolation_constraints(self, pep_context: pc.PEPContext | None = None):
|
337
|
+
interpolation_constraints = []
|
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]:
|
344
|
+
if i == j:
|
345
|
+
continue
|
346
|
+
interpolation_constraints.append(
|
347
|
+
self.smooth_convex_interpolability_constraints(i, j)
|
348
|
+
)
|
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
|
@@ -0,0 +1,134 @@
|
|
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
|
+
import numpy as np
|
21
|
+
|
22
|
+
from pepflow import expression_manager as exm
|
23
|
+
from pepflow import function as fc
|
24
|
+
from pepflow import pep as pep
|
25
|
+
|
26
|
+
|
27
|
+
def test_function_repr():
|
28
|
+
pep_builder = pep.PEPBuilder()
|
29
|
+
with pep_builder.make_context("test"):
|
30
|
+
f = fc.Function(is_basis=True, reuse_gradient=False)
|
31
|
+
print(f) # it should be fine without tag
|
32
|
+
f.add_tag("f")
|
33
|
+
assert str(f) == "f"
|
34
|
+
|
35
|
+
|
36
|
+
def test_stationary_point():
|
37
|
+
pep_builder = pep.PEPBuilder()
|
38
|
+
with pep_builder.make_context("test") as ctx:
|
39
|
+
f = fc.Function(is_basis=True, reuse_gradient=False, tags=["f"])
|
40
|
+
f.add_stationary_point("x_star")
|
41
|
+
|
42
|
+
assert len(ctx.triplets) == 1
|
43
|
+
assert len(ctx.triplets[f]) == 1
|
44
|
+
|
45
|
+
f_triplet = ctx.triplets[f][0]
|
46
|
+
assert f_triplet.name == "x_star_f(x_star)_gradient_f(x_star)"
|
47
|
+
assert f_triplet.gradient.tag == "gradient_f(x_star)"
|
48
|
+
assert f_triplet.function_value.tag == "f(x_star)"
|
49
|
+
|
50
|
+
em = exm.ExpressionManager(ctx)
|
51
|
+
np.testing.assert_allclose(
|
52
|
+
em.eval_point(f_triplet.gradient).vector, np.array([0])
|
53
|
+
)
|
54
|
+
np.testing.assert_allclose(em.eval_point(f_triplet.point).vector, np.array([1]))
|
55
|
+
|
56
|
+
|
57
|
+
def test_stationary_point_scaled():
|
58
|
+
pep_builder = pep.PEPBuilder()
|
59
|
+
with pep_builder.make_context("test") as ctx:
|
60
|
+
f = fc.Function(is_basis=True, reuse_gradient=False, tags=["f"])
|
61
|
+
g = 5 * f
|
62
|
+
g.add_stationary_point("x_star")
|
63
|
+
|
64
|
+
assert len(ctx.triplets) == 1
|
65
|
+
assert len(ctx.triplets[f]) == 1
|
66
|
+
|
67
|
+
f_triplet = ctx.triplets[f][0]
|
68
|
+
assert f_triplet.name == "x_star_f(x_star)_gradient_f(x_star)"
|
69
|
+
assert f_triplet.gradient.tag == "gradient_f(x_star)"
|
70
|
+
assert f_triplet.function_value.tag == "f(x_star)"
|
71
|
+
|
72
|
+
em = exm.ExpressionManager(ctx)
|
73
|
+
np.testing.assert_allclose(
|
74
|
+
em.eval_point(f_triplet.gradient).vector, np.array([0])
|
75
|
+
)
|
76
|
+
np.testing.assert_allclose(em.eval_point(f_triplet.point).vector, np.array([1]))
|
77
|
+
|
78
|
+
|
79
|
+
def test_stationary_point_additive():
|
80
|
+
pep_builder = pep.PEPBuilder()
|
81
|
+
with pep_builder.make_context("test") as ctx:
|
82
|
+
f = fc.Function(is_basis=True, reuse_gradient=False)
|
83
|
+
f.add_tag("f")
|
84
|
+
g = fc.Function(is_basis=True, reuse_gradient=False)
|
85
|
+
g.add_tag("g")
|
86
|
+
h = f + g
|
87
|
+
h.add_tag("h")
|
88
|
+
|
89
|
+
h.add_stationary_point("x_star")
|
90
|
+
assert len(ctx.triplets) == 2
|
91
|
+
assert len(ctx.triplets[f]) == 1
|
92
|
+
assert len(ctx.triplets[g]) == 1
|
93
|
+
|
94
|
+
f_triplet = ctx.triplets[f][0]
|
95
|
+
g_triplet = ctx.triplets[g][0]
|
96
|
+
assert f_triplet.name == "x_star_f(x_star)_gradient_f(x_star)"
|
97
|
+
assert g_triplet.name == "x_star_g(x_star)_gradient_g(x_star)"
|
98
|
+
|
99
|
+
em = exm.ExpressionManager(ctx)
|
100
|
+
np.testing.assert_allclose(
|
101
|
+
em.eval_point(f_triplet.gradient).vector, np.array([0, 1])
|
102
|
+
)
|
103
|
+
np.testing.assert_allclose(
|
104
|
+
em.eval_point(g_triplet.gradient).vector, np.array([0, -1])
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
def test_stationary_point_linear_combination():
|
109
|
+
pep_builder = pep.PEPBuilder()
|
110
|
+
with pep_builder.make_context("test") as ctx:
|
111
|
+
f = fc.Function(is_basis=True, reuse_gradient=False)
|
112
|
+
f.add_tag("f")
|
113
|
+
g = fc.Function(is_basis=True, reuse_gradient=False)
|
114
|
+
g.add_tag("g")
|
115
|
+
h = 3 * f + 2 * g
|
116
|
+
h.add_tag("h")
|
117
|
+
|
118
|
+
h.add_stationary_point("x_star")
|
119
|
+
assert len(ctx.triplets) == 2
|
120
|
+
assert len(ctx.triplets[f]) == 1
|
121
|
+
assert len(ctx.triplets[g]) == 1
|
122
|
+
|
123
|
+
f_triplet = ctx.triplets[f][0]
|
124
|
+
g_triplet = ctx.triplets[g][0]
|
125
|
+
assert f_triplet.name == "x_star_f(x_star)_gradient_f(x_star)"
|
126
|
+
assert g_triplet.name == "x_star_g(x_star)_gradient_g(x_star)"
|
127
|
+
|
128
|
+
em = exm.ExpressionManager(ctx)
|
129
|
+
np.testing.assert_allclose(
|
130
|
+
em.eval_point(f_triplet.gradient).vector, np.array([0, 1])
|
131
|
+
)
|
132
|
+
np.testing.assert_allclose(
|
133
|
+
em.eval_point(g_triplet.gradient).vector, np.array([0, -1.5])
|
134
|
+
)
|