Could be built out of light weight foam. It just seems a good idea so you can leave it alone while it uses the guards as feedback to the neural network when it tips too far. Maybe with sonar obstacle avoidance so it learns to balance while changing direction all by itself.
You definitely have the best solution, but what I'm getting at... is it's a somewhat bigger scale! 🙄
This is going to be the Test Mule! (I'll have to come up with a good name) 700mm tall, 200mm wheels, total weight around 2.5 kg.
The one it will hopefully morph into will be about 1800mm and probably around 4 kg. But it is merely a figment of my imagination still.
Inq
P.S. What a hoot!!! - https://creatures.com/tools/animal/mule-name-generator
Could be built out of light weight foam. It just seems a good idea so you can leave it alone while it uses the guards as feedback to the neural network when it tips too far. Maybe with sonar obstacle avoidance so it learns to balance while changing direction all by itself.
This has been bouncing around in my head for a few days. With a little add-on, it'll solve a problem I've been struggling with:
- I'll be discussing these issues in detail soon, but once I get a genome out of the virtual model and put it on the physical bot, I don't really trust it. Did I model the Physics simulation accurate enough? Did I measure the properties of the physical bot accurately enough? Did I simulate the gyro/accel noise enough that I found in this topic?
- But even if it nails it the first time. Ha! 🤣 I want it to learn on the bot and improve.
- But improving entails throwing another genome at it. There's just as much chance that'll make it do a face plant.
- I was trying to strategize how to gauge "going bad" but still savable and switching back to the "pretty good" genome quick enough to let it recover.
But as you mentioned, I should find an angle that the motors have enough torque and the tires enough traction to re-stand once falling on a kick-stands. Then I went one step further (in my mind) to put wheels on them... aka training wheels.
So you are wanting to evolve a neural network on a virtual robot rather then use a neural network to allow a physical robot to learn stand up with reinforcement learning?
Using the ANN technique described above, the sketch ends up being pretty tiny. It uses three libraries which ought to be no surprise. It uses a library to access the LSM6DSV 6DoF sensor as input, a library to perform the ANN Inference and two instances of a Stepper driver for output.
If you were interested, the 6DoF and Stepper driver could probably be replaced with your preferred ones. These are no-frills versions. For instance:
- The 6DoF returns only raw data and doesn't filter, smooth, or convert to metric units, but it is blazing fast.
- The stepper driver is like-wise bare bones and only has the ability to instantly switch to a new speed. It does not do any acceleration smoothing. But as you'll soon see, the Genome is responsible for acceleration!
The Inference library handles all aspects of the previous tutorial posts. You update the input with data from the 6DoF sensor, call Inference() to get the output and pass on to stepper motors. This runs at 7.5kHz! I plan on publishing all three libraries, but I think @bryon suggested I put them up on GitHub. I need to figure that out.
If there aren't any questions at this point, I'll start doing posts on the theory and coding of the Genetic Evolution. They may be a little slow coming, I do have house projects as well as having started back to school three days a week. Ugh! Homework!
I want to point out that the libraries including the Inference one don't need to change based on enhancements to the robot. Going from the current 2D motion to a full 3D moving, turning, stopping, getting shoved (reasonably 😉) will not require any changes to the libraries. In fact the only changes to this sketch will be to upgrade the Genome, likely getting significantly larger and having two outputs, thus giving commands to the separate stepper motors.
One of the first upgrades once working with the real robot will be to turn on WiFi communications and simply update the Genome OTA. Currently, if you take out the timing code and ignore the spacing and comments, the actual coding is less than 60 lines!
Here is the output, showing the initialization of sensors and the converted Genome into the arrays of the ANN. At the bottom, some timing results showing it is running at 7.5kHz.
Serial Output
Inqster 1.0
LSM6DSV running at 7.5kHz
Brain
GENOME: -1772379524
Input array has 3 floats!
Returned array has 1 floats!
Input[0](3, 1) @ 0x3fcec4c0
0
0
0
Weights[0](12, 3) @ 0x3fcec380
-0.0501952 -0.014164 0.155816
-0.0353582 -0.156778 0.0395821
0.474306 -0.185643 -0.0354818
-0.0371802 -0.538911 -0.218068
... ...
0.0447599 0.0163717 0.113991
-0.00387969 0.145845 -0.0256889
-0.0166419 0.252368 0.205424
-0.151702 -0.111295 0.0126698
Biases[0](12, 1) @ 0x3fcec410
0.0498158
0.643197
0.0625386
0.123186
... ...
0.00932545
0.0832527
0.116721
-0.131441
Hidden[0](12, 1) @ 0x3fcec4f8
0
0
0
0
... ...
0
0
0
0
Weights[1](1, 12) @ 0x3fcec440
0.191943 -0.304522 0.122252 -0.255301 ... 0.33 0.127799 0.358677 -0.175165
Biases[1](1, 1) @ 0x3fcec470
-0.646022
Output[1](1, 1) @ 0x3fcec554
0
(2) DRV8825 Steppers are ready.
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
7.5 kHz
Sketch
#include <InqLSM6DSV.h>
#include <InqInference.h>
#include <Inq8825.h>
#define VERSION "1.0" // Last updated 2026/08/25
#define TIMING true // set to false to remove timing code.
// Pins for SPI
#define PIN_MISO 15
#define PIN_CS 16
#define PIN_MOSI 17
#define PIN_SCLK 18
#define PIN_INTR 3
// Steppers
#define PIN_STEP_L 4
#define PIN_DIR_L 5
#define PIN_STEP_R 6
#define PIN_DIR_R 7
#define MICRO_STEPS 16
#define MAX_RPM 500.0f
// Base64 Genome created by Genetic Evolution Model
const char genome[] PROGMEM =
"AwADAAwAAQB8oluWfJlNvQIQaLwkjh8+y9MQvXaKIL7iICI9NdjyPkcZPr5qVRG9"
"OUoYvRD2Cb8+TV++ffkpPdmJX7rdRSq9gSAePtPh1r0qoyg8oB9BvZ+3cTyyKZc+"
"ByEDvXTU8j6WA2w+M1Y3Pf4dhjw3dOk9W0J+u15YFT5xcdK8hFSIvFE2gT7HWlI+"
"4VcbvsPu473blE88owtMPZeoJD86FIA9uUj8PQFXUz7yngs+8MKevqyfJb3ByRg8"
"YYCqPTgL7z16mAa+x4xEPlnqm76AX/o9y7aCvjRq4D3p2r++EIvSviV7LT+/9ag+"
"pd0CPnOktz5vXjO+sWElv1";
InqLSM6DSV imu(PIN_MISO, PIN_CS, PIN_MOSI, PIN_SCLK, PIN_INTR);
InqInference brain(Activations::Sigmoid);
Inq8825 left(PIN_STEP_L, PIN_DIR_L);
Inq8825 right(PIN_STEP_R, PIN_DIR_R);
float* input;
float* output;
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.printf("\nInqster %s\n", VERSION);
if (!imu.begin())
halt("Sensor Failure");
Serial.printf("LSM6DSV running at 7.5kHz\n");
Serial.println("Brain");
input = brain.begin(genome);
if (!input)
halt("Brain damage");
brain.print(true, true);
Serial.println("(2) DRV8825 Steppers are ready.");
}
void halt(const char* msg)
{
#define LED_PIN 48
Serial.println(msg);
bool on = false;
while(true)
{
neopixelWrite(LED_PIN, (on = !on) ? 16 : 0, 0, 0);
delay(1000);
}
}
void loop()
{
// Get data from 6DoF sensor
RawLSM6DSV rawData = imu.readThrottled();
// Populate input data array and call Brain inference.
input[0] = rawData.gY;
input[1] = rawData.aX;
input[2] = rawData.aZ;
output = brain.inference();
// Pass it on to steppers.
left.setRPM(output[0] * MAX_RPM);
right.setRPM(output[0] * MAX_RPM);
#if TIMING // Timing data for our Inquisitiveness!
static uint32_t t0 = micros();
static uint32_t cnt = 0;
uint32_t t = micros() - t0;
cnt++;
if (t > 1000000)
{
Serial.printf("%.1f kHz\n", cnt * 1000.0f / t);
t0 = micros();
cnt = 0;
}
#endif // TIMING
}
So you are wanting to evolve a neural network on a virtual robot rather then use a neural network to allow a physical robot to learn stand up with reinforcement learning?
Now, to address your question... The short answer, Yes! The long answer:
-
In the lower right corner, there are statistics. The furthest right one is the number of Genomes that have been evaluated for fitness.
-
The text filling out in the left-lower pane represents the most successful (so far). The score is the last number on each row for that Genome. Remember, this is just the collection of data in an array that would replace the data in the Arduino Sketch.
-
The Animations lag behind the last, most successful Genome, but the one currently playing will be in the list and is shown in the upper-left pane.
-
As you can see, the virtual bot can test 38,000 in about 3 minutes. In the real world, those same tests would range from 0.3 seconds for an instant face-plant to tens of minutes if allowed to reach full test time. Taking time to pick it up when it falls or to just recharge it would try my patience.
-
From the primordial goo, an organism is created. We're to ignore the Chicken and the Egg quandary. It has a certain Genetic structure often called a Genome.
-
Then, a whole bunch of them are created with the same basic Genetic structure. This Population represents the birth of the species.
-
Each Genome (the individual) is tested in an environment for the traits we want. This often involves seeing what it does as a function of time... what it does when time starts, then 1 microsecond, 1 second, 1 year... later where it is and what it is doing is monitored. This test is run until the individual does something terrible and "dies" or until a set amount of time. In either case, it is given a Fitness value. Think of it as a score card.
-
The individual now mate. The ones that die aren't in the gene pool anymore, so they cease to exist. The next generation is then created by taking the best scoring ones and combining their Genomes to create the child of the next generation.
-
A new Population is created of these children and is often the same size of the preceding generation.
-
We return to step 3 and re-test the new individuals.
-
This is done until we "see" the traits we're happy with. We get it to converge on the answer. Otherwise, we see it giving us nothing approaching a solution, or we simply stop at some defined number of generations.
