Skip to content

PhaseSpace

The PhaseSpace class provides a flexible representation of phase spaces (subsets of \(\mathbb{R}^n\)) with both symbolic and callable constraint representations.

Example Usage

See the Phase Spaces & Time Horizons example notebook for practical usage examples.

Factory Methods

full(dimension)

Creates a phase space representing R^n (full Euclidean space).

box(bounds)

Creates a box-constrained phase space \([a_1, b_1] \times \cdots \times [a_n, b_n]\).

closed_hypersphere(center, radius)

Creates a closed hypersphere \(\{{\bf x} \in \mathbb{R}^n \; : \; \lVert {\bf x} - {\bf c} \rVert \leq r\}\).

open_hypersphere(center, radius)

Creates an open hypersphere \(\{{\bf x} \in \mathbb{R}^n \; : \; \lVert {\bf x} - {\bf c} \rVert < r\}\).

Exposed Methods

contains_point(x)

Verifies whether the point \(x \in \mathbb{R}^n\) is in the phase space.

contains_points(A)

Verifies whether each point \(x \in A \subset \mathbb{R}^n\) is in the phase space.

Properties

volume

Returns either an analytic volume (via the Lebesgue measure) of the phase space, if available, or a numerically estimated volume by considering the volume of a convex hull. Utilises caching to avoid consistent recomputation.

NOTE: Currently not implemented - deferred to future versions.

Dunder Methods

We've implemented, thus far,

  • __str__
  • __repr__
  • The dunders below are experimental. They invoke sympy subset logic, which is frail at best, and automatically fall back to false if sympy cannot determine the relevant relationships. As such, a false result should not be seen as a certainty, whereas a true result can be.
  • __eq__
  • __ne__
  • __le__
  • __lt__
  • __ge__
  • __gt__

Full Docs

PyDynSys.core.euclidean.phase_space.PhaseSpace dataclass

Phase space X subset of R^n with flexible symbolic/callable representation.

Supports three usage patterns
  1. Symbolic only: Provides symbolic set, constraint auto-compiled (general)
  2. Callable only: Provides constraint directly (fast, no symbolic ops)
  3. Both (recommended): Provides both for optimal performance (fast + symbolic ops)
Symbolic representation enables
  • Rigorous mathematical operations (intersections, closures, etc.)
  • Pretty printing for dynamical system descriptors

Callable representation provides O(1) membership testing for numerical work.

Fields
  • dimension (int): Phase space dimension n
  • symbolic_set (syp.Set | None): Optional SymPy set representation
  • constraint (Callable | None): Optional callable for fast membership testing

Note: At least one of symbolic_set or constraint must be provided, or a ValueError will be raised.

