Skip to content

Commit

Permalink
Fix uncaught C++ exceptions crashing Python (#2484)
Browse files Browse the repository at this point in the history
Catch all exceptions in omp parallel regions.

Fixes #2478.
  • Loading branch information
dweindl authored Jul 10, 2024
1 parent 42dc328 commit 954fe8b
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 10 deletions.
54 changes: 53 additions & 1 deletion python/tests/test_swig_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import copy
import numbers

from math import nan
import pytest

import amici
Expand Down Expand Up @@ -534,3 +534,55 @@ def test_rdataview(sbml_example_presimulation_module):

# field names are included by dir()
assert "x" in dir(rdata)


def test_python_exceptions(sbml_example_presimulation_module):
"""Test that C++ exceptions are correctly caught and re-raised in Python."""

# amici-base extension throws and its swig-wrapper catches
solver = amici.CVodeSolver()
with pytest.raises(
RuntimeError, match="maxsteps must be a positive number"
):
solver.setMaxSteps(-1)

# model extension throws and its swig-wrapper catches
model = sbml_example_presimulation_module.get_model()
with pytest.raises(RuntimeError, match="Steadystate mask has wrong size"):
model.set_steadystate_mask([1] * model.nx_solver * 2)

# amici-base extension throws and its swig-wrapper catches
edata = amici.ExpData(1, 1, 1, [1])
# too short sx0
edata.sx0 = (1, 2)
with pytest.raises(
RuntimeError,
match=r"Number of initial conditions sensitivities \(36\) "
r"in model does not match ExpData \(2\).",
):
amici.runAmiciSimulation(model, solver, edata)

amici.runAmiciSimulations(
model, solver, [edata, edata], failfast=True, num_threads=1
)

# model throws, base catches, swig-exception handling is not involved
model.setParameters([nan] * model.np())
model.setTimepoints([1])
rdata = amici.runAmiciSimulation(model, solver)
assert rdata.status == amici.AMICI_FIRST_RHSFUNC_ERR

edata = amici.ExpData(1, 1, 1, [1])
rdatas = amici.runAmiciSimulations(
model, solver, [edata, edata], failfast=True, num_threads=1
)
assert rdatas[0].status == amici.AMICI_FIRST_RHSFUNC_ERR

# model throws, base catches, swig-exception handling is involved
from amici._amici import runAmiciSimulation

with pytest.raises(
RuntimeError, match="AMICI failed to integrate the forward problem"
):
# rethrow=True
runAmiciSimulation(solver, None, model.get(), True)
29 changes: 20 additions & 9 deletions src/amici.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,17 +282,28 @@ std::vector<std::unique_ptr<ReturnData>> runAmiciSimulations(
#pragma omp parallel for num_threads(num_threads)
#endif
for (int i = 0; i < (int)edatas.size(); ++i) {
auto mySolver = std::unique_ptr<Solver>(solver.clone());
auto myModel = std::unique_ptr<Model>(model.clone());

/* if we fail we need to write empty return datas for the python
interface */
if (skipThrough) {
ConditionContext conditionContext(myModel.get(), edatas[i]);
// must catch exceptions in parallel section to avoid termination
try {
auto mySolver = std::unique_ptr<Solver>(solver.clone());
auto myModel = std::unique_ptr<Model>(model.clone());

/* if we fail we need to write empty return datas for the python
interface */
if (skipThrough) {
ConditionContext conditionContext(myModel.get(), edatas[i]);
results[i]
= std::unique_ptr<ReturnData>(new ReturnData(solver, model)
);
} else {
results[i] = runAmiciSimulation(*mySolver, edatas[i], *myModel);
}
} catch (std::exception const& ex) {
results[i]
= std::unique_ptr<ReturnData>(new ReturnData(solver, model));
} else {
results[i] = runAmiciSimulation(*mySolver, edatas[i], *myModel);
results[i]->status = AMICI_ERROR;
results[i]->messages.push_back(
LogItem(LogSeverity::error, "OTHER", ex.what())
);
}

skipThrough |= failfast && results[i]->status < 0;
Expand Down

0 comments on commit 954fe8b

Please sign in to comment.