#!/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 argparse import inspect import re from pathlib import Path from jinja2 import Environment, FileSystemLoader def output(output_dir, outfn: str, contents: str): output_dir.mkdir(parents=True, exist_ok=True) output_file = output_dir / outfn output_file.write_text(contents, encoding="utf-8", newline="\n") # 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", "Resistance", "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(""" public Frequency asFrequency() { return Hertz.of(baseUnitMagnitude()); } """), }, "Current": { "base_unit": "Amps", "multiply": {"Voltage": "Power", "Resistance": "Voltage"}, "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", "Resistance": "Resistance", "Temperature": "Temperature", "Time": "Time", "Torque": "Torque", "Voltage": "Voltage", }, "divide": { "Time": "Frequency", # TODO: # "AngularVelocity": "Per", # "AngularAcceleration": "Per", # "LinearVelocity": "Per", # "LinearAcceleration": "Per", # "Velocity": "Per", # "Acceleration": "Per> # "Per": "Per" }, }, "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. */ public 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(""" public Measure timesDivisor(Measure multiplier) { return (Measure) baseUnit().numerator().ofBaseUnits(baseUnitMagnitude() * multiplier.baseUnitMagnitude()); } public Measure> 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"}, }, "Resistance": { "base_unit": "Ohms", "multiply": { "Current": "Voltage", }, "divide": {}, }, "Temperature": {"base_unit": "Kelvin", "multiply": {}, "divide": {}}, "Time": { "base_unit": "Seconds", "multiply": { "Frequency": "Dimensionless", "AngularVelocity": "Angle", "AngularAcceleration": "AngularVelocity", "LinearVelocity": "Distance", "LinearAcceleration": "LinearVelocity", # TODO: # "Acceleration": "Velocity" # "Velocity": "Measure" }, "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` (i.e. a time per unit time ratio) "Time": "Dimensionless" }, "extra": inspect.cleandoc(""" public 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": {"Time": {"implementation": inspect.cleandoc(""" @Override public Measure times(Time multiplier) { return (Measure) unit().numerator().ofBaseUnits(baseUnitMagnitude() * multiplier.baseUnitMagnitude()); } """)}}, "divide": {}, }, "Voltage": { "base_unit": "Volts", "multiply": { "Current": "Power", }, "divide": { "Resistance": "Current", "Current": "Resistance", }, }, } 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 indent(multiline_string, indentation): """ Indents a multiline string by `indentation` number of spaces """ return "\n".join( list(map(lambda line: " " * indentation + line, multiline_string.split("\n"))) ) def generate_units(output_directory: Path, template_directory: Path): env = Environment( loader=FileSystemLoader(template_directory / "main/java"), autoescape=False, keep_trailing_newline=True, ) interfaceTemplate = env.get_template("Measure-implementation.java.jinja") rootPath = output_directory / "main/java/org/wpilib/units" helpers = { "type_decl": type_decl, "type_usage": type_usage, "generics_list": generics_list, "generics_usage": generics_usage, "mtou": mtou, "indent": indent, } for unit_name in UNIT_CONFIGURATIONS: interfaceContents = interfaceTemplate.render( name=unit_name, math_units=MATH_OPERATION_UNITS, config=UNIT_CONFIGURATIONS, helpers=helpers, ) output(rootPath / "measure", f"{unit_name}.java", interfaceContents) def main(): script_path = Path(__file__).resolve() dirname = script_path.parent parser = argparse.ArgumentParser() parser.add_argument( "--output_directory", help="Optional. If set, will output the generated files to this directory, otherwise it will use a path relative to the script", default=dirname / "src/generated", type=Path, ) parser.add_argument( "--template_root", help="Optional. If set, will use this directory as the root for the jinja templates", default=dirname / "src/generate", type=Path, ) args = parser.parse_args() generate_units(args.output_directory, args.template_root) if __name__ == "__main__": main()