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.
Files changed (97) hide show
  1. cds/__init__.py +102 -0
  2. cds/__main__.py +8 -0
  3. cds/_version.py +10 -0
  4. cds/cli/__init__.py +104 -0
  5. cds/cli/__main__.py +8 -0
  6. cds/cli/_handlers.py +405 -0
  7. cds/cli/_parser.py +156 -0
  8. cds/cli/_style.py +117 -0
  9. cds/core/__init__.py +5 -0
  10. cds/core/_numeric.py +92 -0
  11. cds/core/models.py +166 -0
  12. cds/data_analysis/__init__.py +30 -0
  13. cds/data_analysis/dataset.py +122 -0
  14. cds/data_analysis/loader.py +75 -0
  15. cds/data_analysis/pandas_io.py +108 -0
  16. cds/data_analysis/transform.py +36 -0
  17. cds/data_analysis/viz.py +88 -0
  18. cds/diffeq/__init__.py +29 -0
  19. cds/diffeq/_implicit.py +349 -0
  20. cds/diffeq/solvers.py +312 -0
  21. cds/graph/__init__.py +31 -0
  22. cds/graph/algorithms.py +399 -0
  23. cds/hypothesis/__init__.py +48 -0
  24. cds/hypothesis/evaluator.py +369 -0
  25. cds/hypothesis/generator.py +275 -0
  26. cds/knowledge/__init__.py +41 -0
  27. cds/knowledge/graph.py +489 -0
  28. cds/knowledge/notes.py +231 -0
  29. cds/knowledge/retrieval.py +167 -0
  30. cds/math_utils/__init__.py +41 -0
  31. cds/math_utils/calculus.py +40 -0
  32. cds/math_utils/linalg.py +461 -0
  33. cds/math_utils/special.py +167 -0
  34. cds/ml/__init__.py +25 -0
  35. cds/ml/clustering.py +190 -0
  36. cds/ml/decomposition.py +172 -0
  37. cds/ml/linear_models.py +225 -0
  38. cds/ml/neighbors.py +162 -0
  39. cds/ml/neural.py +204 -0
  40. cds/ml/preprocessing.py +131 -0
  41. cds/ml/tree.py +202 -0
  42. cds/modeling/__init__.py +40 -0
  43. cds/modeling/_base.py +210 -0
  44. cds/modeling/_nodes.py +526 -0
  45. cds/modeling/expression.py +81 -0
  46. cds/modeling/model.py +134 -0
  47. cds/modeling/solver.py +176 -0
  48. cds/montecarlo/__init__.py +21 -0
  49. cds/montecarlo/methods.py +272 -0
  50. cds/nlp/__init__.py +170 -0
  51. cds/nlp/attention.py +285 -0
  52. cds/nlp/autograd/__init__.py +70 -0
  53. cds/nlp/autograd/_grad.py +120 -0
  54. cds/nlp/autograd/ops.py +133 -0
  55. cds/nlp/autograd/tensor.py +298 -0
  56. cds/nlp/bpe.py +449 -0
  57. cds/nlp/data.py +70 -0
  58. cds/nlp/embed.py +190 -0
  59. cds/nlp/layers.py +259 -0
  60. cds/nlp/model.py +456 -0
  61. cds/nlp/optim.py +163 -0
  62. cds/nlp/training.py +167 -0
  63. cds/nlp/viz.py +254 -0
  64. cds/numerical_integration/__init__.py +31 -0
  65. cds/numerical_integration/quadrature.py +480 -0
  66. cds/optimization/__init__.py +18 -0
  67. cds/optimization/_metaheuristics.py +287 -0
  68. cds/optimization/minimize.py +425 -0
  69. cds/plot/__init__.py +49 -0
  70. cds/plot/_backend.py +32 -0
  71. cds/plot/charts.py +462 -0
  72. cds/probability/__init__.py +55 -0
  73. cds/probability/_advanced.py +283 -0
  74. cds/probability/distributions.py +236 -0
  75. cds/py.typed +0 -0
  76. cds/quantum/__init__.py +51 -0
  77. cds/quantum/circuit.py +92 -0
  78. cds/quantum/multi_qubit.py +255 -0
  79. cds/quantum/simulator.py +38 -0
  80. cds/scientific/__init__.py +34 -0
  81. cds/scientific/constants.py +35 -0
  82. cds/scientific/formulas.py +116 -0
  83. cds/signals/__init__.py +43 -0
  84. cds/signals/filters.py +421 -0
  85. cds/signals/processing.py +224 -0
  86. cds/stats/__init__.py +83 -0
  87. cds/stats/_distributions.py +102 -0
  88. cds/stats/descriptive.py +172 -0
  89. cds/stats/hypothesis_tests.py +403 -0
  90. cds/stats/nonparametric.py +131 -0
  91. cds/stats/regression.py +51 -0
  92. cds/stats/time_series.py +408 -0
  93. scientific_computing_system-1.6.2.dist-info/METADATA +741 -0
  94. scientific_computing_system-1.6.2.dist-info/RECORD +97 -0
  95. scientific_computing_system-1.6.2.dist-info/WHEEL +4 -0
  96. scientific_computing_system-1.6.2.dist-info/entry_points.txt +2 -0
  97. scientific_computing_system-1.6.2.dist-info/licenses/LICENSE +21 -0
@@ -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
+ ]