Source code in src/PyDynSys/core/euclidean/phase_space.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
@dataclass
class PhaseSpace:
    """
    Phase space X subset of R^n with flexible symbolic/callable representation. 

    Supports three usage patterns: 
        1. Symbolic only: Provides symbolic set, constraint auto-compiled (general) 
        2. Callable only: Provides constraint directly (fast, no symbolic ops) 
        3. Both (recommended): Provides both for optimal performance (fast + symbolic ops)

    Symbolic representation enables: 
        - Rigorous mathematical operations (intersections, closures, etc.) 
        - Pretty printing for dynamical system descriptors 

    Callable representation provides O(1) membership testing for numerical work.

    Fields: 
        - dimension (int): Phase space dimension n 
        - symbolic_set (syp.Set | None): Optional SymPy set representation 
        - constraint (Callable | None): Optional callable for fast membership testing

    Note: At least one of symbolic_set or constraint must be provided, or a ValueError will be raised.
    """
    dimension: int
    symbolic_set: Optional[syp.Set] = None
    constraint: Optional[Callable[[NDArray[np.float64]], bool]] = None


        ### --- Construction --- ###


    ## __init__ autogenerated by @dataclass decorator

    def __post_init__(
        self
    ):
        """
        Post-construction Validation:   

        Raises: 
            ValueError: If both symbolic_set and constraint are None
        """
        if self.symbolic_set is None and self.constraint is None:
            raise ValueError(
                "PhaseSpace requires at least one representation: "
                "symbolic_set (sympy.Set) or constraint (Callable[[NDArray[np.float64]], bool]) must be provided"
            )

        if self.constraint is None and self.symbolic_set is not None:
            # Compile constraint from symbolic set: yields slow containment checks
            ## NOTE: I'm not actually sure of the time complexity, need to check sympy docs
            self.constraint = self._compile_constraint() 


        ### --- Factory Methods --- ###


    @classmethod
    def full(cls, dimension: int) -> 'PhaseSpace':
        """
        Factory: X = R^n (full Euclidean space).

        - The phase space of choice for unconstrained systems. 
        - Provides both symbolic representation and optimized constraint, optimal performance (o(1) membership testing)

        Args:
            dimension (int): Phase space dimension n

        Returns:
            PhaseSpace instance representing R^n, optimal performance case

        Raises:
            ValueError: If dimension is not positive
        """
        if dimension <= 0:
            raise ValueError(f"Dimension must be positive, got dimension: {dimension}")
        symbolic = syp.Reals ** dimension
        constraint = lambda x: True # Provide constraint directly to avoid compilation overhead
        return cls(dimension=dimension, symbolic_set=symbolic, constraint=constraint)


    @classmethod
    def box(cls, bounds: NDArray[np.float64]) -> 'PhaseSpace':
        """
        Factory: X = [a_1, b_1] x ... x [a_n, b_n] (box-space constructor).

        - Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
            - Symbolic representation is a sympy.sets.ProductSet of sympy.Interval instances. 
        - Dimension is inferred from the bounds array.

        Args:
            bounds: Array of shape (n, 2) with [[a_1, b_1], ..., [a_n, b_n]]

        Returns:
            PhaseSpace with box constraints and optimal performance

        Raises:
            ValueError: If bounds is not a numpy array of shape (n, 2) for n >= 1
            ValueError: If any i is s.t. b_i <= a_i
            TypeError: If bounds is not a numpy array of type float64
        """
        if bounds.ndim != 2 or bounds.shape[1] != 2 or bounds.shape[0] < 1:
            raise ValueError(f"Bounds must be a numpy array of shape (n, 2) for n >= 1, got {bounds.shape}")
        if np.any(bounds[:, 0] >= bounds[:, 1]):
            raise ValueError(f"For all i, b_i must be greater than a_i, got bounds: {bounds}")
        if bounds.dtype != np.float64:
            raise TypeError(
                f"Center must be a numpy array of type float64, got center: {bounds.dtype}\n"
                "Use numpy.array(bounds, dtype=np.float64) to convert to float64"
            )

        dimension = bounds.shape[0]
        intervals = [syp.Interval(bounds[i, 0], bounds[i, 1]) for i in range(dimension)]
        symbolic = ProductSet(*intervals)

        # Provide pre-compiled constraint for performance: 2*dimension comparisons (O(dimension))
        box_constraint = lambda x: bool(np.all((x >= bounds[:, 0]) & (x <= bounds[:, 1])))

        return cls(dimension=dimension, symbolic_set=symbolic, constraint=box_constraint)


    @classmethod
    def closed_hypersphere(cls, center: NDArray[np.float64], radius: float) -> 'PhaseSpace':
        """
        Factory: X = {x in R^n : ||x - center|| <= radius} (sphere constructor).

        - Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
            - Symbolic representation is a sympy.sets.Ball instance.
        - Dimension is inferred from the center array.

        Args:
            center (NDArray[np.float64]): Array of shape (n,) with the center of the sphere
            radius (float): Radius of the sphere

        Returns:
            PhaseSpace with closed hypersphere constraints and optimal performance

        Raises:
            ValueError: If radius <= 0
            ValueError: If center is not a numpy array of shape (n,) for some n
            TypeError: If center is not a numpy array of type float64
        """
        if radius <= 0:
            raise ValueError(f"Radius must be positive, got radius: {radius}")
        if center.shape[0] <= 0 or len(center.shape) != 1:
            raise ValueError(f"Center must be a numpy array of shape (n,) for some n, got center: {center.shape}")
        if center.dtype != np.float64:
            raise TypeError(
                f"Center must be a numpy array of type float64, got center: {center.dtype}\n"
                "Use numpy.array(center, dtype=np.float64) to convert to float64"
            )

        dimension = center.shape[0]
        R_n = syp.Reals ** dimension
        x = syp.symbols(f'x0:{dimension}', real=True)
        squared_dist = sum((x[i] - center[i])**2 for i in range(dimension))
        symbolic = syp.ConditionSet(syp.Tuple(*x), squared_dist <= radius**2, R_n)

        # Constraint is O(dimension) 
            ## TODO: How does np.linalg.norm work? If they sqrt could be slow, squard sum >>
        closed_hypersphere_constraint = lambda x: bool(np.linalg.norm(x - center) <= radius) 
        return cls(dimension=dimension, symbolic_set=symbolic, constraint=closed_hypersphere_constraint)


    @classmethod
    def open_hypersphere(cls, center: NDArray[np.float64], radius: float) -> 'PhaseSpace':
        """
        Factory: X = {x in R^n : ||x - center|| < radius} (open hypersphere constructor).

        - Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
            - Symbolic representation is a sympy.sets.Ball instance.
        - Dimension is inferred from the center array.
        - Sympy ConditionSet is used to represent the open hypersphere, allowing for set-theoretic operation support.

        Args:
            center (NDArray[np.float64]): Array of shape (n,) with the center of the sphere
            radius (float): Radius of the sphere

        Returns:
            PhaseSpace with open hypersphere constraints and optimal performance

        Raises:
            ValueError: If radius <= 0
            ValueError: If center is not a numpy array of shape (n,) for some n
            TypeError: If center is not a numpy array of type float64
        """
        if radius <= 0:
            raise ValueError(f"Radius must be positive, got radius: {radius}")
        if center.shape[0] <= 0 or len(center.shape) != 1:
            raise ValueError(f"Center must be a numpy array of shape (n,) for some n, got center: {center.shape}")
        if center.dtype != np.float64:
            raise TypeError(
                f"Center must be a numpy array of type float64, got center: {center.dtype}\n"
                "Use numpy.array(center, dtype=np.float64) to convert to float64"
            )

        dimension = center.shape[0]
        R_n = syp.Reals ** dimension
        x = syp.symbols(f'x0:{dimension}', real=True)
        squared_dist = sum((x[i] - center[i])**2 for i in range(dimension))
        symbolic = syp.ConditionSet(syp.Tuple(*x), squared_dist < radius**2, R_n)

        # Constraint is O(dimension) 
            ## TODO: How does np.linalg.norm work? If they sqrt could be slow, squard sum >>
        constraint = lambda x: bool(np.linalg.norm(x - center) < radius)
        return cls(dimension=dimension, symbolic_set=symbolic, constraint=constraint)


    ### --- Public Methods --- ### 


    def contains_point(self, x: NDArray[np.float64]) -> bool:
        """
        Check if x in X using given callable constraint, if provided, if not deferring to compiled constraint (slow).

        Args:
            x (NDArray[np.float64]): Point in R^n to test

        Returns:
            bool: True if x in X, False otherwise

        Raises:
            AssertionError: If constraint is not set in __post_init__
            ValueError: x is not a numpy array of shape (n,)     

        """
        assert self.constraint is not None, "Constraint should be set in __post_init__"
        if x.shape != (self.dimension,):
            raise ValueError(
                f"x has incorrect dimension: expected ({self.dimension},), got {x.shape}"
            )
        return self.constraint(x)


    def contains_points(self, X: NDArray[np.float64]) -> bool:
        """
        Check if all points in X are in X using compiled constraint.

        Args:
            X (NDArray[np.float64]): Points in R^n to test, shape (n_points, n)

        Returns:
            bool: True if all points in X are in X, False otherwise

        Raises:
            ValueError: X is not a numpy array of shape (n_points, n)
        """
        n_points = X.shape[0]
        if X.shape != (n_points, self.dimension):
            raise ValueError(
                f"X has incorrect shape: expected ({n_points}, {self.dimension}), got {X.shape}"
            )
        return np.all([self.contains_point(x) for x in X])


    ### --- Properties --- ### 


    @property
    def volume(self) -> float:
        """
        CURRENTLY NOT IMPLEMENTED - DEFERRED TO FUTURE VERSIONS

        Returns:
            float: Volume of X
        """
        raise NotImplementedError("Volume is not implemented for PhaseSpace")


    ### --- Dunder Methods --- ### 


    def __str__(self) -> str: 
        if self.symbolic_set is not None:
            return str(self.symbolic_set)
        else: 
            return f"PhaseSpace(dimension={self.dimension}): No symbolic representation"


    def __repr__(self) -> str: 
        symbolic_repr = repr(self.symbolic_set) if self.symbolic_set is not None else "None"
        constraint_name = None
        if self.constraint is not None:
            constraint_name = getattr(self.constraint, "__name__", self.constraint.__class__.__name__)
        return (
            f"PhaseSpace(dimension={self.dimension}, "
            f"symbolic_set={symbolic_repr}, "
            f"constraint={constraint_name})"
        )


    def __eq__(self, other: 'PhaseSpace') -> bool:
        if not isinstance(other, PhaseSpace):
            return NotImplemented  # type: ignore[return-value]
        # Equality iff mutual subset
        left = self._subseteq(other)
        if left is False:
            return False
        right = other._subseteq(self)
        return bool(left and right)


    def __ne__(self, other: 'PhaseSpace') -> bool:
        eq = self.__eq__(other)
        if eq is NotImplemented:  # type: ignore[comparison-overlap]
            return NotImplemented  # type: ignore[return-value]
        return not eq


    def __lt__(self, other: 'PhaseSpace') -> bool:
        if not isinstance(other, PhaseSpace):
            return NotImplemented  # type: ignore[return-value]
        le = self._subseteq(other)
        if le is False:
            return False
        return not self.__eq__(other)


    def __le__(self, other: 'PhaseSpace') -> bool:
        if not isinstance(other, PhaseSpace):
            return NotImplemented  # type: ignore[return-value]
        return self._subseteq(other)


    def __gt__(self, other: 'PhaseSpace') -> bool:
        if not isinstance(other, PhaseSpace):
            return NotImplemented  # type: ignore[return-value]
        ge = other._subseteq(self)
        if ge is False:
            return False
        return not self.__eq__(other)


    def __ge__(self, other: 'PhaseSpace') -> bool:
        if not isinstance(other, PhaseSpace):
            return NotImplemented  # type: ignore[return-value]
        return other._subseteq(self)


    ### --- Private Methods --- ### 


    def _compile_constraint(self) -> Callable[[NDArray[np.float64]], bool]:
        """
        Compile symbolic set to callable for fast numerical membership testing.

        Only called when symbolic_set is provided but constraint is not.

        Strategy:
        - For R^n (unbounded): return lambda x: True (no constraint)
        - For ProductSets of Intervals: extract bounds, compile to numpy checks
        - For general sets: use sympy's contains (slow, but general)

        Raises:
            AssertionError: If symbolic_set is None (should never happen)
        """
        assert self.symbolic_set is not None, "Cannot compile constraint without symbolic_set"

        # Special case: R^n (ProductSet of Reals or Reals**n)
        if self._is_full_euclidean_space():
            return lambda x: True

        # Special case: ProductSet of Intervals → box constraints
        if isinstance(self.symbolic_set, ProductSet):
            bounds = self._extract_box_bounds()
            if bounds is not None:
                def box_constraint(x: NDArray[np.float64]) -> bool:
                    return bool(np.all((x >= bounds[:, 0]) & (x <= bounds[:, 1])))
                return box_constraint

        # General case: use sympy (slow)
        # Prefer set.contains(Tuple(...)) over geometric Point for generic sets
        def symbolic_constraint(x: NDArray[np.float64]) -> bool:
            try:
                values = [syp.Float(float(v)) for v in x]
                elem = syp.Tuple(*values)
                contains_expr = self.symbolic_set.contains(elem)
                # SymPy returns a Boolean or a symbolic expression; coerce if possible
                return bool(contains_expr)
            except Exception:
                return False

        return symbolic_constraint


    def _is_full_euclidean_space(self) -> bool:
        """
        Check if symbolic set represents R^n.

        Returns False if symbolic_set is None.
        """
        if self.symbolic_set is None:
            return False

        if isinstance(self.symbolic_set, ProductSet):
            return all(s == syp.Reals for s in self.symbolic_set.args)
        # Also check for Reals**n notation
        if hasattr(self.symbolic_set, 'base') and hasattr(self.symbolic_set, 'exp'):
            return self.symbolic_set.base == syp.Reals and self.symbolic_set.exp == self.dimension
        return False


    def _extract_box_bounds(self) -> Optional[NDArray[np.float64]]:
        """
        Extract box bounds from ProductSet of Intervals.

        Returns:
            Array of shape (n, 2) with [[a1, b1], ..., [an, bn]], or None
            if not a product of intervals or if symbolic_set is None.
        """
        if self.symbolic_set is None or not isinstance(self.symbolic_set, ProductSet):
            return None

        bounds = []
        for component_set in self.symbolic_set.args:
            if isinstance(component_set, syp.Interval):
                a = float(component_set.start) if component_set.start.is_finite else -np.inf
                b = float(component_set.end) if component_set.end.is_finite else np.inf
                bounds.append([a, b])
            elif component_set == syp.Reals:
                bounds.append([-np.inf, np.inf])
            else:
                return None  # Not a simple interval

        return np.array(bounds, dtype=np.float64)


    def _subseteq(self, other: "PhaseSpace"):
        """
        Check if self is a subset of other using symbolic set containment.

        Args:
            other (PhaseSpace): The other PhaseSpace to compare against

        Returns:
            bool: True if self is a subset of other, False otherwise

        Raises:
            ValueError: If self or other has no symbolic representation
            Warning: If subset relation is undecidable symbolically, falling back to false result
        """
        A, B = self.symbolic_set, other.symbolic_set
        if A is None or B is None:
            raise ValueError(f"Cannot compare PhaseSpaces with no symbolic representation: A={A}, B={B}")
        res = A.is_subset(B)
        if res is not None:
            return res
        warnings.warn(
        f"Subset relation is undecidable symbolically; falling back to false result: Tried A <= B"
        "where A: {A}, B: {B}"
        )
        return False

