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