solve-nivp 0.1.3.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.
Files changed (79) hide show
  1. {solve_nivp-0.1.3.dev0/solve_nivp.egg-info → solve_nivp-0.2.0.dev1}/PKG-INFO +1 -1
  2. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/pyproject.toml +1 -1
  3. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/setup.py +1 -1
  4. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/ODESolver.py +102 -36
  5. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/__init__.py +44 -4
  6. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/adaptive_integrator.py +4 -0
  7. solve_nivp-0.2.0.dev1/solve_nivp/block_system.py +408 -0
  8. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/contact.py +2 -1
  9. solve_nivp-0.2.0.dev1/solve_nivp/desaxce_contact.py +1874 -0
  10. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/integrations.py +997 -50
  11. solve_nivp-0.2.0.dev1/solve_nivp/macklin_contact.py +276 -0
  12. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/ncp_contact.py +413 -13
  13. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/nonlinear_solvers.py +20 -4
  14. solve_nivp-0.2.0.dev1/solve_nivp/pcr.py +127 -0
  15. solve_nivp-0.2.0.dev1/solve_nivp/projected_radau_contact.py +1697 -0
  16. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/projections.py +11 -5
  17. solve_nivp-0.2.0.dev1/solve_nivp/rattle_contact.py +1750 -0
  18. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1/solve_nivp.egg-info}/PKG-INFO +1 -1
  19. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/SOURCES.txt +18 -0
  20. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_3d_and_anisotropic_contact.py +0 -113
  21. solve_nivp-0.2.0.dev1/tests/test_block_system.py +90 -0
  22. solve_nivp-0.2.0.dev1/tests/test_bouncing_ball_schur_comparison.py +150 -0
  23. solve_nivp-0.2.0.dev1/tests/test_desaxce_contact.py +453 -0
  24. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_dilatancy.py +0 -70
  25. solve_nivp-0.2.0.dev1/tests/test_general_moreau_projection.py +74 -0
  26. solve_nivp-0.2.0.dev1/tests/test_macklin_contact.py +49 -0
  27. solve_nivp-0.2.0.dev1/tests/test_ncp_schur_integration.py +275 -0
  28. solve_nivp-0.2.0.dev1/tests/test_pcr.py +92 -0
  29. solve_nivp-0.2.0.dev1/tests/test_projected_radau_contact.py +732 -0
  30. solve_nivp-0.2.0.dev1/tests/test_radau_iia.py +502 -0
  31. solve_nivp-0.2.0.dev1/tests/test_rattle_integrator.py +551 -0
  32. solve_nivp-0.2.0.dev1/tests/test_rattle_local_slider.py +248 -0
  33. solve_nivp-0.2.0.dev1/tests/test_t_eval.py +95 -0
  34. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/LICENSE +0 -0
  35. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/README.md +0 -0
  36. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/setup.cfg +0 -0
  37. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/ODESystem.py +0 -0
  38. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/_numba_accel.py +0 -0
  39. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/_selftest.py +0 -0
  40. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/alart_curnier_contact.py +0 -0
  41. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/__init__.py +0 -0
  42. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/callbacks.py +0 -0
  43. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/dependency.py +0 -0
  44. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp/rl/env.py +0 -0
  45. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/dependency_links.txt +0 -0
  46. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/entry_points.txt +0 -0
  47. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/requires.txt +0 -0
  48. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/solve_nivp.egg-info/top_level.txt +0 -0
  49. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/__init__.py +0 -0
  50. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_active_set_filter.py +0 -0
  51. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_alart_curnier_contact.py +0 -0
  52. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_algebraic_constraint_projection.py +0 -0
  53. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_anisotropic_soc_projection.py +0 -0
  54. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_auto_h0_and_dae_weighting.py +0 -0
  55. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_build_impulse_contact.py +0 -0
  56. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_c_extract_contact.py +0 -0
  57. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_composite_contact_projection.py +0 -0
  58. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_coulomb_projection.py +0 -0
  59. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_coulomb_projection_jacobian.py +0 -0
  60. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_error_predictive_rejection.py +0 -0
  61. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_globalization.py +0 -0
  62. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_identity_newton_linear_solver.py +0 -0
  63. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_import_and_api.py +0 -0
  64. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_integrators_added.py +0 -0
  65. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_jacobian_scaling.py +0 -0
  66. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_large_scale_solver_fixes.py +0 -0
  67. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_mu_scaled_soc_projection.py +0 -0
  68. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_ncp_contact.py +0 -0
  69. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_nl_recovery_cap.py +0 -0
  70. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_nonlinear_solvers_added.py +0 -0
  71. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_per_dof_tolerances.py +0 -0
  72. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_prestress_soc.py +0 -0
  73. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_prestressed_fault_dynamic_helper.py +0 -0
  74. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_projection_batch_equivalence.py +0 -0
  75. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_projections_added.py +0 -0
  76. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_sdirk2.py +0 -0
  77. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_sdirk2_soc_contact.py +0 -0
  78. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_sparse_semismooth_newton.py +0 -0
  79. {solve_nivp-0.1.3.dev0 → solve_nivp-0.2.0.dev1}/tests/test_threading_time_and_fk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solve_nivp
