pepflow 0.1.3a1__tar.gz → 0.1.4a1__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.
Files changed (34) hide show
  1. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/PKG-INFO +19 -1
  2. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/README.md +18 -0
  3. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/__init__.py +1 -0
  4. pepflow-0.1.4a1/pepflow/constraint_test.py +71 -0
  5. pepflow-0.1.4a1/pepflow/e2e_test.py +69 -0
  6. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/expression_manager.py +72 -2
  7. pepflow-0.1.4a1/pepflow/expression_manager_test.py +116 -0
  8. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/function.py +142 -48
  9. pepflow-0.1.4a1/pepflow/function_test.py +275 -0
  10. pepflow-0.1.4a1/pepflow/interactive_constraint.py +354 -0
  11. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/pep.py +18 -3
  12. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/pep_context.py +12 -7
  13. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/pep_context_test.py +23 -21
  14. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/pep_test.py +8 -0
  15. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/point.py +43 -8
  16. pepflow-0.1.4a1/pepflow/point_test.py +164 -0
  17. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/scalar.py +39 -1
  18. pepflow-0.1.4a1/pepflow/scalar_test.py +207 -0
  19. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/solver_test.py +7 -7
  20. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/utils.py +14 -1
  21. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow.egg-info/PKG-INFO +19 -1
  22. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow.egg-info/SOURCES.txt +4 -0
  23. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pyproject.toml +10 -1
  24. pepflow-0.1.3a1/pepflow/function_test.py +0 -134
  25. pepflow-0.1.3a1/pepflow/interactive_constraint.py +0 -264
  26. pepflow-0.1.3a1/pepflow/point_test.py +0 -366
  27. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/LICENSE +0 -0
  28. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/constants.py +0 -0
  29. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/constraint.py +0 -0
  30. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow/solver.py +0 -0
  31. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow.egg-info/dependency_links.txt +0 -0
  32. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow.egg-info/requires.txt +0 -0
  33. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/pepflow.egg-info/top_level.txt +0 -0
  34. {pepflow-0.1.3a1 → pepflow-0.1.4a1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pepflow
3
- Version: 0.1.3a1
3
+ Version: 0.1.4a1
4
4
  Summary: PEPFlow: A framework for Performance Estimation Problem (PEP) Workflow
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -53,4 +53,22 @@ We use `pytest` framework to do the test. To run all unit tests, run the followi
53
53
  pytest -s -vv pepflow
54
54
  ```
55
55
 
56
+ ### Build doc website
57
+
58
+ Install the required library (one-time) and `pandoc` in order to build ipynb.
59
+ ```bash
60
+ pip install -r docs/requirements.txt
61
+ ```
62
+
63
+ To build the website, run
64
+ ```bash
65
+ cd docs; make html
66
+ ```
67
+ Make sure it succeeded, then examine it locally through
68
+ ```bash
69
+ cd build/html; python -m http.server
70
+ ```
71
+
72
+
73
+
56
74
 
@@ -31,4 +31,22 @@ We use `pytest` framework to do the test. To run all unit tests, run the followi
31
31
  pytest -s -vv pepflow
32
32
  ```
33
33
 
34
+ ### Build doc website
35
+
36
+ Install the required library (one-time) and `pandoc` in order to build ipynb.
37
+ ```bash
38
+ pip install -r docs/requirements.txt
39
+ ```
40
+
41
+ To build the website, run
42
+ ```bash
43
+ cd docs; make html
44
+ ```
45
+ Make sure it succeeded, then examine it locally through
46
+ ```bash
47
+ cd build/html; python -m http.server
48
+ ```
49
+
50
+
51
+
34
52
 
@@ -35,6 +35,7 @@ from .pep_context import set_current_context as set_current_context
35
35
  # Function, Point, Scalar
36
36
  from .function import Function as Function
37
37
  from .function import SmoothConvexFunction as SmoothConvexFunction
38
+ from .function import ConvexFunction as ConvexFunction
38
39
  from .function import Triplet as Triplet
39
40
  from .point import EvaluatedPoint as EvaluatedPoint
40
41
  from .point import Point as Point
@@ -0,0 +1,71 @@
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
+
21
+ from typing import Iterator
22
+
23
+ import numpy as np
24
+ import pytest
25
+
26
+ from pepflow import expression_manager as exm
27
+ from pepflow import pep as pep
28
+ from pepflow import pep_context as pc
29
+ from pepflow import scalar, utils
30
+
31
+
32
+ @pytest.fixture
33
+ def pep_context() -> Iterator[pc.PEPContext]:
34
+ """Prepare the pep context and reset the context to None at the end."""
35
+ ctx = pc.PEPContext("test").set_as_current()
36
+ yield ctx
37
+ pc.set_current_context(None)
38
+
39
+
40
+ def test_constraint(pep_context: pc.PEPContext):
41
+ s1 = scalar.Scalar(is_basis=True, tags=["s1"])
42
+ s2 = scalar.Scalar(is_basis=True, tags=["s2"])
43
+ s3 = 2 * s1 + s2 / 4 + 5
44
+
45
+ c1 = s3.le(5, name="c1")
46
+ c2 = s3.lt(5, name="c2")
47
+ c3 = s3.ge(5, name="c3")
48
+ c4 = s3.gt(5, name="c4")
49
+ c5 = s3.eq(5, name="c5")
50
+
51
+ pm = exm.ExpressionManager(pep_context)
52
+
53
+ np.testing.assert_allclose(pm.eval_scalar(c1.scalar).vector, np.array([2, 0.25]))
54
+ np.testing.assert_allclose(pm.eval_scalar(c1.scalar).constant, 0)
55
+ assert c1.comparator == utils.Comparator.LT
56
+
57
+ np.testing.assert_allclose(pm.eval_scalar(c2.scalar).vector, np.array([2, 0.25]))
58
+ np.testing.assert_allclose(pm.eval_scalar(c2.scalar).constant, 0)
59
+ assert c2.comparator == utils.Comparator.LT
60
+
61
+ np.testing.assert_allclose(pm.eval_scalar(c3.scalar).vector, np.array([2, 0.25]))
62
+ np.testing.assert_allclose(pm.eval_scalar(c3.scalar).constant, 0)
63
+ assert c3.comparator == utils.Comparator.GT
64
+
65
+ np.testing.assert_allclose(pm.eval_scalar(c4.scalar).vector, np.array([2, 0.25]))
66
+ np.testing.assert_allclose(pm.eval_scalar(c4.scalar).constant, 0)
67
+ assert c4.comparator == utils.Comparator.GT
68
+
69
+ np.testing.assert_allclose(pm.eval_scalar(c5.scalar).vector, np.array([2, 0.25]))
70
+ np.testing.assert_allclose(pm.eval_scalar(c5.scalar).constant, 0)
71
+ assert c5.comparator == utils.Comparator.EQ
@@ -0,0 +1,69 @@
1
+ import math
2
+
3
+ from pepflow import function, pep
4
+ from pepflow import pep_context as pc
5
+
6
+
7
+ def test_gd_e2e():
8
+ ctx = pc.PEPContext("gd").set_as_current()
9
+ pep_builder = pep.PEPBuilder()
10
+ eta = 1
11
+ N = 9
12
+
13
+ f = pep_builder.declare_func(function.SmoothConvexFunction, "f", L=1)
14
+ x = pep_builder.set_init_point("x_0")
15
+ x_star = f.add_stationary_point("x_star")
16
+ pep_builder.set_initial_constraint(
17
+ ((x - x_star) ** 2).le(1, name="initial_condition")
18
+ )
19
+
20
+ # We first build the algorithm with the largest number of iterations.
21
+ for i in range(N):
22
+ x = x - eta * f.gradient(x)
23
+ x.add_tag(f"x_{i + 1}")
24
+
25
+ # To achieve the sweep, we can just update the performance_metric.
26
+ for i in range(1, N + 1):
27
+ p = ctx.get_by_tag(f"x_{i}")
28
+ pep_builder.set_performance_metric(
29
+ f.function_value(p) - f.function_value(x_star)
30
+ )
31
+ result = pep_builder.solve()
32
+ expected_opt_value = 1 / (4 * i + 2)
33
+ assert math.isclose(result.primal_opt_value, expected_opt_value, rel_tol=1e-3)
34
+
35
+
36
+ def test_pgm_e2e():
37
+ ctx = pc.PEPContext("pgm").set_as_current()
38
+ pep_builder = pep.PEPBuilder()
39
+ eta = 1
40
+ N = 1
41
+
42
+ f = pep_builder.declare_func(function.SmoothConvexFunction, "f", L=1)
43
+ g = pep_builder.declare_func(function.ConvexFunction, "g")
44
+
45
+ h = f + g
46
+
47
+ x = pep_builder.set_init_point("x_0")
48
+ x_star = h.add_stationary_point("x_star")
49
+ pep_builder.set_initial_constraint(
50
+ ((x - x_star) ** 2).le(1, name="initial_condition")
51
+ )
52
+
53
+ # We first build the algorithm with the largest number of iterations.
54
+ for i in range(N):
55
+ y = x - eta * f.gradient(x)
56
+ y.add_tag(f"y_{i + 1}")
57
+ x = g.proximal_step(y, eta)
58
+ x.add_tag(f"x_{i + 1}")
59
+
60
+ # To achieve the sweep, we can just update the performance_metric.
61
+ for i in range(1, N + 1):
62
+ p = ctx.get_by_tag(f"x_{i}")
63
+ pep_builder.set_performance_metric(
64
+ h.function_value(p) - h.function_value(x_star)
65
+ )
66
+
67
+ result = pep_builder.solve()
68
+ expected_opt_value = 1 / (4 * i)
69
+ assert math.isclose(result.primal_opt_value, expected_opt_value, rel_tol=1e-3)
@@ -18,6 +18,7 @@
18
18
  # under the License.
19
19
 
20
20
  import functools
21
+ import math
21
22
 
22
23
  import numpy as np
23
24
 
@@ -27,6 +28,17 @@ from pepflow import scalar as sc
27
28
  from pepflow import utils
28
29
 
29
30
 
31
+ def tag_and_coef_to_str(tag: str, v: float) -> str:
32
+ coef = f"{abs(v):.3g}"
33
+ sign = "+" if v >= 0 else "-"
34
+ if math.isclose(abs(v), 1):
35
+ return f"{sign} {tag} "
36
+ elif math.isclose(v, 0):
37
+ return ""
38
+ else:
39
+ return f"{sign} {coef}*{tag} "
40
+
41
+
30
42
  class ExpressionManager:
31
43
  def __init__(self, pep_context: pc.PEPContext):
32
44
  self.context = pep_context
@@ -48,12 +60,18 @@ class ExpressionManager:
48
60
  self._num_basis_points = len(self._basis_points)
49
61
  self._num_basis_scalars = len(self._basis_scalars)
50
62
 
51
- def get_index_of_basis_point(self, point: pt.Point):
63
+ def get_index_of_basis_point(self, point: pt.Point) -> int:
52
64
  return self._basis_point_uid_to_index[point.uid]
53
65
 
54
- def get_index_of_basis_scalar(self, scalar: sc.Scalar):
66
+ def get_index_of_basis_scalar(self, scalar: sc.Scalar) -> int:
55
67
  return self._basis_scalar_uid_to_index[scalar.uid]
56
68
 
69
+ def get_tag_of_basis_point_index(self, index: int) -> str:
70
+ return self._basis_points[index].tag
71
+
72
+ def get_tag_of_basis_scalar_index(self, index: int) -> str:
73
+ return self._basis_scalars[index].tag
74
+
57
75
  @functools.cache
58
76
  def eval_point(self, point: pt.Point | float | int):
59
77
  if utils.is_numerical(point):
@@ -130,3 +148,55 @@ class ExpressionManager:
130
148
  ) / self.eval_scalar(scalar.eval_expression.right_scalar)
