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/function.py CHANGED
@@ -19,7 +19,9 @@
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ import numbers
22
23
  import uuid
24
+ from typing import TYPE_CHECKING
23
25
 
24
26
  import attrs
25
27
 
@@ -28,9 +30,25 @@ from pepflow import point as pt
28
30
  from pepflow import scalar as sc
29
31
  from pepflow import utils
30
32
 
33
+ if TYPE_CHECKING:
34
+ from pepflow.constraint import Constraint
35
+
31
36
 
32
37
  @attrs.frozen
33
38
  class Triplet:
39
+ """
40
+ A data class that represents, for some given function :math:`f`,
41
+ the tuple :math:`\\{x, f(x), \\nabla f(x)\\}`. We can also consider
42
+ a subgradient :math:`\\widetilde{\\nabla} f(x)` instead of the gradient.
43
+
44
+ Attributes:
45
+ point (:class:`Point`): The point :math:`x`.
46
+ function_value (:class:`Scalar`): The function value :math:`f(x)`.
47
+ gradient (:class:`Point`): The gradient :math:`\\nabla f(x)` or
48
+ a subgradient :math:`\\widetilde{\\nabla} f(x)`.
49
+ name (str): The unique name of the :class:`Triplet` object.
50
+ """
51
+
34
52
  point: pt.Point
35
53
  function_value: sc.Scalar
36
54
  gradient: pt.Point
@@ -48,7 +66,7 @@ class AddedFunc:
48
66
 
49
67
  @attrs.frozen
50
68
  class ScaledFunc:
51
- """Represents scale * base_func."""
69
+ """Represents scalar * base_func."""
52
70
 
53
71
  scale: float
54
72
  base_func: Function
@@ -56,8 +74,26 @@ class ScaledFunc:
56
74
 
57
75
  @attrs.mutable
58
76
  class Function:
77
+ """A :class:`Function` object represents a function.
78
+
79
+ :class:`Function` objects can be constructed as linear combinations
80
+ of other :class:`Function` objects. Let `a` and `b` be some numeric
81
+ data type. Let `f` and `g` be :class:`Function` objects. Then, we
82
+ can form a new :class:`Function` object: `a*f+b*g`.
83
+
84
+ A :class:`Function` object should never be explicitly constructed. Only
85
+ children of :class:`Function` such as :class:`ConvexFunction` or
86
+ :class:`SmoothConvexFunction` should be constructed. See their respective
87
+ documentation to see how.
88
+
89
+ Attributes:
90
+ is_basis (bool): `True` if this function is not formed through a linear
91
+ combination of other functions. `False` otherwise.
92
+ tags (list[str]): A list that contains tags that can be used to
93
+ identify the :class:`Function` object. Tags should be unique.
94
+ """
95
+
59
96
  is_basis: bool
60
- reuse_gradient: bool
61
97
 
62
98
  composition: AddedFunc | ScaledFunc | None = None
63
99
 
@@ -75,11 +111,21 @@ class Function:
75
111
 
76
112
  @property
77
113
  def tag(self):
114
+ """Returns the most recently added tag.
115
+
116
+ Returns:
117
+ str: The most recently added tag of this :class:`Function` object.
118
+ """
78
119
  if len(self.tags) == 0:
79
120
  raise ValueError("Function should have a name.")
80
121
  return self.tags[-1]
81
122
 
82
123
  def add_tag(self, tag: str) -> None:
124
+ """Add a new tag for this :class:`Function` object.
125
+
126
+ Args:
127
+ tag (str): The new tag to be added to the `tags` list.
128
+ """
83
129
  self.tags.append(tag)
84
130
 
85
131
  def __repr__(self):
@@ -89,7 +135,7 @@ class Function:
89
135
 
90
136
  def _repr_latex_(self):
91
137
  s = repr(self)
92
- return rf"$\\displaystyle {s}$"
138
+ return rf"$\displaystyle {s}$"
93
139
 
94
140
  def get_interpolation_constraints(self):