volume: float property

CURRENTLY NOT IMPLEMENTED - DEFERRED TO FUTURE VERSIONS

Returns:

Name Type Description
float float

Volume of X

__post_init__()

Post-construction Validation:

Raises:

Type Description
ValueError

If both symbolic_set and constraint are None

Source code in src/PyDynSys/core/euclidean/phase_space.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def __post_init__(
    self
):
    """
    Post-construction Validation:   

    Raises: 
        ValueError: If both symbolic_set and constraint are None
    """
    if self.symbolic_set is None and self.constraint is None:
        raise ValueError(
            "PhaseSpace requires at least one representation: "
            "symbolic_set (sympy.Set) or constraint (Callable[[NDArray[np.float64]], bool]) must be provided"
        )

    if self.constraint is None and self.symbolic_set is not None:
        # Compile constraint from symbolic set: yields slow containment checks
        ## NOTE: I'm not actually sure of the time complexity, need to check sympy docs
        self.constraint = self._compile_constraint() 

box(bounds: NDArray[np.float64]) -> PhaseSpace classmethod

Factory: X = [a_1, b_1] x ... x [a_n, b_n] (box-space constructor).

  • Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
    • Symbolic representation is a sympy.sets.ProductSet of sympy.Interval instances.
  • Dimension is inferred from the bounds array.

