(testmodels-parameterconversiontest)= # parameterConversionTest.py You can view and download this file on Github: [parameterConversionTest.py](https://github.com/jgerstmayr/EXUDYN/tree/master/python/TestModels/parameterConversionTest.py) ```{code-block} python :linenos: #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # This is an EXUDYN example # # Details: Behaviour record of the Python/C++ parameter conversion: # a fixed set of probe values is written into every parameter of every item and of the # simulation and visualization settings, through each access path, and the outcome is # compared with parameterConversionTestReference.txt. An outcome is the exception type # or the type, shape and value that reads back; the error messages are not recorded. # Set recordReference = True to rewrite the reference after an intended change. # # Author: Johannes Gerstmayr, Claude-JG # Date: 2026-09-14 # # 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. # #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import exudyn as exu import exudyn.itemInterface as itemInterface import numpy as np import contextlib import inspect import io import os import warnings recordReference = False #True: write the reference file instead of comparing testIsActive = exu.sys.get('testIsActive', False) #next to this file; runTestSuite.py executes the models from their own directory referenceFile = 'parameterConversionTestReference.txt' if '__file__' in globals() and os.path.basename(__file__) == 'parameterConversionTest.py': referenceFile = os.path.join(os.path.dirname(os.path.abspath(__file__)), referenceFile) #probe values: name, value; the same set for every parameter, whatever its type probes = [('neg', -1), ('zero', 0), ('int2', 2), ('real', 2.5), ('str', 'x'), ('bool', True), ('list2', [1., 2.]), ('list3', [1., 2., 3.]), ('list4', [1., 2., 3., 4.]), ('numpy3', np.array([1., 2., 3.])), ('matrix2', [[1., 0.], [0., 1.]]), ('nodeIndex', exu.NodeIndex(0)), ('objectIndex', exu.ObjectIndex(0)), ('markerList', [exu.MarkerIndex(0), exu.MarkerIndex(1)]), ('none', None)] itemKinds = ['Node', 'Object', 'Marker', 'Load', 'Sensor'] #about 38000 probes end in an error, and every C++ user error is printed and written to the log #file (260000 lines); both are switched off while probing (#2895) writeErrorsToLogFile = exu.special.exceptions.writeErrorsToLogFile exu.special.exceptions.writeErrorsToLogFile = False #every C++ user error also asks inspect.getframeinfo for file and line (Stdoutput.cpp, #PyGetCurrentFileInformation), which scans sys.modules and reads the source: inside the suite, #after scipy, matplotlib and ngsolve were imported, probing took 9 s instead of 1 s (#2423). #The messages are not recorded, so the file name is taken from the frame directly while probing inspectGetFrameInfo = inspect.getframeinfo inspect.getframeinfo = lambda frame, context=1: type('FrameInfo', (), {'filename': frame.f_code.co_filename}) printToConsole = exu.config.printToConsole exu.config.printToConsole = False def Outcome(function): """exception class name, or type/shape/value of what function() returns""" with contextlib.redirect_stdout(io.StringIO()): try: return Describe(function()) except Exception as e: return '!' + type(e).__name__ def Describe(value): if isinstance(value, np.ndarray): text = 'numpy' + 'x'.join(str(n) for n in value.shape) + value.dtype.kind if value.size <= 4: text += '=' + '[' + ','.join(str(round(float(x), 6)) for x in value.flatten()) + ']' return text if isinstance(value, (list, tuple)): text = type(value).__name__ + str(len(value)) if len(value) <= 4 and all(isinstance(x, (int, float, bool)) for x in value): text += '=' + '[' + ','.join(str(round(float(x), 6)) for x in value) + ']' elif len(value): text += ':' + type(value[0]).__name__ return text if isinstance(value, (bool, int, float)): return type(value).__name__ + '=' + str(round(float(value), 6)) if isinstance(value, str): return 'str=' + value[:10] return type(value).__name__ #++++++++++++++++++++++++++++++++++++++++++++++++++ #items: three paths per parameter # set - mbs.SetParameter on an item created from the default dict # dict - mbs.Add with the default dict, this parameter replaced (no Python checks) # class- the itemInterface class with this parameter, then mbs.Add #the default dict is built from the class signature, because some classes reject their own #defaults (e.g. MarkerNodeCoordinate: coordinate=InvalidIndex() fails CheckForValidUInt) records = {} def SetRangeChecks(active): """exudyn.special.exceptions.parameterRangeChecks (#2464)""" if hasattr(exu.special.exceptions, 'parameterRangeChecks'): exu.special.exceptions.parameterRangeChecks = active def Record(key, row): records.setdefault(' '.join(row), []).append(key) def DefaultDict(itemClass, typeKey, itemType): d = {typeKey: itemType} for name, parameter in inspect.signature(itemClass).parameters.items(): if name == 'visualization': for vName, value in parameter.default.items(): d['V' + vName] = value elif name != 'name': d[name] = parameter.default return d for className, itemClass in inspect.getmembers(itemInterface, inspect.isclass): kind = [k for k in itemKinds if className.startswith(k)] if not kind or className.startswith('V'): continue if getattr(itemClass, '_is_protocol', False): continue #the Protocol of a user function is a type, not an item: ObjectGround... (#2664) kind = kind[0] typeKey = kind.lower() + 'Type' itemType = className[len(kind):] Add = lambda mbs, item: getattr(mbs, 'Add' + kind)(item) defaults = DefaultDict(itemClass, typeKey, itemType) SC = exu.SystemContainer() mbs = SC.AddSystem() Record(className + ' defaults', [Outcome(lambda: Add(mbs, dict(defaults))), Outcome(lambda: Add(SC.AddSystem(), itemClass()))]) #the base item is created with range checks switched off, because some defaults violate their #range (e.g. coordinate=InvalidIndex() for a UInt, #2426); a default that is rejected when written #back with checks on is replaced by 1 in the dict and class paths, so those paths probe one #parameter at a time instead of failing on another one. mbs = SC.AddSystem() with contextlib.redirect_stdout(io.StringIO()): try: SetRangeChecks(False) index = Add(mbs, dict(defaults)) except Exception: continue finally: SetRangeChecks(True) members = [m for m in defaults if m != typeKey] fixes = {} for member in members: #every parameter, the deprecated names included: their warnings are expected and kept off the console (#2873) with contextlib.redirect_stdout(io.StringIO()), warnings.catch_warnings(): warnings.simplefilter('ignore', DeprecationWarning) try: getattr(mbs, 'Set' + kind + 'Parameter')(index, member, defaults[member]) except Exception: try: getattr(mbs, 'Set' + kind + 'Parameter')(index, member, 1) fixes[member] = 1 except Exception: pass defaults.update(fixes) classFixes = {m: v for m, v in fixes.items() if not m.startswith('V')} for member in members: GetParameter = lambda mbs, i: getattr(mbs, 'Get' + kind + 'Parameter')(i, member) #left out of the dict: the default is kept ('str=default'), a must-be-given parameter raises def RunOmit(): mbs2 = SC.AddSystem() value = Describe(GetParameter(mbs2, Add(mbs2, {m: v for m, v in defaults.items() if m != member}))) mbs3 = SC.AddSystem() return 'default' if value == Describe(GetParameter(mbs3, Add(mbs3, dict(defaults)))) else value Record(className + '.' + member + ' omit', [Outcome(RunOmit)]) for path in ['set', 'dict', 'class']: row = [] for probeName, probe in probes: if path == 'set': def Run(): getattr(mbs, 'Set' + kind + 'Parameter')(index, member, probe) return GetParameter(mbs, index) elif path == 'dict': def Run(): mbs2 = SC.AddSystem() return GetParameter(mbs2, Add(mbs2, dict(defaults, **{member: probe}))) else: def Run(): if member[0] == 'V' and member[1:] in inspect.signature(itemClass).parameters['visualization'].default: visualization = dict(inspect.signature(itemClass).parameters['visualization'].default) visualization[member[1:]] = probe item = itemClass(visualization=visualization, **classFixes) else: item = itemClass(**dict(classFixes, **{member: probe})) mbs2 = SC.AddSystem() return GetParameter(mbs2, Add(mbs2, item)) row.append(Outcome(Run)) Record(className + '.' + member + ' ' + path, row) SC.Reset() #++++++++++++++++++++++++++++++++++++++++++++++++++ #structures: attribute set on every leaf of simulationSettings and visualizationSettings #the leaves are taken from GetDictionary(), which also skips the deprecated members def Leaves(obj, dictionary, path): for name, value in dictionary.items(): if isinstance(value, dict): yield from Leaves(getattr(obj, name), value, path + '.' + name) else: yield obj, name, path + '.' + name, getattr(obj, name) SC = exu.SystemContainer() for rootName, root in [('SimulationSettings', exu.SimulationSettings()), ('VisualizationSettings', SC.visualizationSettings)]: saved = root.GetDictionary() for owner, name, path, original in list(Leaves(root, saved, rootName)): keys = path.split('.')[1:] for access in ['attribute', 'dict']: row = [] for probeName, probe in probes: def Run(): if access == 'attribute': setattr(owner, name, probe) else: #SetDictionary with this leaf replaced d = root.GetDictionary() sub = d for key in keys[:-1]: sub = sub[key] sub[keys[-1]] = probe root.SetDictionary(d) return getattr(owner, name) row.append(Outcome(Run)) with contextlib.redirect_stdout(io.StringIO()): root.SetDictionary(saved) #restore everything, whatever the probe left behind Record(path + ' ' + access, row) #++++++++++++++++++++++++++++++++++++++++++++++++++ inspect.getframeinfo = inspectGetFrameInfo exu.config.printToConsole = printToConsole exu.special.exceptions.writeErrorsToLogFile = writeErrorsToLogFile #++++++++++++++++++++++++++++++++++++++++++++++++++ #text: one block per distinct outcome row, followed by the parameters that share it, one line #per owner: ' ObjectMassPoint: mass(dict,set) nodeNumber(class)' def SplitKey(key): #'ObjectMassPoint.mass set' -> owner, member, path name, path = key.split(' ') if '.' not in name: #'ObjectMassPoint defaults' return name, '', path owner, member = name.rsplit('.', 1) return owner, member, path lines = ['#probes: ' + ' '.join(p[0] for p in probes)] for row in sorted(records): lines.append('') lines.append('outcome: ' + row) owners = {} for key in records[row]: owner, member, path = SplitKey(key) owners.setdefault(owner, {}).setdefault(member, []).append(path) for owner in sorted(owners): lines.append(' ' + owner + ': ' + ' '.join(member + '(' + ','.join(sorted(paths)) + ')' for member, paths in sorted(owners[owner].items()))) text = '\n'.join(lines) + '\n' numberOfDifferences = 0 if recordReference: with open(referenceFile, 'w', encoding='utf8') as f: f.write(text) exu.Print('parameterConversionTest: reference written, ' + str(len(records)) + ' outcome rows') else: with open(referenceFile, encoding='utf8') as f: reference = f.read() def Parse(t): outcome = {} row = '' for line in t.splitlines(): if line.startswith('outcome: '): row = line[len('outcome: '):] elif line.startswith(' '): owner, members = line.strip().split(': ', 1) for entry in members.split(' '): member, paths = entry[:-1].split('(') for path in paths.split(','): outcome[(owner + '.' + member if member else owner) + ' ' + path] = row return outcome new, old = Parse(text), Parse(reference) for key in sorted(set(new) | set(old)): if new.get(key) != old.get(key): numberOfDifferences += 1 if numberOfDifferences <= 20: exu.Print('parameterConversionTest: ' + key + '\n reference: ' + str(old.get(key)) + '\n now: ' + str(new.get(key))) exu.Print('parameterConversionTest: ' + str(len(new)) + ' parameter paths, ' + str(numberOfDifferences) + ' differences') exu.sys['testResult'] = numberOfDifferences ```