pygeoinf 1.3.8__py3-none-any.whl → 1.4.0__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.
pygeoinf/subsets.py ADDED
@@ -0,0 +1,845 @@
1
+ """
2
+ Defines classes for representing subsets of a Hilbert space.
3
+
4
+ This module provides a hierarchy of classes for sets, ranging from abstract
5
+ definitions to concrete geometric shapes. It supports Constructive Solid
6
+ Geometry (CSG) operations, with specialized handling for convex intersections
7
+ via functional combination.
8
+
9
+ Hierarchy:
10
+ - Subset (Abstract Base)
11
+ - EmptySet / UniversalSet
12
+ - LevelSet (f(x) = c)
13
+ - EllipsoidSurface -> Sphere
14
+ - SublevelSet (f(x) <= c)
15
+ - ConvexSubset -> Ellipsoid -> Ball
16
+ - ConvexIntersection (Max-Functional Combination)
17
+ - Intersection (Generic)
18
+ - Union (Generic)
19
+ - Complement (S^c)
20
+ """
21
+
22
+ from __future__ import annotations
23
+ from abc import ABC, abstractmethod
24
+ from typing import TYPE_CHECKING, Optional, List, Iterable
25
+ import numpy as np
26
+
27
+ from .nonlinear_forms import NonLinearForm
28
+
29
+ if TYPE_CHECKING:
30
+ from .hilbert_space import HilbertSpace, Vector
31
+ from .linear_operators import LinearOperator
32
+
33
+
34
+ class Subset(ABC):
35
+ """
36
+ Abstract base class for a subset of a HilbertSpace.
37
+
38
+ This class defines the minimal interface required for a mathematical set:
39
+ knowing which space it lives in, determining if a vector belongs to it,
40
+ accessing its boundary, and performing logical set operations.
41
+ """
42
+
43
+ def __init__(self, domain: Optional[HilbertSpace] = None) -> None:
44
+ """
45
+ Initializes the subset.
46
+
47
+ Args:
48
+ domain: The Hilbert space containing this subset. Can be None
49
+ for sets that are not strictly attached to a specific domain
50
+ (e.g., a generic EmptySet).
51
+ """
52
+ self._domain = domain
53
+
54
+ @property
55
+ def domain(self) -> HilbertSpace:
56
+ """
57
+ The underlying Hilbert space.
58
+
59
+ Returns:
60
+ The HilbertSpace instance associated with this subset.
61
+
62
+ Raises:
63
+ ValueError: If the domain was not set during initialization.
64
+ """
65
+ if self._domain is None:
66
+ raise ValueError(
67
+ f"{self.__class__.__name__} does not have an associated domain."
68
+ )
69
+ return self._domain
70
+
71
+ @property
72
+ def is_empty(self) -> bool:
73
+ """
74
+ Returns True if the set is known to be empty.
75
+
76
+ Returns:
77
+ bool: True if the set contains no elements, False otherwise.
78
+ Note that returning False does not guarantee the set is non-empty,
79
+ only that it is not trivially known to be empty.
80
+ """
81
+ return False
82
+
83
+ @abstractmethod
84
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
85
+ """
86
+ Returns True if the vector x lies within the subset.
87
+
88
+ Args:
89
+ x: A vector from the domain.
90
+ rtol: Relative tolerance for floating-point comparisons (e.g.,
91
+ checking equality f(x) = c or inequality f(x) <= c).
92
+
93
+ Returns:
94
+ bool: True if x ∈ S, False otherwise.
95
+ """
96
+
97
+ @property
98
+ @abstractmethod
99
+ def boundary(self) -> Subset:
100
+ """
101
+ Returns the boundary of the subset.
102
+
103
+ Returns:
104
+ Subset: A new Subset instance representing ∂S.
105
+ """
106
+
107
+ # --- CSG Operations ---
108
+
109
+ @property
110
+ def complement(self) -> Subset:
111
+ """
112
+ Returns the complement of this set: S^c = {x | x not in S}.
113
+
114
+ Returns:
115
+ Complement: A generic Complement wrapper around this set.
116
+ """
117
+ return Complement(self)
118
+
119
+ def intersect(self, other: Subset) -> Subset:
120
+ """
121
+ Returns the intersection of this set and another: S ∩ O.
122
+
123
+ If both sets are instances of ConvexSubset, this returns a
124
+ ConvexIntersection, which combines their functionals into a single
125
+ convex constraint F(x) = max(f1(x), f2(x)).
126
+
127
+ Args:
128
+ other: Another Subset instance.
129
+
130
+ Returns:
131
+ Subset: A new set representing elements present in both sets.
132
+ """
133
+ # Collect all subsets if we are merging intersections
134
+ subsets_to_merge = []
135
+
136
+ if isinstance(self, Intersection): # Includes ConvexIntersection
137
+ subsets_to_merge.extend(self.subsets)
138
+ else:
139
+ subsets_to_merge.append(self)
140
+
141
+ if isinstance(other, Intersection):
142
+ subsets_to_merge.extend(other.subsets)
143
+ else:
144
+ subsets_to_merge.append(other)
145
+
146
+ # Check if all parts are ConvexSubsets (defined by f(x) <= c)
147
+ all_convex_functional = all(
148
+ isinstance(s, ConvexSubset) for s in subsets_to_merge
149
+ )
150
+
151
+ if all_convex_functional:
152
+ # We can combine them into a single ConvexSubset via max function
153
+ return ConvexIntersection(subsets_to_merge) # type: ignore
154
+
155
+ # Fallback to generic set logic
156
+ return Intersection(subsets_to_merge)
157
+
158
+ def union(self, other: Subset) -> Union:
159
+ """
160
+ Returns the union of this set and another: S ∪ O.
161
+
162
+ Args:
163
+ other: Another Subset instance.
164
+
165
+ Returns:
166
+ Union: A new set representing elements present in either set.
167
+ """
168
+ subsets_to_merge = [self]
169
+ if isinstance(other, Union):
170
+ subsets_to_merge.extend(other.subsets)
171
+ else:
172
+ subsets_to_merge.append(other)
173
+ return Union(subsets_to_merge)
174
+
175
+
176
+ class EmptySet(Subset):
177
+ """
178
+ Represents the empty set (∅).
179
+ """
180
+
181
+ @property
182
+ def is_empty(self) -> bool:
183
+ """Returns True, as this is the empty set."""
184
+ return True
185
+
186
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
187
+ """Returns False for any vector."""
188
+ return False
189
+
190
+ @property
191
+ def boundary(self) -> Subset:
192
+ """The boundary of an empty set is the empty set itself."""
193
+ return self
194
+
195
+ @property
196
+ def complement(self) -> Subset:
197
+ """The complement of the empty set is the whole space (Universal Set)."""
198
+ return UniversalSet(self.domain)
199
+
200
+
201
+ class UniversalSet(Subset):
202
+ """
203
+ Represents the entire domain (Ω).
204
+ """
205
+
206
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
207
+ """Returns True for any vector in the domain."""
208
+ return True
209
+
210
+ @property
211
+ def boundary(self) -> Subset:
212
+ """The boundary of the entire topological space is empty."""
213
+ return EmptySet(self.domain)
214
+
215
+ @property
216
+ def complement(self) -> Subset:
217
+ """The complement of the universe is the empty set."""
218
+ return EmptySet(self.domain)
219
+
220
+
221
+ class Complement(Subset):
222
+ """
223
+ Represents the complement of a set: S^c = {x | x ∉ S}.
224
+ """
225
+
226
+ def __init__(self, subset: Subset) -> None:
227
+ """
228
+ Args:
229
+ subset: The set to complement.
230
+ """
231
+ super().__init__(subset.domain)
232
+ self._subset = subset
233
+
234
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
235
+ """
236
+ Returns True if x is NOT in the underlying subset.
237
+ """
238
+ return not self._subset.is_element(x, rtol=rtol)
239
+
240
+ @property
241
+ def boundary(self) -> Subset:
242
+ """
243
+ Returns the boundary of the complement.
244
+
245
+ Ideally, ∂(S^c) = ∂S.
246
+ """
247
+ return self._subset.boundary
248
+
249
+ @property
250
+ def complement(self) -> Subset:
251
+ """
252
+ Returns the complement of the complement, which is the original set.
253
+ (S^c)^c = S.
254
+ """
255
+ return self._subset
256
+
257
+
258
+ class Intersection(Subset):
259
+ """
260
+ Represents the generic intersection of multiple subsets: S = S_1 ∩ S_2 ...
261
+
262
+ Used when the subsets cannot be mathematically combined into a single functional
263
+ (e.g., non-convex sets).
264
+ """
265
+
266
+ def __init__(self, subsets: Iterable[Subset]) -> None:
267
+ """
268
+ Args:
269
+ subsets: An iterable of Subset objects to intersect.
270
+ All subsets must belong to the same domain.
271
+ """
272
+ subsets_list = list(subsets)
273
+ if not subsets_list:
274
+ raise ValueError("Intersection requires at least one subset.")
275
+ domain = subsets_list[0].domain
276
+
277
+ # Validate domains match
278
+ for s in subsets_list:
279
+ if s.domain != domain:
280
+ raise ValueError("All subsets must belong to the same domain.")
281
+
282
+ super().__init__(domain)
283
+ self._subsets = subsets_list
284
+
285
+ @property
286
+ def subsets(self) -> List[Subset]:
287
+ """Direct access to the component sets."""
288
+ return self._subsets
289
+
290
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
291
+ """Returns True if x is in ALL component subsets."""
292
+ return all(s.is_element(x, rtol=rtol) for s in self._subsets)
293
+
294
+ @property
295
+ def boundary(self) -> Subset:
296
+ """
297
+ Returns the boundary of the intersection.
298
+
299
+ The general topological boundary is complex: ∂(A ∩ B) ⊆ (∂A ∩ B) ∪ (A ∩ ∂B).
300
+ Currently raises NotImplementedError.
301
+ """
302
+ raise NotImplementedError(
303
+ "General boundary of intersection not yet implemented."
304
+ )
305
+
306
+ @property
307
+ def complement(self) -> Subset:
308
+ """
309
+ Returns the complement of the intersection.
310
+
311
+ Applies De Morgan's Law: (A ∩ B)^c = A^c ∪ B^c.
312
+ Returns a Union of the complements.
313
+ """
314
+ return Union(s.complement for s in self._subsets)
315
+
316
+
317
+ class Union(Subset):
318
+ """
319
+ Represents the union of multiple subsets: S = S_1 ∪ S_2 ...
320
+ """
321
+
322
+ def __init__(self, subsets: Iterable[Subset]) -> None:
323
+ """
324
+ Args:
325
+ subsets: An iterable of Subset objects to unite.
326
+ All subsets must belong to the same domain.
327
+ """
328
+ subsets_list = list(subsets)
329
+ if not subsets_list:
330
+ raise ValueError("Union requires at least one subset.")
331
+ domain = subsets_list[0].domain
332
+ for s in subsets_list:
333
+ if s.domain != domain:
334
+ raise ValueError("All subsets must belong to the same domain.")
335
+ super().__init__(domain)
336
+ self._subsets = subsets_list
337
+
338
+ @property
339
+ def subsets(self) -> List[Subset]:
340
+ """Direct access to the component sets."""
341
+ return self._subsets
342
+
343
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
344
+ """Returns True if x is in ANY of the component subsets."""
345
+ return any(s.is_element(x, rtol=rtol) for s in self._subsets)
346
+
347
+ @property
348
+ def boundary(self) -> Subset:
349
+ """
350
+ Returns the boundary of the union.
351
+ Currently raises NotImplementedError.
352
+ """
353
+ raise NotImplementedError("General boundary of union not yet implemented.")
354
+
355
+ @property
356
+ def complement(self) -> Subset:
357
+ """
358
+ Returns the complement of the union.
359
+
360
+ Applies De Morgan's Law: (A ∪ B)^c = A^c ∩ B^c.
361
+ Returns an Intersection of the complements.
362
+ """
363
+ return Intersection(s.complement for s in self._subsets)
364
+
365
+
366
+ class SublevelSet(Subset):
367
+ """
368
+ Represents a sublevel set defined by a functional: S = {x | f(x) <= c}.
369
+
370
+ This class serves as a base for sets defined by inequalities. Unlike
371
+ ConvexSubset, it does not assume the defining functional is convex.
372
+ """
373
+
374
+ def __init__(
375
+ self,
376
+ form: NonLinearForm,
377
+ level: float,
378
+ open_set: bool = False,
379
+ ) -> None:
380
+ """
381
+ Args:
382
+ form: The defining functional f(x).
383
+ level: The scalar upper bound c.
384
+ open_set: If True, uses strict inequality (f(x) < c).
385
+ If False, uses non-strict inequality (f(x) <= c).
386
+ """
387
+ super().__init__(form.domain)
388
+ self._form = form
389
+ self._level = level
390
+ self._open = open_set
391
+
392
+ @property
393
+ def form(self) -> NonLinearForm:
394
+ """The defining functional f(x)."""
395
+ return self._form
396
+
397
+ @property
398
+ def level(self) -> float:
399
+ """The scalar upper bound c."""
400
+ return self._level
401
+
402
+ @property
403
+ def is_open(self) -> bool:
404
+ """True if the set is defined by strict inequality."""
405
+ return self._open
406
+
407
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
408
+ """
409
+ Returns True if f(x) <= c (or < c).
410
+ Tolerance is scaled by max(1.0, |c|).
411
+ """
412
+ val = self._form(x)
413
+ scale = max(1.0, abs(self._level))
414
+ margin = rtol * scale
415
+
416
+ if self._open:
417
+ return val < self._level + margin
418
+ else:
419
+ return val <= self._level + margin
420
+
421
+ @property
422
+ def boundary(self) -> Subset:
423
+ """
424
+ Returns the boundary of the sublevel set.
425
+ The boundary is typically the LevelSet {x | f(x) = c}.
426
+ """
427
+ return LevelSet(self._form, self._level)
428
+
429
+
430
+ class LevelSet(Subset):
431
+ """
432
+ Represents a level set of a functional: S = {x | f(x) = c}.
433
+ """
434
+
435
+ def __init__(
436
+ self,
437
+ form: NonLinearForm,
438
+ level: float,
439
+ ) -> None:
440
+ """
441
+ Args:
442
+ form: The defining functional f(x).
443
+ level: The scalar value c.
444
+ """
445
+ super().__init__(form.domain)
446
+ self._form = form
447
+ self._level = level
448
+
449
+ @property
450
+ def form(self) -> NonLinearForm:
451
+ """The defining functional f(x)."""
452
+ return self._form
453
+
454
+ @property
455
+ def level(self) -> float:
456
+ """The scalar value c."""
457
+ return self._level
458
+
459
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
460
+ """
461
+ Returns True if f(x) is approximately equal to c.
462
+ Tolerance is scaled by max(1.0, |c|).
463
+ """
464
+ val = self._form(x)
465
+ scale = max(1.0, abs(self._level))
466
+ return abs(val - self._level) <= rtol * scale
467
+
468
+ @property
469
+ def boundary(self) -> Subset:
470
+ """
471
+ Returns the boundary of the level set.
472
+ Assuming regularity, a level set is a closed manifold without boundary.
473
+ """
474
+ return EmptySet(self.domain)
475
+
476
+
477
+ class ConvexSubset(SublevelSet):
478
+ """
479
+ Represents a convex set defined as a sublevel set: S = {x | f(x) <= c}.
480
+
481
+ This class assumes the defining form 'f' is convex. It includes tools
482
+ to verify this property locally.
483
+ """
484
+
485
+ def __init__(
486
+ self,
487
+ form: NonLinearForm,
488
+ level: float,
489
+ open_set: bool = False,
490
+ ) -> None:
491
+ """
492
+ Args:
493
+ form: The defining functional f(x). Must be convex.
494
+ level: The scalar upper bound c.
495
+ open_set: If True, uses strict inequality (<).
496
+ """
497
+ super().__init__(form, level, open_set=open_set)
498
+
499
+ def check(
500
+ self, n_samples: int = 10, /, *, rtol: float = 1e-5, atol: float = 1e-8
501
+ ) -> None:
502
+ """
503
+ Performs a randomized check of the convexity inequality:
504
+ f(tx + (1-t)y) <= t*f(x) + (1-t)*f(y)
505
+
506
+ Args:
507
+ n_samples: Number of random pairs to test.
508
+ rtol: Relative tolerance.
509
+ atol: Absolute tolerance.
510
+
511
+ Raises:
512
+ AssertionError: If the function is found to be non-convex.
513
+ """
514
+ for _ in range(n_samples):
515
+ x = self.domain.random()
516
+ y = self.domain.random()
517
+ t = np.random.uniform(0, 1)
518
+
519
+ # Convex combination
520
+ tx = self.domain.multiply(t, x)
521
+ ty = self.domain.multiply(1.0 - t, y)
522
+ z = self.domain.add(tx, ty)
523
+
524
+ # Evaluate form
525
+ fz = self._form(z)
526
+ fx = self._form(x)
527
+ fy = self._form(y)
528
+
529
+ # f(tx + (1-t)y) <= t f(x) + (1-t) f(y)
530
+ rhs = t * fx + (1.0 - t) * fy
531
+
532
+ if fz > rhs + atol + rtol * abs(rhs):
533
+ raise AssertionError(
534
+ f"Convexity check failed. "
535
+ f"LHS={fz:.4e}, RHS={rhs:.4e}. "
536
+ "Functional does not appear convex."
537
+ )
538
+ print(f"[✓] Convexity check passed ({n_samples} samples).")
539
+
540
+
541
+ class ConvexIntersection(ConvexSubset):
542
+ """
543
+ Represents the intersection of multiple convex sets as a single convex set.
544
+
545
+ This class combines the defining functionals of its components into a single
546
+ max-functional: F(x) = max_i (f_i(x) - c_i).
547
+ The intersection is then defined as {x | F(x) <= 0}.
548
+
549
+ This allows the intersection to be treated as a standard ConvexSubset for
550
+ optimization algorithms, providing gradients and Hessians of the active
551
+ constraint, while preserving access to the individual constraints.
552
+ """
553
+
554
+ def __init__(self, subsets: Iterable[ConvexSubset]) -> None:
555
+ """
556
+ Args:
557
+ subsets: An iterable of ConvexSubset objects.
558
+ """
559
+ self._subsets = list(subsets)
560
+ if not self._subsets:
561
+ raise ValueError("ConvexIntersection requires at least one subset.")
562
+
563
+ domain = self._subsets[0].domain
564
+
565
+ # 1. Define the combined max-mapping
566
+ # F(x) = max (f_i(x) - c_i)
567
+ def mapping(x: Vector) -> float:
568
+ values = [s.form(x) - s.level for s in self._subsets]
569
+ return float(np.max(values))
570
+
571
+ # 2. Define the gradient via the active constraint
572
+ # dF(x) = df_k(x) where k = argmax(...)
573
+ # Note: At points where multiple constraints are active, this returns
574
+ # a subgradient (from one of the active sets).
575
+ def gradient(x: Vector) -> Vector:
576
+ values = [s.form(x) - s.level for s in self._subsets]
577
+ idx_max = np.argmax(values)
578
+ active_subset = self._subsets[idx_max]
579
+ return active_subset.form.gradient(x)
580
+
581
+ # 3. Define the Hessian via the active constraint
582
+ def hessian(x: Vector) -> LinearOperator:
583
+ values = [s.form(x) - s.level for s in self._subsets]
584
+ idx_max = np.argmax(values)
585
+ active_subset = self._subsets[idx_max]
586
+ return active_subset.form.hessian(x)
587
+
588
+ combined_form = NonLinearForm(
589
+ domain, mapping, gradient=gradient, hessian=hessian
590
+ )
591
+
592
+ # Determine strictness: if any subset is open, the intersection boundary
593
+ # handling gets complex. We adopt a conservative approach: effectively closed
594
+ # for calculation (level=0), but flagging open if any component is open.
595
+ is_any_open = any(s.is_open for s in self._subsets)
596
+
597
+ super().__init__(combined_form, level=0.0, open_set=is_any_open)
598
+
599
+ @property
600
+ def subsets(self) -> List[ConvexSubset]:
601
+ """Direct access to the individual convex constraints."""
602
+ return self._subsets
603
+
604
+
605
+ # --- Geometric Implementations ---
606
+
607
+
608
+ class _EllipsoidalGeometry:
609
+ """
610
+ Mixin class that holds the common data and logic for ellipsoidal sets.
611
+
612
+ This class constructs the quadratic form f(x) = <A(x-c), x-c> used by
613
+ Ellipsoid and EllipsoidSurface.
614
+ """
615
+
616
+ def __init__(
617
+ self,
618
+ domain: HilbertSpace,
619
+ center: Vector,
620
+ radius: float,
621
+ operator: LinearOperator,
622
+ ) -> None:
623
+ """
624
+ Args:
625
+ domain: The Hilbert space.
626
+ center: The center vector c.
627
+ radius: The radius r.
628
+ operator: The self-adjoint, positive-definite operator A defining the metric.
629
+ """
630
+ if not domain.is_element(center):
631
+ raise ValueError("Center vector must be in the domain.")
632
+ if radius < 0:
633
+ raise ValueError("Radius must be non-negative.")
634
+
635
+ self._center = center
636
+ self._radius = radius
637
+ self._operator = operator
638
+
639
+ # 1. f(x) = <A(x-c), x-c>
640
+ def mapping(x: Vector) -> float:
641
+ diff = domain.subtract(x, center)
642
+ return domain.inner_product(operator(diff), diff)
643
+
644
+ # 2. f'(x) = 2 A (x-c)
645
+ def gradient(x: Vector) -> Vector:
646
+ diff = domain.subtract(x, center)
647
+ return domain.multiply(2.0, operator(diff))
648
+
649
+ # 3. f''(x) = 2 A
650
+ def hessian(x: Vector) -> LinearOperator:
651
+ return 2.0 * operator
652
+
653
+ self._generated_form = NonLinearForm(
654
+ domain, mapping, gradient=gradient, hessian=hessian
655
+ )
656
+ self._generated_level = radius**2
657
+
658
+ @property
659
+ def center(self) -> Vector:
660
+ """The center of the ellipsoid."""
661
+ return self._center
662
+
663
+ @property
664
+ def radius(self) -> float:
665
+ """The 'radius' parameter of the ellipsoid."""
666
+ return self._radius
667
+
668
+ @property
669
+ def operator(self) -> LinearOperator:
670
+ """The defining linear operator A."""
671
+ return self._operator
672
+
673
+
674
+ class Ellipsoid(ConvexSubset, _EllipsoidalGeometry):
675
+ """
676
+ Represents a solid ellipsoid: E = {x | <A(x-c), x-c> <= r^2}.
677
+ """
678
+
679
+ def __init__(
680
+ self,
681
+ domain: HilbertSpace,
682
+ center: Vector,
683
+ radius: float,
684
+ operator: LinearOperator,
685
+ open_set: bool = False,
686
+ ) -> None:
687
+ """
688
+ Args:
689
+ domain: The Hilbert space.
690
+ center: The center vector c.
691
+ radius: The radius r.
692
+ operator: The operator A.
693
+ open_set: If True, defines an open ellipsoid (< r^2).
694
+ """
695
+ _EllipsoidalGeometry.__init__(self, domain, center, radius, operator)
696
+ ConvexSubset.__init__(
697
+ self, self._generated_form, self._generated_level, open_set=open_set
698
+ )
699
+
700
+ @property
701
+ def boundary(self) -> Subset:
702
+ """Returns the boundary EllipsoidSurface."""
703
+ return EllipsoidSurface(self.domain, self.center, self.radius, self.operator)
704
+
705
+ @property
706
+ def normalized(self) -> NormalisedEllipsoid:
707
+ """
708
+ Returns a normalized version of this ellipsoid with radius 1.
709
+ The operator is scaled by 1/r^2 to represent the same set.
710
+ """
711
+ if self.radius == 0:
712
+ raise ValueError("Cannot normalize an ellipsoid with zero radius.")
713
+ scale = 1.0 / (self.radius**2)
714
+ scaled_operator = scale * self.operator
715
+ return NormalisedEllipsoid(
716
+ self.domain, self.center, scaled_operator, open_set=self.is_open
717
+ )
718
+
719
+
720
+ class NormalisedEllipsoid(Ellipsoid):
721
+ """
722
+ Represents a normalised ellipsoid with radius 1: E = {x | <A(x-c), x-c> <= 1}.
723
+ """
724
+
725
+ def __init__(
726
+ self,
727
+ domain: HilbertSpace,
728
+ center: Vector,
729
+ operator: LinearOperator,
730
+ open_set: bool = False,
731
+ ) -> None:
732
+ """
733
+ Args:
734
+ domain: The Hilbert space.
735
+ center: The center vector c.
736
+ operator: The operator A.
737
+ open_set: If True, defines an open ellipsoid.
738
+ """
739
+ super().__init__(domain, center, 1.0, operator, open_set=open_set)
740
+
741
+
742
+ class EllipsoidSurface(LevelSet, _EllipsoidalGeometry):
743
+ """
744
+ Represents the surface of an ellipsoid: S = {x | <A(x-c), x-c> = r^2}.
745
+ """
746
+
747
+ def __init__(
748
+ self,
749
+ domain: HilbertSpace,
750
+ center: Vector,
751
+ radius: float,
752
+ operator: LinearOperator,
753
+ ) -> None:
754
+ """
755
+ Args:
756
+ domain: The Hilbert space.
757
+ center: The center vector c.
758
+ radius: The radius r.
759
+ operator: The operator A.
760
+ """
761
+ _EllipsoidalGeometry.__init__(self, domain, center, radius, operator)
762
+ LevelSet.__init__(self, self._generated_form, self._generated_level)
763
+
764
+ @property
765
+ def boundary(self) -> Subset:
766
+ """Returns EmptySet (manifold without boundary)."""
767
+ return EmptySet(self.domain)
768
+
769
+ @property
770
+ def normalized(self) -> EllipsoidSurface:
771
+ """
772
+ Returns a normalized version of this surface with radius 1.
773
+ """
774
+ if self.radius == 0:
775
+ raise ValueError("Cannot normalize a surface with zero radius.")
776
+ scale = 1.0 / (self.radius**2)
777
+ scaled_operator = scale * self.operator
778
+ return EllipsoidSurface(self.domain, self.center, 1.0, scaled_operator)
779
+
780
+
781
+ class Ball(Ellipsoid):
782
+ """
783
+ Represents a ball in a Hilbert space: B = {x | ||x - c||^2 <= r^2}.
784
+ This is an Ellipsoid where A is the Identity operator.
785
+ """
786
+
787
+ def __init__(
788
+ self,
789
+ domain: HilbertSpace,
790
+ center: Vector,
791
+ radius: float,
792
+ open_set: bool = True,
793
+ ) -> None:
794
+ """
795
+ Args:
796
+ domain: The Hilbert space.
797
+ center: The center vector c.
798
+ radius: The radius r.
799
+ open_set: If True (default), defines an open ball (< r).
800
+ """
801
+ identity = domain.identity_operator()
802
+ super().__init__(domain, center, radius, identity, open_set=open_set)
803
+
804
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
805
+ """
806
+ Returns True if x lies within the ball.
807
+ Optimized to use geometric distance ||x-c|| directly.
808
+ """
809
+ diff = self.domain.subtract(x, self.center)
810
+ dist = self.domain.norm(diff)
811
+ margin = rtol * max(1.0, self.radius)
812
+ if self.is_open:
813
+ return dist < self.radius + margin
814
+ else:
815
+ return dist <= self.radius + margin
816
+
817
+ @property
818
+ def boundary(self) -> Subset:
819
+ """Returns the Sphere bounding this Ball."""
820
+ return Sphere(self.domain, self.center, self.radius)
821
+
822
+
823
+ class Sphere(EllipsoidSurface):
824
+ """
825
+ Represents a sphere in a Hilbert space: S = {x | ||x - c||^2 = r^2}.
826
+ This is an EllipsoidSurface where A is the Identity operator.
827
+ """
828
+
829
+ def __init__(self, domain: HilbertSpace, center: Vector, radius: float) -> None:
830
+ """
831
+ Args:
832
+ domain: The Hilbert space.
833
+ center: The center vector c.
834
+ radius: The radius r.
835
+ """
836
+ identity = domain.identity_operator()
837
+ super().__init__(domain, center, radius, identity)
838
+
839
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
840
+ """
841
+ Returns True if ||x - c|| is approximately equal to r.
842
+ """
843
+ diff = self.domain.subtract(x, self.center)
844
+ dist = self.domain.norm(diff)
845
+ return abs(dist - self.radius) <= rtol * max(1.0, self.radius)