mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
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.
363 lines
11 KiB
Python
Executable File
363 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) FIRST and other WPILib contributors.
|
|
# Open Source Software; you can modify and/or share it under the terms of
|
|
# the WPILib BSD license file in the root directory of this project.
|
|
|
|
# This script generates unit-specific interfaces and mutable and immutable implementations of
|
|
# those interfaces.
|
|
# Generated files will be located in wpiunits/src/generated/main/
|
|
|
|
import inspect
|
|
import os
|
|
import re
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
|
|
def output(outPath, outfn, contents):
|
|
if not os.path.exists(outPath):
|
|
os.makedirs(outPath)
|
|
|
|
outpathname = f"{outPath}/{outfn}"
|
|
|
|
if os.path.exists(outpathname):
|
|
with open(outpathname, "r") as f:
|
|
if f.read() == contents:
|
|
return
|
|
|
|
# File either doesn't exist or has different contents
|
|
with open(outpathname, "w", newline="\n") as f:
|
|
f.write(contents)
|
|
|
|
|
|
# The units for which multiply and divide mathematical operations are defined
|
|
MATH_OPERATION_UNITS = [
|
|
"Acceleration<?>",
|
|
"Angle",
|
|
"AngularAcceleration",
|
|
"AngularMomentum",
|
|
"AngularVelocity",
|
|
"Current",
|
|
"Dimensionless",
|
|
"Distance",
|
|
"Energy",
|
|
"Force",
|
|
"Frequency",
|
|
"LinearAcceleration",
|
|
"LinearMomentum",
|
|
"LinearVelocity",
|
|
"Mass",
|
|
"MomentOfInertia",
|
|
"Mult<?, ?>",
|
|
"Per<?, ?>",
|
|
"Power",
|
|
"Temperature",
|
|
"Time",
|
|
"Torque",
|
|
"Velocity<?>",
|
|
"Voltage",
|
|
]
|
|
|
|
# Configurations for all generated units
|
|
UNIT_CONFIGURATIONS = {
|
|
"Acceleration": {
|
|
"base_unit": "unit()",
|
|
"generics": {"D": {"extends": "Unit"}},
|
|
"multiply": {},
|
|
"divide": {},
|
|
},
|
|
"Angle": {
|
|
"base_unit": "Radians",
|
|
"multiply": {"Frequency": "AngularVelocity"},
|
|
"divide": {"Time": "AngularVelocity"},
|
|
},
|
|
"AngularAcceleration": {
|
|
"base_unit": "RadiansPerSecondPerSecond",
|
|
"multiply": {"Time": "AngularVelocity"},
|
|
"divide": {"Frequency": "AngularVelocity"},
|
|
},
|
|
"AngularMomentum": {
|
|
"base_unit": "KilogramMetersSquaredPerSecond",
|
|
"multiply": {},
|
|
"divide": {"AngularVelocity": "MomentOfInertia"},
|
|
},
|
|
"AngularVelocity": {
|
|
"base_unit": "RadiansPerSecond",
|
|
"multiply": {"Time": "Angle", "Frequency": "AngularAcceleration"},
|
|
"divide": {"Time": "AngularAcceleration"},
|
|
"extra": inspect.cleandoc(
|
|
"""
|
|
default Frequency asFrequency() { return Hertz.of(baseUnitMagnitude()); }
|
|
"""
|
|
),
|
|
},
|
|
"Current": {"base_unit": "Amps", "multiply": {"Voltage": "Power"}, "divide": {}},
|
|
"Dimensionless": {
|
|
"base_unit": "Value",
|
|
"multiply": {
|
|
"Angle": "Angle",
|
|
"AngularAcceleration": "AngularAcceleration",
|
|
"AngularMomentum": "AngularMomentum",
|
|
"AngularVelocity": "AngularVelocity",
|
|
"Current": "Current",
|
|
"Dimensionless": "Dimensionless",
|
|
"Distance": "Distance",
|
|
"Energy": "Energy",
|
|
"Force": "Force",
|
|
"Frequency": "Frequency",
|
|
"LinearAcceleration": "LinearAcceleration",
|
|
"LinearMomentum": "LinearMomentum",
|
|
"LinearVelocity": "LinearVelocity",
|
|
"Mass": "Mass",
|
|
"MomentOfInertia": "MomentOfInertia",
|
|
"Power": "Power",
|
|
"Temperature": "Temperature",
|
|
"Time": "Time",
|
|
"Torque": "Torque",
|
|
"Voltage": "Voltage",
|
|
},
|
|
"divide": {
|
|
"Time": "Frequency",
|
|
# TODO:
|
|
# "AngularVelocity": "Per<TimeUnit, AngleUnit>",
|
|
# "AngularAcceleration": "Per<TimeUnit, AngularVelocityUnit>",
|
|
# "LinearVelocity": "Per<TimeUnit, DistanceUnit>",
|
|
# "LinearAcceleration": "Per<TimeUnit, LinearVelocityUnit>",
|
|
# "Velocity<?>": "Per<TimeUnit, ?>",
|
|
# "Acceleration<?>": "Per<TimeUnit, VelocityUnit<?>>
|
|
# "Per<N, D>": "Per<D, N>"
|
|
},
|
|
},
|
|
"Distance": {
|
|
"base_unit": "Meters",
|
|
"multiply": {
|
|
"Frequency": "LinearVelocity",
|
|
# Distance x Force = Torque
|
|
# Force x Distance = Energy
|
|
"Force": "Torque",
|
|
},
|
|
"divide": {"Time": "LinearVelocity", "LinearVelocity": "Time"},
|
|
},
|
|
"Energy": {
|
|
"base_unit": "Joules",
|
|
"multiply": {"Frequency": "Power"},
|
|
"divide": {"Time": "Power"},
|
|
},
|
|
"Force": {
|
|
"base_unit": "Newtons",
|
|
"multiply": {
|
|
# Distance x Force = Torque
|
|
# Force x Distance = Energy
|
|
"Distance": "Energy"
|
|
},
|
|
"divide": {"Mass": "LinearAcceleration", "LinearAcceleration": "Mass"},
|
|
},
|
|
"Frequency": {
|
|
"base_unit": "Hertz",
|
|
"multiply": {
|
|
"Time": "Dimensionless",
|
|
"Distance": "LinearVelocity",
|
|
"LinearVelocity": "LinearAcceleration",
|
|
"Angle": "AngularVelocity",
|
|
"AngularVelocity": "AngularAcceleration",
|
|
},
|
|
"divide": {},
|
|
"extra": inspect.cleandoc(
|
|
"""
|
|
/** Converts this frequency to the time period between cycles. */
|
|
default Time asPeriod() { return Seconds.of(1 / baseUnitMagnitude()); }
|
|
"""
|
|
),
|
|
},
|
|
"LinearAcceleration": {
|
|
"base_unit": "MetersPerSecondPerSecond",
|
|
"multiply": {"Time": "LinearVelocity"},
|
|
"divide": {"Frequency": "LinearVelocity"},
|
|
},
|
|
"LinearMomentum": {
|
|
"base_unit": "KilogramMetersPerSecond",
|
|
"multiply": {"Frequency": "Force"},
|
|
"divide": {"Mass": "LinearVelocity", "LinearVelocity": "Mass", "Time": "Force"},
|
|
},
|
|
"LinearVelocity": {
|
|
"base_unit": "MetersPerSecond",
|
|
"multiply": {"Time": "Distance", "Frequency": "LinearAcceleration"},
|
|
"divide": {"Time": "LinearAcceleration"},
|
|
},
|
|
"Mass": {
|
|
"base_unit": "Kilograms",
|
|
"multiply": {"LinearAcceleration": "Force"},
|
|
"divide": {},
|
|
},
|
|
"MomentOfInertia": {
|
|
"base_unit": "KilogramSquareMeters",
|
|
"multiply": {"AngularVelocity": "AngularMomentum"},
|
|
"divide": {},
|
|
},
|
|
"Mult": {
|
|
"base_unit": "unit()",
|
|
"generics": {"A": {"extends": "Unit"}, "B": {"extends": "Unit"}},
|
|
"multiply": {},
|
|
"divide": {},
|
|
},
|
|
"Per": {
|
|
"base_unit": "unit()",
|
|
"generics": {"Dividend": {"extends": "Unit"}, "Divisor": {"extends": "Unit"}},
|
|
"multiply": {},
|
|
"divide": {},
|
|
"extra": inspect.cleandoc(
|
|
"""
|
|
default Measure<Dividend> timesDivisor(Measure<? extends Divisor> multiplier) {
|
|
return (Measure<Dividend>) baseUnit().numerator().ofBaseUnits(baseUnitMagnitude() * multiplier.baseUnitMagnitude());
|
|
}
|
|
|
|
default Measure<? extends PerUnit<Divisor, Dividend>> reciprocal() {
|
|
// May return a velocity if Divisor == TimeUnit, so we can't guarantee a "Per" instance
|
|
return baseUnit().reciprocal().ofBaseUnits(1 / baseUnitMagnitude());
|
|
}
|
|
"""
|
|
),
|
|
},
|
|
"Power": {
|
|
"base_unit": "Watts",
|
|
"multiply": {
|
|
"Time": "Energy",
|
|
},
|
|
"divide": {"Voltage": "Current", "Current": "Voltage", "Energy": "Frequency"},
|
|
},
|
|
"Temperature": {"base_unit": "Kelvin", "multiply": {}, "divide": {}},
|
|
"Time": {
|
|
"base_unit": "Seconds",
|
|
"multiply": {
|
|
"Frequency": "Dimensionless",
|
|
"AngularVelocity": "Angle",
|
|
"AngularAcceleration": "AngularVelocity",
|
|
"LinearVelocity": "Distance",
|
|
"LinearAcceleration": "LinearVelocity",
|
|
# TODO:
|
|
# "Acceleration<D>": "Velocity<D>"
|
|
# "Velocity<D>": "Measure<D>"
|
|
},
|
|
"divide": {
|
|
# Time specifically needs this to be called out so generated methods like
|
|
# `per(TimeUnit)` or `divide(Time)` will return dimensionless values instead of
|
|
# `Velocity<TimeUnit>` (i.e. a time per unit time ratio)
|
|
"Time": "Dimensionless"
|
|
},
|
|
"extra": inspect.cleandoc(
|
|
"""
|
|
default Frequency asFrequency() { return Hertz.of(1 / baseUnitMagnitude()); }
|
|
"""
|
|
),
|
|
},
|
|
"Torque": {
|
|
"base_unit": "NewtonMeters",
|
|
"multiply": {},
|
|
"divide": {"Distance": "Force", "Force": "Distance"},
|
|
},
|
|
"Velocity": {
|
|
"base_unit": "unit()",
|
|
"generics": {"D": {"extends": "Unit"}},
|
|
"multiply": {},
|
|
"divide": {},
|
|
},
|
|
"Voltage": {"base_unit": "Volts", "multiply": {"Current": "Power"}, "divide": {}},
|
|
}
|
|
|
|
|
|
def generics_list(measure_name):
|
|
if "generics" in UNIT_CONFIGURATIONS[measure_name]:
|
|
args = []
|
|
for name, config in UNIT_CONFIGURATIONS[measure_name]["generics"].items():
|
|
if "extends" in config:
|
|
args.append("{} extends {}".format(name, config["extends"]))
|
|
elif "super" in config:
|
|
args.append("{} super {}".format(name, config["super"]))
|
|
else:
|
|
args.append(name)
|
|
|
|
return "<{}>".format(", ".join(args))
|
|
else:
|
|
return ""
|
|
|
|
|
|
def generics_usage(measure_name):
|
|
if "generics" in UNIT_CONFIGURATIONS[measure_name]:
|
|
args = UNIT_CONFIGURATIONS[measure_name]["generics"].keys()
|
|
|
|
return "<{}>".format(", ".join(args))
|
|
else:
|
|
return ""
|
|
|
|
|
|
def type_decl(measure_name):
|
|
return measure_name + generics_list(measure_name)
|
|
|
|
|
|
def type_usage(measure_name):
|
|
return measure_name + generics_usage(measure_name)
|
|
|
|
|
|
# measure-to-unit
|
|
def mtou(measure_name):
|
|
if (
|
|
measure_name in UNIT_CONFIGURATIONS
|
|
and "generics" in UNIT_CONFIGURATIONS[measure_name]
|
|
):
|
|
return "{}Unit{}".format(measure_name, generics_usage(measure_name))
|
|
else:
|
|
regex = re.compile(r"^(.*?)(<.*>)?$")
|
|
return re.sub(regex, "\\1Unit\\2", measure_name)
|
|
|
|
|
|
def main():
|
|
|
|
dirname, _ = os.path.split(os.path.abspath(__file__))
|
|
|
|
env = Environment(
|
|
loader=FileSystemLoader(f"{dirname}/src/generate/main/java"),
|
|
autoescape=False,
|
|
keep_trailing_newline=True,
|
|
)
|
|
|
|
interfaceTemplate = env.get_template("Measure-interface.java.jinja")
|
|
immutableTemplate = env.get_template("Measure-immutable.java.jinja")
|
|
mutableTemplate = env.get_template("Measure-mutable.java.jinja")
|
|
rootPath = f"{dirname}/src/generated/main/java/edu/wpi/first/units"
|
|
|
|
helpers = {
|
|
"type_decl": type_decl,
|
|
"type_usage": type_usage,
|
|
"generics_list": generics_list,
|
|
"generics_usage": generics_usage,
|
|
"mtou": mtou,
|
|
}
|
|
|
|
for unit_name in UNIT_CONFIGURATIONS:
|
|
interfaceContents = interfaceTemplate.render(
|
|
name=unit_name,
|
|
math_units=MATH_OPERATION_UNITS,
|
|
config=UNIT_CONFIGURATIONS,
|
|
helpers=helpers,
|
|
)
|
|
immutableContents = immutableTemplate.render(
|
|
name=unit_name,
|
|
units=MATH_OPERATION_UNITS,
|
|
config=UNIT_CONFIGURATIONS,
|
|
helpers=helpers,
|
|
)
|
|
mutableContents = mutableTemplate.render(
|
|
name=unit_name,
|
|
units=MATH_OPERATION_UNITS,
|
|
config=UNIT_CONFIGURATIONS,
|
|
helpers=helpers,
|
|
)
|
|
|
|
output(f"{rootPath}/measure", f"{unit_name}.java", interfaceContents)
|
|
output(f"{rootPath}/measure", f"Immutable{unit_name}.java", immutableContents)
|
|
output(f"{rootPath}/measure", f"Mut{unit_name}.java", mutableContents)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|