Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit-awareness library #1218

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package gov.nasa.jpl.aerie.contrib.streamline.unit_aware;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static java.util.Collections.reverseOrder;
import static java.util.Map.Entry.comparingByValue;
import static java.util.stream.Collectors.joining;

/**
* A kind of quantity, which can be measured.
* For example, length, time, energy, or data rate.
*
* <p>
* Quantities with the same dimension but different units, like meters and miles,
* can be compared, added, and subtracted.
* Quantities with different dimensions, like meters and seconds,
* cannot be added or subtracted, and are never equal.
* </p>
*
* <p>
* Base dimensions are declared using {@link Dimension#createBase}. Base dimensions are definitionally all distinct
* from each other, and are distinct from all combinations of other base dimensions.
* </p>
*
* <p>
* Dimensions can be composed by multiplication, division, and exponentiation by a constant
* to derive new dimensions, and composite units correlate to the composite dimension.
* For example, Energy is the dimension defined as Mass * Length^2 / Time^2, and
* Newton is a unit of Energy defined as Kilogram * Meter^2 / Second^2.
* Internally, all dimensions are a map from base dimensions to their power. For example, Mass is stored (loosely) as
* {@code {"Mass": 1}} and Energy is {@code {"Mass": 1, "Length": 2, "Time": -2}}.
* </p>
*/
public sealed interface Dimension {
Dimension SCALAR = new DerivedDimension(Map.of());

Map<BaseDimension, Rational> basePowers();
boolean isBase();


default Dimension multiply(Dimension other) {
var resultBasePowers = new HashMap<>(basePowers());
for (var dimensionPower : other.basePowers().entrySet()) {
var power = dimensionPower.getValue();
resultBasePowers.compute(
dimensionPower.getKey(),
(k, p) -> p == null ? power : power.add(p));
}
return create(resultBasePowers);
}

default Dimension divide(Dimension other) {
var resultBasePowers = new HashMap<>(basePowers());
for (var dimensionPower : other.basePowers().entrySet()) {
var power = dimensionPower.getValue().negate();
resultBasePowers.compute(
dimensionPower.getKey(),
(k, p) -> p == null ? power : power.add(p));
}
return create(resultBasePowers);
}

default Dimension power(Rational power) {
var resultBasePowers = new HashMap<BaseDimension, Rational>();
for (var dimensionPower : basePowers().entrySet()) {
resultBasePowers.put(dimensionPower.getKey(), dimensionPower.getValue().multiply(power));
}
return create(resultBasePowers);
}

private static Dimension create(Map<BaseDimension, Rational> basePowers) {
var normalizedBasePowers = new HashMap<BaseDimension, Rational>();
for (var entry : basePowers.entrySet()) {
if (!entry.getValue().equals(Rational.ZERO)) {
normalizedBasePowers.put(entry.getKey(), entry.getValue());
}
}

if (normalizedBasePowers.isEmpty()) {
return Dimension.SCALAR;
} else if (normalizedBasePowers.size() == 1) {
final var solePower = normalizedBasePowers.entrySet().stream().findAny().get();
if (solePower.getValue().equals(Rational.ONE)) {
// This actually *is* the base dimension, so return that instead
// Normalizing like this lets us bootstrap using reference equality on base dimensions.
return solePower.getKey();
}
}

// Otherwise, this is some composite dimension, build it anew.
return new DerivedDimension(normalizedBasePowers);
}

static Dimension createBase(String name) {
return new BaseDimension(name);
}

final class BaseDimension implements Dimension {
public final String name;

private BaseDimension(final String name) {
this.name = name;
}

@Override
public Map<BaseDimension, Rational> basePowers() {
return Map.of(this, Rational.ONE);
}

@Override
public boolean isBase() {
return true;
}

// Reference equality is sufficient here. Do *not* override equals/hashCode.

@Override
public String toString() {
return name;
}
}

final class DerivedDimension implements Dimension {
private final Map<BaseDimension, Rational> basePowers;

private DerivedDimension(final Map<BaseDimension, Rational> basePowers) {
this.basePowers = basePowers;
}

@Override
public Map<BaseDimension, Rational> basePowers() {
return basePowers;
}

@Override
public boolean isBase() {
return false;
}

// Use semantic equality defined by the base powers map

@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DerivedDimension that = (DerivedDimension) o;
return Objects.equals(basePowers, that.basePowers);
}

@Override
public int hashCode() {
return Objects.hash(basePowers);
}

@Override
public String toString() {
return basePowers.entrySet().stream()
.sorted(reverseOrder(comparingByValue()))
.map(basePower -> formatBasePower(basePower.getKey(), basePower.getValue()))
.collect(joining(" "));
}

private static String formatBasePower(BaseDimension d, Rational p) {
if (p.equals(Rational.ONE)) {
return "[%s]".formatted(d.name);
} else if (p.denominator() == 1) {
return "[%s]^%s".formatted(d.name, p.numerator());
} else {
return "[%s]^(%d/%d)".formatted(d.name, p.numerator(), p.denominator());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package gov.nasa.jpl.aerie.contrib.streamline.unit_aware;

import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware.unitAware;

/**
* Utilities for <code>UnitAware&lt;Double&gt;</code>, aka a "Quantity"
*/
public final class Quantities {
public static UnitAware<Double> quantity(double amount) {
return quantity(amount, Unit.SCALAR);
}

public static UnitAware<Double> quantity(double amount, Unit unit) {
return unitAware(amount, unit, Quantities::scaling);
}

public static UnitAware<Double> quantity(Duration duration) {
return unitAware(duration.ratioOver(Duration.SECOND), StandardUnits.SECOND, Quantities::scaling);
}

public static UnitAware<Double> add(UnitAware<Double> p, UnitAware<Double> q) {
return UnitAwareOperations.add(Quantities::scaling, (x, y) -> x + y, p, q);
}

public static UnitAware<Double> subtract(UnitAware<Double> p, UnitAware<Double> q) {
return UnitAwareOperations.subtract(Quantities::scaling, (x, y) -> x - y, p, q);
}

public static UnitAware<Double> multiply(UnitAware<Double> p, UnitAware<Double> q) {
return UnitAwareOperations.multiply(Quantities::scaling, (x, y) -> x * y, p, q);
}

public static UnitAware<Double> divide(UnitAware<Double> p, UnitAware<Double> q) {
return UnitAwareOperations.divide(Quantities::scaling, (x, y) -> x / y, p, q);
}

/**
* Absolute value
*/
public static UnitAware<Double> abs(UnitAware<Double> p) {
return p.map(Math::abs);
}

public static boolean lessThan(UnitAware<Double> p, UnitAware<Double> q) {
return p.value() < q.value(p.unit());
}

public static boolean lessThanOrEquals(UnitAware<Double> p, UnitAware<Double> q) {
return p.value() <= q.value(p.unit());
}

public static boolean greaterThan(UnitAware<Double> p, UnitAware<Double> q) {
return p.value() > q.value(p.unit());
}

public static boolean greaterThanOrEquals(UnitAware<Double> p, UnitAware<Double> q) {
return p.value() >= q.value(p.unit());
}

private static double scaling(double x, double y) {
return x * y;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package gov.nasa.jpl.aerie.contrib.streamline.unit_aware;

import static java.lang.Integer.signum;
import static org.apache.commons.math3.util.ArithmeticUtils.gcd;

/**
* Lightweight exact rational number, used primarily for tracking dimensionality
* in the QUDV system.
*/
public record Rational(int numerator, int denominator) implements Comparable<Rational> {
public static final Rational ZERO = new Rational(0, 1);
public static final Rational ONE = new Rational(1, 1);

public Rational(final int numerator, final int denominator) {
if (denominator == 0) {
throw new ArithmeticException("Cannot create a Rational with 0 denominator.");
}

// Normalize by dividing by the GCD and forcing the denominator to be positive.
final int gcd = gcd(numerator, denominator);
final int s = signum(denominator);
this.numerator = s * numerator / gcd;
this.denominator = s * denominator / gcd;
}

public static Rational rational(final int numerator, final int denominator) {
return new Rational(numerator, denominator);
}

public static Rational rational(final int value) {
return new Rational(value, 1);
}

public Rational add(Rational other) {
return new Rational(
numerator * other.denominator + denominator * other.numerator,
denominator * other.denominator);
}

public Rational negate() {
return new Rational(-numerator, denominator);
}

public Rational subtract(Rational other) {
return this.add(other.negate());
}

public Rational multiply(Rational other) {
return new Rational(
numerator * other.numerator,
denominator * other.denominator);
}

/**
* Flip numerator and divisor, equivalent to raising to the power -1.
*/
public Rational invert() {
return new Rational(denominator, numerator);
}

public Rational divide(Rational other) {
return this.multiply(other.invert());
}

@Override
public int compareTo(final Rational o) {
return Integer.compare(numerator * o.denominator, denominator * o.numerator);
}

/**
* Approximate this as a floating-point number.
*/
public double doubleValue() {
return ((double) numerator) / denominator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gov.nasa.jpl.aerie.contrib.streamline.unit_aware;

/**
* Collection of standard dimensions, including all SI base dimensions.
*/
public final class StandardDimensions {
private StandardDimensions() {}

// SI base dimensions:
public static final Dimension TIME = Dimension.createBase("Time");
public static final Dimension LENGTH = Dimension.createBase("Length");
public static final Dimension MASS = Dimension.createBase("Mass");
public static final Dimension CURRENT = Dimension.createBase("Current");
public static final Dimension TEMPERATURE = Dimension.createBase("Temperature");
public static final Dimension LUMINOUS_INTENSITY = Dimension.createBase("Luminous Intensity");
public static final Dimension AMOUNT = Dimension.createBase("Amount of Substance");

// Additional base dimensions we've found useful in practice
public static final Dimension INFORMATION = Dimension.createBase("Information");
public static final Dimension ANGLE = Dimension.createBase("Angle");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gov.nasa.jpl.aerie.contrib.streamline.unit_aware;

/**
* Collection of standard units, including all SI base units.
*/
public final class StandardUnits {
private StandardUnits() {}

// Base units, should correspond 1-1 with base Dimensions
public static final Unit SECOND = Unit.createBase("s", "second", StandardDimensions.TIME);
public static final Unit METER = Unit.createBase("m", "meter", StandardDimensions.LENGTH);
public static final Unit KILOGRAM = Unit.createBase("kg", "kilogram", StandardDimensions.MASS);
public static final Unit AMPERE = Unit.createBase("A", "ampere", StandardDimensions.CURRENT);
public static final Unit KELVIN = Unit.createBase("K", "Kelvin", StandardDimensions.TEMPERATURE);
public static final Unit CANDELA = Unit.createBase("cd", "candela", StandardDimensions.LUMINOUS_INTENSITY);
public static final Unit MOLE = Unit.createBase("mol", "mole", StandardDimensions.AMOUNT);
public static final Unit BIT = Unit.createBase("b", "bit", StandardDimensions.INFORMATION);
public static final Unit RADIAN = Unit.createBase("rad", "radian", StandardDimensions.ANGLE);

// REVIEW: What derived units should be included here?
// Including a few arbitrarily to show a few different styles of derivation
public static final Unit MILLISECOND = Unit.derived("ms", "millisecond", 1e-3, SECOND);
public static final Unit MINUTE = Unit.derived("min", "minute", 60, SECOND);
public static final Unit HOUR = Unit.derived("hr", "hour", 60, MINUTE);
public static final Unit KILOMETER = Unit.derived("km", "kilometer", 1000, METER);
public static final Unit BYTE = Unit.derived("B", "byte", 8, BIT);
public static final Unit NEWTON = Unit.derived("N", "newton", KILOGRAM.multiply(METER).divide(SECOND.power(2)));
public static final Unit MEGABIT_PER_SECOND = Unit.derived("Mbps", "megabit per second", 1e6, BIT.divide(SECOND));
public static final Unit DEGREE = Unit.derived("deg", "degree", 180 / Math.PI, RADIAN);

public static final Unit JOULE = Unit.derived("J", "joule", NEWTON.multiply(METER));
public static final Unit WATT = Unit.derived("W", "watt", JOULE.divide(SECOND));
public static final Unit COULOMB = Unit.derived("C", "coulomb", AMPERE.multiply(SECOND));
public static final Unit VOLT = Unit.derived("V", "volt", JOULE.divide(COULOMB));

/**
* Astronomical unit as defined by
* <a href="https://www.iau.org/static/resolutions/IAU2012_English.pdf">IAU 2012 Resolution B2</a>.
*/
public static final Unit ASTRONOMICAL_UNIT = Unit.derived("au", "astronomical unit", 149_597_870_700.0, METER);
}
Loading
Loading