resultsMonitorTest.py
You can view and download this file on Github: resultsMonitorTest.py
1#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2# This is an EXUDYN example
3#
4# Details: Test for exudyn.misc.resultsMonitor: reading the four results file types, the
5# incremental reading of a file that is still growing, the column selection, and the
6# return codes of the command line. Everything runs with once=True, so no update loop
7# is entered and no window is opened.
8#
9# Author: Johannes Gerstmayr
10# Date: 2026-09-19
11#
12# 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.
13#
14#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
15
16import exudyn as exu
17from exudyn.utilities import *
18
19testIsActive = exu.sys.get('testIsActive', False)
20
21import os
22import numpy as np
23import matplotlib.pyplot as plt
24
25from exudyn.basicUtilities import OutputFilePath
26from exudyn.processing import ParameterVariation, GeneticOptimization
27from exudyn.misc.resultsMonitor import (MonitorResults, ResultsFileColumns,
28 ReadResultsFileHeader, FindResultsFiles, Main)
29
30testSolution = 1
31
32def Check(name, condition, info=''):
33 """count one check; a failed check sets the test result to 0 and says which one it was"""
34 global testSolution
35 if not condition:
36 testSolution = 0
37 exu.Print('FAILED check: ' + name + (' (' + str(info) + ')' if info != '' else ''))
38 return condition
39
40#%%+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
41#create the results files: a sensor file and a solution file from a small model, and a
42#parameter variation and a genetic optimization file from a trivial objective function
43sensorFile = 'solution/resultsMonitorSensor.txt'
44solutionFile = 'solution/resultsMonitorSolution.txt'
45variationFile = 'solution/resultsMonitorVariation.txt'
46geneticFile = 'solution/resultsMonitorGenetic.txt'
47
48SC = exu.SystemContainer()
49mbs = SC.AddSystem()
50node = mbs.AddNode(NodePoint(referenceCoordinates=[0,0,0]))
51mbs.AddObject(MassPoint(mass=1, nodeNumber=node))
52marker = mbs.AddMarker(MarkerNodePosition(nodeNumber=node))
53mbs.AddLoad(Force(markerNumber=marker, loadVector=[1,0,0]))
54mbs.AddSensor(SensorNode(nodeNumber=node, fileName=sensorFile, writeToFile=True,
55 outputVariableType=exu.OutputVariableType.Position))
56mbs.Assemble()
57
58simulationSettings = exu.SimulationSettings()
59simulationSettings.timeIntegration.numberOfSteps = 50
60simulationSettings.timeIntegration.endTime = 1
61simulationSettings.solution.file.write = True
62simulationSettings.solution.file.name = solutionFile
63simulationSettings.show.computationTime = False
64simulationSettings.timeIntegration.verboseMode = 0
65mbs.SolveDynamic(simulationSettings)
66
67def ObjectiveFunction(parameterSet):
68 return (parameterSet['a']-1.5)**2 + (parameterSet['b']-0.5)**2 + 0.01
69
70ParameterVariation(parameterFunction=ObjectiveFunction, parameters={'a':(0,3,4),'b':(0,1,3)},
71 resultsFile=variationFile, showProgress=False)
72GeneticOptimization(objectiveFunction=ObjectiveFunction, parameters={'a':(0,3),'b':(0,1)},
73 numberOfGenerations=3, populationSize=8, elitistRatio=0.1,
74 randomizerInitialization=42, distanceFactor=0.1,
75 resultsFile=geneticFile, showProgress=False)
76
77#the monitor reads files by their name, so the output directory has to be applied here
78sensorPath = OutputFilePath(sensorFile)
79solutionPath = OutputFilePath(solutionFile)
80variationPath = OutputFilePath(variationFile)
81geneticPath = OutputFilePath(geneticFile)
82
83#%%+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
84#header and column recognition of all four file types
85for path, fileType in [[sensorPath,'sensor'], [solutionPath,'solution'],
86 [variationPath,'parameterVariation'], [geneticPath,'geneticOptimization']]:
87 header = ReadResultsFileHeader(path)
88 Check('file type of ' + os.path.basename(path), header.get('type','') == fileType,
89 header.get('type',''))
90
91columns = ResultsFileColumns(sensorPath)
92Check('sensor columns', columns == ['time','Position0','Position1','Position2'], columns)
93Check('optimization columns', ResultsFileColumns(geneticPath)[0:2] == ['globalIndex','value'])
94
95found = FindResultsFiles([os.path.dirname(sensorPath)])
96foundNames = [os.path.basename(info['fileName']) for info in found]
97expectedNames = [os.path.basename(path) for path in [sensorPath, solutionPath,
98 variationPath, geneticPath]]
99Check('all four files are found', #the directory may hold more, from a run before
100 all(name in foundNames for name in expectedNames), foundNames)
101Check('newest file first', all(found[i]['modified'] >= found[i+1]['modified']
102 for i in range(len(found)-1)))
103
104#%%+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
105#the drawn data must be the data in the file
106monitor = MonitorResults(sensorPath, once=True, showPanel=False, useSettingsFile=False)
107Check('sensor file monitored', monitor is not None)
108data = np.loadtxt(sensorPath, comments='#', delimiter=',')
109Check('one curve per column except time', len(monitor.lineList) == 3, len(monitor.lineList))
110Check('x data is time', np.allclose(monitor.lineList[0].get_xdata(), data[:,0]))
111Check('y data is column 1', np.allclose(monitor.lineList[0].get_ydata(), data[:,1]))
112
113#selected columns only, and a logarithmic axis shows absolute values
114monitor = MonitorResults(sensorPath, xColumns=[0], yColumns=[1], logY=True, once=True,
115 showPanel=False, useSettingsFile=False)
116Check('one selected curve', len(monitor.lineList) == 1)
117Check('log y uses absolute values', np.allclose(monitor.lineList[0].get_ydata(), abs(data[:,1])))
118
119#%%+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
120#the file is read incrementally: appending rows must add exactly those rows, an incomplete last
121#line must wait for its newline, and a file that got shorter must be read again from the start
122lines = open(sensorPath).read().split('\n')
123headerLines = [line for line in lines if line.startswith('#')]
124dataLines = [line for line in lines if line != '' and not line.startswith('#')]
125growingPath = OutputFilePath('solution/resultsMonitorGrowing.txt')
126with open(growingPath,'w') as file:
127 file.write('\n'.join(headerLines) + '\n' + '\n'.join(dataLines[0:10]) + '\n')
128
129monitor = MonitorResults(growingPath, once=True, showPanel=False, useSettingsFile=False)
130Check('rows of the partial file', monitor.data.NumberOfRows() == 10, monitor.data.NumberOfRows())
131with open(growingPath,'a') as file: #10 complete rows and one torn row
132 file.write('\n'.join(dataLines[10:20]) + '\n' + dataLines[20][0:10])
133Check('rows added', monitor.UpdatePlot() == 10)
134Check('the torn row is not used yet', monitor.data.NumberOfRows() == 20)
135with open(growingPath,'a') as file: #complete it and add the rest
136 file.write(dataLines[20][10:] + '\n' + '\n'.join(dataLines[21:]) + '\n')
137monitor.UpdatePlot()
138Check('all rows after completion', monitor.data.NumberOfRows() == len(dataLines),
139 monitor.data.NumberOfRows())
140Check('drawn points follow the file', len(monitor.lineList[0].get_xdata()) == len(dataLines))
141with open(growingPath,'w') as file: #the next run overwrites the file
142 file.write('\n'.join(headerLines) + '\n' + '\n'.join(dataLines[0:3]) + '\n')
143monitor.UpdatePlot()
144Check('buffer reset after overwrite', monitor.data.NumberOfRows() == 3,
145 monitor.data.NumberOfRows())
146
147#an update that finds no new data must leave the canvas untouched; redrawing an unchanged figure
148#once per second is what made the window flicker
149monitor.figure.canvas.draw()
150Check('no redraw without new data', monitor.UpdatePlot() == 0 and not monitor.figure.stale)
151#the margins are recomputed at every draw, so the axis labels survive a smaller window
152Check('constrained layout', 'constrained' in
153 str(type(monitor.figure.get_layout_engine())).lower(), monitor.figure.get_layout_engine())
154
155#%%+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
156#parameter variation: one curve per parameter, and one curve per variation with colorVariations
157monitor = MonitorResults(variationPath, once=True, showPanel=False, useSettingsFile=False)
158Check('one plot per varied parameter', len(monitor.lineList) == 2, len(monitor.lineList))
159monitor = MonitorResults(variationPath, colorVariations=True, once=True, showPanel=False,
160 useSettingsFile=False)
161Check('one curve per variation of b', len(monitor.lineList) == 3, len(monitor.lineList))
162monitor = MonitorResults(geneticPath, once=True, showPanel=False, useSettingsFile=False)
163Check('genetic optimization monitored', monitor is not None and len(monitor.lineList) == 2)
164
165#%%+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
166#the command line reports errors with a return code instead of raising
167solutionDirectory = os.path.dirname(sensorPath)
168Check('CLI plots a file', Main([sensorPath,'--once','--no-panel','--no-settings']) == 0)
169Check('CLI lists the columns', Main([sensorPath,'--list-columns','--no-settings']) == 0)
170Check('CLI lists the files', Main(['--list-files','--no-settings',
171 '--dir',solutionDirectory]) == 0)
172Check('CLI takes the newest file', Main(['--last','--once','--no-panel','--no-settings',
173 '--dir',solutionDirectory]) == 0)
174Check('missing file reported', Main(['fileDoesNotExist.txt','--once','--no-settings']) == 1)
175Check('column out of range reported',
176 Main([sensorPath,'--y-cols','99','--once','--no-settings']) == 1)
177notResultsPath = OutputFilePath('solution/resultsMonitorNotAResultsFile.txt')
178with open(notResultsPath,'w') as file:
179 file.write('some other file\n1,2,3\n')
180Check('wrong file type reported', Main([notResultsPath,'--once','--no-settings']) == 1)
181Check('old option names still work',
182 Main([sensorPath,'-logy','-ycols','1','-update','0.5','--once','--no-settings']) == 0)
183
184plt.close('all') #the figures were never shown, but they must not pile up for the next test model
185
186#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
187exu.Print('\nsolution of resultsMonitorTest (should be 1)=',testSolution)
188exu.sys['testResult'] = testSolution