|
10 | 10 | from contextlib import contextmanager |
11 | 11 | from functools import wraps |
12 | 12 | from itertools import chain |
13 | | -from typing import TYPE_CHECKING, Any, Generator, Literal, Optional, Tuple, Union |
| 13 | +from typing import ( |
| 14 | + TYPE_CHECKING, |
| 15 | + Any, |
| 16 | + Generator, |
| 17 | + List, |
| 18 | + Literal, |
| 19 | + Optional, |
| 20 | + Tuple, |
| 21 | +) |
14 | 22 |
|
15 | 23 | import jax |
16 | 24 | import jax.numpy as jnp |
@@ -548,21 +556,19 @@ def _compute_features(self, *xi: ArrayLike | Tsd | TsdFrame | TsdTensor): |
548 | 556 | @support_pynapple(conv_type="jax") |
549 | 557 | def _apply_fill_value(self, *xi: ArrayLike, out: NDArray) -> jax.Array: |
550 | 558 | """Apply fill value to out-of-bounds samples.""" |
551 | | - # Use jnp.where for JAX compatibility |
552 | 559 | to_fill = jnp.any( |
553 | 560 | jnp.stack( |
554 | 561 | [ |
555 | 562 | jnp.any( |
556 | | - (jnp.reshape(x, (x.shape[0], -1)) < self.bounds[0]) |
557 | | - | (jnp.reshape(x, (x.shape[0], -1)) > self.bounds[1]), |
| 563 | + (jnp.reshape(x, (x.shape[0], -1)) < lo) |
| 564 | + | (jnp.reshape(x, (x.shape[0], -1)) > hi), |
558 | 565 | axis=1, |
559 | 566 | ) |
560 | | - for x in xi |
| 567 | + for x, (lo, hi) in zip(xi, self._get_bounds_per_dim(), strict=True) |
561 | 568 | ] |
562 | 569 | ), |
563 | 570 | axis=0, |
564 | 571 | ) |
565 | | - # Reshape to_fill to broadcast correctly: (n_samples,) -> (n_samples, 1, 1, ...) |
566 | 572 | to_fill_broadcast = to_fill.reshape(to_fill.shape[0], *([1] * (out.ndim - 1))) |
567 | 573 | return jnp.where(to_fill_broadcast, self.fill_value, out) |
568 | 574 |
|
@@ -605,10 +611,46 @@ def _set_input_independent_states(self) -> EvalBasisMixin: |
605 | 611 | return self |
606 | 612 |
|
607 | 613 | @property |
608 | | - def bounds(self): |
609 | | - """Range of values covered by the basis.""" |
| 614 | + def bounds(self) -> List[Tuple[float, float]] | Tuple[float, float] | None: |
| 615 | + """Returns bounds, as provided.""" |
610 | 616 | return self._bounds |
611 | 617 |
|
| 618 | + def _get_bounds_per_dim(self) -> List[Tuple[float, float]]: |
| 619 | + """Return bounds, broadcast to one pair per input dimension.""" |
| 620 | + if self._bounds is None or isinstance(self._bounds[0], (int, float)): |
| 621 | + return [self._bounds] * self._n_inputs |
| 622 | + return list(self._bounds) |
| 623 | + |
| 624 | + @bounds.setter |
| 625 | + def bounds(self, values): |
| 626 | + if values is None: |
| 627 | + self._bounds = None |
| 628 | + return |
| 629 | + |
| 630 | + if isinstance(values, np.ndarray): |
| 631 | + values = values.tolist() |
| 632 | + |
| 633 | + if not isinstance(values, (list, tuple)): |
| 634 | + raise TypeError( |
| 635 | + f"Invalid bounds ``{values}`` provided, " |
| 636 | + "bounds should be one or multiple tuples of 2 floats, " |
| 637 | + "matching the inputs of the basis.\n" |
| 638 | + ) |
| 639 | + |
| 640 | + # Validate: single (lo, hi) pair or one per dimension |
| 641 | + if len(values) == 2 and all(not isinstance(v, (list, tuple)) for v in values): |
| 642 | + # Single pair |
| 643 | + self._bounds = self._format_bounds(values) |
| 644 | + else: |
| 645 | + # One pair per dimension |
| 646 | + if len(values) != self._n_inputs: |
| 647 | + raise ValueError( |
| 648 | + f"Invalid bounds ``{values}`` provided, " |
| 649 | + "bounds should be one or multiple tuples of 2 floats, " |
| 650 | + "matching the inputs of the basis.\n" |
| 651 | + ) |
| 652 | + self._bounds = tuple(self._format_bounds(v) for v in values) |
| 653 | + |
612 | 654 | @staticmethod |
613 | 655 | def _format_bounds(values: Any) -> Tuple[Any, Exception | None]: |
614 | 656 | """Check bounds and cast to tuple.""" |
@@ -640,19 +682,6 @@ def _format_bounds(values: Any) -> Tuple[Any, Exception | None]: |
640 | 682 |
|
641 | 683 | return values |
642 | 684 |
|
643 | | - @bounds.setter |
644 | | - def bounds(self, values: Union[None, Tuple[float, float]]): |
645 | | - """Setter for bounds.""" |
646 | | - if values is None: |
647 | | - self._bounds = None |
648 | | - return |
649 | | - values = self._format_bounds(values) |
650 | | - if values is not None and len(values) != 2: |
651 | | - raise ValueError( |
652 | | - f"The provided `bounds` must be of length two. Length {len(values)} provided instead!" |
653 | | - ) |
654 | | - self._bounds = values |
655 | | - |
656 | 685 |
|
657 | 686 | class ConvBasisMixin: |
658 | 687 | """Mixin class for convolutional basis.""" |
@@ -883,7 +912,7 @@ def __init__( |
883 | 912 | self, basis1: BasisMixin, basis2: BasisMixin, label: Optional[str] = None |
884 | 913 | ): |
885 | 914 | # number of input arrays that the basis receives |
886 | | - self._n_input_dimensionality = infer_input_dimensionality( |
| 915 | + self._n_inputs = infer_input_dimensionality( |
887 | 916 | basis1 |
888 | 917 | ) + infer_input_dimensionality(basis2) |
889 | 918 |
|
|
0 commit comments