diff --git a/source/docs/software/basic-programming/java-units.rst b/source/docs/software/basic-programming/java-units.rst index 813d2cd59e..865dde2528 100644 --- a/source/docs/software/basic-programming/java-units.rst +++ b/source/docs/software/basic-programming/java-units.rst @@ -1,5 +1,7 @@ # The Java Units Library +.. note:: New for 2025: The units library has been refactored to have unit-specific measurement classes instead of a single generic ``Measure`` class. The new measurement classes have clearer names (``Distance`` instead of ``Measure``, or ``LinearAcceleration`` instead of ``Measure>>``), and implement math operations to return the most specific result types possible instead of a wildcard ``Measure``. + The units library is a tool that helps programmers avoid mistakes related to units of measurement. It does this by keeping track of the units of measurement, and by ensuring that all operations are performed with the correct units. This can help to prevent errors that can lead to incorrect results, such as adding a distance in inches to a distance in meters. An added benefit is readability and maintainability, which also reduces bugs. By making the units of measurement explicit in your code, it becomes easier to read and understand what your code is doing. This can also help to make your code more maintainable, as it is easier to identify and fix errors related to units of measurement. @@ -27,62 +29,72 @@ These concepts are used within the Units Library. For example, the **measure** * The Java units library is available in the ``edu.wpi.first.units`` package. The most relevant classes are: -- The various classes for predefined dimensions, such as [Distance](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/Distance.html) and [Time](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/Time.html) +- The various classes for predefined dimensions, such as [DistanceUnit](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/DistanceUnit.html) and [TimeUnit](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/TimeUnit.html) - [Units](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/Units.html), which contains a set of predefined units. Take a look a the [Units javadoc](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/Units.html) to browse the available units and their types. -- [Measure](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/Measure.html), which is used to tag a value with a unit. +- [Measure](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/Measure.html), which is used to tag a value with a unit, and the dimension-specific implementations like [Distance](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/measure/Distance.html) and [Time](https://github.wpilib.org/allwpilib/docs/development/java/edu/wpi/first/units/measure/Time.html) .. note:: It is recommended to static import ``edu.wpi.first.units.Units.*`` to get full access to all the predefined units. -### Java Generics -Units of measurement can be complex expressions involving various dimension, such as distance, time, and velocity. Nested [generic type parameters](https://docs.oracle.com/javase/tutorial/java/generics/index.html) allow for the definition of units that can represent such complex expressions. Generics are used to keep the library concise, reusable, and extensible, but it tends to be verbose due to the syntax for Java generics. +### Creating Measures -For instance, consider the type ``Measure>``. This type represents a measurement for velocity, where the velocity itself is expressed as a unit of distance per unit of time. This nested structure allows for the representation of units like meters per second or feet per minute. Similarly, the type ``Measure>>`` represents a measurement for a ratio of voltage to velocity. This type is useful for representing quantities like volts per meter per second, the unit of measure for some :ref:`feedforward` gains. +Every dimension has a measurement class with the corresponding name - for example, a ``Distance`` measures distance, ``Time`` measures time, and ``LinearVelocity`` measures linear velocity. To instantiate one of these measurements, call the ``Unit.of`` method on the appropriate unit object. For example, to create a ``Distance`` object representing a distance of 6 inches, you would write: -It's important to note that not all measurements require such complex nested types. For example, the type ``Measure`` is sufficient for representing simple units like meters or feet. However, for more complex units, the use of nested generic type parameters is essential. +```java +Distance wheelDiameter = Inches.of(6); +``` -For local variables, you may choose to use Java's `var` keyword instead of including the full type name. For example, these are equivalent: +Other measures can also be created using their ``Unit.of`` method: ```java -Measure>> v = VoltsPerMeterPerSecond.of(8); -var v = VoltsPerMeterPerSecond.of(8); +Mass kArmMass = Kilograms.of(1.423); +Distance kArmLength = Inches.of(32.25); +Angle kMinArmAngle = Degrees.of(5); +Angle kArmMaxTravel = Rotations.of(0.45); +LinearVelocity kMaxSpeed = MetersPerSecond.of(2.5); ``` -### Creating Measures +.. warning:: Composite units with ``PerUnit`` and ``MultUnit`` have special requirements, and the ``of`` method is not recommended to be used with them + +#### Using Composite Unit Types -The ``Measure`` class is a generic type that represents a magnitude (physical quantity) with its corresponding unit. It provides a consistent and type-safe way to handle different dimensions of measurements, such as distance, angle, and velocity, but abstracts away the particular unit (e.g. meter vs. inch). To create a ``Measure`` object, you call the ``Unit.of`` method on the appropriate unit object. For example, to create a ``Measure`` object representing a distance of 6 inches, you would write: +Due to requirements of inheritance in Java's type system, ``PerUnit`` and ``MultUnit`` cannot return a normal ``Per`` or ``Mult`` object from their ``of`` factory methods. Instead, they need to return a bounded wildcard ``Measure>`` or ``Measure>`` to allow subclasses like ``LinearVelocity`` to return a compatible type. New ``ofNative`` methods are provided to be able to work with known ``Per`` and ``Mult`` objects ```java -Measure wheelDiameter = Inches.of(6); -``` +// Using ofNative: +Per kP = VoltsPerMeter.ofNative(1); +kP.in(VoltsPerMeter); // 1.0 -Other measures can also be created using their ``Unit.of`` method: +Measure output = kP.timesDivisor(Meters.of(1)); +output.in(Volts); // 1.0 -```java -Measure kArmMass = Kilograms.of(1.423); -Measure kArmLength = Inches.of(32.25); -Measure kMinArmAngle = Degrees.of(5); -Measure kArmMaxTravel = Rotations.of(0.45); -Measure> kMaxSpeed = MetersPerSecond.of(2.5); +// Without ofNative +Measure> kP = VoltsPerMeter.of(1); +kP.in(VoltsPerMeter); // Compilation error! + +Measure output = kP.times(Meters.of(1)); // The compiler can't know what unit this is +output.in(Volts); // Compilation error! ``` ### Performing Calculations -The ``Measure`` class also supports arithmetic operations, such as addition, subtraction, multiplication, and division. These are done by calling methods on the objects. These operations always ensure that the units are compatible before performing the calculation, and they return a new ``Measure`` object. For example, you can add two ``Measure`` objects together, even if they have different units: +The ``Measure`` class also supports arithmetic operations, such as addition, subtraction, multiplication, and division. These are done by calling methods on the objects. These operations always ensure that the units are compatible before performing the calculation, and they return a new ``Measure`` object. For example, you can add two ``Distance`` objects together, even if they have different units: ```java -Measure distance1 = Inches.of(10); -Measure distance2 = Meters.of(0.254); -Measure totalDistance = distance1.plus(distance2); +Distance distance1 = Inches.of(10); +Distance distance2 = Meters.of(0.254); +Distance totalDistance = distance1.plus(distance2); ``` -In this code, the units library will automatically convert the measures to the same unit before adding the two distances. The resulting ``totalDistance`` object will be a new ``Measure`` object that has a value of 0.508 meters, or 20 inches. +In this code, the units library will automatically convert the measures to the same unit before adding the two distances. The resulting ``totalDistance`` object will be a new ``Distance`` object that has a value of 0.508 meters, or 20 inches. + +.. note:: Mathematical operations are type safe. It is impossible to add a distance to a time, or subtract an angle from a voltage. However, multiplication and division operations make a best-effort attempt to return results in the most appropriate unit type; dividing a distance by time results in a ``LinearVelocity`` measurement, and multiplying it by time returns a ``Distance``. This example combines the wheel diameter and gear ratio to calculate the distance per rotation of the wheel: ```java -Measure wheelDiameter = Inches.of(3); +Distance wheelDiameter = Inches.of(3); double gearRatio = 10.48; -Measure distancePerRotation = wheelDiameter.times(Math.PI).divide(gearRatio); +Distance distancePerRotation = wheelDiameter.times(Math.PI).divide(gearRatio); ``` .. warning:: By default, arithmetic operations create new ``Measure`` instances for their results. See :ref:`Java Garbage Collection` for discussion on creating a large number of short-lived objects. See also, the `Mutability and Object Creation`_ section below for a possible workaround. @@ -92,10 +104,11 @@ Measure distancePerRotation = wheelDiameter.times(Math.PI).divide(gear Unit conversions can be done by calling ``Measure.in(Unit)``. The Java type system will prevent units from being converted between incompatible types, such as distances to angles. The returned values will be bare ``double`` values without unit information - it is up to you, the programmer, to interpret them correctly! It is strongly recommended to only use unit conversions when interacting with APIs that do not support the units library. ```java -Measure> kMaxVelocity = FeetPerSecond.of(12.5); -Measure>> kMaxAcceleration = FeetPerSecond.per(Second).of(22.9); +LinearVelocity kMaxVelocity = FeetPerSecond.of(12.5); +LinearAcceleration kMaxAcceleration = FeetPerSecond.per(Second).of(22.9); kMaxVelocity.in(MetersPerSecond); // => OK! Returns 3.81 -kMaxVelocity.in(RadiansPerSecond); // => Compile error! Velocity cannot be converted to Unit> +kMaxVelocity.in(RadiansPerSecond); // => Compile error! LinearVelocity cannot be converted to AngularVelocity + // The WPILib math libraries use SI metric units, so we have to convert to meters: TrapezoidProfile.Constraints kDriveConstraints = new TrapezoidProfile.Constraints( maxVelocity.in(MetersPerSecond), @@ -108,9 +121,9 @@ TrapezoidProfile.Constraints kDriveConstraints = new TrapezoidProfile.Constraint Pulling all of the concepts together, we can create an example that calculates the end effector position of an arm mechanism: ```java -Measure armLength = Feet.of(3).plus(Inches.of(4.25)); -Measure endEffectorX = armLength.times(Math.cos(getArmAngle().in(Radians))); -Measure endEffectorY = armLength.times(Math.sin(getArmAngle().in(Radians))); +Distance armLength = Feet.of(3).plus(Inches.of(4.25)); +Distance endEffectorX = armLength.times(Math.cos(getArmAngle().in(Radians))); +Distance endEffectorY = armLength.times(Math.sin(getArmAngle().in(Radians))); ``` ### Human-readable Formatting @@ -122,7 +135,7 @@ The ``Measure`` class has methods that can be used to get a human-readable repre ## Mutability and Object Creation -To reduce the number of object instances you create, and reduce memory usage, a special ``MutableMeasure`` class is available. You may want to consider using mutable objects if you are using the units library repeatedly, such as in the robot's periodic loop. See :ref:`Java Garbage Collection` for more discussion on creating a large number of short-lived objects. +To reduce the number of object instances you create, and reduce memory usage, a special ``MutableMeasure`` class is available, with unit-specific subtypes like ``MutDistance`` and ``MutTime``. You may want to consider using mutable objects if you are using the units library repeatedly, such as in the robot's periodic loop. See :ref:`Java Garbage Collection` for more discussion on creating a large number of short-lived objects. Mutable measures can be created in a similar way to regular, immutable measures using the ``Unit.mutable`` method (instead of ``Unit.of``). ``MutableMeasure`` allows the internal state of the object to be updated, such as with the results of arithmetic operations, to avoid allocating new objects. Special care needs to be taken when mutating a measure because it will change the value every place that instance is referenced. If the object will be exposed as part of a public method, have that method return a regular ``Measure`` in its signature to prevent the caller from modifying your internal state. @@ -150,7 +163,7 @@ For the full list of methods and API documentation, see [the MutableMeasure API +-------------------------------+--------------------------------------------------------------------------------------------------+ ```java -MutableMeasure measure = MutableMeasure.zero(Feet); +MutDistance measure = Feet.mutable(0); measure.mut_plus(10, Inches); // 0.8333 feet measure.mut_plus(Inches.of(10)); // 1.6667 feet measure.mut_minus(5, Inches); // 1.25 feet @@ -172,27 +185,30 @@ public class Arm { // Note the two ephemeral object allocations for the Feet.of and Inches.of calls. // Because this is a constant value computed just once, they will easily be garbage collected without // any problems with memory use or loop timing jitter. - private static final Measure kArmLength = Feet.of(3).plus(Inches.of(4.25)); + private static final Distance kArmLength = Feet.of(3).plus(Inches.of(4.25)); + // Angle and X/Y locations will likely be called in the main robot loop, let's store them in a MutableMeasure // to avoid allocating lots of short-lived objects - private final MutableMeasure m_angle = MutableMeasure.zero(Degrees); - private final MutableMeasure m_endEffectorX = MutableMeasure.zero(Feet); - private final MutableMeasure m_endEffectorY = MutableMeasure.zero(Feet); + private final MutAngle m_angle = Degrees.mutable(0); + private final MutDistance m_endEffectorX = Feet.mutable(0); + private final MutDistance m_endEffectorY = Feet.mutable(0); private final Encoder m_encoder = new Encoder(...); - public Measure getEndEffectorX() { - m_endEffectorX.mut_replace( + + public Distance getEndEffectorX() { + return m_endEffectorX.mut_replace( Math.cos(getAngle().in(Radians)) * kArmLength.in(Feet), // the new magnitude to store Feet // the units of the new magnitude ); - return m_endEffectorX; } - public Measure getEndEffectorY() { + + public Distance getEndEffectorY() { // An alternative approach so we don't have to unpack and repack the units m_endEffectorY.mut_replace(kArmLength); m_endEffectorY.mut_times(Math.sin(getAngle().in(Radians))); return m_endEffectorY; } - public Measure getAngle() { + + public Angle getAngle() { double rawAngle = m_encoder.getPosition(); m_angle.mut_replace(rawAngle, Degrees); // NOTE: the encoder must be configured with distancePerPulse in terms of degrees! return m_angle; @@ -205,13 +221,13 @@ public class Arm { Can you spot the bug in this code? ```java -private Measure m_lastDistance; -public Measure calculateDelta(Measure currentDistance) { +private Distance m_lastDistance; +public Distance calculateDelta(Distance currentDistance) { if (m_lastDistance == null) { m_lastDistance = currentDistance; return currentDistance; } else { - Measure delta = currentDistance.minus(m_lastDistance); + Distance delta = currentDistance.minus(m_lastDistance); m_lastDistance = currentDistance; return delta; } @@ -221,7 +237,7 @@ public Measure calculateDelta(Measure currentDistance) { If we run the ``calculateDelta`` method a few times, we can see a pattern: ```java -MutableMeasure distance = MutableMeasure.zero(Inches); +MutDistance distance = Inches.mutable(0); distance.mut_plus(10, Inches); calculateDelta(distance); // expect 10 inches and get 10 - good! distance.mut_plus(2, Inches); @@ -230,15 +246,15 @@ distance.mut_plus(8, Inches); calculateDelta(distance); // expect 8 inches, but get 0 instead! ``` -This is because the ``m_lastDistance`` field is a reference to the *same* ``MutableMeasure`` object as the input! Effectively, the delta is calculated as (currentDistance - currentDistance) on every call after the first, which naturally always returns zero. One solution would be to track ``m_lastDistance`` as a *copy* of the input measure to take a snapshot; however, this approach does incur one extra object allocation for the copy. If you need to be careful about object allocations, ``m_lastDistance`` could also be stored as a ``MutableMeasure``. +This is because the ``m_lastDistance`` field is a reference to the *same* ``MutDistance`` object as the input! Effectively, the delta is calculated as (currentDistance - currentDistance) on every call after the first, which naturally always returns zero. One solution would be to track ``m_lastDistance`` as a *copy* of the input measure to take a snapshot; however, this approach does incur one extra object allocation for the copy. If you need to be careful about object allocations, ``m_lastDistance`` could also be stored as a ``MutDistance``. .. tab-set:: .. tab-item:: Immutable Copies ```java - private Measure m_lastDistance; - public Measure calculateDelta(Measure currentDistance) { + private Distance m_lastDistance; + public Distance calculateDelta(Distance currentDistance) { if (m_lastDistance == null) { m_lastDistance = currentDistance.copy(); return currentDistance; @@ -253,9 +269,9 @@ This is because the ``m_lastDistance`` field is a reference to the *same* ``Muta .. tab-item:: Zero-allocation Mutables ```java - private final MutableMeasure m_lastDistance = MutableMeasure.zero(Meters); - private final MutableMeasure m_delta = MutableMeasure.zero(Meters); - public Measure calculateDelta(Measure currentDistance) { + private final MutDistance m_lastDistance = Meters.mutable(0); + private final MutDistance m_delta = Meters.mutable(0); + public Distance calculateDelta(Distance currentDistance) { // m_delta = currentDistance - m_lastDistance m_delta.mut_replace(currentDistance); m_delta.mut_minus(m_lastDistance); @@ -276,9 +292,9 @@ There are four ways to define a new unit that isn't already present in the libra New units can be defined as combinations of existing units using the ``Unit.mult`` and ``Unit.per`` methods. ```java -Per VoltsPerInch = Volts.per(Inch); -Velocity KgPerSecond = Kilograms.per(Second); -Mult> Newtons = Kilograms.mult(MetersPerSecondSquared); +PerUnit VoltsPerInch = Volts.per(Inch); +VelocityUnit KgPerSecond = Kilograms.per(Second); // Could also be declared as PerUnit +DistanceUnit FootMinutesPerSecond = FeetPerSecond.mult(Minutes); ``` Using ``mult`` and ``per`` will store the resulting unit. Every call will return the same object to avoid unnecessary allocations and garbage collector pressure. @@ -294,10 +310,10 @@ public void robotPeriodic() { .. note:: Calling ``Unit.per(Time)`` will return a ``Velocity`` unit, which is different from and incompatible with a ``Per`` unit! -New dimensions can also be created by subclassing ``Unit`` and implementing the two constructors. Note that ``Unit`` is also a parameterized generic type, where the generic type argument is self-referential; ``Distance`` is a ``Unit``. This is what allows us to have stronger guarantees in the type system to prevent conversions between unrelated dimensions. +New dimensions can also be created by subclassing ``Unit`` and implementing the two constructors. Dimension-specific measurement types are recommended, but take considerable effort to implement all the unit-specific math operations. ```java -public class ElectricCharge extends Unit { +public class ElectricChargeUnit extends Unit { public ElectricCharge(double baseUnitEquivalent, String name, String symbol) { super(ElectricCharge.class, baseUnitEquivalent, name, symbol); } @@ -305,7 +321,49 @@ public class ElectricCharge extends Unit { public ElectricCharge(UnaryFunction toBaseConverter, UnaryFunction fromBaseConverter, String name, String symbol) { super(ElectricCharge.class, toBaseConverter, fromBaseConverter, name, symbol); } + + @Override + public ElectricChargeUnit getBaseUnit() { + // The base method must be overridden in order to return the correct type + return (ElectricChargeUnit) super.getBaseUnit(); + } + + @Override + public Measure of(double magnitude) { + return ImmutableMeasure.ofRelativeUnits(magnitude, this); + } + + @Override + public Measure ofBaseUnits(double baseUnitMagnitude) { + return ImmutableMeasure.ofBaseUnits(baseUnitMagnitude, this); + } + + @Override + public Measure zero() { + return (Measure) super.zero(); + } + + @Override + public Measure one() { + return (Measure) super.one(); + } + + @Override + public MutableMeasure mutable(double magnitude) { + return new GenericMutableMeasureImpl(magnitude, toBaseUnits(magnitude), this); + } + + @Override + public VelocityUnit per(TimeUnit period) { + // Note: technically, this would return a CurrentUnit, since electric charge per time is current (measured in Amperes) + return VelocityUnit.combine(this, period); + } + + public double convertFrom(double magnitude, ElectricChargeUnit otherUnit) { + return fromBaseUnits(otherUnit.toBaseUnits(magnitude)); + } } + public static final ElectricCharge Coulomb = new ElectricCharge(1, "Coulomb", "C"); public static final ElectricCharge ElectronCharge = new ElectricCharge(1.60217646e-19, "Electron Charge", "e"); public static final ElectricCharge AmpHour = new ElectricCharge(3600, "Amp Hour", "Ah");