131
149
 
132
150
  raise ValueError("This should never happen!")
151
+
152
+ @functools.cache
153
+ def repr_point_by_basis(self, point: pt.Point) -> str:
154
+ assert isinstance(point, pt.Point)
155
+ repr_array = self.eval_point(point).vector
156
+
157
+ repr_str = ""
158
+ for i, v in enumerate(repr_array):
159
+ ith_tag = self.get_tag_of_basis_point_index(i)
160
+ repr_str += tag_and_coef_to_str(ith_tag, v)
161
+
162
+ # Post processing
163
+ if repr_str == "":
164
+ return "0"
165
+ if repr_str.startswith("+ "):
166
+ repr_str = repr_str[2:]
167
+ if repr_str.startswith("- "):
168
+ repr_str = "-" + repr_str[2:]
169
+ return repr_str.strip()
170
+
171
+ @functools.cache
172
+ def repr_scalar_by_basis(self, scalar: sc.Scalar) -> str:
173
+ assert isinstance(scalar, sc.Scalar)
174
+ evaluated_scalar = self.eval_scalar(scalar)
175
+
176
+ repr_str = ""
177
+ if not math.isclose(evaluated_scalar.constant, 0):
178
+ repr_str += f"{evaluated_scalar.constant:.3g}"
179
+
180
+ for i, v in enumerate(evaluated_scalar.vector):
181
+ # Note the tag is from scalar basis.
182
+ ith_tag = self.get_tag_of_basis_scalar_index(i)
183
+ repr_str += tag_and_coef_to_str(ith_tag, v)
184
+
185
+ for i in range(evaluated_scalar.matrix.shape[0]):
186
+ for j in range(i, evaluated_scalar.matrix.shape[0]):
187
+ ith_tag = self.get_tag_of_basis_point_index(i)
188
+ v = evaluated_scalar.matrix[i, j]
189
+ if i == j:
190
+ repr_str += tag_and_coef_to_str(f"|{ith_tag}|^2", v)
191
+ continue
192
+ jth_tag = self.get_tag_of_basis_point_index(j)
193
+ repr_str += tag_and_coef_to_str(f"<{ith_tag}, {jth_tag}>", 2 * v)
194
+
195
+ # Post processing
196
+ if repr_str == "":
197
+ return "0"
198
+ if repr_str.startswith("+ "):
199
+ repr_str = repr_str[2:]
200
+ if repr_str.startswith("- "):
201
+ repr_str = "-" + repr_str[2:]
202
+ return repr_str.strip()
@@ -0,0 +1,116 @@
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
+
21
+ from typing import Iterator
22
+
23
+ import numpy as np
24
+ import pytest
25
+
26
+ from pepflow import expression_manager as exm
27
+ from pepflow import function as fc
28
+ from pepflow import pep as pep
29
+ from pepflow import pep_context as pc
30
+ from pepflow import point as pt
31
+
32
+
33
+ @pytest.fixture
34
+ def pep_context() -> Iterator[pc.PEPContext]:
35
+ """Prepare the pep context and reset the context to None at the end."""
36
+ ctx = pc.PEPContext("test").set_as_current()
37
+ yield ctx
38
+ pc.set_current_context(None)
39
+
40
+
41
+ def test_repr_point_by_basis(pep_context: pc.PEPContext) -> None:
42
+ x = pt.Point(is_basis=True, tags=["x_0"])
43
+ f = fc.Function(is_basis=True, tags=["f"])
44
+ L = 0.5
45
+ for i in range(2):
46
+ x = x - L * f.gradient(x)
47
+ x.add_tag(f"x_{i + 1}")
48
+
49
+ em = exm.ExpressionManager(pep_context)
50
+ np.testing.assert_allclose(em.eval_point(x).vector, [1, -0.5, -0.5])
51
+ assert (
52
+ em.repr_point_by_basis(x) == "x_0 - 0.5*gradient_f(x_0) - 0.5*gradient_f(x_1)"
53
+ )
54
+
55
+
56
+ def test_repr_point_by_basis_with_zero(pep_context: pc.PEPContext) -> None:
57
+ x = pt.Point(is_basis=True, tags=["x_0"])
58
+ _ = pt.Point(is_basis=True, tags=["x_unused"]) # Add this extra point.
59
+ f = fc.Function(is_basis=True, tags=["f"])
60
+ L = 0.5
61
+ for i in range(2):
62
+ x = x - L * f.gradient(x)
63
+ x.add_tag(f"x_{i + 1}")
64
+
65
+ em = exm.ExpressionManager(pep_context)
66
+ # Note the vector representation of point is different from previous case
67
+ # But the string representation is still the same.
68
+ np.testing.assert_allclose(em.eval_point(x).vector, [1, 0, -0.5, -0.5])
69
+ assert (
70
+ em.repr_point_by_basis(x) == "x_0 - 0.5*gradient_f(x_0) - 0.5*gradient_f(x_1)"
71
+ )
72
+
73
+
74
+ def test_repr_point_by_basis_heavy_ball(pep_context: pc.PEPContext) -> None:
75
+ x_prev = pt.Point(is_basis=True, tags=["x_{-1}"])
76
+ x = pt.Point(is_basis=True, tags=["x_0"])
77
+ f = fc.Function(is_basis=True, tags=["f"])
78
+
79
+ beta = 0.5
80
+ for i in range(2):
81
+ x_next = x - f.gradient(x) + beta * (x - x_prev)
82
+ x_next.add_tag(f"x_{i + 1}")
83
+ x_prev = x
84
+ x = x_next
85
+
86
+ em = exm.ExpressionManager(pep_context)
87
+ np.testing.assert_allclose(em.eval_point(x).vector, [-0.75, 1.75, -1.5, -1])
88
+ assert (
89
+ em.repr_point_by_basis(x)
90
+ == "-0.75*x_{-1} + 1.75*x_0 - 1.5*gradient_f(x_0) - gradient_f(x_1)"
91
+ )
92
+
93
+
94
+ def test_repr_scalar_by_basis(pep_context: pc.PEPContext) -> None:
95
+ x = pt.Point(is_basis=True, tags=["x"])
96
+ f = fc.Function(is_basis=True, tags=["f"])
97
+
98
+ s = f(x) + x * f.gradient(x)
99
+ em = exm.ExpressionManager(pep_context)
100
+ assert em.repr_scalar_by_basis(s) == "f(x) + <x, gradient_f(x)>"
101
+
102
+
103
+ def test_repr_scalar_by_basis_interpolation(pep_context: pc.PEPContext) -> None:
104
+ xi = pt.Point(is_basis=True, tags=["x_i"])
105
+ xj = pt.Point(is_basis=True, tags=["x_j"])
106
+ f = fc.SmoothConvexFunction(is_basis=True, L=1)
107
+ f.add_tag("f")
108
+ fi = f(xi) # noqa: F841
109
+ fj = f(xj) # noqa: F841
110
+ interp_scalar = f.interpolate_ineq("x_i", "x_j")
111
+ em = exm.ExpressionManager(pep_context)
112
+ expected_repr = "-f(x_i) + f(x_j) + <x_i, gradient_f(x_j)> - <x_j, gradient_f(x_j)> + 0.5*|gradient_f(x_i)|^2 - <gradient_f(x_i), gradient_f(x_j)> + 0.5*|gradient_f(x_j)|^2"
113
+ assert em.repr_scalar_by_basis(interp_scalar) == expected_repr
114
+
115
+
116
+ # TODO add more tests about repr_scalar_by_basis