Skip to content

Commit d040f6a

Browse files
Tests
1 parent 2b6a2eb commit d040f6a

File tree

5 files changed

+122
-115
lines changed

5 files changed

+122
-115
lines changed

pysindy/feature_library/weighted_weak_pde_library.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def _build_whitener_from_variance(self):
6969
# corresponding weak RHS weights, flattened to 1D
7070
wk = np.asarray(self.fulltweights[k], dtype=float).ravel(order="C")
7171

72-
# ensure same length (paranoia check)
7372
if wk.shape[0] != lin_idx.shape[0]:
7473
raise RuntimeError(
7574
f"Weight/variance size mismatch on cell {k}: "
@@ -86,16 +85,14 @@ def _build_whitener_from_variance(self):
8685
for k in range(K):
8786
vk = val_lists[k]
8887
Cov[k, k] = np.dot(vk, vk)
89-
# off-diagonals via set intersection of supports
9088
idx_k = idx_lists[k]
91-
# Use a dict for fast overlap accumulation
89+
9290
map_k = dict(zip(idx_k.tolist(), vk.tolist()))
9391
for ell in range(k + 1, K):
9492
s = 0.0
9593
idx_e = idx_lists[ell]
9694
v_e = val_lists[ell]
9795
map_e = dict(zip(idx_e.tolist(), v_e.tolist()))
98-
# iterate the smaller map
9996
if len(map_k) <= len(map_e):
10097
for j, vkj in map_k.items():
10198
ve = map_e.get(j)
@@ -109,12 +106,10 @@ def _build_whitener_from_variance(self):
109106
Cov[k, ell] = s
110107
Cov[ell, k] = s
111108

112-
# diagonal nugget for stability
113109
avg_diag = np.trace(Cov) / max(K, 1)
114110
nugget = 1e-12 * avg_diag
115111
Cov.flat[:: K + 1] += nugget
116112

117-
# robust Cholesky with fallback if needed
118113
try:
119114
self._L_chol = np.linalg.cholesky(Cov)
120115
except np.linalg.LinAlgError:
@@ -126,7 +121,6 @@ def _apply_whitener(self, A):
126121
"""Return L^{-1} A without forming L^{-1} explicitly."""
127122
if self._L_chol is None:
128123
return A
129-
# solve L X = A → X = L^{-1} A
130124
return np.linalg.solve(self._L_chol, A)
131125

132126
# ------------------------------ hooks ------------------------------

pysindy/optimizers/base.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -173,32 +173,7 @@ def fit(self, x_, y, sample_weight=None, **reduce_kws):
173173
y = AxesArray(np.asarray(y), y_axes)
174174
x_, y = drop_nan_samples(x_, y)
175175
x_, y = check_X_y(x_, y, accept_sparse=[], y_numeric=True, multi_output=True)
176-
177-
# The next scope is for when the sample weights
178-
# are different for each output component
179-
# we select the weights of the each output component and
180-
# recursively fit
181-
if y.ndim == 2 and sample_weight is not None and sample_weight.shape == y.shape:
182-
coefs, histories, inds = [], [], []
183-
n_targets = y.shape[1]
184-
subs = []
185-
for j in range(n_targets):
186-
sw_j = sample_weight[:, j].ravel() # flatten to 1D
187-
sub = clone(self)
188-
sub.fit(x_, y[:, j:j+1], sample_weight=sw_j, **reduce_kws)
189-
coefs.append(sub.coef_.reshape(1, -1))
190-
histories.append(sub.history_)
191-
inds.append(sub.ind_.reshape(1, -1))
192-
subs.append(sub)
193-
194-
# stack results: shape (n_targets, n_features)
195-
self.coef_ = np.vstack(coefs)
196-
self.ind_ = np.vstack(inds)
197-
self.history_ = [self.coef_]
198-
self.Theta_ = subs[0].Theta_
199-
self.intercept_ = np.zeros(n_targets, dtype=self.coef_.dtype)
200-
return self
201-
176+
202177
x, y, X_offset, y_offset, X_scale = _preprocess_data(
203178
x_,
204179
y,

test/test_expand_weights.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import numpy as np
2+
import pytest
3+
from pysindy import _expand_sample_weights
4+
5+
6+
class DummyTraj:
7+
def __init__(self, n_time, n_coord):
8+
self.n_time = n_time
9+
self.n_coord = n_coord
10+
11+
12+
@pytest.fixture
13+
def dummy_trajs():
14+
"""Simple fixture for two dummy trajectories."""
15+
return [DummyTraj(5, 2), DummyTraj(5, 2)]
16+
17+
18+
def test_scalar_weights_none(dummy_trajs):
19+
assert _expand_sample_weights(None, dummy_trajs) is None
20+
21+
22+
def test_1d_sample_weights(dummy_trajs):
23+
"""1D weights per trajectory concatenate correctly."""
24+
weights = [np.ones(5), 2 * np.ones(5)]
25+
out = _expand_sample_weights(weights, dummy_trajs)
26+
assert out.shape == (10,)
27+
assert np.all(out[:5] == 1)
28+
assert np.all(out[5:] == 2)
29+
30+
31+
def test_2d_per_coord_weights(dummy_trajs):
32+
"""2D weights (n_time, n_coord) concatenate along samples."""
33+
weights = [np.ones((5, 2)), np.full((5, 2), 2.0)]
34+
out = _expand_sample_weights(weights, dummy_trajs)
35+
assert out.shape == (10, 2)
36+
assert np.allclose(out[:5], 1)
37+
assert np.allclose(out[5:], 2)
38+
39+
40+
def test_promote_1d_to_2d(dummy_trajs):
41+
"""1D weights promoted to (n_time, n_coord)."""
42+
weights = [np.arange(5), np.arange(5)]
43+
out = _expand_sample_weights(weights, dummy_trajs)
44+
assert out.ndim == 1 # still flattened because all dims == 1
45+
46+
47+
def test_weak_mode_expansion(dummy_trajs):
48+
"""Weak mode expands by n_test_funcs."""
49+
weights = [np.ones(5), np.ones(5)]
50+
class DummyLib: K = 3
51+
out = _expand_sample_weights(weights, dummy_trajs, feature_library=DummyLib(), mode="weak")
52+
assert out.shape == (10 * 3,)
53+
assert np.allclose(np.unique(out), 1)

test/test_sample_weights.py

Lines changed: 0 additions & 82 deletions
This file was deleted.

test/test_weights_routing.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import numpy as np
2+
import pytest
3+
from sklearn.linear_model import LinearRegression
4+
from pysindy import SINDy # adjust import to your package path
5+
6+
7+
@pytest.fixture(scope="session")
8+
def simple_systems():
9+
"""Generate two simple 2D dynamical systems:
10+
System A: x' = -y, y' = x
11+
System B: x' = -2y, y' = 2x
12+
"""
13+
t = np.linspace(0, 2 * np.pi, 50)
14+
x_a = np.stack([np.cos(t), np.sin(t)], axis=1)
15+
xdot_a = np.stack([-np.sin(t), np.cos(t)], axis=1)
16+
17+
x_b = np.stack([np.cos(2 * t), np.sin(2 * t)], axis=1)
18+
xdot_b = np.stack([-2 * np.sin(2 * t), 2 * np.cos(2 * t)], axis=1)
19+
20+
return (x_a, xdot_a), (x_b, xdot_b)
21+
22+
23+
def test_metadata_routing_sample_weight(simple_systems):
24+
"""Test that sample weights route correctly through SINDy fit().
25+
26+
The expected coefficients are a convex combination of the systems'
27+
true coefficients, weighted by the number of trajectories and/or
28+
sample weights. This verifies that behind-the-scenes routing of
29+
sample_weight → optimizer.fit() works correctly.
30+
"""
31+
(x_a, xdot_a), (x_b, xdot_b) = simple_systems
32+
33+
# --- Build training trajectories ---
34+
# One system duplicated twice (implicit weighting)
35+
X_trajs = [x_a, x_a, x_b]
36+
Xdot_trajs = [xdot_a, xdot_a, xdot_b]
37+
38+
# --- Simple library and optimizer setup ---
39+
sindy = SINDy(optimizer=LinearRegression(fit_intercept=False))
40+
41+
# --- Fit without explicit sample weights ---
42+
sindy.fit(X_trajs, t=0.1, x_dot=Xdot_trajs)
43+
coef_unweighted = np.copy(sindy.model.named_steps["model"].coef_)
44+
45+
# --- Fit with sample weights to emphasize trajectory 3 (different system) ---
46+
sample_weight = [np.ones(len(x_a)), np.ones(len(x_a)), 10 * np.ones(len(x_b))]
47+
sindy.fit(X_trajs, t=0.1, x_dot=Xdot_trajs, sample_weight=sample_weight)
48+
coef_weighted = np.copy(sindy.model.named_steps["model"].coef_)
49+
50+
# --- Assertions ---
51+
# 1. Shapes are consistent
52+
assert coef_weighted.shape == coef_unweighted.shape
53+
54+
# 2. The coefficients must differ when weighting is applied
55+
assert not np.allclose(coef_weighted, coef_unweighted)
56+
57+
# 3. Weighted model should bias toward system B coefficients
58+
# since trajectory B had much higher weight
59+
# True systems differ by factor of 2
60+
ratio = np.mean(np.abs(coef_weighted / coef_unweighted))
61+
assert ratio > 1.05, "Weighted coefficients should reflect stronger influence from system B"
62+
63+
# 4. Convex combination logic sanity check
64+
# Unweighted: (A + A + B)/3 = A * 2/3 + B * 1/3
65+
# Weighted: (A + A + 10*B)/(12) ≈ A * 2/12 + B * 10/12
66+
# So weighted coefficients should be closer to B's dynamics
67+
assert np.linalg.norm(coef_weighted - 2 * coef_unweighted) < np.linalg.norm(coef_unweighted)

0 commit comments

Comments
 (0)