Performance, errors and solver failures
What Exudyn says when something goes wrong, what to do about it, and how to make a model run faster. None of this is needed to write a first model, and all of it is needed sooner or later.
Errors: what Exudyn raises, and what to do about it
Every error that Exudyn reports from its C++ core arrives in Python as an exception with a type. The type answers the first question a user has – whose mistake was it – before the message is even read.
All of them derive from exudyn.ExudynError, and each of them also derives from
the built-in exception that fits, so an except ValueError written before Exudyn 1.12
keeps working:
exudyn.ExudynTypeError(also aTypeError): the object cannot be that parameter at all – a list where a number belongs, a string where a function belongs.exudyn.ExudynValueError(also aValueError): the kind of value is right and the value is not – a vector of the wrong length, an unknown parameter name,numberOfSteps=-1, an output variable this item does not have.exudyn.ExudynIndexError(also anIndexError): an index outside its range –mbs.GetObject(99)in a system with three objects.exudyn.ModelError(also aValueError): the model as built does not hold together – a load on a marker that does not exist, a node used by an object but never added, a function called beforembs.Assemble(). Most of these are raised byAssemble(), which checks the whole system.exudyn.SolverError(also aRuntimeError): the solver cannot continue – a singular system matrix, no convergence, divergence. See Removing convergence problems and solver failures for what to do.exudyn.NotImplementedFeatureError(also aNotImplementedError): the feature or the combination does not exist – a jacobian that is not implemented for this element, a solver that cannot handle this constraint. Neither your mistake nor a bug; the message usually names a way around it.exudyn.ExudynArithmeticError(also anArithmeticError): division by zero, root of a negative number.exudyn.InternalError(also aRuntimeError): an Exudyn invariant broke. This one is a bug in Exudyn, not in your model – please report it with the message and, if possible, a model that shows it.
So except exudyn.ExudynError catches everything Exudyn raises, while
except IndexError or except ValueError still do what they always did.
Catching a solver failure
The case with a concrete action behind it: a solve that fails is a normal event in a parameter study, and it should not end the study.
try:
mbs.SolveDynamic(simulationSettings)
except exudyn.SolverError:
#the solver stopped: retry with smaller steps
simulationSettings.timeIntegration.numberOfSteps *= 10
mbs.SolveDynamic(simulationSettings)
except exudyn.ModelError as e:
#the model itself is wrong - retrying will not help
print('this model cannot be solved:', e)
python/Notebooks/snippets/solving.ipynb
In a parameter variation, score the failed run instead of letting it stop the sweep:
def ParameterFunction(parameterDict):
...
try: mbs.SolveDynamic(simulationSettings)
except exudyn.ExudynError: return 1e10 #a very bad score, so the optimizer moves away from here
return mbs.GetSensorValues(sensorNumber)[0]
print('score:', ParameterFunction({}))
score: 1.0
python/Notebooks/snippets/solving.ipynb
Note that except exudyn.ExudynError does not catch
KeyboardInterrupt: a long sweep can still be stopped with Ctrl-C.
An error inside your own user function
If a Python user function – springForceUserFunction and its kin – raises, Exudyn
reports it as a ModelError, because a user function is part of the model. The original
exception is not lost: it is attached as __cause__, with its own traceback,
so Spyder and VS Code show the chain and jump to the line inside your function.
try:
mbs.SolveDynamic(simulationSettings)
except exudyn.ModelError as e:
print(type(e.__cause__)) #<class 'ZeroDivisionError'>, raised in your user function
******************************
DYNAMIC SOLVER FAILED:
use showHints=True to show helpful information
******************************
<class 'ZeroDivisionError'>
python/Notebooks/snippets/solving.ipynb
Where the message is written
The exception carries the message and the location, so the console shows it once – as the Python traceback, and not a second time as a printed block. A caught exception therefore prints nothing at all, which is what makes a parameter variation readable.
The log files are the other way round: an error is written to every open log file, whatever raised it.
the
exudynoutput file, if one was opened withexu.SetWriteToFile(...);the solver information file, if
solution.solverInformationFileNameis set.
On a long unattended run those files are the only record that anything happened.
Deprecation warnings
A name that is on its way out raises a real Python DeprecationWarning – once per place
in your code, not once per call. To find every use before a release removes the old name, run
your script with
python -W error::DeprecationWarning yourModel.py
which turns each of them into an error at the line that caused it.
Behaviour that is not in your script
A script that behaves differently than it reads — a different output directory, a renderer that
looks unfamiliar, a dialog in a place you did not put it — may be reading something that was stored
on this machine. Exudyn keeps such settings in one folder, ~/.exudyn, and
deleting that folder returns everything to the defaults; nothing in it is needed to run a model.
To find out whether it is the cause without deleting anything, run the script with
EXUDYN_NO_USER_SETTINGS=1: the difference is either gone (a stored setting caused it) or still
there (it did not). What was read is listed by exudyn.misc.overrideSettings.Print(), and the whole
mechanism is User settings that persist between runs.
Switches
exudyn.config.suppressWarnings = True: no warnings, on the console or in a file.exudyn.special.exceptions.parameterRangeChecks = False: do not check parameter ranges; faster, and a wrong value then reaches the computation instead of being reported.exudyn.special.exceptions.dictionaryVersionMismatch,.dictionaryNonCopyable: turn those two specific errors off.
Removing convergence problems and solver failures
Nonlinear formulations (such as most multibody systems, especially nonlinear finite elements) cause problems and there is no general nonlinear solver which may reliably and accurately solve such problems. Tuning solver parameters is at hand of the user. In general, the Newton solver tries to reduce the error by the factor given in
simulationSettings.staticSolver.newton.relativeTolerance(for static solver),
which is not possible for very small (or zero) initial residuals. The absolute tolerance is helping out as a lower bound for the error, given in
simulationSettings.staticSolver.newton.absoluteTolerance(for static solver),
which is by default rather low (1e-10) – in order to achieve accurate results for small systems or small motion (in mm or \(\mu\)m regime). Increasing this value helps to solve such problems. Nevertheless, you should usually set tolerances as low as possible because otherwise, your solution may become inaccurate.
The following hints / rules for described problems shall be followed.
static solver: load steps get very small even if the solution seems to be smooth (or linear) and less steps are expected:
this may happen for system without loads; larger number of steps may happen for finer discretization;
you may adjust (increase)
.newton.relativeTolerance/.newton.absoluteTolerancein static solver or in time integration to resolve such problems, but check if solution achieves according accuracy
static solver: load steps are reduced significantly for highly nonlinear problems:
solver repeatedly writes that steps are reduced \(\ra\) try to use
loadStepGeometricand use a largeloadStepGeometricRange: this allows to start with very small loads in which the system is nearly linear (e.g. for thin strings or belts under gravity).
static solver: system is (nearly) kinematic:
a static solution can be achieved using
stabilizerODE2term, which adds mass-proportional stiffness terms during load steps \(< 1\); see also hints for singular Jacobians below
very small loads or even zero loads do not converge:
SolveDynamicorSolveStaticterminated due to errorsthe reason is the nonlinearity of formulations (nonlinear kinematics, nonlinear beam, etc.) and round off errors, which restrict Newton to achieve desired tolerances
adjust (increase)
.newton.relativeTolerance/.newton.absoluteTolerancein static solver or in time integrationin many cases, especially for static problems, the
.newton.residualMode = 1evaluates the increments; the nonlinear problems is assumed to be converged, if increments are within given absolute/relative tolerances; this also works usually better for kinematic solutions
for discontinuous problems:
try to adjust solver parameters; especially the
discontinuous.iterationToleranceanddiscontinuous.maxIterations; try to make smaller load or time steps in order to resolve switching points of contact or friction; generalized alpha solvers may cause troubles when reducing step sizes \(\ra\) use TrapezoidalIndex2 solverin case of user functions, make sure that there is no switching inside the user function (if or
signfunction); switching must be done in the PostNewtonStep, otherwise convergence severely suffers
singular Jacobians or redundant constraints:
in case of systems that lead to a singular Jacobian due to redundant constraints or kinematic DOF in static solutions, you may switch to Eigen’s FullPivotLU solver using:
simulationSettings.linearSolver.solverType = exu.LinearSolverType.EigenDenseandsimulationSettings.linearSolver.ignoreSingularJacobian=True;however, check your results as they may be erroneous, because the solver tries to find an optimal solution / compromise which may not be what you intend to get!
if you see further problems, please post them (including relevant example) at the Exudyn github page!
Performance and ways to speed up computations
Multibody dynamics simulation should be accurate and reliable on the one hand side. Most solver settings are such that they lead to comparatively reliable results. However, in some cases there is a significant possibility for speeding up computations, which are described in the following list. Not all recommendations may apply to your models.
The following examples refer to simulationSettings = exu.SimulationSettings().
In general, to see where CPU time is lost, use the option turn on simulationSettings.show.computationTime = True to see which parts of the solver need most of the time (deactivated in exudynFast versions!).
In addition to Exudyn’s internal time measurements, in Spyder (or IPython) you can use magic commands such as %timeit -n10 mbs.SolveDynamic() to evaluate the time spent for a specific command with number of repetitions given after -n. This may be particularly interesting in Python user functions to see where time is lost.
To activate the Exudyn C++ versions without range checks, which may be approx. 30 percent faster in some situations, use the following code snippet before first import of exudyn:
import sys
sys.exudynFast = True #this variable is used to signal to load the fast exudyn module
import exudyn as exu
The faster versions are available for all release versions, but only for some .dev1 development versions (Python 3.10), which can be determined by trying import exudyn.exudynCPPfast.
However, there are many ways to speed up Exudyn in general:
for models with more than 50 coordinates, switching to sparse solvers might greatly improve speed:
simulationSettings.linearSolver.solverType = exu.LinearSolverType.EigenSparsewhen preferring dense direct solvers, switching to Eigen’s PartialPivLU solver might greatly improve speed:
simulationSettings.linearSolver.solverType = exu.LinearSolverType.EigenDense; however, the flagsimulationSettings.linearSolver.ignoreSingularJacobian=Truewill switch to the much slower (but more robust) Eigen’s FullPivLUtry to avoid Python functions or try to speed up Python functions; if this is not possible, see solutions below
instead of user functions in objects or loads (computed in every iteration), some problems would also work if these parameters are only updated in
mbs.SetPreStepUserFunction(...)Python user functions can be speed up (since Exudyn V1.7.40) by converting conventional Python functions into Exudyn (internal) symbolic user functions, which have similar performance as C++ functions with the ability to parallelize; see Symbolic
Alternatively, Python user functions can be speed up using the Python numba package, using
@jitin front of functions (for more options, see https://numba.pydata.org/numba-doc/dev/user/index.html); Example given inExamples/springDamperUserFunctionNumbaJIT.pyshowing speedups of factor 4; more complicated Python functions may see speedups of 10 - 50for discontinuous problems, try to adjust solver parameters; especially the discontinuous.iterationTolerance which may be too tight and cause many iterations; iterations may be limited by discontinuous.maxIterations, which at larger values solely multiplies the computation time with a factor if all iterations are performed
For multiple computations / multiple runs of Exudyn (parameter variation, optimization, compute sensitivities), you can use the processing sub module of Exudyn to parallelize computations and achieve speedups proporional to the number of cores/threads of your computer; specifically using the
multiThreadingoption or even using a cluster (usingdispy, seeParameterVariation(...)function)In case of multiprocessing and cluster computing, you may see a very high CPU usage of “Antimalware Service Executable”, which is the Microsoft Defender Antivirus; you can turn off such problems by excluding
python.exefrom the defender (on your own risk!) in your settings: Settings \(\ra\) Update & Security \(\ra\) Windows Security \(\ra\) Virus & threat protection settings \(\ra\) Manage settings \(\ra\) Exclusions \(\ra\) Add or remove exclusions
Possible speed ups for dynamic simulations:
for implicit integration, turn on modified Newton, which updates jacobians only if needed:
simulationSettings.timeIntegration.newton.useModifiedNewton = Trueuse multi-threading:
simulationSettings.parallel.numberOfThreads = ..., depending on the number of cores (larger values usually do not help); improves greatly for contact problems, but also for some objects computed in parallel; will improve significantly in futuredecrease number of steps (
simulationSettings.timeIntegration.numberOfSteps = int(tEnd/h)) by increasing the step size \(h\) if not needed for accuracy reasons; not that in general, the solver will reduce steps in case of divergence, but not for accuracy reasons, which may still lead to divergence if step sizes are too largeswitch off measuring computation time, if not needed:
simulationSettings.show.computationTime = Falsetry to switch to explicit solvers, if problem has no constraints and if problem is not stiff
try to have constant mass matrices (see according objects, which have constant mass matrices; e.g. rigid bodies using RotationVector Lie group node have constant mass matrix)
for explicit integration, set
computeEndOfStepAccelerations = False, if you do not need accurate evaluation of accelerations at end of time step (will then be taken from beginning)for explicit integration of large systems, use a sparse solver (
simulationSettings.linearSolver.solverType = exu.LinearSolverType.EigenSparse): with the dense default every step multiplies with a dense mass matrix and costs \(O(n^2)\) - 400 times slower than sparse for 2000 point masses; then settimeIntegration.explicit.computeMassMatrixInversePerBody=True, which avoids factorization and back substitution, which may speed up computations with many bodies / particles further (it has no effect with the dense solver)if you are sure that your mass matrix is constant, set:
simulationSettings.timeIntegration.reuseConstantMassMatrix = True; check results!check that
simulationSettings.timeIntegration.realtime.active = False; if set True, it breaks down simulation to real timedo not record images, if not needed:
simulationSettings.solution.recordImagesInterval = -1in case of bad convergence, decreasing the step size might also help; check also other flags for adaptive step size and for Newton
use
simulationSettings.timeIntegration.verboseMode = 1; larger values create lots of output which drastically slows downuse
simulationSettings.timeIntegration.verboseModeFile = 0, otherwise output written to fileadjust
simulationSettings.solution.sensors.writePeriodto avoid time spent on writing sensor filesuse
simulationSettings.solution.file.write = False, otherwise much output may be written to file;if solution file is needed, adjust
simulationSettings.solution.file.writePeriodto larger values and also adjustsimulationSettings.solution.precision, e.g., to 6, in order to avoid larger files; also adjustsimulationSettings.solution.file.export.velocities = FalseandsimulationSettings.solution.file.export.accelerations = Falseto avoid large output files