Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\projections\_basic_projections.py: 73%

1"""Simple projection objects."""

2 import math

3 from typing import List

4 import numpy as np

5 import numpy.typing as npt

6 import matplotlib.pyplot as plt

7 from matplotlib import patches

8

9 from suppy.projections._projections import BasicProjection

10

11 try:

12 import cupy as cp

13

14 NO_GPU = False

15 except ImportError:

16 NO_GPU = True

17 cp = np

18

19# from suppy.utils.decorators import ensure_float_array

20

21

22# Class for basic projections

23

24

25 class BoxProjection(BasicProjection):

26 """

27 BoxProjection class for projecting points onto a box defined by lower

28 and upper bounds.

29

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.

43

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 """

57

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 ):

67

68 super().__init__(relaxation, idx, proximity_flag, use_gpu)

69 self.lb = lb

70 self.ub = ub

71

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`.

76

77 Parameters

78 ----------

79 x : npt.NDArray

80 Input array to be projected. Can be a NumPy array or a CuPy array.

81

82 Returns

83 -------

84 npt.NDArray

85 The projected array with values clipped to the specified bounds.

86

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

94

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

109

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.

113

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.

120

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")

128

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)

141

142 def get_xy(self):

143 """

144 Generate the coordinates for the edges of a box if it is 2D.

145

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.

148

149 Returns

150 -------

151 npt.NDArray

152 A 2D array of shape (2, 400) containing the concatenated coordinates of the four edges.

153

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)

166

167

168 class 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.

173

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.

190

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 """

204

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 ):

215

216 super().__init__(relaxation, idx, proximity_flag, use_gpu)

217 self.lb = lb

218 self.ub = ub

219 self.weights = weights / weights.sum()

220

221 def _project(self, x: npt.NDArray) -> npt.NDArray:

222 """

223 Projects the input array `x`.

224

225 Parameters

226 ----------

227 x : npt.NDArray

228 The input array to be projected.

229

230 Returns

231 -------

232 npt.NDArray

233 The projected array.

234

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

244

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.

249

250 Parameters

251 ----------

252 x : npt.NDArray

253 Input array to be projected.

254

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]))

262

263 return x

264

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

279

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.

283

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.

290

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")

298

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)

311

312 def get_xy(self):

313 """

314 Generate the coordinates for the edges of a box if it is 2D.

315

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.

318

319 Returns

320 -------

321 np.ndarray

322 A 2D array of shape (2, 400) containing the concatenated coordinates of the four edges.

323

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)

336

337

338# Projection onto a single halfspace

339 class HalfspaceProjection(BasicProjection):

340 """

341 A class used to represent a projection onto a halfspace.

342

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.

357

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 """

373

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 ):

383

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

388

389 def _linear_map(self, x):

390 return self.a @ x

391

392 def _project(self, x: npt.NDArray) -> npt.NDArray:

393 """

394 Projects the input array `x`.

395

396 Parameters

397 ----------

398 x : npt.NDArray

399 The input array to be projected.

400

401 Returns

402 -------

403 npt.NDArray

404 The projected array.

405

406 Notes

407 -----

408 This method modifies the input array `x` in place.

409 """

410

411 # TODO: dtype check!

412 y = self._linear_map(x[self.idx])

413

414 if y > self.b:

415 x[self.idx] -= (y - self.b) * self.a_norm

416

417 return x

418

419 def get_xy(self, x: npt.NDArray | None = None):

420 """

421 Generate x and y coordinates for visualization of 2D halfspaces.

422

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.

428

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.

433

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")

441

442 if x is None:

443 x = np.linspace(-10, 10, 100)

444

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]

449

450 return np.array([x, y])

451

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.

461

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.

468

469 Raises

470 ------

471 ValueError

472 If the halfspace is not 2-dimensional.

473 """

474

475 if len(self.a) != 2:

476 raise ValueError("Visualization only possible for 2D halfspaces")

477

478 if ax is None:

479 _, ax = plt.subplots()

480

481 if x is None:

482 x = np.linspace(-10, 10, 100)

483

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 )

504

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)

510

511 ax.fill_between(x, y, y_fill, color=color, label="Halfspace", alpha=0.5)

512

513

514 class BandProjection(BasicProjection):

515 """

516 A class used to represent a projection onto a band.

517

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.

532

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 """

550

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 ):

561

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

567

568 def _project(self, x: npt.NDArray) -> npt.NDArray:

569 """

570 Projects the input array `x`.

571

572 Parameters

573 ----------

574 x : npt.NDArray

575 The input array to be projected.

576

577 Returns

578 -------

579 npt.NDArray

580 The projected array.

581

582 Notes

583 -----

584 This method modifies the input array `x` in place.

585 """

586 y = self.a @ x[self.idx]

587

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

592

593 return x

594

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.

599

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.

605

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.

612

613 Raises

614 ------

615 ValueError

616 If the band is not 2-dimensional.

617 """

618

619 if len(self.a) != 2:

620 raise ValueError("Visualization only possible for 2D bands")

621

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])

631

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.

635

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.

642

643 Raises

644 ------

645 ValueError

646 If the band is not 2-dimensional.

647 """

648

649 if len(self.a) != 2:

650 raise ValueError("Visualization only possible for 2D bands")

651

652 if ax is None:

653 _, ax = plt.subplots()

654

655 if x is None:

656 x = np.linspace(-10, 10, 100)

657

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)

672

673

674 class BallProjection(BasicProjection):

675 """

676 A class used to represent a projection onto a ball.

677

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.

692

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 """

706

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 ):

716

717 super().__init__(relaxation, idx, proximity_flag, use_gpu)

718 self.center = center

719 self.radius = radius

720

721 def _project(self, x: npt.NDArray) -> npt.NDArray:

722 """

723 Projects the input array `x` onto the surface of the ball.

724

725 Parameters

726 ----------

727 x : npt.NDArray

728 The input array to be projected.

729

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 )

740

741 return x

742

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.

746

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.

753

754 Raises

755 ------

756 ValueError

757 If the halfspace is not 2-dimensional.

758 """

759

760 if len(self.center) != 2:

761 raise ValueError("Visualization only possible for 2D balls")

762

763 if ax is None:

764 _, ax = plt.subplots()

765

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)

774

775 def get_xy(self):

776 """

777 Generate x and y coordinates for a 2D ball visualization.

778

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.

785

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")

793

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])

798

799

800 class MaxDVHProjection(BasicProjection):

801 """

802 Class for max dose-volume histogram projections.

803

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.

812

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 """

820

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)

830

831 # max percentage of elements that are allowed to exceed d_max

832 self.max_percentage = max_percentage

833 self.d_max = d_max

834

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

841

842 def _project(self, x: npt.NDArray) -> npt.NDArray:

843 """

844 Projects the input array `x` onto the DVH constraint.

845

846 Parameters

847 ----------

848 x : npt.NDArray

849 The input array to be projected.

850

851 Returns

852 -------

853 npt.NDArray

854 The projected array.

855 """

856 if isinstance(self.idx, slice):

857 return self._project_all(x)

858

859 return self._project_subset(x)

860

861 def _project_all(self, x: npt.NDArray) -> npt.NDArray:

862 n = len(x)

863 am = math.floor(self.max_percentage * n)

864

865 l = (x > self.d_max).sum()

866

867 z = l - am

868

869 if z > 0:

870 x[x.argsort()[n - l : n - am]] = self.d_max

871 return x

872

873 def _project_subset(self, x: npt.NDArray) -> npt.NDArray:

874

875 n = self.idx.sum() if self.idx.dtype == bool else len(self.idx)

876

877 am = math.floor(self.max_percentage * n)

878

879 l = (x[self.idx] > self.d_max).sum()

880

881 z = l - am # number of elements that need to be reduced

882

883 if z > 0:

884 x[self._idx_indices[x[self.idx].argsort()[n - l : n - am]]] = self.d_max

885

886 return x

887

888 # def _project(self, x: npt.NDArray) -> npt.NDArray:

889 # """

890 # Projects the input array `x` onto the DVH constraint.

891

892 # Parameters

893 # ----------

894 # x : npt.NDArray

895 # The input array to be projected.

896

897 # Returns

898 # -------

899 # npt.NDArray

900 # The projected array.

901

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)

911

912 # # number of elements in structure with dose greater than d_max

913 # l = (x[self.idx] > self.d_max).sum()

914

915 # z = l - am # number of elements that need to be reduced

916

917 # if z > 0:

918 # x[x[self.idx].argsort()[n - l : n - am]] = self.d_max

919

920 # return x

921

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.

926

927 Parameters

928 ----------

929 x : npt.NDArray

930 Input array to be evaluated.

931

932 Returns

933 -------

934 float

935 The proximity value as a percentage.

936 """

937 # TODO: Find appropriate proximity measure

938 raise NotImplementedError

939

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

942

943

944 class MinDVHProjection(BasicProjection):

945 """"""

946

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)

956

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

966

967 def _project(self, x: npt.NDArray) -> npt.NDArray:

968 """

969 Projects the input array `x` onto the DVH constraint.

970

971 Parameters

972 ----------

973 x : npt.NDArray

974 The input array to be projected.

975

976 Returns

977 -------

978 npt.NDArray

979 The projected array.

980 """

981 if isinstance(self.idx, slice):

982 return self._project_all(x)

983

984 return self._project_subset(x)

985

986 def _project_all(self, x: npt.NDArray) -> npt.NDArray:

987 n = len(x)

988 am = math.ceil(self.min_percentage * n)

989

990 l = (x > self.d_min).sum()

991

992 z = am - l

993

994 if z > 0:

995 x[x.argsort()[n - am : n - l]] = self.d_min

996 return x

997

998 def _project_subset(self, x: npt.NDArray) -> npt.NDArray:

999

1000 n = self.idx.sum() if self.idx.dtype == bool else len(self.idx)

1001

1002 am = math.ceil(self.min_percentage * n)

1003

1004 l = (x[self.idx] > self.d_min).sum()

1005

1006 z = am - l

1007

1008 if z > 0:

1009 x[self._idx_indices[x[self.idx].argsort()[n - am : n - l]]] = self.d_min

1010

1011 return x

1012

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.

1017

1018 Parameters

1019 ----------

1020 x : npt.NDArray

1021 Input array to be evaluated.

1022

1023 Returns

1024 -------

1025 float

1026 The proximity value as a percentage.

1027 """

1028 # TODO: Find appropriate proximity measure

1029 raise NotImplementedError