exceptionTypesTest.py

You can view and download this file on Github: exceptionTypesTest.py

  1#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  2# This is an EXUDYN example
  3#
  4# Details:  What an Exudyn error looks like from Python.
  5#           Ten things a user can get wrong - a bad item number, a parameter of the wrong type, a
  6#           size that does not fit, a feature an item does not have, a model the solver cannot
  7#           solve, and a Python user function that raises - each provoked on purpose, caught, and
  8#           reported with the exception CLASS it arrived as and the message str(exception) carries.
  9#
 10#           It is a test model and a record at the same time. As a test it checks one thing only:
 11#           every case must raise SOMETHING. The classes and the messages are printed rather than
 12#           asserted: pinning a class or a wording here would mean editing this file whenever
 13#           one of them is improved, and the log of a test run already shows what they were
 14#           on that day.
 15#
 16# Usage:    Run it as it is and read the table.
 17#           To see an error the way an IDE shows it - the traceback in Spyder or VS Code - set
 18#           catchErrors = False below, and singleCase to the number of the case to let fly.
 19#
 20# Author:   Johannes Gerstmayr
 21# Date:     2026-09-18
 22#
 23# Copyright:This file is part of Exudyn. Exudyn is free software. You can redistribute it and/or modify it under the terms of the Exudyn license. See 'LICENSE.txt' for more details.
 24#
 25#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 26
 27import exudyn as exu
 28from exudyn.utilities import * #includes itemInterface and rigidBodyUtilities
 29import numpy as np
 30
 31testIsActive = exu.sys.get('testIsActive', False)
 32
 33#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 34#the two switches this file exists for: with catchErrors=False the selected case is NOT caught, so
 35#the exception reaches the IDE and is shown the way a user sees it
 36catchErrors = True
 37singleCase = 0      #which case to run uncaught (1-based); 0 means "all of them", caught
 38
 39cases = []          #(name, what the user did wrong, callable)
 40
 41
 42def Case(name, whatTheUserDidWrong):
 43    """collect a case; the decorated function provokes exactly one error"""
 44    def Decorate(function):
 45        cases.append((name, whatTheUserDidWrong, function))
 46        return function
 47    return Decorate
 48
 49
 50#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 51#a small system every case can use; nothing here is meant to fail
 52SC = exu.SystemContainer()
 53mbs = SC.AddSystem()
 54nodeNumber = mbs.AddNode(NodePoint(referenceCoordinates=[0, 0, 0]))
 55objectNumber = mbs.AddObject(MassPoint(mass=1, nodeNumber=nodeNumber))
 56markerNumber = mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=nodeNumber, coordinate=0))
 57mbs.Assemble()
 58
 59
 60#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 61#the cases: bad input first, the solver and the user function last, because an error inside the
 62#solver sets the global error flag and what happens after it is no longer a clean measurement
 63
 64@Case('item number out of range', 'asks for object 999 in a system that has one object')
 65def ObjectNumberOutOfRange():
 66    mbs.GetObject(999)
 67
 68
 69@Case('output variable index', 'asks for the output of a node that does not exist')
 70def NodeNumberOutOfRange():
 71    mbs.GetNodeOutput(999, exu.OutputVariableType.Position)
 72
 73
 74#the two cases that ADD an item build their own system: an item added to the assembled mbs above
 75#would leave it inconsistent, and the later cases would then measure that instead of themselves
 76@Case('parameter of the wrong type', 'writes a string into a parameter that is a number')
 77def ParameterOfWrongType():
 78    scratch = exu.SystemContainer().AddSystem()
 79    scratchNode = scratch.AddNode(NodePoint(referenceCoordinates=[0, 0, 0]))
 80    scratch.AddObject(MassPoint(mass='heavy', nodeNumber=scratchNode))
 81
 82
 83@Case('parameter of the wrong value', 'gives a 3D reference position two components')
 84def ParameterOfWrongValue():
 85    scratch = exu.SystemContainer().AddSystem()
 86    scratch.AddNode(NodePoint(referenceCoordinates=[1, 2]))
 87
 88
 89@Case('object that is not a matrix', 'hands a list to something that wants a scipy sparse matrix')
 90def NotAMatrix():
 91    exu.MatrixContainer().SetWithSparseMatrix([1, 2, 3])
 92
 93
 94@Case('vector of the wrong size', 'writes five coordinates into a system that has three')
 95def VectorOfWrongSize():
 96    mbs.systemData.SetODE2Coordinates([1, 2, 3, 4, 5])
 97
 98
 99@Case('output an item does not have', 'asks a mass point for a strain')
