import { Player } from '../_lib/Player/dist/player.js';
import { WMRSimulator, WMR2DMode } from '../_lib/WMR2D/dist/wmr2d.js';
uiTimeEnd = 16.0;
uiTimeStep = 0.1;
wmr1 = new WMRSimulator( 'wmr-canvas1', WMR2DMode.Numerical );
player1 = new Player( uiTimeEnd, uiTimeStep, (time) => wmr1.step(time), () => wmr1.reset() );
player1.create();
Demo 2: Numerical Models
Let’s revisit our problem statement:
We want to evolve an autonomous wheeled mobile robot (WMR) to navigator quickly over obstacles and then stop in front of a wall.
With an analytical model, we can simulate the position of the WMR, but we did not attempt to handle obstacles and we learned that dynamic control was not possible.
Numerical Simulation
We’ll now mve on to a numerical simulation of the WMR. This will enable us to handle obstacles and dynamic control. But before we do so, let me first explain the concept of a numerical simulation.
Instead of solving an equation analytically, we can approximate the solution by taking small steps in time. Here are egestions for both analytical and numerical simulations:
\[ x_t = x_0 + \omega r t \\ \tag{1}\]
\[ x_t = x_{t-1} + \omega_{t-1} r \Delta t \tag{2}\]
where \(x_t\) is the position of the robot at time \(t\), \(\omega\) is the angular velocity of the robot’s wheels, \(r\) is the radius of its wheels, and \(\Delta t\) is the time step. The differences between Equation 1 and Equation 2 are in the dependency of the numerical solution on the previous position.
Essentially, the numerical solution computes the position by integrating the velocity over time. Here is the equivalent code:
// Closed-form solution
= initialPosition + angularVelocity * wheelRadius * time;
chassisPosition
// Numerical solution
+= angularVelocity * wheelRadius * timeStep; chassisPosition
And here is a numerical simulation:
Dynamic Control
Although the numerical simulation requires more computational resources to compute the position, it enables us to handle obstacles and dynamic control. Here is the simulation with a wall in front of the WMR and the following controller:
\[ \omega = \max( -\Omega, \min( \Omega, m d + b) \tag{3}\]
And here is the corresponding code snippet:
= 0;
initialPosition = initialPosition;
chassisPosition
= 1;
angularVelocity = 1;
wheelRadius
= 1;
wheelChassisOffset = wheelChassisOffset;
wheelPositionFront = -wheelChassisOffset;
wheelPositionRear
= 0;
wheelAngleFront = 0;
wheelAngleRear
= 0;
timeAccumulator = 0.01;
timeStep = 0;
time
= 3;
speedMax = 2;
speedSlope = -8;
speedIntercept
= 0.1;
controlPeriod = 0;
controlLastUpdate
function simulateStep( frameTime: number ) {
+= frameTime;
timeAccumulator
while ( timeAccumulator >= timeStep ) {
// p = v t = ω r t
+= angularVelocity * wheelRadius * timeStep;
chassisPosition
+= angularVelocity * timeStep;
wheelAngleFront += angularVelocity * timeStep;
wheelAngleRear
-= timeStep;
timeAccumulator += timeStep;
time
if ( time >= controlLastUpdate ) {
const dist = getDistanceToWall();
= Math.max( - speedMax, Math.min( speedMax, speedSlope * dist + speedIntercept ) );
angularVelocity
+= controlPeriod;
controlLastUpdate
}
}
= chassisPosition + wheelChassisOffset;
wheelPositionFront = chassisPosition - wheelChassisOffset;
wheelPositionRear
}
while ( !done ) {
= getFrameTime();
frameTime simulateStep( frameTime );
render();
}
Here are some key points to note:
- We can no longer compute the position of the robot for any time \(t\) without knowing the previous position.
- We’ve decoupled simulation and visualization (rendering).
- We’ve set a fixed time step, which is better for numerical stability.
- In this simple example, we only need to update the wheel positions once per render time. This is not a general case, but a simplification that we can take since the wheels are not independent of the chassis.
Read Fix Your Timestep! for more information on this code structure.
Adding Complex Collisions and Dynamics
How could we change our code if we:
- Added an incline to the ground plane?
- Added a second wall in front of the robot?
- Added a step in front of the robot?
Although these changes seem similar, (1) and (3) are quite a bit harder to implement. Why?
Complex Collisions and Dynamics
Changing the incline will require us to move to a more advanced method for integration. The code above implements a simple Euler integration, which works perfectly well for constant velocity dynamics (constant between time steps).
Adding a step requires us to change the orientations of all objects, implement more complex collision detection, and implement a friction model.
Adding a second wall is as simple as the first wall.
This simple numerical simulation does not work with a step. In the next demo we will explore how to handle complex collisions and dynamics.