3
- Version: 0.1.3.dev0
3
+ Version: 0.2.0.dev1
4
4
  Summary: A Python toolkit for integrating nonsmooth dynamical systems
5
5
  Author: David Riley, Ioannis Stefanou
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "solve_nivp"
7
- version = "0.1.3.dev0"
7
+ version = "0.2.0.dev1"
8
8
  description = "A Python toolkit for integrating nonsmooth dynamical systems"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -50,7 +50,7 @@ class BuildSphinx(Command):
50
50
 
51
51
  setup(
52
52
  name="solve_nivp",
53
- version="0.1.3.dev0",
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
- self.t_values: List[float] = [self.t0]
67
- self.y_values: List[np.ndarray] = [self.system.current_y.copy()]
68
- self.h_values: List[float] = [h]
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
- # Thin output: only store every Nth step (always store last)
137
- _is_last = (t >= self.tf - 1e-14 * abs(self.tf))
138
- if _step_count % _thin == 0 or _is_last:
139
- self.t_values.append(t)
140
- self.y_values.append(y_new.copy())
141
- if self.store_fk:
142
- self.fk.append(fk_new.copy() if fk_new is not None else None)
143
- else:
144
- self.fk.append(None)
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
- _is_last = (t >= self.tf - 1e-14 * abs(self.tf))
166
- if _step_count % _thin == 0 or _is_last:
167
- self.t_values.append(t)
168
- self.y_values.append(y_new.copy())
169
- if self.store_fk:
170
- self.fk.append(fk_new.copy() if fk_new is not None else None)
171
- else:
172
- self.fk.append(None)
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
- _is_last = (t >= self.tf - 1e-14 * abs(self.tf))
191
- if _step_count % _thin == 0 or _is_last:
192
- self.t_values.append(t)
193
- self.y_values.append(y_new.copy())
194
- if self.store_fk:
195
- self.fk.append(fk_new.copy() if fk_new is not None else None)
196
- else:
197
- self.fk.append(None)
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()
@@ -62,7 +62,8 @@ from .projections import (
62
62
  CompositeContactProjection,
63
63
  )
64
64
  from .nonlinear_solvers import ImplicitEquationSolver, UMFPACK_AVAILABLE, PETSC_AVAILABLE
65
- from .integrations import BackwardEuler, Trapezoidal, ThetaMethod, CompositeMethod, EmbeddedBETR, SDIRK2 # , BDFMethod
65
+ from .integrations import BackwardEuler, BackwardEulerSchur, RadauIIASchur, Trapezoidal, ThetaMethod, CompositeMethod, EmbeddedBETR, SDIRK2, RadauIIA # , BDFMethod
66
+ from .block_system import SchurComplementSolver, BlockStructuredSystem
66
67
  from .ODESystem import ODESystem
67
68
  from .ODESolver import ODESolver
68
69
  from .contact import build_impulse_contact, ContactSystem
@@ -73,6 +74,24 @@ from .alart_curnier_contact import (
73
74
  from .ncp_contact import (
74
75
  build_ncp_contact,
75
76
  build_dynamic_ncp_contact,
77
+ build_ncp_contact_blocked,
78
+ )
79
+ from .desaxce_contact import (
80
+ build_dynamic_desaxce_contact,
81
+ build_dynamic_desaxce_projected_contact,
82
+ build_dynamic_desaxce_residual_contact,
83
+ )
84
+ from .rattle_contact import (
85
+ build_dynamic_rattle_contact,
86
+ solve_dynamic_rattle_contact,
87
+ build_rattle_system,
88
+ RattleMechanicalSystem,
89
+ RattleContactSpec,
90
+ RattleBilateralSpec,
91
+ RattleAlgebraicSpec,
92
+ RattleContactSystem,
93
+ RattleSolveResult,
94
+ RattleSolver,
76
95
  )
77
96
 
78
97
  # Curated public API
@@ -83,7 +102,7 @@ __all__ = [
83
102
  # Nonlinear solver
84
103
  'ImplicitEquationSolver',
85
104
  # Integrators
86
- 'BackwardEuler', 'Trapezoidal', 'ThetaMethod', 'CompositeMethod', 'EmbeddedBETR', 'SDIRK2',
105
+ 'BackwardEuler', 'BackwardEulerSchur', 'RadauIIASchur', 'Trapezoidal', 'ThetaMethod', 'CompositeMethod', 'EmbeddedBETR', 'SDIRK2', 'RadauIIA',
87
106
  # Projections
88
107
  'Projection',
89
108
  'CoulombProjection', 'SignProjection', 'IdentityProjection',
@@ -93,7 +112,22 @@ __all__ = [
93
112
  # Contact helpers
94
113
  'build_impulse_contact', 'build_alart_curnier_contact',
95
114
  'build_dynamic_alart_curnier_contact', 'build_ncp_contact',
96
- 'build_dynamic_ncp_contact', 'ContactSystem',
115
+ 'build_dynamic_ncp_contact', 'build_ncp_contact_blocked',
116
+ 'SchurComplementSolver', 'BlockStructuredSystem',
117
+ 'build_dynamic_desaxce_contact',
118
+ 'build_dynamic_desaxce_projected_contact',
119
+ 'build_dynamic_desaxce_residual_contact',
120
+ 'build_dynamic_rattle_contact',
121
+ 'solve_dynamic_rattle_contact',
122
+ 'build_rattle_system',
123
+ 'ContactSystem',
124
+ 'RattleMechanicalSystem',
125
+ 'RattleContactSpec',
126
+ 'RattleBilateralSpec',
127
+ 'RattleAlgebraicSpec',
128
+ 'RattleContactSystem',
129
+ 'RattleSolveResult',
130
+ 'RattleSolver',
97
131
  ]
98
132
 
99
133
 
@@ -126,6 +160,7 @@ def solve_nivp(
126
160
  abort_on_fixed_failure=True,
127
161
  jacobian_scaling=None,
128
162
  active_set_filter=False,
163
+ t_eval=None,
129
164
  ):
130
165
  """Integrate an ODE / simple index–1 DAE with optional nonsmooth projection.
131
166
 
@@ -140,7 +175,9 @@ def solve_nivp(
140
175
  Initial state.
141
176
  method : str, default 'composite'
142
177
  Time stepping scheme: ``'backward_euler'``, ``'trapezoidal'``, ``'theta'``,
143
- ``'composite'`` (TR-BE like second order), ``'embedded_betr'``, ``'sdirk2'``.
178
+ ``'composite'`` (TR-BE like second order), ``'embedded_betr'``, ``'sdirk2'``,
179
+ ``'radau_iia'`` (L-stable, stiffly accurate, order 3 or 5; stages controlled
180
+ via ``integrator_opts={'stages': 2}`` or ``{'stages': 3}``).
144
181
  projection : str or Projection or None, default 'identity'
145
182
  Name of projection to build: ``'coulomb'``, ``'sign'``, ``'identity'`` or
146
183
  ``None``. At the high-level API, ``None`` is promoted to the identity
@@ -348,6 +385,8 @@ def solve_nivp(
348
385
  integrator = EmbeddedBETR(solver=solver_instance, A=A)
349
386
  elif m == 'sdirk2':
350
387
  integrator = SDIRK2(solver=solver_instance, A=A, **_integrator_opts)
388
+ elif m in ('radau_iia', 'radau'):
389
+ integrator = RadauIIA(solver=solver_instance, A=A, **_integrator_opts)
351
390
  # elif m == 'bdf':
352
391
  # integrator = BDFMethod(solver=solver_instance, atol=atol, rtol=rtol)
353
392
  else:
@@ -466,6 +505,7 @@ def solve_nivp(
466
505
  store_fk=store_fk,
467
506
  gc_interval=gc_interval,
468
507
  abort_on_fixed_failure=abort_on_fixed_failure,
508
+ t_eval=t_eval,
469
509
  )
470
510
  return solver_obj.solve(return_attempts=return_attempts)
471
511
 
@@ -416,6 +416,10 @@ class AdaptiveStepping:
416
416
  # ------------------------------------------------------------------
417
417
 
418
418
  def _infer_method_order(self, integrator) -> int:
419
+ p_emb = getattr(integrator, 'embedded_order', None)
420
+ if isinstance(p_emb, (int, float)) and p_emb > 0:
421
+ return int(p_emb)
422
+
419
423
  p = getattr(integrator, 'order', None)
420
424
  if isinstance(p, (int, float)) and p > 0:
421
425
  return int(p)