StochasticForceInference 2.0.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.
- SFI/__init__.py +64 -0
- SFI/bases/__init__.py +85 -0
- SFI/bases/constants.py +492 -0
- SFI/bases/linear.py +325 -0
- SFI/bases/monomials.py +218 -0
- SFI/bases/pairs.py +998 -0
- SFI/bases/spde.py +1537 -0
- SFI/diagnostics/__init__.py +60 -0
- SFI/diagnostics/assess.py +87 -0
- SFI/diagnostics/dynamics_order.py +621 -0
- SFI/diagnostics/plotting.py +226 -0
- SFI/diagnostics/report.py +238 -0
- SFI/diagnostics/residual_tests.py +395 -0
- SFI/diagnostics/residuals.py +688 -0
- SFI/inference/__init__.py +58 -0
- SFI/inference/base.py +1460 -0
- SFI/inference/optimizers.py +200 -0
- SFI/inference/overdamped.py +1214 -0
- SFI/inference/parametric_core/__init__.py +34 -0
- SFI/inference/parametric_core/covariance.py +232 -0
- SFI/inference/parametric_core/driver.py +272 -0
- SFI/inference/parametric_core/flow.py +149 -0
- SFI/inference/parametric_core/flow_multi.py +362 -0
- SFI/inference/parametric_core/flow_ud.py +168 -0
- SFI/inference/parametric_core/jacobians.py +540 -0
- SFI/inference/parametric_core/objective.py +286 -0
- SFI/inference/parametric_core/objective_ud.py +253 -0
- SFI/inference/parametric_core/precision.py +229 -0
- SFI/inference/parametric_core/solve.py +763 -0
- SFI/inference/result.py +362 -0
- SFI/inference/serialization.py +245 -0
- SFI/inference/sparse/__init__.py +67 -0
- SFI/inference/sparse/base.py +43 -0
- SFI/inference/sparse/beam.py +303 -0
- SFI/inference/sparse/greedy.py +151 -0
- SFI/inference/sparse/hillclimb.py +307 -0
- SFI/inference/sparse/lasso.py +178 -0
- SFI/inference/sparse/metrics.py +78 -0
- SFI/inference/sparse/result.py +278 -0
- SFI/inference/sparse/scorer.py +323 -0
- SFI/inference/sparse/stlsq.py +165 -0
- SFI/inference/sparsity.py +40 -0
- SFI/inference/underdamped.py +1355 -0
- SFI/integrate/__init__.py +38 -0
- SFI/integrate/api.py +920 -0
- SFI/integrate/integrand.py +402 -0
- SFI/integrate/rk4.py +156 -0
- SFI/integrate/timeops.py +174 -0
- SFI/langevin/__init__.py +29 -0
- SFI/langevin/base.py +863 -0
- SFI/langevin/chunked.py +225 -0
- SFI/langevin/noise.py +446 -0
- SFI/langevin/overdamped.py +560 -0
- SFI/langevin/underdamped.py +448 -0
- SFI/statefunc/__init__.py +49 -0
- SFI/statefunc/basis.py +87 -0
- SFI/statefunc/core/runtime.py +47 -0
- SFI/statefunc/factory.py +346 -0
- SFI/statefunc/interactor.py +90 -0
- SFI/statefunc/layout/__init__.py +29 -0
- SFI/statefunc/layout/_base.py +186 -0
- SFI/statefunc/layout/_eval_compiler.py +453 -0
- SFI/statefunc/layout/_fd_atoms.py +196 -0
- SFI/statefunc/layout/_grid.py +878 -0
- SFI/statefunc/layout/_sectors.py +166 -0
- SFI/statefunc/memhint.py +222 -0
- SFI/statefunc/nodes/__init__.py +56 -0
- SFI/statefunc/nodes/base.py +318 -0
- SFI/statefunc/nodes/contract.py +422 -0
- SFI/statefunc/nodes/interactions/__init__.py +23 -0
- SFI/statefunc/nodes/interactions/dispatcher.py +1365 -0
- SFI/statefunc/nodes/interactions/prepare.py +161 -0
- SFI/statefunc/nodes/interactions/specs.py +362 -0
- SFI/statefunc/nodes/interactions/stencils.py +718 -0
- SFI/statefunc/nodes/leaf.py +530 -0
- SFI/statefunc/nodes/ops/__init__.py +27 -0
- SFI/statefunc/nodes/ops/concat.py +28 -0
- SFI/statefunc/nodes/ops/derivative.py +447 -0
- SFI/statefunc/nodes/ops/einsum.py +120 -0
- SFI/statefunc/nodes/ops/linear.py +153 -0
- SFI/statefunc/nodes/ops/mapn.py +138 -0
- SFI/statefunc/nodes/ops/reshape_rank.py +158 -0
- SFI/statefunc/nodes/ops/slice.py +83 -0
- SFI/statefunc/params.py +309 -0
- SFI/statefunc/psf.py +112 -0
- SFI/statefunc/sf.py +104 -0
- SFI/statefunc/stateexpr.py +1243 -0
- SFI/statefunc/structexpr.py +1003 -0
- SFI/trajectory/__init__.py +31 -0
- SFI/trajectory/collection.py +1122 -0
- SFI/trajectory/dataset.py +1164 -0
- SFI/trajectory/degrade.py +1108 -0
- SFI/trajectory/io.py +1027 -0
- SFI/trajectory/reserved_extras.py +129 -0
- SFI/utils/__init__.py +17 -0
- SFI/utils/formatting.py +308 -0
- SFI/utils/maths.py +185 -0
- SFI/utils/neighbors.py +162 -0
- SFI/utils/plotting.py +1936 -0
- stochasticforceinference-2.0.0.dist-info/METADATA +203 -0
- stochasticforceinference-2.0.0.dist-info/RECORD +104 -0
- stochasticforceinference-2.0.0.dist-info/WHEEL +5 -0
- stochasticforceinference-2.0.0.dist-info/licenses/LICENSE +21 -0
- stochasticforceinference-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SFI.inference.sparse.stlsq — Sequential Thresholded Least Squares
|
|
3
|
+
=================================================================
|
|
4
|
+
|
|
5
|
+
The :class:`STLSQStrategy` implements the iterative hard-thresholding
|
|
6
|
+
algorithm popularised by SINDy (Brunton *et al.*, 2016):
|
|
7
|
+
|
|
8
|
+
1. Solve the full problem :math:`G\\,C = M`.
|
|
9
|
+
2. Zero out coefficients whose magnitude falls below a threshold.
|
|
10
|
+
3. Re-solve on the surviving support.
|
|
11
|
+
4. Repeat until convergence.
|
|
12
|
+
|
|
13
|
+
The threshold can be **absolute** (``mode="absolute"``) or
|
|
14
|
+
**relative** to the current maximum coefficient magnitude
|
|
15
|
+
(``mode="relative"``, default — matches the PySINDy convention).
|
|
16
|
+
|
|
17
|
+
Running over a sweep of thresholds produces a Pareto front.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from .base import SparsityStrategy
|
|
29
|
+
from .result import SparsityResult
|
|
30
|
+
from .scorer import SparseScorer
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class STLSQStrategy(SparsityStrategy):
|
|
36
|
+
"""Sequential Thresholded Least Squares (SINDy-style).
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
threshold : float or None
|
|
41
|
+
Single threshold value. If *None*, an automatic log-spaced
|
|
42
|
+
sweep from a small fraction of the maximum coefficient up to
|
|
43
|
+
the maximum is performed.
|
|
44
|
+
mode : ``"relative"`` | ``"absolute"``, default ``"relative"``
|
|
45
|
+
Whether ``threshold`` is interpreted as a fraction of
|
|
46
|
+
:math:`\\max|C|` (relative) or a fixed value (absolute).
|
|
47
|
+
max_iter : int, default 50
|
|
48
|
+
Maximum STLSQ iterations per threshold.
|
|
49
|
+
n_thresholds : int, default 30
|
|
50
|
+
Number of thresholds in the automatic sweep (used only when
|
|
51
|
+
``threshold is None``).
|
|
52
|
+
report_time : bool, default False
|
|
53
|
+
Log elapsed wall-clock time when done.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name = "stlsq"
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
threshold: float | None = None,
|
|
62
|
+
mode: str = "relative",
|
|
63
|
+
max_iter: int = 50,
|
|
64
|
+
n_thresholds: int = 30,
|
|
65
|
+
report_time: bool = False,
|
|
66
|
+
):
|
|
67
|
+
if mode not in ("relative", "absolute"):
|
|
68
|
+
raise ValueError(f"mode must be 'relative' or 'absolute'; got {mode!r}")
|
|
69
|
+
self.threshold = threshold
|
|
70
|
+
self.mode = mode
|
|
71
|
+
self.max_iter = max_iter
|
|
72
|
+
self.n_thresholds = n_thresholds
|
|
73
|
+
self.report_time = report_time
|
|
74
|
+
|
|
75
|
+
# -----------------------------------------------------------------
|
|
76
|
+
def _stlsq_once(
|
|
77
|
+
self, scorer: SparseScorer, threshold: float, *, is_relative: bool
|
|
78
|
+
) -> tuple[list[int], float, jnp.ndarray]:
|
|
79
|
+
"""Run STLSQ for a single threshold. Returns (support, info, coeffs)."""
|
|
80
|
+
support = list(range(scorer.p))
|
|
81
|
+
|
|
82
|
+
for _ in range(self.max_iter):
|
|
83
|
+
B = jnp.array(support, dtype=jnp.int32)
|
|
84
|
+
info, C = scorer.info_and_coeffs(B)
|
|
85
|
+
|
|
86
|
+
abs_C = jnp.abs(C)
|
|
87
|
+
cutoff = threshold * float(jnp.max(abs_C)) if is_relative else threshold
|
|
88
|
+
mask = abs_C >= cutoff
|
|
89
|
+
|
|
90
|
+
new_support = [support[i] for i in range(len(support)) if mask[i]]
|
|
91
|
+
|
|
92
|
+
if new_support == support:
|
|
93
|
+
break
|
|
94
|
+
support = sorted(new_support)
|
|
95
|
+
|
|
96
|
+
if len(support) == 0:
|
|
97
|
+
return [], 0.0, jnp.zeros(0, dtype=scorer.M.dtype)
|
|
98
|
+
|
|
99
|
+
# Final solve on converged support
|
|
100
|
+
B = jnp.array(support, dtype=jnp.int32)
|
|
101
|
+
info, C = scorer.info_and_coeffs(B)
|
|
102
|
+
return support, float(info), C
|
|
103
|
+
|
|
104
|
+
# -----------------------------------------------------------------
|
|
105
|
+
def run(self, scorer: SparseScorer, *, max_k: int, **_kwargs) -> SparsityResult:
|
|
106
|
+
t0 = time.perf_counter()
|
|
107
|
+
max_k = min(max_k, scorer.p)
|
|
108
|
+
|
|
109
|
+
best_info = [-np.inf] * (max_k + 1)
|
|
110
|
+
best_support = [[] for _ in range(max_k + 1)]
|
|
111
|
+
best_coeffs = [None] * (max_k + 1)
|
|
112
|
+
|
|
113
|
+
# Null model
|
|
114
|
+
best_info[0] = 0.0
|
|
115
|
+
|
|
116
|
+
is_relative = self.mode == "relative"
|
|
117
|
+
|
|
118
|
+
if self.threshold is not None:
|
|
119
|
+
thresholds = [self.threshold]
|
|
120
|
+
else:
|
|
121
|
+
# Automatic sweep: from ~0.001 to ~1.0 (relative)
|
|
122
|
+
# or from a small absolute value to max(|C_full|) (absolute)
|
|
123
|
+
if is_relative:
|
|
124
|
+
thresholds = np.logspace(-3, -0.05, self.n_thresholds).tolist()
|
|
125
|
+
else:
|
|
126
|
+
max_abs = float(jnp.max(jnp.abs(scorer.total_C)))
|
|
127
|
+
if max_abs > 0:
|
|
128
|
+
thresholds = np.logspace(
|
|
129
|
+
np.log10(max_abs * 1e-4),
|
|
130
|
+
np.log10(max_abs * 0.9),
|
|
131
|
+
self.n_thresholds,
|
|
132
|
+
).tolist()
|
|
133
|
+
else:
|
|
134
|
+
thresholds = [0.0]
|
|
135
|
+
|
|
136
|
+
for thr in thresholds:
|
|
137
|
+
support, info, coeffs = self._stlsq_once(scorer, thr, is_relative=is_relative)
|
|
138
|
+
k = len(support)
|
|
139
|
+
if k <= max_k and info > best_info[k]:
|
|
140
|
+
best_info[k] = info
|
|
141
|
+
best_support[k] = support
|
|
142
|
+
best_coeffs[k] = coeffs
|
|
143
|
+
|
|
144
|
+
# Also record the full model if within max_k
|
|
145
|
+
if scorer.p <= max_k:
|
|
146
|
+
full_info = float(scorer.total_info)
|
|
147
|
+
if full_info > best_info[scorer.p]:
|
|
148
|
+
best_info[scorer.p] = full_info
|
|
149
|
+
best_support[scorer.p] = list(range(scorer.p))
|
|
150
|
+
best_coeffs[scorer.p] = scorer.total_C
|
|
151
|
+
|
|
152
|
+
if self.report_time:
|
|
153
|
+
dt = time.perf_counter() - t0
|
|
154
|
+
logger.info("STLSQ done in %.2fs (%d thresholds).", dt, len(thresholds))
|
|
155
|
+
|
|
156
|
+
return SparsityResult(
|
|
157
|
+
p=scorer.p,
|
|
158
|
+
total_info=float(scorer.total_info),
|
|
159
|
+
method=self.name,
|
|
160
|
+
best_info_by_k=best_info,
|
|
161
|
+
best_support_by_k=best_support,
|
|
162
|
+
best_coeffs_by_k=best_coeffs,
|
|
163
|
+
second_info_by_k=[-np.inf] * (max_k + 1),
|
|
164
|
+
second_support_by_k=[[] for _ in range(max_k + 1)],
|
|
165
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
SFI.inference.sparsity — Sparse model selection
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
Public façade that re-exports the key symbols from
|
|
7
|
+
:mod:`SFI.inference.sparse`. User code should import from here or from
|
|
8
|
+
the ``SFI.inference`` namespace::
|
|
9
|
+
|
|
10
|
+
from SFI.inference import SparseScorer, SparsityResult
|
|
11
|
+
from SFI.inference.sparse import BeamSearchStrategy, LassoStrategy
|
|
12
|
+
|
|
13
|
+
The heavy lifting lives in ``SFI.inference.sparse``; this module keeps
|
|
14
|
+
the import path short and backward-compatible.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Re-export everything downstream code may need
|
|
18
|
+
from SFI.inference.sparse import ( # noqa: F401
|
|
19
|
+
BeamSearchStrategy,
|
|
20
|
+
GreedyStepwiseStrategy,
|
|
21
|
+
LassoStrategy,
|
|
22
|
+
SparseScorer,
|
|
23
|
+
SparsityResult,
|
|
24
|
+
SparsityStrategy,
|
|
25
|
+
STLSQStrategy,
|
|
26
|
+
overlap_metrics,
|
|
27
|
+
predictive_nmse,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"SparseScorer",
|
|
32
|
+
"SparsityResult",
|
|
33
|
+
"SparsityStrategy",
|
|
34
|
+
"BeamSearchStrategy",
|
|
35
|
+
"GreedyStepwiseStrategy",
|
|
36
|
+
"STLSQStrategy",
|
|
37
|
+
"LassoStrategy",
|
|
38
|
+
"overlap_metrics",
|
|
39
|
+
"predictive_nmse",
|
|
40
|
+
]
|