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