scientific-computing-system 1.6.2__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.
- cds/__init__.py +102 -0
- cds/__main__.py +8 -0
- cds/_version.py +10 -0
- cds/cli/__init__.py +104 -0
- cds/cli/__main__.py +8 -0
- cds/cli/_handlers.py +405 -0
- cds/cli/_parser.py +156 -0
- cds/cli/_style.py +117 -0
- cds/core/__init__.py +5 -0
- cds/core/_numeric.py +92 -0
- cds/core/models.py +166 -0
- cds/data_analysis/__init__.py +30 -0
- cds/data_analysis/dataset.py +122 -0
- cds/data_analysis/loader.py +75 -0
- cds/data_analysis/pandas_io.py +108 -0
- cds/data_analysis/transform.py +36 -0
- cds/data_analysis/viz.py +88 -0
- cds/diffeq/__init__.py +29 -0
- cds/diffeq/_implicit.py +349 -0
- cds/diffeq/solvers.py +312 -0
- cds/graph/__init__.py +31 -0
- cds/graph/algorithms.py +399 -0
- cds/hypothesis/__init__.py +48 -0
- cds/hypothesis/evaluator.py +369 -0
- cds/hypothesis/generator.py +275 -0
- cds/knowledge/__init__.py +41 -0
- cds/knowledge/graph.py +489 -0
- cds/knowledge/notes.py +231 -0
- cds/knowledge/retrieval.py +167 -0
- cds/math_utils/__init__.py +41 -0
- cds/math_utils/calculus.py +40 -0
- cds/math_utils/linalg.py +461 -0
- cds/math_utils/special.py +167 -0
- cds/ml/__init__.py +25 -0
- cds/ml/clustering.py +190 -0
- cds/ml/decomposition.py +172 -0
- cds/ml/linear_models.py +225 -0
- cds/ml/neighbors.py +162 -0
- cds/ml/neural.py +204 -0
- cds/ml/preprocessing.py +131 -0
- cds/ml/tree.py +202 -0
- cds/modeling/__init__.py +40 -0
- cds/modeling/_base.py +210 -0
- cds/modeling/_nodes.py +526 -0
- cds/modeling/expression.py +81 -0
- cds/modeling/model.py +134 -0
- cds/modeling/solver.py +176 -0
- cds/montecarlo/__init__.py +21 -0
- cds/montecarlo/methods.py +272 -0
- cds/nlp/__init__.py +170 -0
- cds/nlp/attention.py +285 -0
- cds/nlp/autograd/__init__.py +70 -0
- cds/nlp/autograd/_grad.py +120 -0
- cds/nlp/autograd/ops.py +133 -0
- cds/nlp/autograd/tensor.py +298 -0
- cds/nlp/bpe.py +449 -0
- cds/nlp/data.py +70 -0
- cds/nlp/embed.py +190 -0
- cds/nlp/layers.py +259 -0
- cds/nlp/model.py +456 -0
- cds/nlp/optim.py +163 -0
- cds/nlp/training.py +167 -0
- cds/nlp/viz.py +254 -0
- cds/numerical_integration/__init__.py +31 -0
- cds/numerical_integration/quadrature.py +480 -0
- cds/optimization/__init__.py +18 -0
- cds/optimization/_metaheuristics.py +287 -0
- cds/optimization/minimize.py +425 -0
- cds/plot/__init__.py +49 -0
- cds/plot/_backend.py +32 -0
- cds/plot/charts.py +462 -0
- cds/probability/__init__.py +55 -0
- cds/probability/_advanced.py +283 -0
- cds/probability/distributions.py +236 -0
- cds/py.typed +0 -0
- cds/quantum/__init__.py +51 -0
- cds/quantum/circuit.py +92 -0
- cds/quantum/multi_qubit.py +255 -0
- cds/quantum/simulator.py +38 -0
- cds/scientific/__init__.py +34 -0
- cds/scientific/constants.py +35 -0
- cds/scientific/formulas.py +116 -0
- cds/signals/__init__.py +43 -0
- cds/signals/filters.py +421 -0
- cds/signals/processing.py +224 -0
- cds/stats/__init__.py +83 -0
- cds/stats/_distributions.py +102 -0
- cds/stats/descriptive.py +172 -0
- cds/stats/hypothesis_tests.py +403 -0
- cds/stats/nonparametric.py +131 -0
- cds/stats/regression.py +51 -0
- cds/stats/time_series.py +408 -0
- scientific_computing_system-1.6.2.dist-info/METADATA +741 -0
- scientific_computing_system-1.6.2.dist-info/RECORD +97 -0
- scientific_computing_system-1.6.2.dist-info/WHEEL +4 -0
- scientific_computing_system-1.6.2.dist-info/entry_points.txt +2 -0
- scientific_computing_system-1.6.2.dist-info/licenses/LICENSE +21 -0
cds/diffeq/_implicit.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Implicit / A-stable ODE solvers for stiff problems.
|
|
2
|
+
|
|
3
|
+
Implements backward Euler (θ = 1) and the trapezoidal / Crank–Nicolson method
|
|
4
|
+
(θ = 1/2) as one unified θ-method core. Every implicit step solves the stage
|
|
5
|
+
equation ``x = y + h·[(1−θ)·f(t,y) + θ·f(t+h,x)]`` with damped-free Newton
|
|
6
|
+
iteration, using :func:`cds.math_utils.linalg.solve_linear` for the linear
|
|
7
|
+
subsystems (partial-pivoted LU) and an analytic or central-difference
|
|
8
|
+
Jacobian.
|
|
9
|
+
|
|
10
|
+
References:
|
|
11
|
+
- Hairer, E. & Wanner, G. (1996). Solving ODEs II: Stiff and
|
|
12
|
+
Differential-Algebraic Problems (2nd ed.), §IV.3.
|
|
13
|
+
- Crank, J. & Nicolson, P. (1947). Proc. Camb. Phil. Soc. 43(1).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
from cds.core._numeric import LOOP_EPSILON, RK45_DEFAULT_DT
|
|
22
|
+
from cds.diffeq.solvers import ODESolution
|
|
23
|
+
from cds.math_utils.linalg import solve_linear
|
|
24
|
+
|
|
25
|
+
SystemRHS = Callable[[float, list[float]], list[float]]
|
|
26
|
+
Jacobian = Callable[[float, list[float]], list[list[float]]]
|
|
27
|
+
|
|
28
|
+
DEFAULT_NEWTON_TOL = 1e-10
|
|
29
|
+
DEFAULT_NEWTON_MAX_ITER = 50
|
|
30
|
+
_FD_RELATIVE_H = 1e-7
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _finite_difference_jacobian(
|
|
34
|
+
f: SystemRHS,
|
|
35
|
+
t: float,
|
|
36
|
+
y: list[float],
|
|
37
|
+
) -> list[list[float]]:
|
|
38
|
+
"""Central-difference approximation of ∂f/∂y at ``(t, y)``."""
|
|
39
|
+
n = len(y)
|
|
40
|
+
jac = [[0.0] * n for _ in range(n)]
|
|
41
|
+
for j in range(n):
|
|
42
|
+
h = _FD_RELATIVE_H * max(1.0, abs(y[j]))
|
|
43
|
+
y_plus = list(y)
|
|
44
|
+
y_minus = list(y)
|
|
45
|
+
y_plus[j] += h
|
|
46
|
+
y_minus[j] -= h
|
|
47
|
+
f_plus = f(t, y_plus)
|
|
48
|
+
f_minus = f(t, y_minus)
|
|
49
|
+
for i in range(n):
|
|
50
|
+
jac[i][j] = (f_plus[i] - f_minus[i]) / (2.0 * h)
|
|
51
|
+
return jac
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _newton_solve(
|
|
55
|
+
residual: Callable[[list[float]], list[float]],
|
|
56
|
+
jac: Callable[[list[float]], list[list[float]]],
|
|
57
|
+
x0: list[float],
|
|
58
|
+
*,
|
|
59
|
+
tol: float,
|
|
60
|
+
max_iter: int,
|
|
61
|
+
) -> list[float]:
|
|
62
|
+
"""Solve ``residual(x) = 0`` with plain Newton iteration.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: when the linearized Jacobian is singular or the iteration
|
|
66
|
+
budget is exhausted without meeting ``tol``.
|
|
67
|
+
"""
|
|
68
|
+
x = list(x0)
|
|
69
|
+
for _ in range(max_iter):
|
|
70
|
+
r = residual(x)
|
|
71
|
+
if max(abs(v) for v in r) <= tol:
|
|
72
|
+
return x
|
|
73
|
+
try:
|
|
74
|
+
delta = solve_linear(jac(x), [-v for v in r])
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
raise ValueError("implicit step failed: singular Jacobian") from exc
|
|
77
|
+
x = [xi + d for xi, d in zip(x, delta)]
|
|
78
|
+
raise ValueError("implicit solve did not converge within max_iter")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _integrate_system(
|
|
82
|
+
f: SystemRHS,
|
|
83
|
+
t0: float,
|
|
84
|
+
y0: list[float],
|
|
85
|
+
t_end: float,
|
|
86
|
+
dt: float,
|
|
87
|
+
*,
|
|
88
|
+
theta: float,
|
|
89
|
+
jac: Jacobian | None,
|
|
90
|
+
tol: float,
|
|
91
|
+
max_iter: int,
|
|
92
|
+
) -> tuple[list[float], list[list[float]]]:
|
|
93
|
+
"""Fixed-step θ-method driver shared by every public implicit solver."""
|
|
94
|
+
if not y0:
|
|
95
|
+
raise ValueError("y0 must be non-empty")
|
|
96
|
+
if dt <= 0:
|
|
97
|
+
raise ValueError("dt must be positive")
|
|
98
|
+
if tol <= 0:
|
|
99
|
+
raise ValueError("tol must be positive")
|
|
100
|
+
if max_iter < 1:
|
|
101
|
+
raise ValueError("max_iter must be >= 1")
|
|
102
|
+
|
|
103
|
+
# Same direction rule as the explicit solvers (see euler_method): the sign
|
|
104
|
+
# of ``t_end - t0`` decides forward/backward integration; ``dt`` is a
|
|
105
|
+
# magnitude. copysign(1, 0) is +1, so t_end == t0 skips the loop cleanly.
|
|
106
|
+
direction = math.copysign(1.0, t_end - t0)
|
|
107
|
+
t_vals = [t0]
|
|
108
|
+
y_vals = [list(y0)]
|
|
109
|
+
t = t0
|
|
110
|
+
y = list(y0)
|
|
111
|
+
|
|
112
|
+
while (t_end - t) * direction > LOOP_EPSILON:
|
|
113
|
+
h = direction * min(dt, abs(t_end - t))
|
|
114
|
+
t_new = t + h
|
|
115
|
+
f_old = f(t, y)
|
|
116
|
+
|
|
117
|
+
def residual(x: list[float], t_new: float = t_new) -> list[float]:
|
|
118
|
+
f_new = f(t_new, x)
|
|
119
|
+
return [
|
|
120
|
+
xv - yv - h * ((1.0 - theta) * fv + theta * fnv)
|
|
121
|
+
for xv, yv, fv, fnv in zip(x, y, f_old, f_new)
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
def system_jac(x: list[float], t_new: float = t_new) -> list[list[float]]:
|
|
125
|
+
# Newton needs the Jacobian of the *residual*, not of f:
|
|
126
|
+
# d/dx [x - y - h((1-θ)f(t,y) + θ f(t+h,x))] = I - hθ·∂f/∂y.
|
|
127
|
+
jf = jac(t_new, x) if jac is not None else _finite_difference_jacobian(f, t_new, x)
|
|
128
|
+
n = len(x)
|
|
129
|
+
return [
|
|
130
|
+
[(1.0 if i == j else 0.0) - h * theta * jf[i][j] for j in range(n)]
|
|
131
|
+
for i in range(n)
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
y = _newton_solve(residual, system_jac, y, tol=tol, max_iter=max_iter)
|
|
135
|
+
t = t_new
|
|
136
|
+
t_vals.append(t)
|
|
137
|
+
y_vals.append(list(y))
|
|
138
|
+
|
|
139
|
+
return t_vals, y_vals
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _wrap_scalar_jac(jac: Callable[[float, float], float] | None) -> Jacobian | None:
|
|
143
|
+
"""Lift a scalar Jacobian ``df/dy`` into 1×1-matrix form."""
|
|
144
|
+
if jac is None:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def wrapped(t: float, y: list[float]) -> list[list[float]]:
|
|
148
|
+
return [[jac(t, y[0])]]
|
|
149
|
+
|
|
150
|
+
return wrapped
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def backward_euler(
|
|
154
|
+
f: Callable[[float, float], float],
|
|
155
|
+
t0: float,
|
|
156
|
+
y0: float,
|
|
157
|
+
t_end: float,
|
|
158
|
+
dt: float = RK45_DEFAULT_DT,
|
|
159
|
+
*,
|
|
160
|
+
jac: Callable[[float, float], float] | None = None,
|
|
161
|
+
tol: float = DEFAULT_NEWTON_TOL,
|
|
162
|
+
max_iter: int = DEFAULT_NEWTON_MAX_ITER,
|
|
163
|
+
) -> ODESolution:
|
|
164
|
+
"""Backward Euler for dy/dt = f(t, y): unconditionally stable, O(dt) accurate.
|
|
165
|
+
|
|
166
|
+
The method of choice for stiff decay problems where explicit methods
|
|
167
|
+
require absurdly small steps. [Hairer & Wanner 1996, §IV.3]
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
f: right-hand side function f(t, y)
|
|
171
|
+
t0: initial time
|
|
172
|
+
y0: initial value y(t0)
|
|
173
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
174
|
+
dt: time step magnitude (direction follows sign of ``t_end - t0``)
|
|
175
|
+
jac: optional analytic df/dy(t, y); central differences otherwise
|
|
176
|
+
tol: Newton convergence tolerance on the residual (∞-norm)
|
|
177
|
+
max_iter: Newton iteration budget per step
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
An :class:`ODESolution` with ``method == "backward-euler"``.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: on invalid parameters or Newton failure (singular
|
|
184
|
+
Jacobian / exhausted budget).
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def f_sys(t: float, y: list[float]) -> list[float]:
|
|
188
|
+
return [f(t, y[0])]
|
|
189
|
+
|
|
190
|
+
t_vals, y_vals = _integrate_system(
|
|
191
|
+
f_sys,
|
|
192
|
+
t0,
|
|
193
|
+
[y0],
|
|
194
|
+
t_end,
|
|
195
|
+
dt,
|
|
196
|
+
theta=1.0,
|
|
197
|
+
jac=_wrap_scalar_jac(jac),
|
|
198
|
+
tol=tol,
|
|
199
|
+
max_iter=max_iter,
|
|
200
|
+
)
|
|
201
|
+
return ODESolution(
|
|
202
|
+
t=t_vals,
|
|
203
|
+
y=[v[0] for v in y_vals],
|
|
204
|
+
method="backward-euler",
|
|
205
|
+
steps=len(t_vals) - 1,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def trapezoid_method(
|
|
210
|
+
f: Callable[[float, float], float],
|
|
211
|
+
t0: float,
|
|
212
|
+
y0: float,
|
|
213
|
+
t_end: float,
|
|
214
|
+
dt: float = RK45_DEFAULT_DT,
|
|
215
|
+
*,
|
|
216
|
+
jac: Callable[[float, float], float] | None = None,
|
|
217
|
+
tol: float = DEFAULT_NEWTON_TOL,
|
|
218
|
+
max_iter: int = DEFAULT_NEWTON_MAX_ITER,
|
|
219
|
+
) -> ODESolution:
|
|
220
|
+
"""Trapezoidal (Crank–Nicolson) method: A-stable, second order.
|
|
221
|
+
|
|
222
|
+
Averages the explicit and implicit slopes, giving O(dt²) global error —
|
|
223
|
+
strictly more accurate than backward Euler on smooth problems while
|
|
224
|
+
remaining stable on stiff ones. [Crank & Nicolson 1947]
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
f: right-hand side function f(t, y)
|
|
228
|
+
t0: initial time
|
|
229
|
+
y0: initial value y(t0)
|
|
230
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
231
|
+
dt: time step magnitude (direction follows sign of ``t_end - t0``)
|
|
232
|
+
jac: optional analytic df/dy(t, y); central differences otherwise
|
|
233
|
+
tol: Newton convergence tolerance on the residual (∞-norm)
|
|
234
|
+
max_iter: Newton iteration budget per step
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
An :class:`ODESolution` with ``method == "trapezoid"``.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValueError: on invalid parameters or Newton failure (singular
|
|
241
|
+
Jacobian / exhausted budget).
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def f_sys(t: float, y: list[float]) -> list[float]:
|
|
245
|
+
return [f(t, y[0])]
|
|
246
|
+
|
|
247
|
+
t_vals, y_vals = _integrate_system(
|
|
248
|
+
f_sys,
|
|
249
|
+
t0,
|
|
250
|
+
[y0],
|
|
251
|
+
t_end,
|
|
252
|
+
dt,
|
|
253
|
+
theta=0.5,
|
|
254
|
+
jac=_wrap_scalar_jac(jac),
|
|
255
|
+
tol=tol,
|
|
256
|
+
max_iter=max_iter,
|
|
257
|
+
)
|
|
258
|
+
return ODESolution(
|
|
259
|
+
t=t_vals,
|
|
260
|
+
y=[v[0] for v in y_vals],
|
|
261
|
+
method="trapezoid",
|
|
262
|
+
steps=len(t_vals) - 1,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def backward_euler_system(
|
|
267
|
+
f: SystemRHS,
|
|
268
|
+
t0: float,
|
|
269
|
+
y0: list[float],
|
|
270
|
+
t_end: float,
|
|
271
|
+
dt: float = RK45_DEFAULT_DT,
|
|
272
|
+
*,
|
|
273
|
+
jac: Jacobian | None = None,
|
|
274
|
+
tol: float = DEFAULT_NEWTON_TOL,
|
|
275
|
+
max_iter: int = DEFAULT_NEWTON_MAX_ITER,
|
|
276
|
+
) -> tuple[list[float], list[list[float]]]:
|
|
277
|
+
"""Backward Euler for coupled systems dy/dt = f(t, y).
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
f: right-hand side f(t, y) returning derivative vectors
|
|
281
|
+
t0: initial time
|
|
282
|
+
y0: initial state vector (non-empty)
|
|
283
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
284
|
+
dt: time step magnitude (direction follows sign of ``t_end - t0``)
|
|
285
|
+
jac: optional analytic Jacobian matrix df_i/dy_j(t, y)
|
|
286
|
+
tol: Newton convergence tolerance on the residual (∞-norm)
|
|
287
|
+
max_iter: Newton iteration budget per step
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
``(t_values, y_values)`` with ``y_values[i]`` the state at
|
|
291
|
+
``t_values[i]`` — the same shape as :func:`cds.diffeq.solve_system`.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
ValueError: on invalid parameters or Newton failure.
|
|
295
|
+
"""
|
|
296
|
+
return _integrate_system(
|
|
297
|
+
f,
|
|
298
|
+
t0,
|
|
299
|
+
y0,
|
|
300
|
+
t_end,
|
|
301
|
+
dt,
|
|
302
|
+
theta=1.0,
|
|
303
|
+
jac=jac,
|
|
304
|
+
tol=tol,
|
|
305
|
+
max_iter=max_iter,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def trapezoid_method_system(
|
|
310
|
+
f: SystemRHS,
|
|
311
|
+
t0: float,
|
|
312
|
+
y0: list[float],
|
|
313
|
+
t_end: float,
|
|
314
|
+
dt: float = RK45_DEFAULT_DT,
|
|
315
|
+
*,
|
|
316
|
+
jac: Jacobian | None = None,
|
|
317
|
+
tol: float = DEFAULT_NEWTON_TOL,
|
|
318
|
+
max_iter: int = DEFAULT_NEWTON_MAX_ITER,
|
|
319
|
+
) -> tuple[list[float], list[list[float]]]:
|
|
320
|
+
"""Trapezoidal (Crank–Nicolson) method for coupled systems dy/dt = f(t, y).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
f: right-hand side f(t, y) returning derivative vectors
|
|
324
|
+
t0: initial time
|
|
325
|
+
y0: initial state vector (non-empty)
|
|
326
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
327
|
+
dt: time step magnitude (direction follows sign of ``t_end - t0``)
|
|
328
|
+
jac: optional analytic Jacobian matrix df_i/dy_j(t, y)
|
|
329
|
+
tol: Newton convergence tolerance on the residual (∞-norm)
|
|
330
|
+
max_iter: Newton iteration budget per step
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
``(t_values, y_values)`` with ``y_values[i]`` the state at
|
|
334
|
+
``t_values[i]`` — the same shape as :func:`cds.diffeq.solve_system`.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ValueError: on invalid parameters or Newton failure.
|
|
338
|
+
"""
|
|
339
|
+
return _integrate_system(
|
|
340
|
+
f,
|
|
341
|
+
t0,
|
|
342
|
+
y0,
|
|
343
|
+
t_end,
|
|
344
|
+
dt,
|
|
345
|
+
theta=0.5,
|
|
346
|
+
jac=jac,
|
|
347
|
+
tol=tol,
|
|
348
|
+
max_iter=max_iter,
|
|
349
|
+
)
|
cds/diffeq/solvers.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Numerical ODE solvers.
|
|
2
|
+
|
|
3
|
+
References:
|
|
4
|
+
- Euler, L. (1768). Institutionum calculi integralis.
|
|
5
|
+
- Runge, C. (1895). Über die numerische Auflösung von Differentialgleichungen.
|
|
6
|
+
- Kutta, M.W. (1901). Beitrag zur näherungsweisen Integration totaler DGL.
|
|
7
|
+
- Butcher, J.C. Numerical Methods for Ordinary Differential Equations (3rd ed.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from cds.core._numeric import (
|
|
18
|
+
LOOP_EPSILON,
|
|
19
|
+
RK45_DEFAULT_ATOL,
|
|
20
|
+
RK45_DEFAULT_DT,
|
|
21
|
+
RK45_DEFAULT_RTOL,
|
|
22
|
+
RK45_STEP_GROW,
|
|
23
|
+
RK45_STEP_SAFETY,
|
|
24
|
+
RK45_STEP_SHRINK,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ODESolution:
|
|
30
|
+
"""Result of an ODE integration."""
|
|
31
|
+
|
|
32
|
+
t: list[float]
|
|
33
|
+
y: list[float]
|
|
34
|
+
method: str
|
|
35
|
+
steps: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def euler_method(
|
|
39
|
+
f: Callable[[float, float], float],
|
|
40
|
+
t0: float,
|
|
41
|
+
y0: float,
|
|
42
|
+
t_end: float,
|
|
43
|
+
dt: float = RK45_DEFAULT_DT,
|
|
44
|
+
) -> ODESolution:
|
|
45
|
+
"""Euler's method for dy/dt = f(t, y).
|
|
46
|
+
|
|
47
|
+
First-order explicit method. Local truncation error O(dt²),
|
|
48
|
+
global error O(dt). [Euler 1768]
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
f: right-hand side function f(t, y)
|
|
52
|
+
t0: initial time
|
|
53
|
+
y0: initial value y(t0)
|
|
54
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
55
|
+
dt: time step (always positive; direction follows the sign of ``t_end - t0``)
|
|
56
|
+
"""
|
|
57
|
+
# Direction of integration: +1 forward, -1 backward. ``copysign(1, 0)``
|
|
58
|
+
# is +1, so a zero-length span (``t_end == t0``) still skips the loop and
|
|
59
|
+
# returns just the initial condition. Without this, ``t_end < t0`` made the
|
|
60
|
+
# ``while t < t_end`` guard immediately False and the solver silently
|
|
61
|
+
# returned the initial value instead of integrating backward.
|
|
62
|
+
direction = math.copysign(1.0, t_end - t0)
|
|
63
|
+
t_vals = [t0]
|
|
64
|
+
y_vals = [y0]
|
|
65
|
+
t, y = t0, y0
|
|
66
|
+
steps = 0
|
|
67
|
+
|
|
68
|
+
while (t_end - t) * direction > LOOP_EPSILON:
|
|
69
|
+
h = direction * min(dt, abs(t_end - t))
|
|
70
|
+
y = y + h * f(t, y)
|
|
71
|
+
t = t + h
|
|
72
|
+
t_vals.append(t)
|
|
73
|
+
y_vals.append(y)
|
|
74
|
+
steps += 1
|
|
75
|
+
|
|
76
|
+
return ODESolution(t=t_vals, y=y_vals, method="euler", steps=steps)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rk4(
|
|
80
|
+
f: Callable[[float, float], float],
|
|
81
|
+
t0: float,
|
|
82
|
+
y0: float,
|
|
83
|
+
t_end: float,
|
|
84
|
+
dt: float = RK45_DEFAULT_DT,
|
|
85
|
+
) -> ODESolution:
|
|
86
|
+
"""Classical 4th-order Runge-Kutta method.
|
|
87
|
+
|
|
88
|
+
Local truncation error O(dt⁵), global error O(dt⁴). [Runge 1895, Kutta 1901]
|
|
89
|
+
|
|
90
|
+
The standard Butcher tableau:
|
|
91
|
+
0 |
|
|
92
|
+
1/2 | 1/2
|
|
93
|
+
1/2 | 0 1/2
|
|
94
|
+
1 | 0 0 1
|
|
95
|
+
----|----------------
|
|
96
|
+
| 1/6 1/3 1/3 1/6
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
f: right-hand side function f(t, y)
|
|
100
|
+
t0: initial time
|
|
101
|
+
y0: initial value y(t0)
|
|
102
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
103
|
+
dt: time step (always positive; direction follows the sign of ``t_end - t0``)
|
|
104
|
+
"""
|
|
105
|
+
# See ``euler_method`` for why direction is derived from the sign of
|
|
106
|
+
# ``t_end - t0`` rather than reading ``dt``'s sign: it keeps ``dt`` an
|
|
107
|
+
# unambiguous magnitude and makes backward integration actually progress.
|
|
108
|
+
direction = math.copysign(1.0, t_end - t0)
|
|
109
|
+
t_vals = [t0]
|
|
110
|
+
y_vals = [y0]
|
|
111
|
+
t, y = t0, y0
|
|
112
|
+
steps = 0
|
|
113
|
+
|
|
114
|
+
while (t_end - t) * direction > LOOP_EPSILON:
|
|
115
|
+
h = direction * min(dt, abs(t_end - t))
|
|
116
|
+
k1 = f(t, y)
|
|
117
|
+
k2 = f(t + h / 2, y + h * k1 / 2)
|
|
118
|
+
k3 = f(t + h / 2, y + h * k2 / 2)
|
|
119
|
+
k4 = f(t + h, y + h * k3)
|
|
120
|
+
y = y + (h / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
121
|
+
t = t + h
|
|
122
|
+
t_vals.append(t)
|
|
123
|
+
y_vals.append(y)
|
|
124
|
+
steps += 1
|
|
125
|
+
|
|
126
|
+
return ODESolution(t=t_vals, y=y_vals, method="rk4", steps=steps)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def midpoint_method(
|
|
130
|
+
f: Callable[[float, float], float],
|
|
131
|
+
t0: float,
|
|
132
|
+
y0: float,
|
|
133
|
+
t_end: float,
|
|
134
|
+
dt: float = RK45_DEFAULT_DT,
|
|
135
|
+
) -> ODESolution:
|
|
136
|
+
"""Explicit midpoint method (2nd-order Runge-Kutta).
|
|
137
|
+
|
|
138
|
+
Local truncation error O(dt³), global error O(dt²).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
f: right-hand side function f(t, y)
|
|
142
|
+
t0: initial time
|
|
143
|
+
y0: initial value y(t0)
|
|
144
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
145
|
+
dt: time step (always positive; direction follows the sign of ``t_end - t0``)
|
|
146
|
+
"""
|
|
147
|
+
direction = math.copysign(1.0, t_end - t0)
|
|
148
|
+
t_vals = [t0]
|
|
149
|
+
y_vals = [y0]
|
|
150
|
+
t, y = t0, y0
|
|
151
|
+
steps = 0
|
|
152
|
+
|
|
153
|
+
while (t_end - t) * direction > LOOP_EPSILON:
|
|
154
|
+
h = direction * min(dt, abs(t_end - t))
|
|
155
|
+
k1 = f(t, y)
|
|
156
|
+
k2 = f(t + h / 2, y + h * k1 / 2)
|
|
157
|
+
y = y + h * k2
|
|
158
|
+
t = t + h
|
|
159
|
+
t_vals.append(t)
|
|
160
|
+
y_vals.append(y)
|
|
161
|
+
steps += 1
|
|
162
|
+
|
|
163
|
+
return ODESolution(t=t_vals, y=y_vals, method="midpoint", steps=steps)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def rk45(
|
|
167
|
+
f: Callable[[float, float], float],
|
|
168
|
+
t0: float,
|
|
169
|
+
y0: float,
|
|
170
|
+
t_end: float,
|
|
171
|
+
dt: float = RK45_DEFAULT_DT,
|
|
172
|
+
atol: float = RK45_DEFAULT_ATOL,
|
|
173
|
+
rtol: float = RK45_DEFAULT_RTOL,
|
|
174
|
+
) -> ODESolution:
|
|
175
|
+
"""Dormand-Prince (RK45) adaptive step-size method.
|
|
176
|
+
|
|
177
|
+
Computes 4th and 5th order estimates to approximate local error
|
|
178
|
+
and adjust the step size automatically. [Dormand & Prince 1980]
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
f: right-hand side f(t, y)
|
|
182
|
+
t0: initial time
|
|
183
|
+
y0: initial value
|
|
184
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
185
|
+
dt: initial time step (always positive; direction follows the sign of ``t_end - t0``)
|
|
186
|
+
atol: absolute tolerance
|
|
187
|
+
rtol: relative tolerance
|
|
188
|
+
"""
|
|
189
|
+
# Dormand-Prince Butcher Tableau coefficients
|
|
190
|
+
a = [0, 1 / 5, 3 / 10, 4 / 5, 8 / 9, 1, 1]
|
|
191
|
+
b = [
|
|
192
|
+
[],
|
|
193
|
+
[1 / 5],
|
|
194
|
+
[3 / 40, 9 / 40],
|
|
195
|
+
[44 / 45, -56 / 15, 32 / 9],
|
|
196
|
+
[19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729],
|
|
197
|
+
[9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656],
|
|
198
|
+
[35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84],
|
|
199
|
+
]
|
|
200
|
+
c5 = [35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84, 0]
|
|
201
|
+
c4 = [5179 / 57600, 0, 7571 / 16695, 393 / 640, -92097 / 339200, 187 / 2100, 1 / 40]
|
|
202
|
+
|
|
203
|
+
# Direction of integration (+1 forward, -1 backward). The step size is
|
|
204
|
+
# tracked as an always-positive magnitude ``h_mag`` and applied as
|
|
205
|
+
# ``direction * h_mag`` so the min/max shrink/grow logic below keeps its
|
|
206
|
+
# forward semantics regardless of integration direction.
|
|
207
|
+
direction = math.copysign(1.0, t_end - t0)
|
|
208
|
+
t, y = t0, y0
|
|
209
|
+
t_vals = [t]
|
|
210
|
+
y_vals = [y]
|
|
211
|
+
h_mag = dt
|
|
212
|
+
steps = 0
|
|
213
|
+
|
|
214
|
+
# Absolute step-size floor, scaled to the integration span, below which no
|
|
215
|
+
# further progress can be made (a "machine precision floor"). Prevents the
|
|
216
|
+
# adaptive loop from spinning forever on stiff/diverging problems.
|
|
217
|
+
span = abs(t_end - t0) if t_end != t0 else 1.0
|
|
218
|
+
eps_floor = 16 * sys.float_info.epsilon * max(abs(t), span)
|
|
219
|
+
|
|
220
|
+
while (t_end - t) * direction > LOOP_EPSILON:
|
|
221
|
+
# Signed step for the RK stages and time advancement. Snap the last
|
|
222
|
+
# step to land exactly on t_end when the remaining span is smaller
|
|
223
|
+
# than the proposed magnitude (avoids overshooting the endpoint).
|
|
224
|
+
if abs(t_end - t) < h_mag:
|
|
225
|
+
h = t_end - t
|
|
226
|
+
h_mag = abs(h)
|
|
227
|
+
else:
|
|
228
|
+
h = direction * h_mag
|
|
229
|
+
|
|
230
|
+
k = [0.0] * 7
|
|
231
|
+
k[0] = f(t, y)
|
|
232
|
+
for i in range(1, 7):
|
|
233
|
+
y_next = y + h * sum(b[i][j] * k[j] for j in range(i))
|
|
234
|
+
k[i] = f(t + a[i] * h, y_next)
|
|
235
|
+
|
|
236
|
+
# Estimate 5th and 4th order solutions
|
|
237
|
+
y5 = y + h * sum(c5[i] * k[i] for i in range(7))
|
|
238
|
+
y4 = y + h * sum(c4[i] * k[i] for i in range(7))
|
|
239
|
+
|
|
240
|
+
# Local error estimate
|
|
241
|
+
error = abs(y5 - y4)
|
|
242
|
+
tolerance = atol + rtol * abs(y)
|
|
243
|
+
|
|
244
|
+
if error <= tolerance:
|
|
245
|
+
# Step accepted
|
|
246
|
+
t += h
|
|
247
|
+
y = y5
|
|
248
|
+
t_vals.append(t)
|
|
249
|
+
y_vals.append(y)
|
|
250
|
+
steps += 1
|
|
251
|
+
|
|
252
|
+
# Adjust step size (operates on the magnitude only, preserving direction)
|
|
253
|
+
if error > 0:
|
|
254
|
+
h_opt = h_mag * (tolerance / error) ** 0.2
|
|
255
|
+
h_mag = min(
|
|
256
|
+
max(RK45_STEP_SHRINK * h_mag, RK45_STEP_SAFETY * h_opt), RK45_STEP_GROW * h_mag
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
h_mag *= 10.0 # Error is zero, aggressively increase step up to max scale
|
|
260
|
+
|
|
261
|
+
# Precision floor to prevent infinite loop: either the step size has
|
|
262
|
+
# shrunk below the span-scaled epsilon floor, or it has become so small
|
|
263
|
+
# that adding it to t makes no progress (t + h == t).
|
|
264
|
+
if h_mag < eps_floor or t + direction * h_mag == t:
|
|
265
|
+
raise RuntimeError("Step size h reached machine precision floor.")
|
|
266
|
+
|
|
267
|
+
return ODESolution(t=t_vals, y=y_vals, method="rk45", steps=steps)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def solve_system(
|
|
271
|
+
f: Callable[[float, list[float]], list[float]],
|
|
272
|
+
t0: float,
|
|
273
|
+
y0: list[float],
|
|
274
|
+
t_end: float,
|
|
275
|
+
dt: float = RK45_DEFAULT_DT,
|
|
276
|
+
) -> tuple[list[float], list[list[float]]]:
|
|
277
|
+
"""RK4 for systems of ODEs: dy/dt = f(t, y) where y is a vector.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
f: right-hand side f(t, y) returning a list of derivatives
|
|
281
|
+
t0: initial time
|
|
282
|
+
y0: initial state vector
|
|
283
|
+
t_end: end time (may be less than ``t0`` for backward integration)
|
|
284
|
+
dt: time step (always positive; direction follows the sign of ``t_end - t0``)
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
(t_values, y_values) where y_values[i] is the state vector at t_values[i]
|
|
288
|
+
"""
|
|
289
|
+
direction = math.copysign(1.0, t_end - t0)
|
|
290
|
+
n = len(y0)
|
|
291
|
+
t_vals = [t0]
|
|
292
|
+
y_vals = [list(y0)]
|
|
293
|
+
t = t0
|
|
294
|
+
y = list(y0)
|
|
295
|
+
|
|
296
|
+
while (t_end - t) * direction > LOOP_EPSILON:
|
|
297
|
+
h = direction * min(dt, abs(t_end - t))
|
|
298
|
+
|
|
299
|
+
k1 = f(t, y)
|
|
300
|
+
y_tmp = [y[i] + h * k1[i] / 2 for i in range(n)]
|
|
301
|
+
k2 = f(t + h / 2, y_tmp)
|
|
302
|
+
y_tmp = [y[i] + h * k2[i] / 2 for i in range(n)]
|
|
303
|
+
k3 = f(t + h / 2, y_tmp)
|
|
304
|
+
y_tmp = [y[i] + h * k3[i] for i in range(n)]
|
|
305
|
+
k4 = f(t + h, y_tmp)
|
|
306
|
+
|
|
307
|
+
y = [y[i] + (h / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) for i in range(n)]
|
|
308
|
+
t = t + h
|
|
309
|
+
t_vals.append(t)
|
|
310
|
+
y_vals.append(list(y))
|
|
311
|
+
|
|
312
|
+
return t_vals, y_vals
|
cds/graph/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Graph theory algorithms — BFS, DFS, Dijkstra, Kruskal MST."""
|
|
2
|
+
|
|
3
|
+
from cds.graph.algorithms import (
|
|
4
|
+
Graph,
|
|
5
|
+
bellman_ford,
|
|
6
|
+
bfs,
|
|
7
|
+
connected_components,
|
|
8
|
+
degree,
|
|
9
|
+
dfs,
|
|
10
|
+
dijkstra,
|
|
11
|
+
floyd_warshall,
|
|
12
|
+
has_cycle,
|
|
13
|
+
kruskal_mst,
|
|
14
|
+
prim_mst,
|
|
15
|
+
topological_sort,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Graph",
|
|
20
|
+
"bfs",
|
|
21
|
+
"dfs",
|
|
22
|
+
"dijkstra",
|
|
23
|
+
"kruskal_mst",
|
|
24
|
+
"topological_sort",
|
|
25
|
+
"has_cycle",
|
|
26
|
+
"degree",
|
|
27
|
+
"connected_components",
|
|
28
|
+
"bellman_ford",
|
|
29
|
+
"floyd_warshall",
|
|
30
|
+
"prim_mst",
|
|
31
|
+
]
|