Parameters:

Name Type Description Default
bounds NDArray[float64]

Array of shape (n, 2) with [[a_1, b_1], ..., [a_n, b_n]]

required

Returns:

Type Description
PhaseSpace

PhaseSpace with box constraints and optimal performance

Raises:

Type Description
ValueError

If bounds is not a numpy array of shape (n, 2) for n >= 1

ValueError

If any i is s.t. b_i <= a_i

TypeError

If bounds is not a numpy array of type float64

Source code in src/PyDynSys/core/euclidean/phase_space.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
@classmethod
def box(cls, bounds: NDArray[np.float64]) -> 'PhaseSpace':
    """
    Factory: X = [a_1, b_1] x ... x [a_n, b_n] (box-space constructor).

    - Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
        - Symbolic representation is a sympy.sets.ProductSet of sympy.Interval instances. 
    - Dimension is inferred from the bounds array.

    Args:
        bounds: Array of shape (n, 2) with [[a_1, b_1], ..., [a_n, b_n]]

    Returns:
        PhaseSpace with box constraints and optimal performance

    Raises:
        ValueError: If bounds is not a numpy array of shape (n, 2) for n >= 1
        ValueError: If any i is s.t. b_i <= a_i
        TypeError: If bounds is not a numpy array of type float64
    """
    if bounds.ndim != 2 or bounds.shape[1] != 2 or bounds.shape[0] < 1:
        raise ValueError(f"Bounds must be a numpy array of shape (n, 2) for n >= 1, got {bounds.shape}")
    if np.any(bounds[:, 0] >= bounds[:, 1]):
        raise ValueError(f"For all i, b_i must be greater than a_i, got bounds: {bounds}")
    if bounds.dtype != np.float64:
        raise TypeError(
            f"Center must be a numpy array of type float64, got center: {bounds.dtype}\n"
            "Use numpy.array(bounds, dtype=np.float64) to convert to float64"
        )

    dimension = bounds.shape[0]
    intervals = [syp.Interval(bounds[i, 0], bounds[i, 1]) for i in range(dimension)]
    symbolic = ProductSet(*intervals)

    # Provide pre-compiled constraint for performance: 2*dimension comparisons (O(dimension))
    box_constraint = lambda x: bool(np.all((x >= bounds[:, 0]) & (x <= bounds[:, 1])))

    return cls(dimension=dimension, symbolic_set=symbolic, constraint=box_constraint)