100def OutputVariableNotAvailable():
101    mbs.GetObjectOutput(objectNumber, exu.OutputVariableType.StrainLocal)
102
103
104@Case('marker that does not exist', 'loads marker 999 and assembles')
105def MarkerDoesNotExist():
106    localSC = exu.SystemContainer()
107    localMbs = localSC.AddSystem()
108    localNode = localMbs.AddNode(NodePoint(referenceCoordinates=[0, 0, 0]))
109    localMbs.AddObject(MassPoint(mass=1, nodeNumber=localNode))
110    localMbs.AddLoad(LoadCoordinate(markerNumber=999, load=1))
111    localMbs.Assemble()
112
113
114@Case('a system the solver cannot solve', 'runs a static solution of a body that is not held')
115def SolverCannotSolve():
116    localSC = exu.SystemContainer()
117    localMbs = localSC.AddSystem()
118    localNode = localMbs.AddNode(NodePoint(referenceCoordinates=[0, 0, 0]))
119    localMbs.AddObject(MassPoint(mass=1, nodeNumber=localNode))
120    localMarker = localMbs.AddMarker(MarkerNodeCoordinate(nodeNumber=localNode, coordinate=0))
121    localMbs.AddLoad(LoadCoordinate(markerNumber=localMarker, load=10))
122    localMbs.Assemble()
123
124    simulationSettings = exu.SimulationSettings()
125    simulationSettings.solution.file.write = False
126    simulationSettings.staticSolver.verboseMode = 0
127    localMbs.SolveStatic(simulationSettings)
128
129
130@Case('a user function that raises', 'divides by zero inside a load user function')
131def UserFunctionRaises():
132    localSC = exu.SystemContainer()
133    localMbs = localSC.AddSystem()
134    localNode = localMbs.AddNode(NodePoint(referenceCoordinates=[0, 0, 0]))
135    localMbs.AddObject(MassPoint(mass=1, nodeNumber=localNode))
136    localMarker = localMbs.AddMarker(MarkerNodeCoordinate(nodeNumber=localNode, coordinate=0))
137
138    def LoadUserFunction(mbs, t, load):
139        if t > 0.02:
140            return load / 0.0          #the user's own mistake, inside the user's own function
141        return load
142
143    localMbs.AddLoad(LoadCoordinate(markerNumber=localMarker, load=1,
144                                    loadUserFunction=LoadUserFunction))
145    localMbs.Assemble()
146
147    simulationSettings = exu.SimulationSettings()
148    simulationSettings.timeIntegration.numberOfSteps = 10
149    simulationSettings.timeIntegration.endTime = 0.05
150    simulationSettings.timeIntegration.verboseMode = 0
151    simulationSettings.solution.file.write = False
152    localMbs.SolveDynamic(simulationSettings)
153
154
155#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
156#run them
157if not catchErrors:
158    #the point of this branch: no try/except anywhere, so the exception travels all the way out and
159    #the IDE shows it with its traceback
160    number = singleCase if singleCase > 0 else 1
161    (name, whatTheUserDidWrong, function) = cases[number - 1]
162    exu.Print('case ' + str(number) + ': ' + name + ' (' + whatTheUserDidWrong + ')')
163    function()
164    exu.Print('NOTHING WAS RAISED')
165    exu.sys['testResult'] = -1
166else:
167    results = []
168    silent = 0
169    for (number, (name, whatTheUserDidWrong, function)) in enumerate(cases, start=1):
170        try:
171            function()
172            results.append((number, name, 'NOTHING RAISED', ''))
173            silent += 1
174        except BaseException as exception:
175            message = str(exception).replace('\n', ' | ')
176            #an Exudyn exception carries the error that caused it as __cause__ - the object,
177            #with its traceback - and not only its words (#2537)
178            cause = exception.__cause__
179            if cause is not None:
180                message = '[__cause__ ' + type(cause).__name__ + ': ' + str(cause) + '] ' + message
181            results.append((number, name, type(exception).__name__, message))
182
183    exu.Print('')
184    exu.Print('exceptionTypesTest: what a user gets, ' + str(len(cases)) + ' cases')
185    exu.Print('-' * 110)
186    for (number, name, className, message) in results:
187        exu.Print('%2d  %-32s %-22s %s' % (number, name, className, message[:62]))
188    exu.Print('-' * 110)
189
190    #the ONLY thing this model asserts: every case must raise something. Which class and which
191    #message are printed above (#2527, #2528)
192    exu.sys['testResult'] = silent
193    exu.Print('exceptionTypesTest: cases that raised nothing = ' + str(silent))