Coverage for suppy\projections\_subgradient_projections.py: 85%
60 statements
« prev ^ index » next coverage.py v7.6.4, created at 2026-05-08 13:56 +0200
« prev ^ index » next coverage.py v7.6.4, created at 2026-05-08 13:56 +0200
1"""Subgradient projections for feasibility algorithms."""
2from typing import Callable, List
3import numpy as np
4import numpy.typing as npt
6from suppy.projections._projections import BasicProjection
8try:
9 import cupy as cp
11 NO_GPU = False
12except ImportError:
13 NO_GPU = True
14 cp = np
17class SubgradientProjection(BasicProjection):
18 """Projection using subgradients."""
20 def __init__(
21 self,
22 func: Callable,
23 grad: Callable,
24 level: float = 0,
25 func_args: List | None = None,
26 grad_args: List | None = None,
27 relaxation: float = 1.0,
28 idx: npt.NDArray | None = None,
29 proximity_flag: bool = True,
30 use_gpu: bool = False,
31 ):
32 """
33 Initialize the SubgradientProjection object.
35 Parameters:
36 - func (Callable): The objective function.
37 - grad (Callable): The gradient function.
38 - level (float): The level at which to project.
39 - func_args (Any): Additional arguments for the objective function.
40 - grad_args (Any): Additional arguments for the gradient function.
41 - relaxation (float): The relaxation parameter.
42 - idx (npt.NDArray | None): The indices to project on.
43 - proximity_flag (bool): Flag to use proximity function.
44 - use_gpu (bool): Flag to show whether the function and gradient calls are performed on the GPU or not.
46 Returns:
47 - None
48 """
49 super().__init__(relaxation, idx, proximity_flag, _use_gpu=use_gpu)
50 self.func = func
51 self.grad = grad
52 self.level = level
53 self.func_args = func_args if func_args is not None else []
54 self.grad_args = grad_args if grad_args is not None else []
56 def func_call(self, x):
57 """
58 Call the objective function.
60 Parameters:
61 - x (npt.NDArray): The input array.
63 Returns:
64 - float: The value of the objective function.
65 """
66 return self.func(x[self.idx], *self.func_args)
68 def grad_call(self, x):
69 """
70 Call the gradient function.
72 Parameters:
73 - x (npt.NDArray): The input array.
75 Returns:
76 - npt.NDArray: The gradient of the objective function.
77 """
78 return self.grad(x[self.idx], *self.grad_args)
80 def _project(self, x: npt.NDArray) -> np.ndarray:
81 """
82 Project the input array onto the specified level.
84 Parameters:
85 - x (npt.NDArray): The input array.
87 Returns:
88 - npt.NDArray: The projected array.
89 """
90 xp = cp if isinstance(x, cp.ndarray) else np
91 f_x = self.func_call(x)
92 g_x = self.grad_call(x)
94 if f_x > self.level and xp.linalg.norm(g_x) > 0:
95 x[self.idx] -= (f_x - self.level) * g_x / (g_x @ g_x)
96 return x
98 def level_diff(self, x: npt.NDArray) -> float:
99 """
100 Calculate the difference between the objective function value and
101 the set level.
103 Parameters:
104 - x (npt.NDArray): The input array.
106 Returns:
107 - float: The difference between the objective function value and the set level.
108 """
109 return self.func_call(x) - self.level
111 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> list[float]:
112 dist = self.level_diff(x)
113 dist = dist if dist > 0 else 0
114 measures = []
115 for measure in proximity_measures:
116 if isinstance(measure, tuple):
117 if measure[0] == "p_norm":
118 measures.append(dist ** measure[1])
119 else:
120 raise ValueError("Invalid proximity measure")
121 elif isinstance(measure, str) and measure == "max_norm":
122 measures.append(dist)
123 else:
124 raise ValueError("Invalid proximity measure")
125 return measures
128class EUDProjection(SubgradientProjection):
129 """
130 Class representing the EUDProjection.
132 This class inherits from the SubgradientProjection class
133 and implements the EUD (Equivalent Uniform Dose) projection.
135 Parameters:
136 - a (float): Exponent used in the EUD projection.
137 - level (int): The level of the projection.
139 Attributes:
140 - a (float): Exponent used in the EUD projection.
142 Methods:
143 - func_call(x): Computes the EUD projection function.
144 - grad_call(x): Computes the gradient of the EUD projection function.
145 """
147 def __init__(
148 self,
149 a: float,
150 EUD_max: float = 10,
151 relaxation: float = 1.0,
152 idx: npt.NDArray | None = None,
153 proximity_flag: bool = True,
154 use_gpu: bool = False,
155 ):
156 """Initializes the EUDProjection object."""
157 super().__init__(
158 self._func,
159 self._grad,
160 relaxation=relaxation,
161 level=EUD_max,
162 idx=idx,
163 proximity_flag=proximity_flag,
164 use_gpu=use_gpu,
165 )
166 self.a = a
168 def _func(self, x):
169 """
170 Computes the EUD projection function.
172 Parameters:
173 - x (npt.NDArray): The input array.
175 Returns:
176 - npt.NDArray: The result of the EUD projection function.
177 """
178 return (((x**self.a).sum(axis=0)) / len(x)) ** (1 / self.a)
180 def _grad(self, x):
181 """
182 Computes the gradient of the EUD projection function.
184 Parameters:
185 - x (npt.NDArray): The input array.
187 Returns:
188 - npt.NDArray: The gradient of the EUD projection function.
189 """
190 return (
191 ((x**self.a).sum()) ** (1 / self.a - 1)
192 * (x ** (self.a - 1))
193 / len(x) ** (1 / self.a)
194 )
197class WeightEUDProjection(EUDProjection):
198 """EUD projection with a linear dose-influence matrix A.
200 Applies the EUD constraint to the weighted dose ``A @ x[idx]`` rather
201 than directly to ``x[idx]``.
203 Parameters
204 ----------
205 A : npt.NDArray
206 Dose-influence matrix applied before computing the EUD.
207 a : float
208 Exponent used in the EUD calculation.
209 EUD_max : float, optional
210 Upper bound on the EUD, by default 10.
211 relaxation : float, optional
212 Relaxation parameter, by default 1.0.
213 idx : npt.NDArray or None, optional
214 Index subset of x to use, by default None.
215 proximity_flag : bool, optional
216 Flag to use proximity function, by default True.
217 use_gpu : bool, optional
218 Flag for GPU computation, by default False.
219 """
221 def __init__(
222 self,
223 A: npt.NDArray,
224 a: float,
225 EUD_max: float = 10,
226 relaxation: float = 1.0,
227 idx: npt.NDArray | None = None,
228 proximity_flag: bool = True,
229 use_gpu: bool = False,
230 ):
231 super().__init__(
232 a,
233 EUD_max,
234 relaxation=relaxation,
235 idx=idx,
236 proximity_flag=proximity_flag,
237 use_gpu=use_gpu,
238 )
239 self.A = A
241 def func_call(self, x):
242 """
243 Call the objective function.
245 Parameters:
246 - x (npt.NDArray): The input array.
248 Returns:
249 - float: The value of the objective function.
250 """
251 return self.func(self.A @ x[self.idx], *self.func_args)
253 def grad_call(self, x):
254 """
255 Call the gradient function.
257 Parameters:
258 - x (npt.NDArray): The input array.
260 Returns:
261 - npt.NDArray: The gradient of the objective function.
262 """
263 return (self.A).T @ (self.grad(self.A @ x[self.idx], *self.grad_args))