solve-nivp 0.2.0.dev0__tar.gz → 0.2.0.dev1__tar.gz
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.
- {solve_nivp-0.2.0.dev0/solve_nivp.egg-info → solve_nivp-0.2.0.dev1}/PKG-INFO +1 -1
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/pyproject.toml +1 -1
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/setup.py +1 -1
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/ODESolver.py +102 -36
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/__init__.py +2 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1/solve_nivp.egg-info}/PKG-INFO +1 -1
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/SOURCES.txt +1 -0
- solve_nivp-0.2.0.dev1/tests/test_t_eval.py +95 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/LICENSE +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/README.md +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/setup.cfg +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/ODESystem.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/_numba_accel.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/_selftest.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/adaptive_integrator.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/alart_curnier_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/block_system.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/desaxce_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/integrations.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/macklin_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/ncp_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/nonlinear_solvers.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/pcr.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/projected_radau_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/projections.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rattle_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/__init__.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/callbacks.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/dependency.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/env.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/dependency_links.txt +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/entry_points.txt +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/requires.txt +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/top_level.txt +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/__init__.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_3d_and_anisotropic_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_active_set_filter.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_alart_curnier_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_algebraic_constraint_projection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_anisotropic_soc_projection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_auto_h0_and_dae_weighting.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_block_system.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_bouncing_ball_schur_comparison.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_build_impulse_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_c_extract_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_composite_contact_projection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_coulomb_projection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_coulomb_projection_jacobian.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_desaxce_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_dilatancy.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_error_predictive_rejection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_general_moreau_projection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_globalization.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_identity_newton_linear_solver.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_import_and_api.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_integrators_added.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_jacobian_scaling.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_large_scale_solver_fixes.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_macklin_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_mu_scaled_soc_projection.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_ncp_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_ncp_schur_integration.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_nl_recovery_cap.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_nonlinear_solvers_added.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_pcr.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_per_dof_tolerances.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_prestress_soc.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_prestressed_fault_dynamic_helper.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_projected_radau_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_projection_batch_equivalence.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_projections_added.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_radau_iia.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_rattle_integrator.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_rattle_local_slider.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_sdirk2.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_sdirk2_soc_contact.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_sparse_semismooth_newton.py +0 -0
- {solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_threading_time_and_fk.py +0 -0
|
@@ -50,7 +50,7 @@ class BuildSphinx(Command):
|
|
|
50
50
|
|
|
51
51
|
setup(
|
|
52
52
|
name="solve_nivp",
|
|
53
|
-
version="0.2.0.
|
|
53
|
+
version="0.2.0.dev1",
|
|
54
54
|
packages=find_packages(), # automatically discovers packages
|
|
55
55
|
description="A solver package for implicit ODEs and projection-based solvers",
|
|
56
56
|
cmdclass={'build_sphinx': BuildSphinx},
|
|
@@ -40,10 +40,11 @@ class ODESolver:
|
|
|
40
40
|
store_fk: bool = True,
|
|
41
41
|
gc_interval: int = 0,
|
|
42
42
|
abort_on_fixed_failure: bool = True,
|
|
43
|
+
t_eval: Optional[np.ndarray] = None,
|
|
43
44
|
):
|
|
44
45
|
"""
|
|
45
46
|
Initialize the ODESolver.
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
Parameters:
|
|
48
49
|
system: The ODE system to be integrated.
|
|
49
50
|
t_span: A tuple (t0, tf) specifying the start and end times.
|
|
@@ -59,15 +60,57 @@ class ODESolver:
|
|
|
59
60
|
nonlinear failure instead of marching forward with the failed
|
|
60
61
|
state. The failed attempt is still recorded in
|
|
61
62
|
``error_estimates``.
|
|
63
|
+
t_eval: Strictly increasing array of times in ``[t0, tf]`` that
|
|
64
|
+
must be evaluated. When provided, the time loop clips the
|
|
65
|
+
step so it lands exactly on each ``t_eval`` entry, and the
|
|
66
|
+
returned histories contain only those entries (matches the
|
|
67
|
+
scipy ``solve_ivp`` convention). Pass ``None`` to keep the
|
|
68
|
+
adaptive/fixed grid as the output.
|
|
62
69
|
"""
|
|
63
70
|
self.system = system
|
|
64
71
|
self.t0, self.tf = t_span
|
|
65
72
|
self.h_initial = h
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
self.
|
|
73
|
+
|
|
74
|
+
# ---- t_eval validation and bookkeeping ----
|
|
75
|
+
self._use_t_eval: bool = False
|
|
76
|
+
self._t_eval: Optional[np.ndarray] = None
|
|
77
|
+
self._t_eval_idx: int = 0
|
|
78
|
+
if t_eval is not None:
|
|
79
|
+
t_eval_arr = np.asarray(t_eval, dtype=float).reshape(-1)
|
|
80
|
+
if t_eval_arr.size > 0:
|
|
81
|
+
if np.any(np.diff(t_eval_arr) <= 0.0):
|
|
82
|
+
raise ValueError("t_eval must be strictly increasing")
|
|
83
|
+
tf_eps = 1.0e-12 * max(abs(self.t0), abs(self.tf), 1.0)
|
|
84
|
+
if (t_eval_arr[0] < self.t0 - tf_eps
|
|
85
|
+
or t_eval_arr[-1] > self.tf + tf_eps):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"t_eval out of range [{self.t0}, {self.tf}]: "
|
|
88
|
+
f"got [{t_eval_arr[0]}, {t_eval_arr[-1]}]"
|
|
89
|
+
)
|
|
90
|
+
self._t_eval = np.clip(t_eval_arr, self.t0, self.tf)
|
|
91
|
+
self._use_t_eval = True
|
|
92
|
+
|
|
93
|
+
if self._use_t_eval:
|
|
94
|
+
self.t_values: List[float] = []
|
|
95
|
+
self.y_values: List[np.ndarray] = []
|
|
96
|
+
self.h_values: List[float] = []
|
|
97
|
+
self.fk: List[Any] = []
|
|
98
|
+
# If t_eval[0] coincides with t0, record the initial state.
|
|
99
|
+
te0 = float(self._t_eval[0])
|
|
100
|
+
t0_eps = 1.0e-12 * max(abs(self.t0), 1.0)
|
|
101
|
+
if abs(te0 - self.t0) <= t0_eps:
|
|
102
|
+
self.t_values.append(te0)
|
|
103
|
+
self.y_values.append(self.system.current_y.copy())
|
|
104
|
+
self.h_values.append(h)
|
|
105
|
+
self.fk.append(None)
|
|
106
|
+
self._t_eval_idx = 1
|
|
107
|
+
else:
|
|
108
|
+
self.t_values = [self.t0]
|
|
109
|
+
self.y_values = [self.system.current_y.copy()]
|
|
110
|
+
self.h_values = [h]
|
|
111
|
+
self.fk = []
|
|
112
|
+
|
|
69
113
|
self.error_estimates: List[Tuple[Any, bool, int]] = []
|
|
70
|
-
self.fk: List[Any] = []
|
|
71
114
|
# Memory-saving options
|
|
72
115
|
self.thin_output = max(1, int(thin_output))
|
|
73
116
|
self.store_fk = bool(store_fk)
|
|
@@ -123,9 +166,38 @@ class ODESolver:
|
|
|
123
166
|
# fail, falsely reporting "reached minimum step size".
|
|
124
167
|
_tf_eps = 4.0 * np.finfo(float).eps * max(abs(self.t0), abs(self.tf), 1.0)
|
|
125
168
|
|
|
169
|
+
# Helper to record a stored sample (handles store_fk + fk fallback).
|
|
170
|
+
def _record(t_store, y, fk_val, h_taken, errinfo):
|
|
171
|
+
self.t_values.append(t_store)
|
|
172
|
+
self.y_values.append(y.copy())
|
|
173
|
+
if self.store_fk:
|
|
174
|
+
self.fk.append(fk_val.copy() if fk_val is not None else None)
|
|
175
|
+
else:
|
|
176
|
+
self.fk.append(None)
|
|
177
|
+
self.h_values.append(h_taken)
|
|
178
|
+
self.error_estimates.append(errinfo)
|
|
179
|
+
|
|
180
|
+
# Helper to drain any t_eval points that the latest accepted step
|
|
181
|
+
# advanced over (catches both exact landings and tiny float drift).
|
|
182
|
+
def _drain_t_eval(t_now, y_now, fk_val, h_taken, errinfo):
|
|
183
|
+
if not self._use_t_eval:
|
|
184
|
+
return
|
|
185
|
+
te_eps = 1.0e-9 * max(abs(t_now), 1.0)
|
|
186
|
+
while (self._t_eval_idx < len(self._t_eval)
|
|
187
|
+
and self._t_eval[self._t_eval_idx] <= t_now + te_eps):
|
|
188
|
+
te = float(self._t_eval[self._t_eval_idx])
|
|
189
|
+
_record(te, y_now, fk_val, h_taken, errinfo)
|
|
190
|
+
self._t_eval_idx += 1
|
|
191
|
+
|
|
126
192
|
while self.tf - t > _tf_eps:
|
|
127
193
|
# Ensure we do not overshoot the final time.
|
|
128
194
|
h_step = min(h, self.tf - t)
|
|
195
|
+
# When t_eval is given, also clip the step so it lands exactly
|
|
196
|
+
# on the next required output time.
|
|
197
|
+
if self._use_t_eval and self._t_eval_idx < len(self._t_eval):
|
|
198
|
+
next_te = float(self._t_eval[self._t_eval_idx])
|
|
199
|
+
if next_te > t:
|
|
200
|
+
h_step = min(h_step, next_te - t)
|
|
129
201
|
if self.system.adaptive:
|
|
130
202
|
# Adaptive stepping returns:
|
|
131
203
|
# (y_new, fk_new, h_new, E, success, solver_error, iterations)
|
|
@@ -133,17 +205,15 @@ class ODESolver:
|
|
|
133
205
|
if success:
|
|
134
206
|
t += h_step
|
|
135
207
|
_step_count += 1
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
self.h_values.append(h_step)
|
|
146
|
-
self.error_estimates.append((solver_error, success, iterations))
|
|
208
|
+
if self._use_t_eval:
|
|
209
|
+
_drain_t_eval(t, y_new, fk_new, h_step,
|
|
210
|
+
(solver_error, success, iterations))
|
|
211
|
+
else:
|
|
212
|
+
# Thin output: only store every Nth step (always store last)
|
|
213
|
+
_is_last = (t >= self.tf - 1e-14 * abs(self.tf))
|
|
214
|
+
if _step_count % _thin == 0 or _is_last:
|
|
215
|
+
_record(t, y_new, fk_new, h_step,
|
|
216
|
+
(solver_error, success, iterations))
|
|
147
217
|
self.system.current_y = y_new
|
|
148
218
|
h = h_new # Update step size for next iteration.
|
|
149
219
|
# Periodic garbage collection for large problems
|
|
@@ -162,16 +232,14 @@ class ODESolver:
|
|
|
162
232
|
if success:
|
|
163
233
|
t += h_step
|
|
164
234
|
_step_count += 1
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self.h_values.append(h_step)
|
|
174
|
-
self.error_estimates.append((solver_error, success, iterations))
|
|
235
|
+
if self._use_t_eval:
|
|
236
|
+
_drain_t_eval(t, y_new, fk_new, h_step,
|
|
237
|
+
(solver_error, success, iterations))
|
|
238
|
+
else:
|
|
239
|
+
_is_last = (t >= self.tf - 1e-14 * abs(self.tf))
|
|
240
|
+
if _step_count % _thin == 0 or _is_last:
|
|
241
|
+
_record(t, y_new, fk_new, h_step,
|
|
242
|
+
(solver_error, success, iterations))
|
|
175
243
|
self.system.current_y = y_new
|
|
176
244
|
if _gc_iv > 0 and _step_count % _gc_iv == 0:
|
|
177
245
|
gc.collect()
|
|
@@ -187,16 +255,14 @@ class ODESolver:
|
|
|
187
255
|
break
|
|
188
256
|
t += h_step
|
|
189
257
|
_step_count += 1
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
self.h_values.append(h_step)
|
|
199
|
-
self.error_estimates.append((solver_error, success, iterations))
|
|
258
|
+
if self._use_t_eval:
|
|
259
|
+
_drain_t_eval(t, y_new, fk_new, h_step,
|
|
260
|
+
(solver_error, success, iterations))
|
|
261
|
+
else:
|
|
262
|
+
_is_last = (t >= self.tf - 1e-14 * abs(self.tf))
|
|
263
|
+
if _step_count % _thin == 0 or _is_last:
|
|
264
|
+
_record(t, y_new, fk_new, h_step,
|
|
265
|
+
(solver_error, success, iterations))
|
|
200
266
|
self.system.current_y = y_new
|
|
201
267
|
if _gc_iv > 0 and _step_count % _gc_iv == 0:
|
|
202
268
|
gc.collect()
|
|
@@ -160,6 +160,7 @@ def solve_nivp(
|
|
|
160
160
|
abort_on_fixed_failure=True,
|
|
161
161
|
jacobian_scaling=None,
|
|
162
162
|
active_set_filter=False,
|
|
163
|
+
t_eval=None,
|
|
163
164
|
):
|
|
164
165
|
"""Integrate an ODE / simple index–1 DAE with optional nonsmooth projection.
|
|
165
166
|
|
|
@@ -504,6 +505,7 @@ def solve_nivp(
|
|
|
504
505
|
store_fk=store_fk,
|
|
505
506
|
gc_interval=gc_interval,
|
|
506
507
|
abort_on_fixed_failure=abort_on_fixed_failure,
|
|
508
|
+
t_eval=t_eval,
|
|
507
509
|
)
|
|
508
510
|
return solver_obj.solve(return_attempts=return_attempts)
|
|
509
511
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Regressions for the ``t_eval`` output-time feature on solve_nivp."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import solve_nivp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _decay_rhs(t, y):
|
|
10
|
+
return -y
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _harmonic_rhs(t, y):
|
|
14
|
+
return np.array([y[1], -y[0]])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_t_eval_lands_exactly_on_requested_points_adaptive():
|
|
18
|
+
"""Output times must be element-wise equal to the requested t_eval."""
|
|
19
|
+
t_eval = np.array([0.0, 0.1, 0.4, 0.9, 1.5, 2.0])
|
|
20
|
+
t, y, h, _, _ = solve_nivp.solve_nivp(
|
|
21
|
+
fun=_decay_rhs, t_span=(0.0, 2.0), y0=np.array([1.0]),
|
|
22
|
+
method='backward_euler', solver='semismooth_newton',
|
|
23
|
+
adaptive=True, h0=0.05, atol=1e-10, rtol=1e-8,
|
|
24
|
+
t_eval=t_eval,
|
|
25
|
+
)
|
|
26
|
+
np.testing.assert_array_equal(t, t_eval)
|
|
27
|
+
assert y.shape == (t_eval.size, 1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_t_eval_accuracy_matches_exact_solution():
|
|
31
|
+
"""Adaptive solve at t_eval points must converge to the exact solution."""
|
|
32
|
+
t_eval = np.array([0.0, 0.5, 1.0, 1.5, 2.0])
|
|
33
|
+
_, y, *_ = solve_nivp.solve_nivp(
|
|
34
|
+
fun=_decay_rhs, t_span=(0.0, 2.0), y0=np.array([1.0]),
|
|
35
|
+
method='backward_euler', solver='semismooth_newton',
|
|
36
|
+
adaptive=True, h0=0.01, atol=1.0e-8, rtol=1.0e-6,
|
|
37
|
+
t_eval=t_eval,
|
|
38
|
+
)
|
|
39
|
+
np.testing.assert_allclose(y.flatten(), np.exp(-t_eval), rtol=5.0e-3)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_t_eval_works_with_radau_iia():
|
|
43
|
+
"""t_eval must work with the higher-order RadauIIA integrator."""
|
|
44
|
+
t_eval = np.linspace(0.0, np.pi, 7)
|
|
45
|
+
y0 = np.array([1.0, 0.0])
|
|
46
|
+
_, y, *_ = solve_nivp.solve_nivp(
|
|
47
|
+
fun=_harmonic_rhs, t_span=(0.0, np.pi), y0=y0,
|
|
48
|
+
method='radau_iia', solver='semismooth_newton',
|
|
49
|
+
integrator_opts=dict(stages=2),
|
|
50
|
+
adaptive=True, h0=0.05, atol=1e-10, rtol=1e-8,
|
|
51
|
+
t_eval=t_eval,
|
|
52
|
+
)
|
|
53
|
+
np.testing.assert_allclose(y[:, 0], np.cos(t_eval), atol=1.0e-4)
|
|
54
|
+
np.testing.assert_allclose(y[:, 1], -np.sin(t_eval), atol=1.0e-4)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_t_eval_works_in_fixed_step_mode():
|
|
58
|
+
"""Fixed-step integration also clips to t_eval points."""
|
|
59
|
+
t_eval = np.array([0.0, 0.3, 0.7, 1.0])
|
|
60
|
+
t, y, *_ = solve_nivp.solve_nivp(
|
|
61
|
+
fun=_decay_rhs, t_span=(0.0, 1.0), y0=np.array([1.0]),
|
|
62
|
+
method='backward_euler', solver='semismooth_newton',
|
|
63
|
+
adaptive=False, h0=0.01,
|
|
64
|
+
t_eval=t_eval,
|
|
65
|
+
)
|
|
66
|
+
np.testing.assert_array_equal(t, t_eval)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_t_eval_skipping_t0_returns_only_listed_points():
|
|
70
|
+
"""When t_eval omits t0, the initial state is not included in output."""
|
|
71
|
+
t_eval = np.array([0.5, 1.0, 1.5])
|
|
72
|
+
t, y, *_ = solve_nivp.solve_nivp(
|
|
73
|
+
fun=_decay_rhs, t_span=(0.0, 1.5), y0=np.array([1.0]),
|
|
74
|
+
method='backward_euler', solver='semismooth_newton',
|
|
75
|
+
adaptive=True, h0=0.05,
|
|
76
|
+
t_eval=t_eval,
|
|
77
|
+
)
|
|
78
|
+
np.testing.assert_array_equal(t, t_eval)
|
|
79
|
+
assert y.shape == (3, 1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_t_eval_validates_monotone():
|
|
83
|
+
with pytest.raises(ValueError, match='strictly increasing'):
|
|
84
|
+
solve_nivp.solve_nivp(
|
|
85
|
+
fun=_decay_rhs, t_span=(0.0, 1.0), y0=np.array([1.0]),
|
|
86
|
+
t_eval=np.array([0.0, 0.5, 0.4, 1.0]),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_t_eval_validates_range():
|
|
91
|
+
with pytest.raises(ValueError, match='out of range'):
|
|
92
|
+
solve_nivp.solve_nivp(
|
|
93
|
+
fun=_decay_rhs, t_span=(0.0, 1.0), y0=np.array([1.0]),
|
|
94
|
+
t_eval=np.array([0.0, 0.5, 1.5]),
|
|
95
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_algebraic_constraint_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_bouncing_ball_schur_comparison.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{solve_nivp-0.2.0.dev0 → solve_nivp-0.2.0.dev1}/tests/test_prestressed_fault_dynamic_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|