95
141
  raise NotImplementedError(
@@ -157,12 +203,32 @@ class Function:
157
203
  return triplet
158
204
 
159
205
  def add_stationary_point(self, name: str) -> pt.Point:
206
+ """
207
+ Return a stationary point for this :class:`Function` object.
208
+ A :class:`Function` object can only have one stationary point.
209
+
210
+ Args:
211
+ name (str): The tag for the :class:`Point` object which
212
+ will serve as the stationary point.
213
+
214
+ Returns:
215
+ :class:`Point`: The stationary point for this :class:`Function`
216
+ object.
217
+ """
160
218
  # assert we can only add one stationary point?
219
+ pep_context = pc.get_current_context()
220
+ if pep_context is None:
221
+ raise RuntimeError("Did you forget to create a context?")
222
+ if len(pep_context.stationary_triplets[self]) > 0:
223
+ raise ValueError(
224
+ "You are trying to add a stationary point to a function that already has a stationary point."
225
+ )
161
226
  point = pt.Point(is_basis=True)
162
227
  point.add_tag(name)
163
- desired_grad = 0 * point
228
+ desired_grad = pt.Point.zero() # zero point
164
229
  desired_grad.add_tag(f"gradient_{self.tag}({name})")
165
- self.add_point_with_grad_restriction(point, desired_grad)
230
+ triplet = self.add_point_with_grad_restriction(point, desired_grad)
231
+ pep_context.add_stationary_triplet(self, triplet)
166
232
  return point
167
233
 
168
234
  # The following the old gradient(opt) = 0 constraint style.
@@ -183,43 +249,26 @@ class Function:
183
249
  if pep_context is None:
184
250
  raise RuntimeError("Did you forget to create a context?")
185
251
 
252
+ if not isinstance(point, pt.Point):
253
+ raise ValueError("The Function can only take point as input.")
254
+
186
255
  if self.is_basis:
187
- generate_new_basis = True
188
- instances_of_point = 0
189
256
  for triplet in pep_context.triplets[self]:
190
257
  if triplet.point.uid == point.uid:
191
- instances_of_point += 1
192
- generate_new_basis = False
193
- previous_triplet = triplet
258
+ return triplet
194
259
 
195
- if generate_new_basis:
196
- function_value = sc.Scalar(is_basis=True)
197
- function_value.add_tag(f"{self.tag}({point.tag})")
198
- gradient = pt.Point(is_basis=True)
199
- gradient.add_tag(f"gradient_{self.tag}({point.tag})")
260
+ function_value = sc.Scalar(is_basis=True)
261
+ function_value.add_tag(f"{self.tag}({point.tag})")
262
+ gradient = pt.Point(is_basis=True)
263
+ gradient.add_tag(f"gradient_{self.tag}({point.tag})")
200
264
 
201
- new_triplet = Triplet(
202
- point,
203
- function_value,
204
- gradient,
205
- name=f"{point.tag}_{function_value.tag}_{gradient.tag}",
206
- )
207
- self.add_triplet_to_func(new_triplet)
208
- elif not generate_new_basis and self.reuse_gradient:
209
- function_value = previous_triplet.function_value
210
- gradient = previous_triplet.gradient
211
- elif not generate_new_basis and not self.reuse_gradient:
212
- function_value = previous_triplet.function_value
213
- gradient = pt.Point(is_basis=True)
214
- gradient.add_tag(f"gradient_{self.tag}({point.tag})")
215
-
216
- new_triplet = Triplet(
217
- point,
218
- previous_triplet.function_value,
219
- gradient,
220
- name=f"{point.tag}_{function_value.tag}_{gradient.tag}_{instances_of_point}",
221
- )
222
- self.add_triplet_to_func(new_triplet)
265
+ new_triplet = Triplet(
266
+ point,
267
+ function_value,
268
+ gradient,
269
+ name=f"{point.tag}_{function_value.tag}_{gradient.tag}",
270
+ )
271
+ self.add_triplet_to_func(new_triplet)
223
272
  else:
224
273
  if isinstance(self.composition, AddedFunc):
225
274
  left_triplet = self.composition.left_func.generate_triplet(point)
@@ -240,14 +289,50 @@ class Function:
240
289
  return Triplet(point, function_value, gradient, name=None)
241
290
 
242
291
  def gradient(self, point: pt.Point) -> pt.Point:
292
+ """
293
+ Returns a :class:`Point` object that is the gradient of the
294
+ :class:`Function` at the given :class:`Point`.
295
+
296
+ Args:
297
+ point (:class:`Point`): Any :class:`Point`.
298
+
299
+ Returns:
300
+ :class:`Point`: The gradient of the :class:`Function` at the
301
+ given :class:`Point`.
302
+ """
243
303
  triplet = self.generate_triplet(point)
244
304
  return triplet.gradient
245
305
 
246
306
  def subgradient(self, point: pt.Point) -> pt.Point:
307
+ """
308
+ Returns a :class:`Point` object that is the subgradient of the
309
+ :class:`Function` at the given :class:`Point`.
310
+
311
+ Args:
312
+ point (:class:`Point`): Any :class:`Point`.
313
+
314
+ Returns:
315
+ :class:`Point`: The subgradient of the :class:`Function` at the
316
+ given :class:`Point`.
317
+
318
+ Note:
319
+ The method `gradient` is exactly the same.
320
+ """
247
321
  triplet = self.generate_triplet(point)
248
322
  return triplet.gradient
249
323
 
250
324
  def function_value(self, point: pt.Point) -> sc.Scalar:
325
+ """
326
+ Returns a :class:`Scalar` object that is the function value of the
327
+ :class:`Function` at the given :class:`Point`.
328
+
329
+ Args:
330
+ point (:class:`Point`): Any :class:`Point`.
331
+
332
+ Returns:
333
+ :class:`Point`: The function value of the :class:`Function` at the
334
+ given :class:`Point`.
335
+ """
251
336
  triplet = self.generate_triplet(point)
252
337
  return triplet.function_value
253
338
 
@@ -255,46 +340,46 @@ class Function:
255
340
  return self.function_value(point)
256
341
 
257
342
  def __add__(self, other):
258
- assert isinstance(other, Function)
343
+ if not isinstance(other, Function):
344
+ return NotImplemented
259
345
  return Function(
260
346
  is_basis=False,
261
- reuse_gradient=self.reuse_gradient and other.reuse_gradient,
262
347
  composition=AddedFunc(self, other),
263
348
  tags=[f"{self.tag}+{other.tag}"],
264
349
  )
265
350
 
266
351
  def __sub__(self, other):
267
- assert isinstance(other, Function)
352
+ if not isinstance(other, Function):
353
+ return NotImplemented
268
354
  tag_other = other.tag
269
355
  if isinstance(other.composition, AddedFunc):
270
356
  tag_other = f"({other.tag})"
271
357
  return Function(
272
358
  is_basis=False,
273
- reuse_gradient=self.reuse_gradient and other.reuse_gradient,
274
359
  composition=AddedFunc(self, -other),
275
360
  tags=[f"{self.tag}-{tag_other}"],
276
361
  )
277
362
 
278
363
  def __mul__(self, other):
279
- assert utils.is_numerical(other)
364
+ if not utils.is_numerical(other):
365
+ return NotImplemented
280
366
  tag_self = self.tag
281
367
  if isinstance(self.composition, AddedFunc):
282
368
  tag_self = f"({self.tag})"
283
369
  return Function(
284
370
  is_basis=False,
285
- reuse_gradient=self.reuse_gradient,
286
371
  composition=ScaledFunc(scale=other, base_func=self),
287
372
  tags=[f"{other:.4g}*{tag_self}"],
288
373
  )
289
374
 
290
375
  def __rmul__(self, other):
291
- assert utils.is_numerical(other)
376
+ if not utils.is_numerical(other):
377
+ return NotImplemented
292
378
  tag_self = self.tag
293
379
  if isinstance(self.composition, AddedFunc):
294
380
  tag_self = f"({self.tag})"
295
381
  return Function(
296
382
  is_basis=False,
297
- reuse_gradient=self.reuse_gradient,
298
383
  composition=ScaledFunc(scale=other, base_func=self),
299
384
  tags=[f"{other:.4g}*{tag_self}"],
300
385
  )
@@ -305,19 +390,18 @@ class Function:
305
390
  tag_self = f"({self.tag})"
306
391
  return Function(
307
392
  is_basis=False,
308
- reuse_gradient=self.reuse_gradient,
309
393
  composition=ScaledFunc(scale=-1, base_func=self),
310
394
  tags=[f"-{tag_self}"],
311
395
  )
312
396
 
313
397
  def __truediv__(self, other):
314
- assert utils.is_numerical(other)
398
+ if not utils.is_numerical(other):
399
+ return NotImplemented
315
400
  tag_self = self.tag
316
401
  if isinstance(self.composition, AddedFunc):
317
402
  tag_self = f"({self.tag})"
318
403
  return Function(
319
404
  is_basis=False,
320
- reuse_gradient=self.reuse_gradient,
321
405
  composition=ScaledFunc(scale=1 / other, base_func=self),
322
406
  tags=[f"1/{other:.4g}*{tag_self}"],
323
407
  )
@@ -331,10 +415,159 @@ class Function:
331
415
  return self.uid == other.uid
332
416
 
333
417
 
418
+ class ConvexFunction(Function):
419
+ """
420
+ The :class:`ConvexFunction` class is a child of :class:`Function.`
421
+ The :class:`ConvexFunction` class represents a closed, convex, and
422
+ proper (CCP) function, i.e., a convex function whose epigraph is a
423
+ non-empty closed set.
424
+
425
+ A CCP function typically has no parameters. We can instantiate a
426
+ :class:`ConvexFunction` object as follows:
427
+
428
+ Example:
429
+ >>> import pepflow as pf
430
+ >>> ctx = pf.PEPContext("example").set_as_current()
431
+ >>> pep_builder = pf.PEPBuilder()
432
+ >>> g = pep_builder.declare_func(pf.ConvexFunction, "g")
433
+ """
434
+
435
+ def __init__(
436
+ self,
437
+ is_basis=True,
438
+ composition=None,
439
+ ):
440
+ super().__init__(
441
+ is_basis=is_basis,
442
+ composition=composition,
443
+ )
444
+
445
+ def convex_interpolability_constraints(
446
+ self, triplet_i: Triplet, triplet_j: Triplet
447
+ ) -> Constraint:
448
+ point_i = triplet_i.point
449
+ function_value_i = triplet_i.function_value
450
+
451
+ point_j = triplet_j.point
452
+ function_value_j = triplet_j.function_value
453
+ grad_j = triplet_j.gradient
454
+
455
+ func_diff = function_value_j - function_value_i
456
+ cross_term = grad_j * (point_i - point_j)
457
+
458
+ return (func_diff + cross_term).le(
459
+ 0, name=f"{self.tag}:{point_i.tag},{point_j.tag}"
460
+ )
461
+
462
+ def get_interpolation_constraints(
463
+ self, pep_context: pc.PEPContext | None = None
464
+ ) -> list[Constraint]:
465
+ interpolation_constraints = []
466
+ if pep_context is None:
467
+ pep_context = pc.get_current_context()
468
+ if pep_context is None:
469
+ raise RuntimeError("Did you forget to create a context?")
470
+ for i in pep_context.triplets[self]:
471
+ for j in pep_context.triplets[self]:
472
+ if i == j:
473
+ continue
474
+ interpolation_constraints.append(
475
+ self.convex_interpolability_constraints(i, j)
476
+ )
477
+ return interpolation_constraints
478
+
479
+ def interpolate_ineq(
480
+ self, p1_tag: str, p2_tag: str, pep_context: pc.PEPContext | None = None
481
+ ) -> sc.Scalar:
482
+ """Generate the interpolation inequality :class:`Scalar` by tags.
483
+ The interpolation inequality between two points :math:`p_1, p_2` for a
484
+ CCP function :math:`f` is
485
+
486
+ .. math:: f(p_2) - f(p_1) + \\langle \\nabla f(p_2), p_1 - p_2 \\rangle.
487
+
488
+ Args:
489
+ p1_tag (str): A tag of the :class:`Point` :math:`p_1`.
490
+ p2_tag (str): A tag of the :class:`Point` :math:`p_2`.
491
+ """
492
+ if pep_context is None:
493
+ pep_context = pc.get_current_context()
494
+ if pep_context is None:
495
+ raise RuntimeError("Did you forget to specify a context?")
496
+ # TODO: we definitely need a more robust tag system
497
+ x1 = pep_context.get_by_tag(p1_tag)
498
+ x2 = pep_context.get_by_tag(p2_tag)
499
+ f1 = pep_context.get_by_tag(f"{self.tag}({p1_tag})")
500
+ f2 = pep_context.get_by_tag(f"{self.tag}({p2_tag})")
501
+ g2 = pep_context.get_by_tag(f"gradient_{self.tag}({p2_tag})")
502
+ return f2 - f1 + g2 * (x1 - x2)
503
+
504
+ def proximal_step(self, x_0: pt.Point, stepsize: numbers.Number) -> pt.Point:
505
+ """ Define the proximal operator as
506
+
507
+ .. math:: \\text{prox}_{\\gamma f}(x_0) := \\arg\\min_x \\left\\{ \\gamma f(x) + \\frac{1}{2} \\|x - x_0\\|^2 \\right\\}.
508
+
509
+ This function performs a proximal step with respect to some
510
+ :class:`Function` :math:`f` on the :class:`Point` :math:`x_0`
511
+ with stepsize :math:`\\gamma`:
512
+
513
+ .. math::
514
+ :nowrap:
515
+
516
+ \\begin{eqnarray}
517
+ x := \\text{prox}_{\\gamma f}(x_0) & := & \\arg\\min_x \\left\\{ \\gamma f(x) + \\frac{1}{2} \\|x - x_0\\|^2 \\right\\}, \\\\
518
+ & \\Updownarrow & \\\\
519
+ 0 & = & \\gamma \\partial f(x) + x - x_0,\\\\
520
+ & \\Updownarrow & \\\\
521
+ x & = & x_0 - \\gamma \\widetilde{\\nabla} f(x) \\text{ where } \\widetilde{\\nabla} f(x)\\in\\partial f(x).
522
+ \\end{eqnarray}
523
+
524
+ Args:
525
+ x_0 (:class:`Point`): The initial point.
526
+ stepsize (int | float): The stepsize.
527
+ """
528
+ gradient = pt.Point(is_basis=True)
529
+ gradient.add_tag(
530
+ f"gradient_{self.tag}(prox_{{{stepsize}*{self.tag}}}({x_0.tag}))"
531
+ )
532
+ function_value = sc.Scalar(is_basis=True)
533
+ function_value.add_tag(f"{self.tag}(prox_{{{stepsize}*{self.tag}}}({x_0.tag}))")
534
+ x = x_0 - stepsize * gradient
535
+ x.add_tag(f"prox_{{{stepsize}*{self.tag}}}({x_0.tag})")
536
+ new_triplet = Triplet(
537
+ x,
538
+ function_value,
539
+ gradient,
540
+ name=f"{x.tag}_{function_value.tag}_{gradient.tag}",
541
+ )
542
+ self.add_triplet_to_func(new_triplet)
543
+ return x
544
+
545
+
334
546
  class SmoothConvexFunction(Function):
335
- def __init__(self, L, is_basis=True, composition=None, reuse_gradient=True):
547
+ """
548
+ The :class:`SmoothConvexFunction` class is a child of :class:`Function.`
549
+ The :class:`SmoothConvexFunction` class represents a smooth,
550
+ convex function.
551
+
552
+ A smooth, convex function has a smoothness parameter :math:`L`.
553
+ We can instantiate a :class:`SmoothConvexFunction` object as follows:
554
+
555
+ Example:
556
+ >>> import pepflow as pf
557
+ >>> ctx = pf.PEPContext("example").set_as_current()
558
+ >>> pep_builder = pf.PEPBuilder()
559
+ >>> f = pep_builder.declare_func(pf.SmoothConvexFunction, "f", L=1)
560
+ """
561
+
562
+ def __init__(
563
+ self,
564
+ L,
565
+ is_basis=True,
566
+ composition=None,
567
+ ):
336
568
  super().__init__(
337
- is_basis=is_basis, composition=composition, reuse_gradient=reuse_gradient
569
+ is_basis=is_basis,
570
+ composition=composition,
338
571
  )
339
572
  self.L = L
340
573
 
@@ -372,8 +605,17 @@ class SmoothConvexFunction(Function):
372
605
 
373
606
  def interpolate_ineq(
374
607
  self, p1_tag: str, p2_tag: str, pep_context: pc.PEPContext | None = None
375
- ) -> pt.Scalar:
376
- """Generate the interpolation inequality scalar by tags."""
608
+ ) -> sc.Scalar:
609
+ """Generate the interpolation inequality :class:`Scalar` by tags.
610
+ The interpolation inequality between two points :math:`p_1, p_2` for a
611
+ smooth, convex function :math:`f` is
612
+
613
+ .. math:: f(p_2) - f(p_1) + \\langle \\nabla f(p_2), p_1 - p_2 \\rangle + \\tfrac{1}{2} \\lVert \\nabla f(p_1) - \\nabla f(p_2) \\rVert^2.
614
+
615
+ Args:
616
+ p1_tag (str): A tag of the :class:`Point` :math:`p_1`.
617
+ p2_tag (str): A tag of the :class:`Point` :math:`p_2`.
618
+ """
377
619
  if pep_context is None:
378
620
  pep_context = pc.get_current_context()
379
621
  if pep_context is None: