Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5be9c56

Browse files
authoredJul 19, 2024··
Merge pull request #123 from DeepBlueRobotics/merge-MotorControllerTests
Fix Merge `MotorController` Tests #115
2 parents 449abc6 + 1dfaf0f commit 5be9c56

File tree

1 file changed

+159
-132
lines changed

1 file changed

+159
-132
lines changed
 
Lines changed: 159 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package frc.robot;
22

3+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertTrue;
56

67
import org.carlmontrobotics.libdeepbluesim.WebotsSimulator;
8+
import org.carlmontrobotics.libdeepbluesim.WebotsSimulator.SimulationState;
79
import org.junit.jupiter.api.RepeatedTest;
8-
import org.junit.jupiter.api.Test;
910
import org.junit.jupiter.api.Timeout;
1011
import org.junit.jupiter.api.parallel.ResourceLock;
1112

1213
import java.lang.System.Logger;
1314
import java.lang.System.Logger.Level;
15+
import java.util.ArrayList;
1416
import java.util.concurrent.TimeUnit;
17+
import java.util.function.BiConsumer;
18+
import java.util.function.Consumer;
1519

1620
import edu.wpi.first.math.MathUtil;
1721
import edu.wpi.first.math.system.plant.DCMotor;
@@ -29,7 +33,7 @@ public class MotorControllerTest {
2933
System.getLogger(MotorControllerTest.class.getName());
3034

3135
// Should be the same as the basicTimeStep in the world's WorldInfo (default to 32ms)
32-
private double simStepSizeSecs = 0.032;
36+
private static final double simStepSizeSecs = 0.032;
3337

3438
// Derivation of the math used below:
3539
// Let tau be the torque applied by the motor (Nm).
@@ -87,8 +91,9 @@ public class MotorControllerTest {
8791
// Rearranging gives:
8892
// theta(t) = theta_0 + w_f*t + (w_0 - w_f) * t_c * (1-exp(-t/t_c))
8993

90-
private double computeSpeedRadPerSec(DCMotor gearMotor, double moiKgM2,
91-
double throttle, double initialSpeedRadPerSec, double tSecs) {
94+
private static double computeSpeedRadPerSec(DCMotor gearMotor,
95+
double moiKgM2, double throttle, double initialSpeedRadPerSec,
96+
double tSecs) {
9297
double targetSpeedRadPerSec = throttle * gearMotor.nominalVoltageVolts
9398
* gearMotor.KvRadPerSecPerVolt;
9499
double timeConstantSecs = computeTimeConstantSecs(gearMotor, moiKgM2);
@@ -98,8 +103,8 @@ private double computeSpeedRadPerSec(DCMotor gearMotor, double moiKgM2,
98103
* Math.exp(-tSecs / timeConstantSecs);
99104
}
100105

101-
private double computeAngleRadians(DCMotor gearMotor, double moiKgM2,
102-
double throttle, double initialSpeedRadPerSec,
106+
private static double computeAngleRadians(DCMotor gearMotor, double moiKgM2,
107+
double throttle, double initialSpeedRadPerSec,
103108
double initialAngleRad, double tSecs) {
104109
double timeConstantSecs = computeTimeConstantSecs(gearMotor, moiKgM2);
105110
double targetSpeedRadPerSec = throttle * gearMotor.nominalVoltageVolts
@@ -111,25 +116,26 @@ private double computeAngleRadians(DCMotor gearMotor, double moiKgM2,
111116
* (1 - Math.exp(-tSecs / timeConstantSecs));
112117
}
113118

114-
private double computeTimeConstantSecs(DCMotor gearMotor, double moiKgM2) {
119+
private static double computeTimeConstantSecs(DCMotor gearMotor,
120+
double moiKgM2) {
115121
// t_c = R*J*k_v/k_T
116122
return gearMotor.rOhms * moiKgM2 * gearMotor.KvRadPerSecPerVolt
117123
/ gearMotor.KtNMPerAmp;
118124
}
119125

120-
private double computeCylinderMoiKgM2(double radiusMeters,
121-
double heightMeters, double densityKgPerM3) {
126+
private static double computeCylinderMoiKgM2(double radiusMeters,
127+
double heightMeters, double densityKgPerM3) {
122128
double massKg = densityKgPerM3 * Math.PI * radiusMeters * radiusMeters
123129
* heightMeters;
124130
return massKg * radiusMeters * radiusMeters / 2.0;
125131
}
126132

127-
private double timeOfNextStepSecs(double tSecs) {
133+
private static double timeOfNextStepSecs(double tSecs) {
128134
return Math.ceil(tSecs / simStepSizeSecs) * simStepSizeSecs;
129135
}
130136

131-
private double expectedSpeedRadPerSec(DCMotor gearMotor, double moiKgM2,
132-
double tSecs) {
137+
private static double expectedSpeedRadPerSec(DCMotor gearMotor,
138+
double moiKgM2, double tSecs) {
133139
// The robot autonomous code runs the motor at 0.5 for 2 seconds and then stops it (i.e.
134140
// sets the speed to 0 and lets it stop on it's own).
135141
double tMotorStopsSecs = timeOfNextStepSecs(2.0);
@@ -142,8 +148,8 @@ private double expectedSpeedRadPerSec(DCMotor gearMotor, double moiKgM2,
142148
tSecs - tMotorStopsSecs);
143149
}
144150

145-
private double expectedAngleRadians(DCMotor gearMotor, double moiKgM2,
146-
double tSecs) {
151+
private static double expectedAngleRadians(DCMotor gearMotor,
152+
double moiKgM2, double tSecs) {
147153
// The robot autonomous code runs the motor at 0.5 for 2 seconds and then stops it (i.e.
148154
// sets the speed to 0 and lets it stop on it's own).
149155
double tMotorStopsSecs = timeOfNextStepSecs(2.0);
@@ -156,49 +162,13 @@ private double expectedAngleRadians(DCMotor gearMotor, double moiKgM2,
156162
tSecs - tMotorStopsSecs);
157163
}
158164

159-
private void assertShaftCorrectAtSecs(DCMotor gearMotor, double moiKgM2,
160-
double actualSpeedRadPerSec, double actualAngleRadians,
161-
double tSecs) {
162-
// TODO: Make sim accurate enough that this can be less than 1*simStepSizeSecs
163-
// (https://github.com/DeepBlueRobotics/DeepBlueSim/issues/101)
164-
double jitterSecs = 5 * simStepSizeSecs;
165-
double expectedEarlierSpeedRadPerSec =
166-
expectedSpeedRadPerSec(gearMotor, moiKgM2, tSecs - jitterSecs);
167-
double expectedLaterSpeedRadPerSec =
168-
expectedSpeedRadPerSec(gearMotor, moiKgM2, tSecs + jitterSecs);
169-
LOG.log(Level.DEBUG, "expectedEarlierSpeedRadPerSec = {0}",
170-
expectedEarlierSpeedRadPerSec);
171-
LOG.log(Level.DEBUG, "expectedLaterSpeedRadPerSec = {0}",
172-
expectedLaterSpeedRadPerSec);
173-
assertEquals(
174-
(expectedEarlierSpeedRadPerSec + expectedLaterSpeedRadPerSec)
175-
/ 2,
176-
actualSpeedRadPerSec,
177-
Math.abs(expectedEarlierSpeedRadPerSec
178-
- expectedLaterSpeedRadPerSec) / 2,
179-
"Shaft not close enough to target angular velocity");
180-
double expectedEarlierAngleRadians =
181-
expectedAngleRadians(gearMotor, moiKgM2, tSecs - jitterSecs);
182-
double expectedLaterAngleRadians =
183-
expectedAngleRadians(gearMotor, moiKgM2, tSecs + jitterSecs);
184-
double expectedAngleRadians =
185-
(expectedEarlierAngleRadians + expectedLaterAngleRadians) / 2;
186-
double toleranceRadians = Math.abs(
187-
expectedEarlierAngleRadians - expectedLaterAngleRadians) / 2;
188-
assertTrue(
189-
MathUtil.isNear(expectedAngleRadians, actualAngleRadians,
190-
toleranceRadians, 0, 2 * Math.PI),
191-
"Shaft not close enough to target rotation. Expected %g +/- %g radians, but got %g radians."
192-
.formatted(expectedAngleRadians, toleranceRadians,
193-
actualAngleRadians));
194-
}
195-
196-
private double computeGearing(DCMotor motor, double desiredFreeSpeedRPS) {
165+
private static double computeGearing(DCMotor motor,
166+
double desiredFreeSpeedRPS) {
197167
return motor.freeSpeedRadPerSec / (2 * Math.PI) / desiredFreeSpeedRPS;
198168
}
199169

200-
private double computeFlywheelThickness(DCMotor gearMotor,
201-
double flywheelRadiusMeters, double flywheelDensityKgPerM3,
170+
private static double computeFlywheelThickness(DCMotor gearMotor,
171+
double flywheelRadiusMeters, double flywheelDensityKgPerM3,
202172
double desiredTimeConstantSecs) {
203173
// for a time constant of t_c seconds:
204174
// t_c = R*J*k_v/k_T
@@ -216,99 +186,156 @@ private static record Measurement(double simTimeSec,
216186
double speedRadPerSec) {
217187
}
218188

219-
private Measurement m1, m2;
189+
private static class Model {
190+
String shaftDefPath;
191+
String motorModelName;
192+
DCMotor gearMotor;
193+
double moiKgM2;
194+
Measurement m1, m2;
220195

221-
// @Test
222-
@RepeatedTest(1)
223-
void testCANMotorRotationInAutonomous() throws Exception {
224-
assertCorrectRotationInAutonomous("NEO", "CAN_SHAFT");
225-
}
196+
Model(String motorModelName, String shaftDefPath) throws Exception {
197+
this.motorModelName = motorModelName;
198+
this.shaftDefPath = shaftDefPath;
199+
// To ensure we the flywheel doesn't spin or accelerate too fast...
200+
var desiredFlywheelFreeSpeedRPS = 1.0;
201+
var desiredTimeConstantSecs = 1.0;
202+
203+
// For this test to pass, the motor and flywheel need to be setup in the world as
204+
// follows.
205+
var flywheelRadiusMeters = 0.5;
206+
var flywheelDensityKgPerM3 = 1000.0;
207+
var motor = (DCMotor) (DCMotor.class
208+
.getDeclaredMethod("get" + motorModelName, int.class)
209+
.invoke(null, 1));
210+
var gearing = computeGearing(motor, desiredFlywheelFreeSpeedRPS);
211+
gearMotor = motor.withReduction(gearing);
212+
LOG.log(Level.INFO,
213+
"For a free speed of {0} RPS when using a {1}, assuming the gearing is {2}.\n",
214+
desiredFlywheelFreeSpeedRPS, motorModelName, gearing);
215+
var flywheelThicknessMeters =
216+
computeFlywheelThickness(gearMotor, flywheelRadiusMeters,
217+
flywheelDensityKgPerM3, desiredTimeConstantSecs);
218+
LOG.log(Level.INFO,
219+
"For a time constant of {0} second when using a {1} with a gearing of {2} and a flywheel with radius {3} meters and density {4} kg/m^3, assuming the flywheel thickness is {5} meters.\n",
220+
desiredTimeConstantSecs, motorModelName, gearing,
221+
flywheelRadiusMeters, flywheelDensityKgPerM3,
222+
flywheelThicknessMeters);
223+
224+
// With those settings, the flywheel MOI will be:
225+
moiKgM2 = computeCylinderMoiKgM2(flywheelRadiusMeters,
226+
flywheelThicknessMeters, flywheelDensityKgPerM3);
227+
}
226228

227-
@Test
228-
void testPWMMotorRotationInAutonomous() throws Exception {
229-
assertCorrectRotationInAutonomous("MiniCIM", "PWM_SHAFT");
229+
public String toString() {
230+
return "Model(\"%s\", \"%s\")".formatted(motorModelName,
231+
shaftDefPath);
232+
}
233+
234+
private void assertShaftCorrectAtSecs(double actualSpeedRadPerSec,
235+
double actualAngleRadians, double tSecs) {
236+
// TODO: Make sim accurate enough that this can be less than 1*simStepSizeSecs
237+
// (https://github.com/DeepBlueRobotics/DeepBlueSim/issues/101)
238+
double jitterSecs = 2.5 * simStepSizeSecs;
239+
double expectedEarlierSpeedRadPerSec = expectedSpeedRadPerSec(
240+
gearMotor, moiKgM2, tSecs - jitterSecs);
241+
double expectedLaterSpeedRadPerSec = expectedSpeedRadPerSec(
242+
gearMotor, moiKgM2, tSecs + jitterSecs);
243+
LOG.log(Level.DEBUG, "expectedEarlierSpeedRadPerSec = {0}",
244+
expectedEarlierSpeedRadPerSec);
245+
LOG.log(Level.DEBUG, "expectedLaterSpeedRadPerSec = {0}",
246+
expectedLaterSpeedRadPerSec);
247+
assertEquals(
248+
(expectedEarlierSpeedRadPerSec
249+
+ expectedLaterSpeedRadPerSec) / 2,
250+
actualSpeedRadPerSec,
251+
Math.abs(expectedEarlierSpeedRadPerSec
252+
- expectedLaterSpeedRadPerSec) / 2,
253+
"Shaft not close enough to target angular velocity");
254+
double expectedEarlierAngleRadians = expectedAngleRadians(gearMotor,
255+
moiKgM2, tSecs - jitterSecs);
256+
double expectedLaterAngleRadians = expectedAngleRadians(gearMotor,
257+
moiKgM2, tSecs + jitterSecs);
258+
double expectedAngleRadians =
259+
(expectedEarlierAngleRadians + expectedLaterAngleRadians)
260+
/ 2;
261+
double toleranceRadians = Math.abs(
262+
expectedEarlierAngleRadians - expectedLaterAngleRadians)
263+
/ 2;
264+
assertTrue(
265+
MathUtil.isNear(expectedAngleRadians, actualAngleRadians,
266+
toleranceRadians, 0, 2 * Math.PI),
267+
"Shaft not close enough to target rotation. Expected %g +/- %g radians, but got %g radians."
268+
.formatted(expectedAngleRadians, toleranceRadians,
269+
actualAngleRadians));
270+
}
271+
272+
private void assertCorrectTimeConstant(double throttle) {
273+
var targetSpeedRadPerSec = throttle * gearMotor.nominalVoltageVolts
274+
* gearMotor.KvRadPerSecPerVolt;
275+
// Use the 2 measurements to measure the time constant.
276+
// w(t) = w_f + (w_0 - w_f) * exp(-t/t_c)
277+
// Let w'(t) = w(t) - w_f = (w_0 - w_f) * exp(-t/t_c)
278+
// w'(t1) = (w_0 - w_f) * exp(-t1/t_c)
279+
// w'(t2) = (w_0 - w_f) * exp(-t2/t_c)
280+
// w'(t2)/w'(t1) = exp((t1-t2)/t_c)
281+
// ln(w'(t2)/w'(t1)) = (t1-t2)/t_c
282+
// t_c = (t1-t2)/ln(w'(t2)/w'(t1))
283+
var actualTimeConstantSecs = (m1.simTimeSec() - m2.simTimeSec())
284+
/ (Math.log((m2.speedRadPerSec() - targetSpeedRadPerSec)
285+
/ (m1.speedRadPerSec() - targetSpeedRadPerSec)));
286+
assertEquals(computeTimeConstantSecs(gearMotor, moiKgM2),
287+
actualTimeConstantSecs, 0.02, "Time constant incorrect");
288+
}
230289
}
231290

232-
void assertCorrectRotationInAutonomous(String motorModelName,
233-
String shaftDefPath) throws Exception {
234-
// To ensure we the flywheel doesn't spin or accelerate too fast...
235-
var desiredFlywheelFreeSpeedRPS = 1.0;
236-
var desiredTimeConstantSecs = 1.0;
291+
private ArrayList<Model> models = new ArrayList<>();
237292

238-
// For this test to pass, the motor and flywheel need to be setup in the world as follows.
239-
var flywheelRadiusMeters = 0.5;
240-
var flywheelDensityKgPerM3 = 1000.0;
241-
var motor = (DCMotor) (DCMotor.class
242-
.getDeclaredMethod("get" + motorModelName, int.class)
243-
.invoke(null, 1));
244-
var gearing = computeGearing(motor, desiredFlywheelFreeSpeedRPS);
245-
var gearMotor = motor.withReduction(gearing);
246-
LOG.log(Level.INFO,
247-
"For a free speed of {0} RPS when using a {1}, assuming the gearing is {2}.\n",
248-
desiredFlywheelFreeSpeedRPS, motorModelName, gearing);
249-
var flywheelThicknessMeters =
250-
computeFlywheelThickness(gearMotor, flywheelRadiusMeters,
251-
flywheelDensityKgPerM3, desiredTimeConstantSecs);
252-
LOG.log(Level.INFO,
253-
"For a time constant of {0} second when using a {1} with a gearing of {2} and a flywheel with radius {3} meters and density {4} kg/m^3, assuming the flywheel thickness is {5} meters.\n",
254-
desiredTimeConstantSecs, motorModelName, gearing,
255-
flywheelRadiusMeters, flywheelDensityKgPerM3,
256-
flywheelThicknessMeters);
293+
private Consumer<SimulationState> stateChecker(
294+
BiConsumer<SimulationState, Model> checker) {
295+
return (s) -> {
296+
models.forEach((m) -> {
297+
assertDoesNotThrow(() -> checker.accept(s, m),
298+
"Check failed for model %s".formatted(m));
299+
});
300+
};
301+
}
257302

258-
// With those settings, the flywheel MOI will be:
259-
var moiKgM2 = computeCylinderMoiKgM2(flywheelRadiusMeters,
260-
flywheelThicknessMeters, flywheelDensityKgPerM3);
303+
@RepeatedTest(1)
304+
void testCorrectRotationInAutonomous() throws Exception {
305+
models.add(new Model("NEO", "CAN_SHAFT"));
306+
models.add(new Model("MiniCIM", "PWM_SHAFT"));
261307

262308
try (var manager = new WebotsSimulator(
263309
"../plugin/controller/src/webotsFolder/dist/worlds/MotorController.wbt",
264310
MotorControllerRobot::new)) {
265311
manager.atSec(0.0, s -> {
266312
s.enableAutonomous();
267-
}).setMaxJitterSecs(0).everyStep(s -> {
313+
}).setMaxJitterSecs(0).everyStep(stateChecker((s, m) -> {
268314
LOG.log(Level.DEBUG,
269315
"robotTime = {0}, simTimeSec = {1}, speedRadPerSec = {2}",
270316
s.getRobotTimeSec(), s.getSimTimeSec(),
271-
s.angularVelocity(shaftDefPath).getAngle());
272-
}).atSec(5 * simStepSizeSecs, s -> {
273-
m1 = new Measurement(s.getSimTimeSec(),
274-
s.angularVelocity(shaftDefPath).getAngle());
275-
}).atSec(1.0, s -> {
276-
m2 = new Measurement(s.getSimTimeSec(),
277-
s.angularVelocity(shaftDefPath).getAngle());
278-
assertCorrectTimeConstant(gearMotor, moiKgM2, 0.5);
279-
assertShaftCorrectAtSecs(gearMotor, moiKgM2,
280-
s.angularVelocity(shaftDefPath).getAngle(),
281-
s.rotation(shaftDefPath).getZ(), s.getRobotTimeSec());
282-
}).atSec(2.0 + 5 * simStepSizeSecs, s -> {
283-
m1 = new Measurement(s.getSimTimeSec(),
284-
s.angularVelocity(shaftDefPath).getAngle());
285-
}).atSec(3.0, s -> {
286-
m2 = new Measurement(s.getSimTimeSec(),
287-
s.angularVelocity(shaftDefPath).getAngle());
288-
assertCorrectTimeConstant(gearMotor, moiKgM2, 0.0);
289-
assertShaftCorrectAtSecs(gearMotor, moiKgM2,
290-
s.angularVelocity(shaftDefPath).getAngle(),
291-
s.rotation(shaftDefPath).getZ(), s.getRobotTimeSec());
292-
}).run();
317+
s.angularVelocity(m.shaftDefPath).getAngle());
318+
})).atSec(5 * simStepSizeSecs, stateChecker((s, m) -> {
319+
m.m1 = new Measurement(s.getSimTimeSec(),
320+
s.angularVelocity(m.shaftDefPath).getAngle());
321+
})).atSec(1.0, stateChecker((s, m) -> {
322+
m.m2 = new Measurement(s.getSimTimeSec(),
323+
s.angularVelocity(m.shaftDefPath).getAngle());
324+
m.assertCorrectTimeConstant(0.5);
325+
m.assertShaftCorrectAtSecs(
326+
s.angularVelocity(m.shaftDefPath).getAngle(),
327+
s.rotation(m.shaftDefPath).getZ(), s.getRobotTimeSec());
328+
})).atSec(2.0 + 5 * simStepSizeSecs, stateChecker((s, m) -> {
329+
m.m1 = new Measurement(s.getSimTimeSec(),
330+
s.angularVelocity(m.shaftDefPath).getAngle());
331+
})).atSec(3.0, stateChecker((s, m) -> {
332+
m.m2 = new Measurement(s.getSimTimeSec(),
333+
s.angularVelocity(m.shaftDefPath).getAngle());
334+
m.assertCorrectTimeConstant(0.0);
335+
m.assertShaftCorrectAtSecs(
336+
s.angularVelocity(m.shaftDefPath).getAngle(),
337+
s.rotation(m.shaftDefPath).getZ(), s.getRobotTimeSec());
338+
})).run();
293339
}
294340
}
295-
296-
private void assertCorrectTimeConstant(DCMotor gearMotor, double moiKgM2,
297-
double throttle) {
298-
var targetSpeedRadPerSec = throttle * gearMotor.nominalVoltageVolts
299-
* gearMotor.KvRadPerSecPerVolt;
300-
// Use the 2 measurements to measure the time constant.
301-
// w(t) = w_f + (w_0 - w_f) * exp(-t/t_c)
302-
// Let w'(t) = w(t) - w_f = (w_0 - w_f) * exp(-t/t_c)
303-
// w'(t1) = (w_0 - w_f) * exp(-t1/t_c)
304-
// w'(t2) = (w_0 - w_f) * exp(-t2/t_c)
305-
// w'(t2)/w'(t1) = exp((t1-t2)/t_c)
306-
// ln(w'(t2)/w'(t1)) = (t1-t2)/t_c
307-
// t_c = (t1-t2)/ln(w'(t2)/w'(t1))
308-
var actualTimeConstantSecs = (m1.simTimeSec() - m2.simTimeSec())
309-
/ (Math.log((m2.speedRadPerSec() - targetSpeedRadPerSec)
310-
/ (m1.speedRadPerSec() - targetSpeedRadPerSec)));
311-
assertEquals(computeTimeConstantSecs(gearMotor, moiKgM2),
312-
actualTimeConstantSecs, 0.02, "Time constant incorrect");
313-
}
314341
}

0 commit comments

Comments
 (0)
Please sign in to comment.