[wpiunits] Java units API rewrite (#6958)

Java generics are too limited to do what we need. This refactors generic code previously in Unit and Measure into unit-specific classes that can have unit-safe math operations (notably, times and divide) that can return values in known units instead of a wildcarded Measure<?>.

Unit-specific measure implementations are automatically generated by ./wpiunits/generate_units.py, which generates generic interfaces and mutable and immutable implementations of those interfaces. These make up the bulk of the diff of this PR (approximately 9300 LOC).

This also adds units for angular and linear velocities, accelerations, and momenta; moment of inertia; and torque.
This commit is contained in:
Sam Carlberg
2024-09-07 13:59:29 -04:00
committed by GitHub
parent 496e7c1bba
commit a9b885070e
178 changed files with 14750 additions and 2158 deletions

View File

@@ -11,69 +11,106 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.units.measure.Angle;
import edu.wpi.first.units.measure.AngularMomentum;
import edu.wpi.first.units.measure.Distance;
import edu.wpi.first.units.measure.LinearVelocity;
import edu.wpi.first.units.measure.Per;
import edu.wpi.first.units.measure.Time;
import org.junit.jupiter.api.Test;
class MeasureTest {
@Test
void testBasics() {
Unit<Distance> unit = Units.Feet;
DistanceUnit unit = Units.Feet;
double magnitude = 10;
Measure<Distance> m = unit.of(magnitude);
Distance m = unit.of(magnitude);
assertEquals(unit, m.unit(), "Wrong units");
assertEquals(magnitude, m.magnitude(), 0, "Wrong magnitude");
}
@Test
void testMultiply() {
Measure<Distance> m = Units.Feet.of(1);
Measure<Distance> m2 = m.times(10);
assertEquals(10, m2.magnitude(), 1e-12);
Distance m = Units.Feet.of(1);
Distance m2 = m.times(10);
assertEquals(10, m2.in(Units.Feet), 1e-12);
assertNotSame(m2, m); // make sure state wasn't changed
}
@Test
void testTimesConversionFactor() {
Distance m = Units.Feet.of(10);
Per<AngleUnit, DistanceUnit> conversion = Units.Degrees.of(10).divide(Units.Feet.of(1));
Angle result = m.timesConversionFactor(conversion);
assertEquals(Units.Degrees.of(100), result);
}
@Test
void testTimesConversionFactorComplex() {
Distance m = Units.Feet.of(1);
// Using a complex compound unit here
// (Per<Mult<Mult<Mass, Per<Distance, Time>>, Distance>, Distance>)
Per<AngularMomentumUnit, DistanceUnit> conversion =
Units.KilogramMetersSquaredPerSecond.of(1).divide(Units.Foot.one());
AngularMomentum result = m.timesConversionFactor(conversion);
assertEquals(Units.KilogramMetersSquaredPerSecond.of(1), result);
}
@Test
void testTimesVelocityConversionFactor() {
Time m = Units.Seconds.of(10);
LinearVelocity conversion = Units.MetersPerSecond.of(10);
Distance result = m.timesConversionFactor(conversion);
assertEquals(Units.Meters.of(100), result);
}
@Test
void testDivide() {
Measure<Distance> m = Units.Meters.of(1);
Measure<Distance> m2 = m.divide(10);
Distance m = Units.Meters.of(1);
Distance m2 = m.divide(10);
assertEquals(0.1, m2.magnitude(), 0);
assertNotSame(m2, m);
}
@Test
void testAdd() {
Measure<Distance> m1 = Units.Feet.of(1);
Measure<Distance> m2 = Units.Inches.of(2);
Distance m1 = Units.Feet.of(1);
Distance m2 = Units.Inches.of(2);
assertTrue(m1.plus(m2).isEquivalent(Units.Feet.of(1 + 2 / 12d)));
assertTrue(m2.plus(m1).isEquivalent(Units.Inches.of(14)));
}
@Test
void testSubtract() {
Measure<Distance> m1 = Units.Feet.of(1);
Measure<Distance> m2 = Units.Inches.of(2);
Distance m1 = Units.Feet.of(1);
Distance m2 = Units.Inches.of(2);
assertTrue(m1.minus(m2).isEquivalent(Units.Feet.of(1 - 2 / 12d)));
assertTrue(m2.minus(m1).isEquivalent(Units.Inches.of(-10)));
}
@Test
void testNegate() {
Measure<Distance> m = Units.Feet.of(123);
Measure<Distance> n = m.negate();
assertEquals(-m.magnitude(), n.magnitude(), 1e-12);
assertEquals(m.unit(), n.unit());
void testUnaryMinus() {
Distance m = Units.Feet.of(123);
Distance negated = m.unaryMinus();
assertEquals(-123, negated.in(Units.Feet), 1e-12);
assertEquals(Units.Feet, negated.unit());
}
@Test
void testEquivalency() {
Measure<Distance> inches = Units.Inches.of(12);
Measure<Distance> feet = Units.Feet.of(1);
Distance inches = Units.Inches.of(12);
Distance feet = Units.Feet.of(1);
assertTrue(inches.isEquivalent(feet));
assertTrue(feet.isEquivalent(inches));
}
@Test
void testAs() {
Measure<Distance> m = Units.Inches.of(12);
Distance m = Units.Inches.of(12);
assertEquals(1, m.in(Units.Feet), Measure.EQUIVALENCE_THRESHOLD);
}
@@ -84,7 +121,7 @@ class MeasureTest {
// 144 Kg / (53 ms) = (1000 / 53) * 144 Kg/s = (144,000 / 53) Kg/s
var result = measure.per(dt);
var result = measure.divide(dt);
assertEquals(144_000.0 / 53, result.baseUnitMagnitude(), 1e-5);
assertEquals(Units.Kilograms.per(Units.Milliseconds), result.unit());
}
@@ -94,106 +131,33 @@ class MeasureTest {
var measure = Units.Kilograms.of(144);
var result = measure.per(Units.Millisecond);
assertEquals(Velocity.class, result.unit().getClass());
assertEquals(VelocityUnit.class, result.unit().getClass());
assertEquals(144_000.0, result.baseUnitMagnitude(), 1e-5);
assertEquals(Units.Kilograms.per(Units.Milliseconds), result.unit());
}
@Test
void testTimesMeasure() {
var m1 = Units.Volts.of(1.567);
var m2 = Units.Kilograms.of(8.4e-5);
assertEquals(Units.Volts.mult(Units.Kilograms).of(1.567 * 8.4e-5), m1.times(m2));
}
@Test
void testTimesUnitless() {
var unit = new ExampleUnit(6);
var measure = unit.of(2.5);
var multiplier = Units.Percent.of(125); // 125% or 1.25x
Measure<?> result = measure.times(multiplier);
assertSame(unit, result.unit());
assertEquals(2.5 * 1.25, result.magnitude());
assertEquals(2.5 * 1.25 * 6, result.baseUnitMagnitude());
}
@Test
void testTimesPerWithDimensionalAnalysis() {
var measureA = Units.Feet.of(62); // 62 feet
var measureB = Units.Radians.of(6).per(Units.Inches); // 6 radians per inch
Measure<?> aTimesB = measureA.times(measureB); // (62 feet) * (6 rad/inch) = 4464 rad
assertEquals(Units.Radians, aTimesB.unit());
assertEquals(4464, aTimesB.magnitude(), 1e-12);
Measure<?> bTimesA = measureB.times(measureA); // should be identical to the above
assertTrue(bTimesA.isEquivalent(aTimesB));
assertTrue(aTimesB.isEquivalent(bTimesA));
}
@Test
void testPerTimesPerWithDimensionalAnalysis() {
var measureA = Units.Inches.of(16).per(Units.Volts);
var measureB = Units.Millivolts.of(14).per(Units.Meters);
Measure<?> aTimesB = measureA.times(measureB);
assertEquals(Units.Value, aTimesB.unit());
assertEquals((16 * 25.4 / 1000) * (14 / 1000.0), aTimesB.magnitude());
assertEquals((16 * 25.4 / 1000) * (14 / 1000.0), aTimesB.baseUnitMagnitude());
Measure<?> bTimesA = measureB.times(measureA); // should be identical to the above
assertTrue(bTimesA.isEquivalent(aTimesB));
assertTrue(aTimesB.isEquivalent(bTimesA));
}
@Test
void testPerTimesMeasure() {
var m1 = Units.Feet.per(Units.Milliseconds).of(19);
var m2 = Units.Seconds.of(44);
// 19 ft/ms = 19,000 ft/s
// 19,000 ft/s * 44s = 836,000 ft
assertTrue(Units.Feet.of(836_000).isNear(m1.times(m2), 1e-12));
// 42 ex per foot * 17mm = 42 ex * 17mm / (304.8mm/ft) = 42 * 17 / 304.8 = 2.34252
var exampleUnit = new ExampleUnit(1);
var m3 = exampleUnit.per(Units.Feet).of(42);
var m4 = Units.Millimeters.of(17);
assertEquals(exampleUnit.of(42 * 17 / (12 * 25.4)), m3.times(m4));
}
@Test
void testDivideMeasure() {
// Dimensionless divide
var m1 = Units.Meters.of(6);
var m2 = Units.Value.of(3);
var result = m1.divide(m2);
assertEquals(m1.divide(m2).magnitude(), 2);
assertEquals(result.unit(), Units.Meters);
assertEquals(2, m1.divide(m2).magnitude());
assertEquals(Units.Meters, result.unit());
// Velocity divide
var m3 = Units.Meters.of(8);
var m4 = Units.Meters.per(Units.Second).of(4);
result = m3.divide(m4);
assertEquals(result.magnitude(), 2);
assertEquals(result.unit(), Units.Second);
// Per divide
var time = m3.divide(m4);
assertEquals(2, time.magnitude());
assertEquals(Units.Second, time.unit());
// PerUnit divide
var m5 = Units.Volts.of(6);
var m6 = Units.Volts.per(Units.Meter).of(2);
result = m5.divide(m6);
assertEquals(result.magnitude(), 3);
assertEquals(result.unit(), Units.Meter);
// Fallthrough divide
var m7 = Units.Seconds.of(10);
var m8 = Units.Amps.of(2);
result = m7.divide(m8);
assertEquals(result.magnitude(), 5);
assertEquals(result.unit(), Units.Seconds.per(Units.Amps));
// Same base unit divide
var m9 = Units.Meters.of(8);
var m10 = Units.Meters.of(4);
result = m9.divide(m10);
assertEquals(result.magnitude(), 2);
assertEquals(result.unit(), Units.Value);
// Voltage/(Voltage/Distance) -> Voltage * Distance/Voltage -> Distance
var dist = m5.divide(m6);
assertEquals(3, dist.magnitude());
assertEquals(Units.Meter, dist.unit());
}
@Test
@@ -249,57 +213,6 @@ class MeasureTest {
assertFalse(base.gt(base));
}
@Test
void testTimesScalar() {
var unit = new ExampleUnit(42);
var measure = unit.of(4.2);
var scalar = 18;
var result = measure.times(scalar);
assertNotSame(measure, result);
assertSame(unit, result.unit());
assertEquals(4.2 * 18, result.magnitude());
assertEquals(4.2 * 42 * 18, result.baseUnitMagnitude());
}
@Test
void testPerUnit() {
var unitA = new ExampleUnit(10);
var unitB = new ExampleUnit(12);
var measure = unitA.of(1.2);
var result = measure.per(unitB);
assertEquals(Per.combine(unitA, unitB), result.unit()); // A/B has base equivalent of 10/12
assertEquals(1, result.baseUnitMagnitude()); // 10/12 * 12/10 = 1
assertEquals(measure.magnitude(), result.magnitude());
}
@Test
void testAddMeasureSameUnit() {
var unit = new ExampleUnit(8.2);
var measureA = unit.of(3.1);
var measureB = unit.of(91.6);
var result = measureA.plus(measureB);
assertEquals(unit, result.unit());
assertEquals(94.7, result.magnitude(), 1e-12);
}
@Test
void testAddMeasuresDifferentUnits() {
var unitA = new ExampleUnit(8.2);
var unitB = new ExampleUnit(7.3);
var measureA = unitA.of(5);
var measureB = unitB.of(16);
var aPlusB = measureA.plus(measureB);
assertEquals(unitA, aPlusB.unit());
assertEquals(8.2 * 5 + 7.3 * 16, aPlusB.baseUnitMagnitude(), 1e-12);
assertEquals(5 + (16 * 7.3 / 8.2), aPlusB.magnitude(), 1e-12);
var bPlusA = measureB.plus(measureA);
assertEquals(unitB, bPlusA.unit());
assertEquals(8.2 * 5 + 7.3 * 16, bPlusA.baseUnitMagnitude(), 1e-12);
assertEquals(16 + (5 * 8.2 / 7.3), bPlusA.magnitude(), 1e-12);
}
@Test
void testMinNoArgs() {
var min = Measure.min();
@@ -388,29 +301,29 @@ class MeasureTest {
assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(300)));
assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(10)));
measureCompared = measureCompared.negate();
measureComparing = measureComparing.negate();
measureCompared = measureCompared.unaryMinus();
measureComparing = measureComparing.unaryMinus();
// Negative value with positive tolerance
assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(300)));
assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(10)));
measureCompared = measureCompared.negate();
measureComparing = measureComparing.negate();
measureCompared = measureCompared.unaryMinus();
measureComparing = measureComparing.unaryMinus();
// Positive value with negative tolerance
assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(-300)));
assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(-10)));
measureCompared = measureCompared.negate();
measureComparing = measureComparing.negate();
measureCompared = measureCompared.unaryMinus();
measureComparing = measureComparing.unaryMinus();
// Negative value with negative tolerance.
assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(-300)));
assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(-10)));
measureCompared = measureCompared.negate();
measureComparing = measureComparing.negate();
measureCompared = measureCompared.unaryMinus();
measureComparing = measureComparing.unaryMinus();
// Tolerance exact difference between measures.
assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(200)));