Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\projections\_basic_projections.py: 73%
260 statements
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 17:36 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 17:36 +0100
1"""Simple projection objects."""
2import math
3from typing import List
4import numpy as np
5import numpy.typing as npt
6import matplotlib.pyplot as plt
7from matplotlib import patches
9from suppy.projections._projections import BasicProjection
11try:
12 import cupy as cp
14 NO_GPU = False
15except ImportError:
16 NO_GPU = True
17 cp = np
19# from suppy.utils.decorators import ensure_float_array
22# Class for basic projections
25class BoxProjection(BasicProjection):
26 """
27 BoxProjection class for projecting points onto a box defined by lower
28 and upper bounds.
30 Parameters
31 ----------
32 lb : npt.NDArray
33 Lower bounds of the box.
34 ub : npt.NDArray
35 Upper bounds of the box.
36 idx : npt.NDArray or None
37 Subset of the input vector to apply the projection on.
38 relaxation : float, optional
39 Relaxation parameter for the projection, by default 1.
40 proximity_flag : bool
41 Flag to indicate whether to take this object into account when calculating proximity,
42 by default True.
44 Attributes
45 ----------
46 lb : npt.NDArray
47 Lower bounds of the box.
48 ub : npt.NDArray
49 Upper bounds of the box.
50 relaxation : float
51 Relaxation parameter for the projection.
52 proximity_flag : bool
53 Flag to indicate whether to take this object into account when calculating proximity.
54 idx : npt.NDArray
55 Subset of the input vector to apply the projection on.
56 """
58 def __init__(
59 self,
60 lb: npt.NDArray,
61 ub: npt.NDArray,
62 relaxation: float = 1,
63 idx: npt.NDArray | None = None,
64 proximity_flag=True,
65 use_gpu=False,
66 ):
68 super().__init__(relaxation, idx, proximity_flag, use_gpu)
69 self.lb = lb
70 self.ub = ub
72 def _project(self, x: npt.NDArray) -> npt.NDArray:
73 """
74 Projects the input array `x` onto the bounds defined by `self.lb`
75 and `self.ub`.
77 Parameters
78 ----------
79 x : npt.NDArray
80 Input array to be projected. Can be a NumPy array or a CuPy array.
82 Returns
83 -------
84 npt.NDArray
85 The projected array with values clipped to the specified bounds.
87 Notes
88 -----
89 This method modifies the input array `x` in place.
90 """
91 xp = cp if isinstance(x, cp.ndarray) else np
92 x[self.idx] = xp.maximum(self.lb, xp.minimum(self.ub, x[self.idx]))
93 return x
95 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
96 res = abs(x[self.idx] - self._project(x.copy())[self.idx])
97 measures = []
98 for measure in proximity_measures:
99 if isinstance(measure, tuple):
100 if measure[0] == "p_norm":
101 measures.append(1 / len(res) * (res ** measure[1]).sum())
102 else:
103 raise ValueError("Invalid proximity measure")
104 elif isinstance(measure, str) and measure == "max_norm":
105 measures.append(res.max())
106 else:
107 raise ValueError("Invalid proximity measure")
108 return measures
110 def visualize(self, ax: plt.Axes | None = None, color=None):
111 """
112 Visualize the box if it is 2D on a given matplotlib Axes.
114 Parameters
115 ----------
116 ax : plt.Axes, optional
117 The matplotlib Axes to plot on. If None, a new figure and axes are created.
118 color : str or None, optional
119 The color to fill the box with. If None, the box will be filled with the default color.
121 Raises
122 ------
123 ValueError
124 If the box is not 2-dimensional.
125 """
126 if len(self.lb) != 2:
127 raise ValueError("Visualization only possible for 2D boxes")
129 if ax is None:
130 _, ax = plt.subplots()
131 box = patches.Rectangle(
132 (self.lb[0], self.lb[1]),
133 self.ub[0] - self.lb[0],
134 self.ub[1] - self.lb[1],
135 linewidth=1,
136 edgecolor="black",
137 facecolor=color,
138 alpha=0.5,
139 )
140 ax.add_patch(box)
142 def get_xy(self):
143 """
144 Generate the coordinates for the edges of a box if it is 2D.
146 This method creates four edges of a 2D box defined by the lower bounds (lb) and upper bounds (ub).
147 The edges are generated using 100 points each.
149 Returns
150 -------
151 npt.NDArray
152 A 2D array of shape (2, 400) containing the concatenated coordinates of the four edges.
154 Raises
155 ------
156 ValueError
157 If the box is not 2-dimensional.
158 """
159 if len(self.lb) != 2:
160 raise ValueError("Visualization only possible for 2D boxes")
161 edge_1 = np.array([np.linspace(self.lb[0], self.ub[0], 100), np.ones(100) * self.lb[1]])
162 edge_2 = np.array([np.ones(100) * self.ub[0], np.linspace(self.lb[1], self.ub[1], 100)])
163 edge_3 = np.array([np.linspace(self.lb[0], self.ub[0], 100), np.ones(100) * self.ub[1]])
164 edge_4 = np.array([np.ones(100) * self.lb[0], np.linspace(self.lb[1], self.ub[1], 100)])
165 return np.concatenate((edge_1, edge_2, edge_3[:, ::-1], edge_4[:, ::-1]), axis=1)
168class WeightedBoxProjection(BasicProjection):
169 """
170 WeightedBoxProjection applies a weighted projection on a box defined by
171 lower and upper bounds.
172 The idea is a "simultaneous" variant to the "sequential" BoxProjection.
174 Parameters
175 ----------
176 lb : npt.NDArray
177 Lower bounds of the box.
178 ub : npt.NDArray
179 Upper bounds of the box.
180 weights : npt.NDArray
181 Weights for the projection.
182 relaxation : float, optional
183 Relaxation parameter, by default 1.
184 idx : npt.NDArray or None
185 Subset of the input vector to apply the projection on.
186 proximity_flag : bool, optional
187 Flag to indicate if proximity should be calculated, by default True.
188 use_gpu : bool, optional
189 Flag to indicate if GPU should be used, by default False.
191 Attributes
192 ----------
193 lb : npt.NDArray
194 Lower bounds of the box.
195 ub : npt.NDArray
196 Upper bounds of the box.
197 relaxation : float
198 Relaxation parameter for the projection.
199 proximity_flag : bool
200 Flag to indicate whether to take this object into account when calculating proximity.
201 idx : npt.NDArray
202 Subset of the input vector to apply the projection on.
203 """
205 def __init__(
206 self,
207 lb: npt.NDArray,
208 ub: npt.NDArray,
209 weights: npt.NDArray,
210 relaxation: float = 1,
211 idx: npt.NDArray | None = None,
212 proximity_flag=True,
213 use_gpu=False,
214 ):
216 super().__init__(relaxation, idx, proximity_flag, use_gpu)
217 self.lb = lb
218 self.ub = ub
219 self.weights = weights / weights.sum()
221 def _project(self, x: npt.NDArray) -> npt.NDArray:
222 """
223 Projects the input array `x`.
225 Parameters
226 ----------
227 x : npt.NDArray
228 The input array to be projected.
230 Returns
231 -------
232 npt.NDArray
233 The projected array.
235 Notes
236 -----
237 This method modifies the input array `x` in place.
238 """
239 xp = cp if isinstance(x, cp.ndarray) else np
240 x[self.idx] += self.weights * (
241 xp.maximum(self.lb, xp.minimum(self.ub, x[self.idx])) - x[self.idx]
242 )
243 return x
245 def _full_project(self, x: npt.NDArray) -> npt.NDArray:
246 """
247 Projects the elements of the input array `x` within the specified
248 bounds.
250 Parameters
251 ----------
252 x : npt.NDArray
253 Input array to be projected.
255 Returns
256 -------
257 npt.NDArray
258 The projected array with elements constrained within the bounds.
259 """
260 xp = cp if isinstance(x, cp.ndarray) else np
261 x[self.idx] = xp.maximum(self.lb, xp.minimum(self.ub, x[self.idx]))
263 return x
265 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
266 res = abs(x[self.idx] - self._project(x.copy())[self.idx])
267 measures = []
268 for measure in proximity_measures:
269 if isinstance(measure, tuple):
270 if measure[0] == "p_norm":
271 measures.append(self.weights @ (res ** measure[1]))
272 else:
273 raise ValueError("Invalid proximity measure")
274 elif isinstance(measure, str) and measure == "max_norm":
275 measures.append(res.max())
276 else:
277 raise ValueError("Invalid proximity measure")
278 return measures
280 def visualize(self, ax: plt.Axes | None = None, color=None):
281 """
282 Visualize the box if it is 2D on a given matplotlib Axes.
284 Parameters
285 ----------
286 ax : plt.Axes, optional
287 The matplotlib Axes to plot on. If None, a new figure and axes are created.
288 color : str or None, optional
289 The color to fill the box with. If None, the box will be filled with the default color.
291 Raises
292 ------
293 ValueError
294 If the box is not 2-dimensional.
295 """
296 if len(self.lb) != 2:
297 raise ValueError("Visualization only possible for 2D boxes")
299 if ax is None:
300 _, ax = plt.subplots()
301 box = patches.Rectangle(
302 (self.lb[0], self.lb[1]),
303 self.ub[0] - self.lb[0],
304 self.ub[1] - self.lb[1],
305 linewidth=1,
306 edgecolor="black",
307 facecolor=color,
308 alpha=0.5,
309 )
310 ax.add_patch(box)
312 def get_xy(self):
313 """
314 Generate the coordinates for the edges of a box if it is 2D.
316 This method creates four edges of a 2D box defined by the lower bounds (lb) and upper bounds (ub).
317 The edges are generated using 100 points each.
319 Returns
320 -------
321 np.ndarray
322 A 2D array of shape (2, 400) containing the concatenated coordinates of the four edges.
324 Raises
325 ------
326 ValueError
327 If the box is not 2-dimensional.
328 """
329 if len(self.lb) != 2:
330 raise ValueError("Visualization only possible for 2D boxes")
331 edge_1 = np.array([np.linspace(self.lb[0], self.ub[0], 100), np.ones(100) * self.lb[1]])
332 edge_2 = np.array([np.ones(100) * self.ub[0], np.linspace(self.lb[1], self.ub[1], 100)])
333 edge_3 = np.array([np.linspace(self.lb[0], self.ub[0], 100), np.ones(100) * self.ub[1]])
334 edge_4 = np.array([np.ones(100) * self.lb[0], np.linspace(self.lb[1], self.ub[1], 100)])
335 return np.concatenate((edge_1, edge_2, edge_3[:, ::-1], edge_4[:, ::-1]), axis=1)
338# Projection onto a single halfspace
339class HalfspaceProjection(BasicProjection):
340 """
341 A class used to represent a projection onto a halfspace.
343 Parameters
344 ----------
345 a : npt.NDArray
346 The normal vector defining the halfspace.
347 b : float
348 The offset value defining the halfspace.
349 relaxation : float, optional
350 The relaxation parameter, by default 1.
351 idx : npt.NDArray or None
352 Subset of the input vector to apply the projection on.
353 proximity_flag : bool, optional
354 Flag to indicate whether to take this object into account when calculating proximity, by default True.
355 use_gpu : bool, optional
356 Flag to indicate if GPU should be used, by default False.
358 Attributes
359 ----------
360 a : npt.NDArray
361 The normal vector defining the halfspace.
362 a_norm : npt.NDArray
363 The normalized normal vector.
364 b : float
365 The offset value defining the halfspace.
366 relaxation : float
367 The relaxation parameter for the projection.
368 proximity_flag : bool
369 Flag to indicate whether to take this object into account when calculating proximity.
370 idx : npt.NDArray
371 Subset of the input vector to apply the projection on.
372 """
374 def __init__(
375 self,
376 a: npt.NDArray,
377 b: float,
378 relaxation: float = 1,
379 idx: npt.NDArray | None = None,
380 proximity_flag=True,
381 use_gpu=False,
382 ):
384 super().__init__(relaxation, idx, proximity_flag, use_gpu)
385 self.a = a
386 self.a_norm = self.a / (self.a @ self.a)
387 self.b = b
389 def _linear_map(self, x):
390 return self.a @ x
392 def _project(self, x: npt.NDArray) -> npt.NDArray:
393 """
394 Projects the input array `x`.
396 Parameters
397 ----------
398 x : npt.NDArray
399 The input array to be projected.
401 Returns
402 -------
403 npt.NDArray
404 The projected array.
406 Notes
407 -----
408 This method modifies the input array `x` in place.
409 """
411 # TODO: dtype check!
412 y = self._linear_map(x[self.idx])
414 if y > self.b:
415 x[self.idx] -= (y - self.b) * self.a_norm
417 return x
419 def get_xy(self, x: npt.NDArray | None = None):
420 """
421 Generate x and y coordinates for visualization of 2D halfspaces.
423 Parameters
424 ----------
425 x : npt.NDArray or None, optional
426 The x-coordinates for which to compute the corresponding y-coordinates.
427 If None, a default range of x values from -10 to 10 is used.
429 Returns
430 -------
431 np.ndarray
432 A 2D array where the first row contains the x-coordinates and the second row contains the corresponding y-coordinates.
434 Raises
435 ------
436 ValueError
437 If the halfspace is not 2-dimensional.
438 """
439 if len(self.a) != 2:
440 raise ValueError("Visualization only possible for 2D halfspaces")
442 if x is None:
443 x = np.linspace(-10, 10, 100)
445 if self.a[1] == 0:
446 y = np.array([np.ones(100) * self.b, np.linspace(-10, 10, 100)])
447 else:
448 y = (self.b - self.a[0] * x) / self.a[1]
450 return np.array([x, y])
452 def visualize(
453 self,
454 ax: plt.Axes | None = None,
455 x: npt.NDArray | None = None,
456 y_fill: npt.NDArray | None = None,
457 color=None,
458 ):
459 """
460 Visualize the halfspace if it is 2D on a given matplotlib Axes.
462 Parameters
463 ----------
464 ax : plt.Axes, optional
465 The matplotlib Axes to plot on. If None, a new figure and axes are created.
466 color : str or None, optional
467 The color to fill the box with. If None, the halfspace will be filled with the default color.
469 Raises
470 ------
471 ValueError
472 If the halfspace is not 2-dimensional.
473 """
475 if len(self.a) != 2:
476 raise ValueError("Visualization only possible for 2D halfspaces")
478 if ax is None:
479 _, ax = plt.subplots()
481 if x is None:
482 x = np.linspace(-10, 10, 100)
484 if self.a[1] == 0:
485 ax.axvline(x=self.b / self.a[0], label="Halfspace", color=color)
486 if np.sign(self.a[0]) == 1:
487 ax.fill_betweenx(
488 x,
489 ax.get_xlim()[0],
490 self.b,
491 color=color,
492 label="Halfspace",
493 alpha=0.5,
494 )
495 else:
496 ax.fill_betweenx(
497 x,
498 self.b,
499 ax.get_xlim()[1],
500 color=color,
501 label="Halfspace",
502 alpha=0.5,
503 )
505 else:
506 y = (self.b - self.a[0] * x) / self.a[1]
507 ax.plot(x, y, color="xkcd:black")
508 if y_fill is None:
509 y_fill = np.min(y) if self.a[1] > 0 else np.max(y)
511 ax.fill_between(x, y, y_fill, color=color, label="Halfspace", alpha=0.5)
514class BandProjection(BasicProjection):
515 """
516 A class used to represent a projection onto a band.
518 Parameters
519 ----------
520 a : npt.NDArray
521 The normal vector defining the halfspace.
522 lb : float
523 The lower bound of the band.
524 ub : float
525 The upper bound of the band.
526 idx : npt.NDArray or None
527 Subset of the input vector to apply the projection on.
528 relaxation : float, optional
529 The relaxation parameter, by default 1.
530 idx : npt.NDArray or None
531 Subset of the input vector to apply the projection on.
533 Attributes
534 ----------
535 a : npt.NDArray
536 The normal vector defining the halfspace.
537 a_norm : npt.NDArray
538 The normalized normal vector.
539 lb : float
540 The lower bound of the band.
541 ub : float
542 The upper bound of the band.
543 relaxation : float
544 The relaxation parameter for the projection.
545 proximity_flag : bool
546 Flag to indicate whether to take this object into account when calculating proximity.
547 idx : npt.NDArray
548 Subset of the input vector to apply the projection on.
549 """
551 def __init__(
552 self,
553 a: npt.NDArray,
554 lb: float,
555 ub: float,
556 relaxation: float = 1,
557 idx: npt.NDArray | None = None,
558 proximity_flag=True,
559 use_gpu=False,
560 ):
562 super().__init__(relaxation, idx, proximity_flag, use_gpu)
563 self.a = a
564 self.a_norm = self.a / (self.a @ self.a)
565 self.lb = lb
566 self.ub = ub
568 def _project(self, x: npt.NDArray) -> npt.NDArray:
569 """
570 Projects the input array `x`.
572 Parameters
573 ----------
574 x : npt.NDArray
575 The input array to be projected.
577 Returns
578 -------
579 npt.NDArray
580 The projected array.
582 Notes
583 -----
584 This method modifies the input array `x` in place.
585 """
586 y = self.a @ x[self.idx]
588 if y > self.ub:
589 x[self.idx] -= (y - self.ub) * self.a_norm
590 elif y < self.lb:
591 x[self.idx] -= (y - self.lb) * self.a_norm
593 return x
595 def get_xy(self, x: npt.NDArray | None = None):
596 """
597 Calculate the x and y coordinates for the lower and upper bounds of
598 a 2D band.
600 Parameters
601 ----------
602 x : npt.NDArray or None, optional
603 The x-coordinates at which to evaluate the bounds. If None, a default range
604 from -10 to 10 with 100 points is used.
606 Returns
607 -------
608 tuple of np.ndarray
609 A tuple containing two numpy arrays:
610 - The first array represents the x and y coordinates for the lower bound.
611 - The second array represents the x and y coordinates for the upper bound.
613 Raises
614 ------
615 ValueError
616 If the band is not 2-dimensional.
617 """
619 if len(self.a) != 2:
620 raise ValueError("Visualization only possible for 2D bands")
622 if x is None:
623 x = np.linspace(-10, 10, 100)
624 if self.a[1] == 0:
625 y_lb = np.array([np.ones(100) * self.lb, np.linspace(-10, 10, 100)])
626 y_ub = np.array([np.ones(100) * self.ub, np.linspace(-10, 10, 100)])
627 else:
628 y_lb = (self.lb - self.a[0] * x) / self.a[1]
629 y_ub = (self.ub - self.a[0] * x) / self.a[1]
630 return np.array([x, y_lb]), np.array([x, y_ub])
632 def visualize(self, ax: plt.Axes | None = None, x: npt.NDArray | None = None, color=None):
633 """
634 Visualize the band if it is 2D on a given matplotlib Axes.
636 Parameters
637 ----------
638 ax : plt.Axes, optional
639 The matplotlib Axes to plot on. If None, a new figure and axes are created.
640 color : str or None, optional
641 The color to fill the box with. If None, the band will be filled with the default color.
643 Raises
644 ------
645 ValueError
646 If the band is not 2-dimensional.
647 """
649 if len(self.a) != 2:
650 raise ValueError("Visualization only possible for 2D bands")
652 if ax is None:
653 _, ax = plt.subplots()
655 if x is None:
656 x = np.linspace(-10, 10, 100)
658 if self.a[1] == 0:
659 ax.plot(np.ones(100) * self.lb, x, color="xkcd:black")
660 ax.plot(np.ones(100) * self.ub, x, color="xkcd:black")
661 # ax.axvline(x = self.b/self.a[0],label='Halfspace',color = color)
662 if np.sign(self.a[0]) == 1:
663 ax.fill_betweenx(x, self.lb, self.ub, color=color, label="Band", alpha=0.5)
664 else:
665 ax.fill_betweenx(x, self.lb, self.ub, color=color, label="Band", alpha=0.5)
666 else:
667 y_lb = (self.lb - self.a[0] * x) / self.a[1]
668 y_ub = (self.ub - self.a[0] * x) / self.a[1]
669 ax.plot(x, y_lb, color="xkcd:black")
670 ax.plot(x, y_ub, color="xkcd:black")
671 ax.fill_between(x, y_lb, y_ub, color=color, label="Band", alpha=0.5)
674class BallProjection(BasicProjection):
675 """
676 A class used to represent a projection onto a ball.
678 Parameters
679 ----------
680 center : npt.NDArray
681 The center of the ball.
682 radius : float
683 The radius of the ball.
684 relaxation : float, optional
685 The relaxation parameter (default is 1).
686 idx : npt.NDArray or None
687 Subset of the input vector to apply the projection on.
688 proximity_flag : bool, optional
689 Flag to indicate whether to take this object into account when calculating proximity, by default True.
690 use_gpu : bool, optional
691 Flag to indicate if GPU should be used, by default False.
693 Attributes
694 ----------
695 center : npt.NDArray
696 The center of the ball.
697 radius : float
698 The radius of the ball.
699 relaxation : float
700 The relaxation parameter for the projection.
701 proximity_flag : bool
702 Flag to indicate whether to take this object into account when calculating proximity.
703 idx : npt.NDArray
704 Subset of the input vector to apply the projection on.
705 """
707 def __init__(
708 self,
709 center: npt.NDArray,
710 radius: float,
711 relaxation: float = 1,
712 idx: npt.NDArray | None = None,
713 proximity_flag=True,
714 use_gpu=False,
715 ):
717 super().__init__(relaxation, idx, proximity_flag, use_gpu)
718 self.center = center
719 self.radius = radius
721 def _project(self, x: npt.NDArray) -> npt.NDArray:
722 """
723 Projects the input array `x` onto the surface of the ball.
725 Parameters
726 ----------
727 x : npt.NDArray
728 The input array to be projected.
730 Returns
731 -------
732 npt.NDArray
733 The projected array.
734 """
735 xp = cp if isinstance(x, cp.ndarray) else np
736 if xp.linalg.norm(x[self.idx] - self.center) > self.radius:
737 x[self.idx] -= (x[self.idx] - self.center) * (
738 1 - self.radius / xp.linalg.norm(x[self.idx] - self.center)
739 )
741 return x
743 def visualize(self, ax: plt.Axes | None = None, color=None, edgecolor=None):
744 """
745 Visualize the halfspace if it is 2D on a given matplotlib Axes.
747 Parameters
748 ----------
749 ax : plt.Axes, optional
750 The matplotlib Axes to plot on. If None, a new figure and axes are created.
751 color : str or None, optional
752 The color to fill the box with. If None, the halfspace will be filled with the default color.
754 Raises
755 ------
756 ValueError
757 If the halfspace is not 2-dimensional.
758 """
760 if len(self.center) != 2:
761 raise ValueError("Visualization only possible for 2D balls")
763 if ax is None:
764 _, ax = plt.subplots()
766 circle = plt.Circle(
767 (self.center[0], self.center[1]),
768 self.radius,
769 facecolor=color,
770 alpha=0.5,
771 edgecolor=edgecolor,
772 )
773 ax.add_artist(circle)
775 def get_xy(self):
776 """
777 Generate x and y coordinates for a 2D ball visualization.
779 Returns
780 -------
781 np.ndarray
782 A 2x50 array where the first row contains the x coordinates and the
783 second row contains the y coordinates of the points on the circumference
784 of the 2D ball.
786 Raises
787 ------
788 ValueError
789 If the center does not have exactly 2 dimensions.
790 """
791 if len(self.center) != 2:
792 raise ValueError("Visualization only possible for 2D balls")
794 theta = np.linspace(0, 2 * np.pi, 50)
795 x = self.center[0] + self.radius * np.cos(theta)
796 y = self.center[1] + self.radius * np.sin(theta)
797 return np.array([x, y])
800class MaxDVHProjection(BasicProjection):
801 """
802 Class for max dose-volume histogram projections.
804 Parameters
805 ----------
806 d_max : float
807 The maximum dose value.
808 max_percentage : float
809 The maximum percentage of elements allowed to exceed d_max.
810 idx : npt.NDArray or None
811 Subset of the input vector to apply the projection on.
813 Attributes
814 ----------
815 d_max : float
816 The maximum dose value.
817 max_percentage : float
818 The maximum percentage of elements allowed to exceed d_max.
819 """
821 def __init__(
822 self,
823 d_max: float,
824 max_percentage: float,
825 idx: npt.NDArray | None = None,
826 proximity_flag=True,
827 use_gpu=False,
828 ):
829 super().__init__(1, idx, proximity_flag, use_gpu)
831 # max percentage of elements that are allowed to exceed d_max
832 self.max_percentage = max_percentage
833 self.d_max = d_max
835 if isinstance(self.idx, slice):
836 self._idx_indices = None
837 elif self.idx.dtype == bool:
838 raise ValueError("Boolean indexing is not supported for this projection.")
839 else:
840 self._idx_indices = self.idx
842 def _project(self, x: npt.NDArray) -> npt.NDArray:
843 """
844 Projects the input array `x` onto the DVH constraint.
846 Parameters
847 ----------
848 x : npt.NDArray
849 The input array to be projected.
851 Returns
852 -------
853 npt.NDArray
854 The projected array.
855 """
856 if isinstance(self.idx, slice):
857 return self._project_all(x)
859 return self._project_subset(x)
861 def _project_all(self, x: npt.NDArray) -> npt.NDArray:
862 n = len(x)
863 am = math.floor(self.max_percentage * n)
865 l = (x > self.d_max).sum()
867 z = l - am
869 if z > 0:
870 x[x.argsort()[n - l : n - am]] = self.d_max
871 return x
873 def _project_subset(self, x: npt.NDArray) -> npt.NDArray:
875 n = self.idx.sum() if self.idx.dtype == bool else len(self.idx)
877 am = math.floor(self.max_percentage * n)
879 l = (x[self.idx] > self.d_max).sum()
881 z = l - am # number of elements that need to be reduced
883 if z > 0:
884 x[self._idx_indices[x[self.idx].argsort()[n - l : n - am]]] = self.d_max
886 return x
888 # def _project(self, x: npt.NDArray) -> npt.NDArray:
889 # """
890 # Projects the input array `x` onto the DVH constraint.
892 # Parameters
893 # ----------
894 # x : npt.NDArray
895 # The input array to be projected.
897 # Returns
898 # -------
899 # npt.NDArray
900 # The projected array.
902 # Notes
903 # -----
904 # - The method calculates the number of elements that should receive a dose lower than `d_max` based on `max_percentage`.
905 # - It then determines how many elements in the input array exceed `d_max`.
906 # - If the number of elements exceeding `d_max` is greater than the allowed maximum, it reduces the highest values to `d_max`.
907 # """
908 # # percentage of elements that should receive a dose lower than d_max
909 # n = len(x) if isinstance(self.idx, slice) else self.idx.sum()
910 # am = math.floor(self.max_percentage * n)
912 # # number of elements in structure with dose greater than d_max
913 # l = (x[self.idx] > self.d_max).sum()
915 # z = l - am # number of elements that need to be reduced
917 # if z > 0:
918 # x[x[self.idx].argsort()[n - l : n - am]] = self.d_max
920 # return x
922 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
923 """
924 Calculate the proximity of the given array to a specified maximum
925 percentage.
927 Parameters
928 ----------
929 x : npt.NDArray
930 Input array to be evaluated.
932 Returns
933 -------
934 float
935 The proximity value as a percentage.
936 """
937 # TODO: Find appropriate proximity measure
938 raise NotImplementedError
940 # n = len(x) if isinstance(self.idx, slice) else self.idx.sum()
941 # return abs((1 / n * (x[self.idx] > self.d_max).sum()) - self.max_percentage) * 100
944class MinDVHProjection(BasicProjection):
945 """"""
947 def __init__(
948 self,
949 d_min: float,
950 min_percentage: float,
951 idx: npt.NDArray | None = None,
952 proximity_flag=True,
953 use_gpu=False,
954 ):
955 super().__init__(1, idx, proximity_flag, use_gpu)
957 # percentage of elements that need to have at least d_min
958 self.min_percentage = min_percentage
959 self.d_min = d_min
960 if isinstance(self.idx, slice):
961 self._idx_indices = None
962 elif self.idx.dtype == bool:
963 raise ValueError("Boolean indexing is not supported for this projection.")
964 else:
965 self._idx_indices = self.idx
967 def _project(self, x: npt.NDArray) -> npt.NDArray:
968 """
969 Projects the input array `x` onto the DVH constraint.
971 Parameters
972 ----------
973 x : npt.NDArray
974 The input array to be projected.
976 Returns
977 -------
978 npt.NDArray
979 The projected array.
980 """
981 if isinstance(self.idx, slice):
982 return self._project_all(x)
984 return self._project_subset(x)
986 def _project_all(self, x: npt.NDArray) -> npt.NDArray:
987 n = len(x)
988 am = math.ceil(self.min_percentage * n)
990 l = (x > self.d_min).sum()
992 z = am - l
994 if z > 0:
995 x[x.argsort()[n - am : n - l]] = self.d_min
996 return x
998 def _project_subset(self, x: npt.NDArray) -> npt.NDArray:
1000 n = self.idx.sum() if self.idx.dtype == bool else len(self.idx)
1002 am = math.ceil(self.min_percentage * n)
1004 l = (x[self.idx] > self.d_min).sum()
1006 z = am - l
1008 if z > 0:
1009 x[self._idx_indices[x[self.idx].argsort()[n - am : n - l]]] = self.d_min
1011 return x
1013 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
1014 """
1015 Calculate the proximity of the given array to a specified maximum
1016 percentage.
1018 Parameters
1019 ----------
1020 x : npt.NDArray
1021 Input array to be evaluated.
1023 Returns
1024 -------
1025 float
1026 The proximity value as a percentage.
1027 """
1028 # TODO: Find appropriate proximity measure
1029 raise NotImplementedError