closed_hypersphere(center: NDArray[np.float64], radius: float) -> PhaseSpace classmethod

Factory: X = {x in R^n : ||x - center|| <= radius} (sphere constructor).

  • Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
    • Symbolic representation is a sympy.sets.Ball instance.
  • Dimension is inferred from the center array.

Parameters:

Name Type Description Default
center NDArray[float64]

Array of shape (n,) with the center of the sphere

required
radius float

Radius of the sphere

required

Returns:

Type Description
PhaseSpace

PhaseSpace with closed hypersphere constraints and optimal performance

Raises:

Type Description
ValueError

If radius <= 0

ValueError

If center is not a numpy array of shape (n,) for some n

TypeError

If center is not a numpy array of type float64

Source code in src/PyDynSys/core/euclidean/phase_space.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@classmethod
def closed_hypersphere(cls, center: NDArray[np.float64], radius: float) -> 'PhaseSpace':
    """
    Factory: X = {x in R^n : ||x - center|| <= radius} (sphere constructor).

    - Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
        - Symbolic representation is a sympy.sets.Ball instance.
    - Dimension is inferred from the center array.

    Args:
        center (NDArray[np.float64]): Array of shape (n,) with the center of the sphere
        radius (float): Radius of the sphere

    Returns:
        PhaseSpace with closed hypersphere constraints and optimal performance

    Raises:
        ValueError: If radius <= 0
        ValueError: If center is not a numpy array of shape (n,) for some n
        TypeError: If center is not a numpy array of type float64
    """
    if radius <= 0:
        raise ValueError(f"Radius must be positive, got radius: {radius}")
    if center.shape[0] <= 0 or len(center.shape) != 1:
        raise ValueError(f"Center must be a numpy array of shape (n,) for some n, got center: {center.shape}")
    if center.dtype != np.float64:
        raise TypeError(
            f"Center must be a numpy array of type float64, got center: {center.dtype}\n"
            "Use numpy.array(center, dtype=np.float64) to convert to float64"
        )

    dimension = center.shape[0]
    R_n = syp.Reals ** dimension
    x = syp.symbols(f'x0:{dimension}', real=True)
    squared_dist = sum((x[i] - center[i])**2 for i in range(dimension))
    symbolic = syp.ConditionSet(syp.Tuple(*x), squared_dist <= radius**2, R_n)

    # Constraint is O(dimension) 
        ## TODO: How does np.linalg.norm work? If they sqrt could be slow, squard sum >>
    closed_hypersphere_constraint = lambda x: bool(np.linalg.norm(x - center) <= radius) 
    return cls(dimension=dimension, symbolic_set=symbolic, constraint=closed_hypersphere_constraint)

