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.
Files changed (104) hide show
  1. SFI/__init__.py +64 -0
  2. SFI/bases/__init__.py +85 -0
  3. SFI/bases/constants.py +492 -0
  4. SFI/bases/linear.py +325 -0
  5. SFI/bases/monomials.py +218 -0
  6. SFI/bases/pairs.py +998 -0
  7. SFI/bases/spde.py +1537 -0
  8. SFI/diagnostics/__init__.py +60 -0
  9. SFI/diagnostics/assess.py +87 -0
  10. SFI/diagnostics/dynamics_order.py +621 -0
  11. SFI/diagnostics/plotting.py +226 -0
  12. SFI/diagnostics/report.py +238 -0
  13. SFI/diagnostics/residual_tests.py +395 -0
  14. SFI/diagnostics/residuals.py +688 -0
  15. SFI/inference/__init__.py +58 -0
  16. SFI/inference/base.py +1460 -0
  17. SFI/inference/optimizers.py +200 -0
  18. SFI/inference/overdamped.py +1214 -0
  19. SFI/inference/parametric_core/__init__.py +34 -0
  20. SFI/inference/parametric_core/covariance.py +232 -0
  21. SFI/inference/parametric_core/driver.py +272 -0
  22. SFI/inference/parametric_core/flow.py +149 -0
  23. SFI/inference/parametric_core/flow_multi.py +362 -0
  24. SFI/inference/parametric_core/flow_ud.py +168 -0
  25. SFI/inference/parametric_core/jacobians.py +540 -0
  26. SFI/inference/parametric_core/objective.py +286 -0
  27. SFI/inference/parametric_core/objective_ud.py +253 -0
  28. SFI/inference/parametric_core/precision.py +229 -0
  29. SFI/inference/parametric_core/solve.py +763 -0
  30. SFI/inference/result.py +362 -0
  31. SFI/inference/serialization.py +245 -0
  32. SFI/inference/sparse/__init__.py +67 -0
  33. SFI/inference/sparse/base.py +43 -0
  34. SFI/inference/sparse/beam.py +303 -0
  35. SFI/inference/sparse/greedy.py +151 -0
  36. SFI/inference/sparse/hillclimb.py +307 -0
  37. SFI/inference/sparse/lasso.py +178 -0
  38. SFI/inference/sparse/metrics.py +78 -0
  39. SFI/inference/sparse/result.py +278 -0
  40. SFI/inference/sparse/scorer.py +323 -0
  41. SFI/inference/sparse/stlsq.py +165 -0
  42. SFI/inference/sparsity.py +40 -0
  43. SFI/inference/underdamped.py +1355 -0
  44. SFI/integrate/__init__.py +38 -0
  45. SFI/integrate/api.py +920 -0
  46. SFI/integrate/integrand.py +402 -0
  47. SFI/integrate/rk4.py +156 -0
  48. SFI/integrate/timeops.py +174 -0
  49. SFI/langevin/__init__.py +29 -0
  50. SFI/langevin/base.py +863 -0
  51. SFI/langevin/chunked.py +225 -0
  52. SFI/langevin/noise.py +446 -0
  53. SFI/langevin/overdamped.py +560 -0
  54. SFI/langevin/underdamped.py +448 -0
  55. SFI/statefunc/__init__.py +49 -0
  56. SFI/statefunc/basis.py +87 -0
  57. SFI/statefunc/core/runtime.py +47 -0
  58. SFI/statefunc/factory.py +346 -0
  59. SFI/statefunc/interactor.py +90 -0
  60. SFI/statefunc/layout/__init__.py +29 -0
  61. SFI/statefunc/layout/_base.py +186 -0
  62. SFI/statefunc/layout/_eval_compiler.py +453 -0
  63. SFI/statefunc/layout/_fd_atoms.py +196 -0
  64. SFI/statefunc/layout/_grid.py +878 -0
  65. SFI/statefunc/layout/_sectors.py +166 -0
  66. SFI/statefunc/memhint.py +222 -0
  67. SFI/statefunc/nodes/__init__.py +56 -0
  68. SFI/statefunc/nodes/base.py +318 -0
  69. SFI/statefunc/nodes/contract.py +422 -0
  70. SFI/statefunc/nodes/interactions/__init__.py +23 -0
  71. SFI/statefunc/nodes/interactions/dispatcher.py +1365 -0
  72. SFI/statefunc/nodes/interactions/prepare.py +161 -0
  73. SFI/statefunc/nodes/interactions/specs.py +362 -0
  74. SFI/statefunc/nodes/interactions/stencils.py +718 -0
  75. SFI/statefunc/nodes/leaf.py +530 -0
  76. SFI/statefunc/nodes/ops/__init__.py +27 -0
  77. SFI/statefunc/nodes/ops/concat.py +28 -0
  78. SFI/statefunc/nodes/ops/derivative.py +447 -0
  79. SFI/statefunc/nodes/ops/einsum.py +120 -0
  80. SFI/statefunc/nodes/ops/linear.py +153 -0
  81. SFI/statefunc/nodes/ops/mapn.py +138 -0
  82. SFI/statefunc/nodes/ops/reshape_rank.py +158 -0
  83. SFI/statefunc/nodes/ops/slice.py +83 -0
  84. SFI/statefunc/params.py +309 -0
  85. SFI/statefunc/psf.py +112 -0
  86. SFI/statefunc/sf.py +104 -0
  87. SFI/statefunc/stateexpr.py +1243 -0
  88. SFI/statefunc/structexpr.py +1003 -0
  89. SFI/trajectory/__init__.py +31 -0
  90. SFI/trajectory/collection.py +1122 -0
  91. SFI/trajectory/dataset.py +1164 -0
  92. SFI/trajectory/degrade.py +1108 -0
  93. SFI/trajectory/io.py +1027 -0
  94. SFI/trajectory/reserved_extras.py +129 -0
  95. SFI/utils/__init__.py +17 -0
  96. SFI/utils/formatting.py +308 -0
  97. SFI/utils/maths.py +185 -0
  98. SFI/utils/neighbors.py +162 -0
  99. SFI/utils/plotting.py +1936 -0
  100. stochasticforceinference-2.0.0.dist-info/METADATA +203 -0
  101. stochasticforceinference-2.0.0.dist-info/RECORD +104 -0
  102. stochasticforceinference-2.0.0.dist-info/WHEEL +5 -0
  103. stochasticforceinference-2.0.0.dist-info/licenses/LICENSE +21 -0
  104. 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
+ ]