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