contains_point(x: NDArray[np.float64]) -> bool

Check if x in X using given callable constraint, if provided, if not deferring to compiled constraint (slow).

Parameters:

Name Type Description Default
x NDArray[float64]

Point in R^n to test

required

Returns:

Name Type Description
bool bool

True if x in X, False otherwise

Raises:

Type Description
AssertionError

If constraint is not set in post_init

ValueError

x is not a numpy array of shape (n,)

Source code in src/PyDynSys/core/euclidean/phase_space.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def contains_point(self, x: NDArray[np.float64]) -> bool:
    """
    Check if x in X using given callable constraint, if provided, if not deferring to compiled constraint (slow).

    Args:
        x (NDArray[np.float64]): Point in R^n to test

    Returns:
        bool: True if x in X, False otherwise

    Raises:
        AssertionError: If constraint is not set in __post_init__
        ValueError: x is not a numpy array of shape (n,)     

    """
    assert self.constraint is not None, "Constraint should be set in __post_init__"
    if x.shape != (self.dimension,):
        raise ValueError(
            f"x has incorrect dimension: expected ({self.dimension},), got {x.shape}"
        )
    return self.constraint(x)

contains_points(X: NDArray[np.float64]) -> bool

Check if all points in X are in X using compiled constraint.

Parameters:

