pysindy icon indicating copy to clipboard operation
pysindy copied to clipboard

[BUG] Catch and re-raise simulation errors when ODE blows up

Open Jacob-Stevens-Haas opened this issue 2 years ago • 9 comments

When the discovered equation blows up numerically, scipy.integrate.solve_ivp emits a ValueError. I would like to catch this error, but ValueError is also raised in SINDy.simulate() because it's a pretty general exception. I don't want to catch those ValueErrors

I believe SINDy.simulate() should catch the ValueError and re-raise it as another class, e.g.

SimulationError = type("SimulationError", (UserWarning,), {})

Also - usually this failure is preceded by a long period of the integrator struggling through LSODA warnings (which are just UserWarning). UserWarning is pretty generic too; should we a add an arg to simulate() that raises the LSODA warnings to Exceptions?

Reproducing code example:

import warnings

import pysindy as ps
import numpy as np
import matplotlib.pyplot as plt

from scipy.integrate import solve_ivp

seed = 20
rng = np.random.default_rng(seed)
t = np.linspace(0,1,100)
x_true = np.stack((np.sin(t), np.cos(t)), axis=-1)
x_train = rng.normal(size=x_true.shape)

lib = ps.PolynomialLibrary(degree=3)
opt = ps.STLSQ(threshold=.001, alpha=0)
model = ps.SINDy(feature_library=lib, optimizer=opt)
model.fit(x=x_train, t=t)

x_sim = model.simulate((.5, .5), t)

plt.plot(x_sim)

Error message:

