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))