Name Type Description Default
X NDArray[float64]

Points in R^n to test, shape (n_points, n)

required

Returns:

Name Type Description
bool bool

True if all points in X are in X, False otherwise

Raises:

Type Description
ValueError

X is not a numpy array of shape (n_points, n)

Source code in src/PyDynSys/core/euclidean/phase_space.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def contains_points(self, X: NDArray[np.float64]) -> bool:
    """
    Check if all points in X are in X using compiled constraint.

    Args:
        X (NDArray[np.float64]): Points in R^n to test, shape (n_points, n)

    Returns:
        bool: True if all points in X are in X, False otherwise

    Raises:
        ValueError: X is not a numpy array of shape (n_points, n)
    """
    n_points = X.shape[0]
    if X.shape != (n_points, self.dimension):
        raise ValueError(
            f"X has incorrect shape: expected ({n_points}, {self.dimension}), got {X.shape}"
        )
    return np.all([self.contains_point(x) for x in X])

full(dimension: int) -> PhaseSpace classmethod

Factory: X = R^n (full Euclidean space).

  • The phase space of choice for unconstrained systems.
  • Provides both symbolic representation and optimized constraint, optimal performance (o(1) membership testing)

Parameters:

Name Type Description Default
dimension int

Phase space dimension n

required

Returns:

