tutorialNeuralNetwork.py

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

  1#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  2# This is an EXUDYN example
  3#
  4# Details:  tutorial for machine learning with Exudyn;
  5#           correct positioning of rigid body mounted on strings by model trained with machine learning
  6#           data is created with static computations, then inverse model is trained with pytorch
  7#
  8# Model:    A rigid body with height 0.4, width 0.2 and depth 0.2 and 1 kg, mounted on two soft strings with stiffness 500N/m
  9#
 10# Author:   Johannes Gerstmayr
 11# Date:     2024-02-16
 12#
 13# 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.
 14#
 15#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 16
 17import exudyn as exu
 18#a local run must not open a plot window when windows are suppressed (#2477)
 19if exu.special.userInterface.suppressPlots: import matplotlib; matplotlib.use('Agg')
 20from exudyn.utilities import * #includes itemInterface and rigidBodyUtilities
 21import exudyn.graphics as graphics
 22
 23import sys
 24import numpy as np
 25# #from math import sin, cos, sqrt,pi
 26import os
 27os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" #for multiprocessing problems with pytorch
 28
 29from numpy.random import rand
 30np.random.seed(0)
 31
 32doTraining = True
 33
 34#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++
 35#create an Exudyn model of a rigid body on two strings
 36#this is a simple model for a 2D cable-driven robot
 37    
 38SC = exu.SystemContainer()
 39mbs = SC.AddSystem()
 40
 41
 42np.random.seed(0)
 43
 44#Geometry:
 45#[0,0,0] is at bottom of left tower
 46#[L,0,0] is at bottom of right tower
 47#[L,H,0] is at top of left tower
 48
 49L = 4       #m; distance of columns
 50H = 3       #m; height of columns
 51w = 0.2     #m; size of rigid body
 52m = 1       #kg; weight of mass
 53g = 9.81    #m/s^2; gravity
 54
 55sideLengths = [w,w*2,w]
 56density = m/w**3 #average density of rigid body with dimensions
 57
 58#left and right tower
 59pTower0 = np.array([0,H,0])
 60pTower1 = np.array([L,H,0])
 61#local attachment points at mass
 62localPosMass0=[-0.5*w,w,0]
 63localPosMass1=[ 0.5*w,w,0]
 64#center location of rigid body
 65#pRigidMid = np.array([0.5*L, 0.5*H,0])
 66pRigidMid = np.array([0.5*L, 0.5*H,0])
 67
 68# pInit = [1.2,0.78,0] #for testing
 69pInit = pRigidMid
 70
 71k = 500         #string stiffness
 72d = 0.01*k      #string damping for dynamic simulation
 73tEnd = 1
 74stepSize = 0.01
 75
 76gGround = [graphics.Brick(centerPoint=[0,0.5*H,0],size=[0.5*w,H,w],color=graphics.color.grey)]
 77gGround += [graphics.Brick(centerPoint=[L,0.5*H,0],size=[0.5*w,H,w],color=graphics.color.grey)]
 78gGround += [graphics.Brick(centerPoint=[0.5*L,-0.5*w,0],size=[L,w,w],color=graphics.color.grey)]
 79oGround = mbs.CreateGround(graphicsDataList=gGround)
 80
 81gRigid = [graphics.Brick(size=sideLengths, color=graphics.color.dodgerblue)]
 82oRigid = mbs.CreateRigidBody(referencePosition=pInit,
 83                             inertia = InertiaCuboid(density, sideLengths),
 84                             gravity = [0,-g,0],
 85                             graphicsDataList=gRigid)
 86nRigid = mbs.GetObject(oRigid)['nodeNumber'] #used later
 87
 88oString0 = mbs.CreateSpringDamper(itemNumbers=[oGround, oRigid],
 89                                  localPosition0=[0,H,0],
 90                                  localPosition1=localPosMass0,
 91                                  stiffness = k,
 92                                  damping = d,
 93                                  drawSize = 0, #draw as line
 94                                  )
 95oString1 = mbs.CreateSpringDamper(itemNumbers=[oGround, oRigid],
 96                                  localPosition0=[L,H,0],
 97                                  localPosition1=localPosMass1,
 98                                  stiffness = k,
 99                                  damping = d,
100                                  drawSize = 0, #draw as line
101                                  )
102sRigid = mbs.AddSensor(SensorBody(bodyNumber=oRigid, storeInternal=True,
103                                outputVariableType=exu.OutputVariableType.Position))
104
105# compute string lengths for given rigid body center position in straight configuration
106# used for initialization of static computation 
107def ComputeStringLengths(pRigid):
108    L0 = np.array(pRigid)+localPosMass0-pTower0
109    L1 = np.array(pRigid)+localPosMass1-pTower1
110    
111    return [np.linalg.norm(L0), np.linalg.norm(L1)]
112
113
114mbs.Assemble()
115simulationSettings = exu.SimulationSettings() #takes currently set values or default values
116simulationSettings.solution.file.write = not doTraining
117
118# # this leads to flipped results => good example !
119# simulationSettings.staticSolver.numberOfLoadSteps = 10
120# simulationSettings.staticSolver.stabilizerODE2term = 2        #add virtual stiffness due to mass; helps static solver to converge for such cases
121
122simulationSettings.staticSolver.numberOfLoadSteps = 2
123simulationSettings.staticSolver.stabilizerODE2term = 20       #add virtual stiffness due to mass; helps static solver to converge for such cases
124simulationSettings.staticSolver.computeLoadsJacobian = False #due to bug in loadsJacobian
125simulationSettings.staticSolver.verboseMode = 0
126
127
128if False: #set to true to perform static/dynamic analysis and visualize results
129    tEnd = 20 #for visualization of dynamic case
130    simulationSettings.solution.sensors.writePeriod = stepSize
131    # simulationSettings.timeIntegration.realtime.active = True
132    simulationSettings.timeIntegration.endTime = tEnd
133    simulationSettings.timeIntegration.numberOfSteps = int(tEnd/stepSize)
134    
135    SC.renderer.Start()
136    SC.renderer.DoIdleTasks()
137    
138    mbs.SolveStatic(simulationSettings)
139    SC.renderer.DoIdleTasks()
140    mbs.SolveDynamic(simulationSettings)
141    
142    SC.renderer.DoIdleTasks()
143    SC.renderer.Stop()
144
145    mbs.PlotSensor(sRigid, components=[0,1,2]) #plot vertical displacement
146
147    #this shows the deviation due to string stiffness and rotation of rigid body
148    print('final pos=',mbs.GetSensorValues(sRigid))
149
150    sys.exit()
151
152#this function is called to compute real position from ideal position p
153def ComputePositionFromStringLengths(p):
154    [L0,L1] = ComputeStringLengths(p)
155    refCoordsRigid[0:3] = p #override position
156    mbs.SetNodeParameter(nodeNumber=nRigid, 
157                         parameterName='referenceCoordinates',
158                         value=refCoordsRigid)
159    mbs.SetObjectParameter(objectNumber=oString0, 
160                           parameterName='referenceLength',
161                           value=L0)
162    mbs.SetObjectParameter(objectNumber=oString1, 
163                           parameterName='referenceLength',
164                           value=L1)
165    mbs.Assemble()
166    
167    try:
168        mbs.SolveStatic(simulationSettings)
169        positionList.append(p) #this is the ideal position; used to calculate deviation
170        #we map targeted (real) positions to original lengths
171        realPos = mbs.GetSensorValues(sRigid)
172        #compute original lengths to realPos
173        [L0orig,L1orig] = ComputeStringLengths(realPos)
174
175        #targetList.append([L0,L1]) #we only need to store the deviation
176        diff = [L0-L0orig,L1-L1orig]
177        return [realPos, diff, [L0, L1]]
178    except:
179        print('solver failed for:',p,',Ls=',[L0,L1])
180        return [None]
181
182#%%+++++++++++++++++++++++++++++++++++++++++++++
183#now create data: mapping of 2D position of object to difference of 
184#  ideal lengths to real lengths (which gives the necessary correction 
185#  for positioning)
186
187gridX = 20*2 # gridX * gridY is the number of samples for training
188gridY = 20*2
189nExamples = gridX*gridY
190nExamplesTest = int(nExamples*0.1) # additional samples for testing
191pRangeX = 2.4
192pRangeY = 2.4
193positionList = []
194inputList = []
195targetList = []
196#store reference coordinates for rotations
197refCoordsRigid = mbs.GetNodeParameter(nodeNumber=nRigid, parameterName='referenceCoordinates')
198
199gridValues=np.zeros((gridX,gridY,4))
200i=0
201ix = 0
202iy = 0
203while i < nExamples+nExamplesTest:
204    if i < nExamples:
205        ix = i%gridX
206        iy = int(i/gridX)
207        x0 = pRangeX*(ix/gridX-0.5)
208        y0 = pRangeY*(iy/gridY-0.5)
209    else:
210        x0 = pRangeX*(rand()-0.5)
211        y0 = pRangeY*(rand()-0.5)
212
213    p = pRigidMid + [x0, y0, 0]
214
215    rv = ComputePositionFromStringLengths(p)        
216    
217    if rv[0] is not None:
218        realPos = rv[0]
219        diff = rv[1]
220        [L0,L1] = rv[2]
221        targetList.append(diff)
222        inputList.append(list(realPos)[0:2]) #correction on position
223        
224        if i < nExamples:
225            gridValues[ix,iy,0:2] = diff
226            gridValues[ix,iy,2:4] = realPos[0:2]
227        
228        if max(diff)>0.5:
229            print('++++++++++++++++++++++++++')
230            print('ideal pos=',p)
231            print('realPos  =',realPos)
232            print('lengths  =',L0,L1)
233        
234        i += 1    
235    
236inputsExudynList = inputList[0:nExamples]
237targetsExudynList = targetList[0:nExamples]
238inputsExudynTestList = inputList[nExamples:]
239targetsExudynTestList = targetList[nExamples:]
240
241print('created',nExamples+nExamplesTest,'samples')
242
243
244#%%++++++++++++++++++++++++++++++++++++++++++++++++++++
245#function to 3D-plot errors and corrections
246def PlotGridFunction(data, name='error', closeAll=False):
247    import numpy as np
248    import matplotlib.pyplot as plt
249    from mpl_toolkits.mplot3d import Axes3D
250    
251    if closeAll: plt.close('all')
252    
253    # Generate grid coordinates (if not already generated)
254    # For example, if gridX and gridY are the dimensions of your grid
255    gridX, gridY = data.shape
256    x = np.linspace( -0.5*pRangeX, 0.5*pRangeX, gridX)
257    y = np.linspace(-0.5*pRangeY, 0.5*pRangeY, gridY)
258    X, Y = np.meshgrid(x, y)
259    
260    # Create the contour plot
261    fig=plt.figure(figsize=(8, 6))
262    ax = fig.add_subplot(111, projection='3d')
263
264    # contour = plt.contourf(X, Y, dataX, cmap='hot', levels=100)
265    contour = ax.plot_surface(X, Y, data, cmap='viridis')
266
267    plt.colorbar(contour)
268    plt.title(name)
269    plt.xlabel('X-axis')
270    plt.ylabel('Y-axis')
271
272    # Display the plot
273    plt.show()
274
275PlotGridFunction(gridValues[:, :, 0], name='positioning error X', closeAll=True)
276PlotGridFunction(gridValues[:, :, 1], name='positioning error Y')
277
278
279if not doTraining:
280    sys.exit()
281
282
283
284
285
286#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
287# MACHINE LEARNING: Model and training
288#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
289
290#now train
291import torch
292import torch.nn as nn
293import torch.optim as optim
294from torch.utils.data import DataLoader, TensorDataset
295
296hiddenLayerSize = 8*4       #example size, adjust as needed
297batchSize = 16*8
298learningRate = 0.002
299nTrainingEpochs = 10000     #max epochs
300lossThreshold = 0.0002      #desired max. loss
301
302# torch.set_num_threads(1)
303#prepare data:
304
305# Convert lists to PyTorch tensors
306dtype=torch.float32
307inputsExudyn = torch.tensor(inputsExudynList, dtype=dtype)
308targetsExudyn = torch.tensor(targetsExudynList, dtype=dtype)
309inputsExudynTest = torch.tensor(inputsExudynTestList, dtype=dtype)
310targetsExudynTest = torch.tensor(targetsExudynTestList, dtype=dtype)
311
312# Create TensorDatasets
313train_dataset = TensorDataset(inputsExudyn, targetsExudyn)
314test_dataset = TensorDataset(inputsExudynTest, targetsExudynTest)
315
316# Create DataLoaders
317trainLoader = DataLoader(train_dataset, batch_size=batchSize, shuffle=True)
318testLoader = DataLoader(test_dataset, batch_size=batchSize, shuffle=False)
319
320
321class ModelNN(nn.Module):
322    def __init__(self, input_size, hiddenLayerSize, output_size):
323        super(ModelNN, self).__init__()
324        self.fc1 = nn.Linear(input_size, hiddenLayerSize,dtype=dtype)
325        self.relu = nn.ReLU()
326        self.leakyRelu= nn.LeakyReLU()
327        self.elu = nn.ELU()
328        # self.relu = nn.Sigmoid() #alternatives
329        # self.relu = nn.Tanh()
330        self.fc2 = nn.Linear(hiddenLayerSize, hiddenLayerSize,dtype=dtype)
331        self.fc3 = nn.Linear(hiddenLayerSize, hiddenLayerSize,dtype=dtype)
332        self.lastLayer = nn.Linear(hiddenLayerSize, output_size,dtype=dtype)
333
334    def forward(self, x):
335        x = self.fc1(x)
336        x = self.relu(x)
337        x = self.fc2(x)
338        x = self.leakyRelu(x)
339        x = self.fc3(x)
340        x = self.elu(x)
341        x = self.lastLayer(x)
342        return x
343
344
345
346input_size = inputsExudyn.shape[1]  # Number of input features
347output_size = targetsExudyn.shape[1]  # Assuming regression task, adjust for classification
348
349model = ModelNN(input_size, hiddenLayerSize, output_size)
350lossFunction = nn.MSELoss()  # Mean Squared Error Loss for regression, adjust for classification
351optimizer = optim.Adam(model.parameters(), lr=learningRate,)
352
353#++++++++++++++++++++++++++++++++++++++++++++++++++++
354#perform training and store loss over epochs
355#stop when lossThreshold reached
356
357lossHistory = []
358minLoss = 1e10
359# Train the network
360for epoch in range(nTrainingEpochs):  # 100 epochs
361    for inputs, targets in trainLoader:
362        optimizer.zero_grad()
363
364        # Forward pass
365        outputs = model(inputs)
366        loss = lossFunction(outputs, targets)
367
368        # Backward pass and optimization
369        loss.backward()
370        optimizer.step()
371        
372    lossHistory.append([epoch, np.sqrt(loss.item())])
373    minLoss = min(minLoss, np.sqrt(loss.item()))
374    lossVal = np.sqrt(loss.item())
375    
376    if lossVal < lossThreshold:
377        print(f'loss threshold reached at: epoch {epoch+1}/{nTrainingEpochs}, Loss: {lossVal}')
378        break
379
380    if (epoch+1) % 50 == 0:
381        print(f'Epoch {epoch+1}/{nTrainingEpochs}, Loss: {lossVal}')
382
383print('min loss=',minLoss)
384
385#%%++++++++++++++++++++++++++++++++++++++++++++++++++++++
386#evaluate model using test data:
387model.eval()  # Set the model to evaluation mode
388totalLoss = 0
389count = 0
390
391with torch.no_grad():  # No need to track gradients for evaluation
392    for inputs, targets in testLoader:
393        outputs = model(inputs)
394        loss = torch.sqrt(((outputs - targets) ** 2).mean())  # Calculating RMSE for each batch
395        totalLoss += loss.item()
396        count += 1
397
398averageRMSE = totalLoss / count
399
400# Call the evaluate_model function with the test_loader and your model
401print(f"\nTest RMSE: {averageRMSE}\n")
402    
403for test in range(10):
404    x = inputsExudynTest[test:test+1]
405    print('x=',x,', shape',x.shape)
406    y = model(x).tolist()[0] #convert output to list
407    
408    yRef = targetsExudynTest[test:test+1]
409
410    print('++++++++++++++++++++')
411    print('input:  ',x,'\ntarget: ',yRef,'\npredict:',y)
412    # mbs.PlotSensor(result, components=[0], closeAll=test==0, newFigure=False,
413    #                labels=['RNN'], yLabel='displacement (m)',
414    #                colorCodeOffset=test)
415    # mbs.PlotSensor(result, components=[1], newFigure=False,
416    #                labels=['reference'], yLabel='displacement (m)',
417    #                colorCodeOffset=test,lineStyles=[':'])
418
419
420#compute 2D grid with error
421testErrorGrid = np.zeros((gridX, gridY))
422maxError = 0
423for iy in range(gridY):
424    for ix in range(gridX):
425        x0 = pRangeX*((ix+0.5)/gridX-0.5)*0.5
426        y0 = pRangeY*((iy+0.5)/gridY-0.5)*0.5
427
428        p = pRigidMid + [x0, y0, 0]
429
430        x = torch.tensor([list(p[0:2])], dtype=dtype)
431        y = model(x).tolist()[0]
432
433        rv = ComputePositionFromStringLengths(p)        
434        
435        diff = rv[1]
436        err = np.array(diff) - y
437        maxError = max(maxError, abs(err[0]))
438        
439        testErrorGrid[ix,iy] = err[0]
440
441print('maxError', maxError)
442PlotGridFunction(testErrorGrid, name='test error X', closeAll=False)
443
444#%%+++++++++++++++    
445if True: #plot loss history
446    lossData = np.array(lossHistory)
447    import matplotlib.pyplot as plt
448    from exudyn.plot import PlotSensor
449    PlotSensor(None,lossData,components=[0], closeAll=False, logScaleY=True,
450               labels=['loss'])