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'])