Type Description
PhaseSpace

PhaseSpace instance representing R^n, optimal performance case

Raises:

Type Description
ValueError

If dimension is not positive

Source code in src/PyDynSys/core/euclidean/phase_space.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@classmethod
def full(cls, dimension: int) -> 'PhaseSpace':
    """
    Factory: X = R^n (full Euclidean space).

    - The phase space of choice for unconstrained systems. 
    - Provides both symbolic representation and optimized constraint, optimal performance (o(1) membership testing)

    Args:
        dimension (int): Phase space dimension n

    Returns:
        PhaseSpace instance representing R^n, optimal performance case

    Raises:
        ValueError: If dimension is not positive
    """
    if dimension <= 0:
        raise ValueError(f"Dimension must be positive, got dimension: {dimension}")
    symbolic = syp.Reals ** dimension
    constraint = lambda x: True # Provide constraint directly to avoid compilation overhead
    return cls(dimension=dimension, symbolic_set=symbolic, constraint=constraint)

open_hypersphere(center: NDArray[np.float64], radius: float) -> PhaseSpace classmethod

Factory: X = {x in R^n : ||x - center|| < radius} (open hypersphere constructor).

  • Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
    • Symbolic representation is a sympy.sets.Ball instance.
  • Dimension is inferred from the center array.
  • Sympy ConditionSet is used to represent the open hypersphere, allowing for set-theoretic operation support.

Parameters:

Name Type Description Default
center NDArray[float64]

Array of shape (n,) with the center of the sphere

required
radius float

Radius of the sphere

required

Returns:

Type Description
PhaseSpace

PhaseSpace with open hypersphere constraints and optimal performance

Raises:

Type Description
ValueError

If radius <= 0

ValueError

If center is not a numpy array of shape (n,) for some n

TypeError

If center is not a numpy array of type float64

Source code in src/PyDynSys/core/euclidean/phase_space.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
@classmethod
def open_hypersphere(cls, center: NDArray[np.float64], radius: float) -> 'PhaseSpace':
    """
    Factory: X = {x in R^n : ||x - center|| < radius} (open hypersphere constructor).

    - Invokes PhaseSpace constructor with both symbolic representation and in-built callable constraint.
        - Symbolic representation is a sympy.sets.Ball instance.
    - Dimension is inferred from the center array.
    - Sympy ConditionSet is used to represent the open hypersphere, allowing for set-theoretic operation support.

    Args:
        center (NDArray[np.float64]): Array of shape (n,) with the center of the sphere
        radius (float): Radius of the sphere

    Returns:
        PhaseSpace with open hypersphere constraints and optimal performance

    Raises:
        ValueError: If radius <= 0
        ValueError: If center is not a numpy array of shape (n,) for some n
        TypeError: If center is not a numpy array of type float64
    """
    if radius <= 0:
        raise ValueError(f"Radius must be positive, got radius: {radius}")
    if center.shape[0] <= 0 or len(center.shape) != 1:
        raise ValueError(f"Center must be a numpy array of shape (n,) for some n, got center: {center.shape}")
    if center.dtype != np.float64:
        raise TypeError(
            f"Center must be a numpy array of type float64, got center: {center.dtype}\n"
            "Use numpy.array(center, dtype=np.float64) to convert to float64"
        )

    dimension = center.shape[0]
    R_n = syp.Reals ** dimension
    x = syp.symbols(f'x0:{dimension}', real=True)
    squared_dist = sum((x[i] - center[i])**2 for i in range(dimension))
    symbolic = syp.ConditionSet(syp.Tuple(*x), squared_dist < radius**2, R_n)

    # Constraint is O(dimension) 
        ## TODO: How does np.linalg.norm work? If they sqrt could be slow, squard sum >>
    constraint = lambda x: bool(np.linalg.norm(x - center) < radius)
    return cls(dimension=dimension, symbolic_set=symbolic, constraint=constraint)