Skip to content

Commit

Permalink
ENH: Add function to calculate number of polynomial features
Browse files Browse the repository at this point in the history
This is much faster than fitting the features, and can be used
by trapping to replace some constraint index calculations.

Also:
Add docstring/type annotations
Rename a variable so it doesn't shadow newly-imported name
  • Loading branch information
Jacob-Stevens-Haas committed Dec 8, 2023
1 parent 2662af3 commit 953822b
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 8 deletions.
66 changes: 58 additions & 8 deletions pysindy/feature_library/polynomial_library.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from itertools import chain
from math import comb
from typing import Iterator

import numpy as np
from numpy.typing import NDArray
from scipy import sparse
from sklearn.preprocessing import PolynomialFeatures
from sklearn.utils.validation import check_is_fitted
Expand Down Expand Up @@ -73,8 +75,22 @@ def __init__(

@staticmethod
def _combinations(
n_features, degree, include_interaction, interaction_only, include_bias
) -> Iterator[tuple]:
n_features: int,
degree: int,
include_interaction: bool,
interaction_only: bool,
include_bias: bool,
) -> Iterator[tuple[int, ...]]:
"""
Create selection tuples of input indexes for each polynomail term
Selection tuple iterates the input indexes present in a single term.
For example, (x+y+1)^2 would be in iterator of the tuples:
(), (0,), (1,), (0, 0), (0, 1), (1, 1)
1 x y x^2 x*y y^2
Order of terms is preserved regardless of include_interation.
"""
if not include_interaction:
return chain(
[()] if include_bias else [],
Expand All @@ -93,7 +109,12 @@ def _combinations(
)

@property
def powers_(self):
def powers_(self) -> NDArray[np.dtype("int")]:
"""
The exponents of the polynomial as an array of shape
(n_features_out, n_features_in), where each item is the exponent of the
jth input variable in the ith polynomial term.
"""
check_is_fitted(self)
combinations = self._combinations(
n_features=self.n_features_in_,
Expand Down Expand Up @@ -208,10 +229,10 @@ def transform(self, x_full):
)
if sparse.isspmatrix(x):
columns = []
for comb in combinations:
if comb:
for combo in combinations:
if combo:
out_col = 1
for col_idx in comb:
for col_idx in combo:
out_col = x[..., col_idx].multiply(out_col)
columns.append(out_col)
else:
Expand All @@ -227,7 +248,36 @@ def transform(self, x_full):
),
x.__dict__,
)
for i, comb in enumerate(combinations):
xp[..., i] = x[..., comb].prod(-1)
for i, combo in enumerate(combinations):
xp[..., i] = x[..., combo].prod(-1)
xp_full = xp_full + [xp]
return xp_full


def n_poly_features(
n_in_feat: int,
degree: int,
include_bias: bool = False,
include_interation: bool = True,
interaction_only: bool = False,
) -> int:
"""Calculate number of polynomial features
Args:
n_in_feat: number of input features, e.g. 3 for x, y, z
degree: polynomial degree, e.g. 2 for quadratic
include_bias: whether to include a constant term
include_interaction: whether to include terms mixing multiple inputs
interaction_only: whether to omit terms of x_m * x_n^p for p > 1
"""
if not include_interation and interaction_only:
raise ValueError("Cannot set interaction only if include_interaction is False")
n_feat = include_bias
if not include_interation:
return n_feat + n_in_feat * degree
for deg in range(1, degree + 1):
if interaction_only:
n_feat += comb(n_in_feat, deg)
else:
n_feat += comb(n_in_feat + deg - 1, deg)
return n_feat
4 changes: 4 additions & 0 deletions test/test_feature_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pysindy.feature_library import TensoredLibrary
from pysindy.feature_library import WeakPDELibrary
from pysindy.feature_library.base import BaseFeatureLibrary
from pysindy.feature_library.polynomial_library import n_poly_features
from pysindy.optimizers import SINDyPI
from pysindy.optimizers import STLSQ

Expand Down Expand Up @@ -880,3 +881,6 @@ def test_polynomial_combinations(include_interaction, interaction_only, bias, ex
)
result = tuple(sorted(list(combos)))
assert result == expected
assert len(expected) == n_poly_features(
2, 2, bias, include_interaction, interaction_only
)

0 comments on commit 953822b

Please sign in to comment.