File [~/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py:898](https://file+.vscode-resource.vscode-cdn.net/home/xenophon/github/gen-experiments/~/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py:898), in SINDy.simulate(self, x0, t, u, integrator, stop_condition, interpolator, integrator_kws, interpolator_kws)
    [894](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=893) # Need to hard-code below, because odeint and solve_ivp
    [895](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=894) # have different syntax and integration options.
    [896](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=895) if integrator == "solve_ivp":
    [897](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=896)     return (
--> [898](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=897)         (solve_ivp(rhs, (t[0], t[-1]), x0, t_eval=t, **integrator_kws)).y
    [899](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=898)     ).T
    [900](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=899) elif integrator == "odeint":
    [901](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/pysindy/pysindy.py?line=900)     if integrator_kws.get("method") == "LSODA":

File [~/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py:591](https://file+.vscode-resource.vscode-cdn.net/home/xenophon/github/gen-experiments/~/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py:591), in solve_ivp(fun, t_span, y0, method, t_eval, dense_output, events, vectorized, args, **options)
    [589](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py?line=588) status = None
    [590](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py?line=589) while status is None:
--> [591](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py?line=590)     message = solver.step()
    [593](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py?line=592)     if solver.status == 'finished':
    [594](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py?line=593)         status = 0

File [~/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py:181](https://file+.vscode-resource.vscode-cdn.net/home/xenophon/github/gen-experiments/~/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py:181), in OdeSolver.step(self)
    [179](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py?line=178) else:
    [180](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py?line=179)     t = self.t
--> [181](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py?line=180)     success, message = self._step_impl()
    [183](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py?line=182)     if not success:
    [184](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py?line=183)         self.status = 'failed'

File [~/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py:152](https://file+.vscode-resource.vscode-cdn.net/home/xenophon/github/gen-experiments/~/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py:152), in LSODA._step_impl(self)
    [150](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=149) itask = integrator.call_args[2]
    [151](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=150) integrator.call_args[2] = 5
--> [152](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=151) solver._y, solver.t = integrator.run(
    [153](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=152)     solver.f, solver.jac or (lambda: None), solver._y, solver.t,
    [154](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=153)     self.t_bound, solver.f_params, solver.jac_params)
    [155](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=154) integrator.call_args[2] = itask
    [157](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/scipy/integrate/_ivp/lsoda.py?line=156) if solver.successful():
...
    [159](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/sklearn/utils/validation.py?line=158)         "#estimators-that-handle-nan-values"
    [160](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/sklearn/utils/validation.py?line=159)     )
--> [161](file:///home/xenophon/github/gen-experiments/env/lib/python3.10/site-packages/sklearn/utils/validation.py?line=160) raise ValueError(msg_err)

ValueError: Input contains infinity or a value too large for dtype('float64').

Context:

1.7.6.dev55+g5f224ab https://github.com/dynamicslab/pysindy/blob/5f224ab35508dbd0e8ed564a3a5d522b6dc5f19a/pysindy/pysindy.py#L896

Jacob-Stevens-Haas avatar Jun 21 '23 22:06 Jacob-Stevens-Haas

I think it's a good idea to catch the ValueError in simulate and raise a less generic error. As for the LSODA warnings, I found in past experience that it's actually quite difficult to suppress or catch all of them. This is because they are actually printed to the system stderr from a FORTRAN routine that is invoked rather than being raised as warnings that python can actually notice. You can do some elaborate hacks to get around it: https://stackoverflow.com/questions/31681946/disable-warnings-originating-from-scipy. But I would advise just leaving the LSODA warnings as they are -- its only worth the effort if you really need to get rid of them.

znicolaou avatar Jun 23 '23 20:06 znicolaou

Yes Zach - I also found the scipy issue on that topic. I don't want to silence them - I want to add a warning filter to elevate them to an error, so that that trial quits. Generally LSODA warnings mean that the trial failed, but that the integrator will churn a long time. I just want to catch that exception and move to the next trial.

Jacob-Stevens-Haas avatar Jun 23 '23 22:06 Jacob-Stevens-Haas

Yes, its a big headache to catch the warnings...my understanding is that you'll have to get a good understanding of the stdout and stderr streams that exist in the process. I think that jupyter and python create some additional stdout and stderr streams for the notebook, and you can use dup2 to redirect these streams. (All this may be architecture/bash dependent) You can probably redirect the system stderr stream and raise a python error/warning if LSODA prints to it somehow, but yeah not easy

znicolaou avatar Jun 23 '23 22:06 znicolaou

ps: this is really all bash nonsense that python wraps around. I suggest trying to understand bash from resources like this: https://man7.org/linux/man-pages/man2/dup.2.html

znicolaou avatar Jun 23 '23 22:06 znicolaou

Dealing with the warning message isn't really where I'm going. My point is that even though the fortran code prints the warning, scipy still detects it and raises a UserWarning quietly. A warnings filter that ignores the warning can't prevent the fortran print-out, but a filter that elevates the UserWarning to an Exception will kill the integration (after fortran prints the warning once).

If the exception is raised, my experimental code can catch it and then move to the next trial.

Jacob-Stevens-Haas avatar Jun 23 '23 22:06 Jacob-Stevens-Haas

Ah, gotcha, I didn't know that scipy raised the UserWarning. Well just note that the LSODA warnings are not always an indication of instability or a problem, so sometimes they don't mean that the integration has failed...it is very problem dependent whether they will appear or not. I've seen times where it is just a stiff event that needs care to integrate past and a warning pops up.

znicolaou avatar Jun 23 '23 22:06 znicolaou

One extra note: sometimes I want to show visually that the integrator blew up. solve_ivp doesn't output things if it fails/blows up, but odeint does. This is really nice for e.g. showing that the trapping SINDy produces stable trajectories but regular SINDy does not. So I often use odeint to compare these optimizers and show divergent integration.

akaptano avatar Jun 25 '23 17:06 akaptano

LSODA warnings are not always an indication of instability or a problem, so sometimes they don't mean that the integration has failed...it is very problem dependent whether they will appear or not. I've seen times where it is just a stiff event that needs care to integrate past and a warning pops up.

@znicolaou that's a good point! It would be impossible (at least in single-threaded python) to catching and convert the warning, but continuing the integration unless user elevates it to an exception. Let me step back from that suggestion then.

sometimes I want to show visually that the integrator blew up. solve_ivp doesn't output things if it fails/blows up, but odeint does.

@akaptano I've seen solve_ivp raise a ValueError when it blows up enough to get to np.inf/np.nan, but nothing before. What does odeint do?

Jacob-Stevens-Haas avatar Jun 26 '23 16:06 Jacob-Stevens-Haas

odeint just returns the trajectory anyways! So you can plot the finite values and see the blow up

akaptano avatar Jun 26 '23 16:06 akaptano