unitsofmeasurement / unit-api

Units of Measurement API
http://unitsofmeasurement.github.io/unit-api/
Other
183 stars 42 forks source link

Where to apply an Absolute/Relative distinction? #140

Closed keilw closed 5 years ago

keilw commented 5 years ago

Based on input from https://reference.wolfram.com/language/tutorial/TemperatureUnits.html or https://www.quora.com/What-are-absolute-units (the latter contradicts itself quite a bit because after saying "Measurement units can be either absolute or relative… " it continues like "However, physical measurements are often relative. ") Let's figure out, if Unit or Quantity are the best place for such Absolute/Relative indicator?

It is drafted in Quantity (mainly to find out the payload it adds on top of other elements like the measurement level) but e.g. the Mathematica/Wolfram examples are pretty clear about using specialized Unit definitions like Kelvin vs. KelvinDifference for that.

Not entirely sure, which is best, something like an attribute on Unit which could allow Units.KELVIN vs. Units.KELVIN_DIFFERENCE (I recall @desruisseaux mentioned that option once in an earlier thread) or an attribute on Quantity?

Also how should it be defined either statically or dynamically and how does it affect unit and quantity operations?

Needs #95 Needs #131

keilw commented 5 years ago

Is it possible to answer this? Especially those who already participated in other parts of this "epic" like @desruisseaux, @dautelle, @filipvanlaenen or @andi-huber could you tell your preference please? I sense from some discussions in other threads, it may change e.g. based on a particular operation. If that's a requirement, I believe, Quantity is the safer option. If placed on a Unit it should be a constant value on that particular Unit instance, even if that would mean having something like CELSIUS and CELSIUS_INTERVAL.

keilw commented 5 years ago

A) Define the absolute/relative indicator in Quantity

keilw commented 5 years ago

B) Define it in Unit

keilw commented 5 years ago

C) Define it somewhere else, please comment where you prefer.

andi-huber commented 5 years ago

As demonstrated with [1], the Mathematica way of handling this seems almost trivial to implement. Note: That would be Option c), where the distinction (absolute vs relative) is made by using different units.

That's not my vote, because I'm not sure how well the other options could be implemented. But I have a feeling, that option c) could be the most simple one.

[1] https://github.com/unitsofmeasurement/indriya/pull/164

andi-huber commented 5 years ago

We also need to carefully consider impact on Unit formatting and parsing. E.g. currently I'm using this JAXB-Adapter to allow for Quantities to be marshaled/unmarshaled ...

public static final class QuantityAdapter extends XmlAdapter<String, Quantity<?>>{

    private final UnitFormat format = SimpleUnitFormat.getInstance();

    public Quantity<?> unmarshal(String v) throws Exception {
        final String unitLiteral = Strings.substringEmbeddedBy(v, "[", "]");
        final String valueLiteral = Strings.substringEmbeddedBy(v, null, "[");
        final double value = Double.parseDouble(valueLiteral);
        final Unit<?> unit = format.parse(unitLiteral);
        return Quantities.getQuantity(value, unit);
    }

    public String marshal(Quantity<?> v) throws Exception {

        String formattedUnit = format
                .format(v.getUnit(), new StringBuilder()).toString();

        return v.getValue().doubleValue() + "[" + formattedUnit + "]";
    }

}

Having different Units that yield the same format, would break this code. So even if we choose option c) an additional flag indicating 'absolute' vs 'relative' would be required somewhere.

desruisseaux commented 5 years ago

Absolute/relative indicator in Quantity is fine. Note however that it has the following impact: Since Unit.getConverterTo(Unit) does not know if we want to convert absolute or relative quantities, we may need another mechanism to distinguish the two cases at UnitConverter level. One possible approach is to add a new method in UnitConverter interface, which would apply on relative values:

double deltaConvert(double);

This is the same approach than java.awt.geom.AffineTransform, which has a deltaTransform method (javadoc here).

keilw commented 5 years ago

@andi-huber Thanks a lot for demonstrating this. I thought using different units would also rather be option B. If we don't need a "flag" (as you demonstrated it should work) it would be using the Unitof a Quantity, not the Quantityitself.

dautelle commented 5 years ago

Thank you andi-huber, the Wolfram article made me change my mind. It is elegant and makes senses mathematically (indeed Wolfram is a reference for that matter). For the user, it is quite simple, there can be a property in Unit (e.g. isDifference()) and a few additional units (e.g. CELSIUS_DIFF, FAHRENHEIT_DIFF). Of course, some quantity operations will not be allowed (e.g. adding quantity in CELSIUS and quantity in FAHRENHEIT not allowed, but adding quantity in CELSIUS_DIFF and quantity in FAHRENHEIT_DIFF allowed).

keilw commented 5 years ago

@andi-huber @dautelle Thanks for helping with the decision making and showing how the Wolfram calculations may best work here. A property (not really biassed towards isAbsolute(), isRelative() or isDifference()) on Unit instead of Quantity looks fine in this case. As the Units class also implements a SystemOfUnits, does it make sense to add those _DIFF or _DIFFERENCEunits there? Keep in mind, SI in https://github.com/unitsofmeasurement/si-units extends this class. It is not written in stone, but adds reusability. Thus is it better to define those units in the Units class or add a new SystemOfUnits implementation like UnitsDifference?

desruisseaux commented 5 years ago

I'm not convinced by the Wolfram examples. They are mathematically inconsistent (they said themselves that their operations are not commutative) and their approach would bring new problems in JSR-363 context, discussed below. The Wolfram example stating 3°F + 2°C as illegal but 3°F + 2°F = 5°F as legal is inconsistent: for 3°F + 2°F they implicitly handle at least one °F value as a temperature difference despite the two units declared as "DegreesFahrenheit" (and not "DegreesFahrenheitDifference"). In addition of bringing the mathematical problems described in #95, this is inconsistent with the distinction they make between "DegreesFahrenheit" and "DegreesFahrenheitDifference" in other examples, and is also inconsistent with the fact that they do not handle any value as a temperature difference in 3°F + 2°C.

The examples they provide can be practically the same with absolute/difference flag in Quantity instead of Unit. For example instead of (in Mathematica syntax):

Quantity[3., "DegreesCelciusDifference"]

We would have (in Java syntax)

Quantities.create(3., Units.CELSIUS, someFlagForDifference)

Having "DegreesCelciusDifference" and "DegreesCelcius" as two different Unit instances raises new problems in JSR 363 context. Do we create a new TemperatureDifference quantity in addition of Temperature?

keilw commented 5 years ago

Based on @andi-huber's sample it would beQuantities.create(3., Units.CELSIUS_DIFFERENCE) or similar, The flag if any was needed (@dautelle hinted it might help) would be on the Unit. Having separate Unit instances with different properties, underneath seems relatively uninvasive, but having to introduce Temperature vs. TemperatureDifference, that would totally blow up the API (even if it was only done for Temperature) and introduce all sorts of compatibility problems.

Btw, what about LevelOfMeasurement, is there any use for that at all?

desruisseaux commented 5 years ago

That is my point: putting this flag at Unit level as implications on parameterization: either we add quantities like TemperatureDifference and we get problems with Quantity.add and Quantity.subtract methods (in addition of increasing the amount of quantities subclasses), or either we don't and we compromise the Unit.getConverterTo(Unit) type safety (i.e. compiling the code without warnings is no longer a guarantee that getConverterTo will not fail), unless we provide some arbitrary definition of what a conversion between "temperature" and "temperature difference" should be (Wolfram does not - they are supposed to not be the same thing).

By contrast, if we add this flag in Quantity instead, the only implication I can see at this time is to add a UnitConverter.deltaConvert(double) method.

About LevelOfMeasurement: we need this information at least implicitly. An explicit enum is not strictly needed because its value can be partially inferred from unit.getConverterTo(unit.getSystemUnit()).isLinear(), but explicit enum may help to make things clearer. That LevelOfMeasurement enum would be in Unit instead of Quantity. As explained before, I think its role is complementary with the absolute/difference flag rather than an overlap.

keilw commented 5 years ago

LevelOfMeasurement on Unit contradicts everything that was discussed and concluded earlier in https://github.com/unitsofmeasurement/unit-api/issues/131

While it currently sits on Quantityand (if that needs to explicitly be set remains to be seen) can be set via calls to Quantities.create(3., Units.CELSIUS, INTERVAL) or similar. Unless we end up with CELSIUS_INTERVAL after all (the on with INTERVAL set by default) where would it be set on a Unit? Claiming that Mathematica, a system that is considered the de facto standard for these kinds of mathematical calculations is wrong or insufficient seems a bit bold. Sure, there are several approaches to many things especially in the JDK itself (Date/Time, how to deal with nullness, Jigsaw or the multi-release JARs) and you find wide-spread criticism of most in one form or another. As a temporary precondition, I gave anisAbsolute() flag to both Unitand Quantity. @andi-huber already demonstrated an approach that solves most of them. We should find a way to get in7, in8 and in12 likely throw a MeasurementException (I would hesitate to create yet another sub-exception beyond what we already have) based on a combination of newly added properties or something else. To control multiply or divide we could use the LevelOfMeasurement INTERVAL to prevent those, based on https://cf.ppt-online.org/files/slide/b/Belm5PkGts9ORjA0N4Qzu6VJa2dFW8L7iETghU/slide-13.jpg (the Wikipedia comparison is a bit misleading but RATIO allows both addition and multiplication)

For add and subtract it is not so obvious. The Mathematica example i7states:

A sum cannot generally be calculated for two different absolute temperatures.

With the Unit being different. However, should we really throw an exception every time an operation between two absolute values with a different unit is attempted?

Just one example 10m + 5ft should be possible. And unless we looked at the difference above sea level, there's no case where either of them are not absolute. So how to tell, simply by the Quantity? Which due to type erasure at runtime Java still isn't good at telling apart.

dautelle commented 5 years ago

What Wolfram shows is that to be mathematically consistent some operations may be forbidden. This can be done at the Quantity level or at the Unit level. The problem occurs with offset units (e.g. Temperature), no need to create a new Quantity class (e.g. TemperatureDifference). It can be resolved elegantly at the Unit level. For exemple by adding the "add" and "subtract" operation to Unit returning themselves except for offset units such as CELSIUS for which 'add' raises an exception and 'subtract' returns CELSIUS_DIFFERENCE (note: KELVIN will inherit the default behaviour of returning itself for both operations since it is not an offset unit).

andi-huber commented 5 years ago

I believe Martin aims for higher goals, but as a user of UOM I'm satisfied with what Mathematica has to offer. If certain operations are not allowed, at least if I understand why, than I'm fine with it. Also Wolfram's TemperatureUnits guide is short and easy to comprehend.

The discussion whether or not the math with Mathematica is 'consistent', in my opinion depends on how one defines 'consistent'. I do have a feeling, that avoiding surprises for the UOM user is more important.

So, regarding the big picture, it appears to me that we have 2 competing ideas: 1) keep it simple, disallow ambiguous calculations, do it the Mathematica way 2) implement Martin's requirement for 'physical consistency' of arithmetic, but also introduce/allow calculations that might surprise users

As a user, I have no strong preference for 1) or 2), all I'm saying is that for me 1) is sufficient.

... regarding exceptions: One way to deal with operations, that are not allowed, is to return a special class like NonEvaluatedQuantity, analog to Double.NaN when Number calculation yields 'not a number' result

andi-huber commented 5 years ago

Also note, with Mathematica ... 1) 10m + 5ft -> 37.8084 ft 2) UnitConvert[Quantity[3., "KelvinsDifference"], "Kelvins"] -> 'KelvinsDifference and Kelvins are incompatible units' 3) there is no "MetersDifference"

desruisseaux commented 5 years ago

@keilw: LevelOfMeasurement in Unit contradicting #131 is the reason why I asked for not too early call for vote (before the issue is fully understood) and for a wiki page summarizing the big picture.

The Mathematica examples are inconsistent in the following ways:

@dautelle: Mathematica is not showing that to be mathematically consistent some operations must be forbidden. We can be mathematically consistent by requiring all operations to be performed in system units - which is also consistent with thermodynamic laws. Furthermore Mathematica approach is not mathematically consistent - see above-cited contradictions, which I guess have historical roots.

@andi-huber: I fully agree about the goal to not be surprising to the user. This is precisely the problem I see with Mathematica approach. They have a whole section warnings the user that their operations are not commutative (A*B*C != C*B*A). If a user write (from thermodynamic equations):

    E1 = 1.5 * k * T1
    E2 = 1.5 * k * T2
    E = E1 + E2

If we can not rely on commutativity and associativity laws, rearranging those equations as E = 1.5 * k * (T1 + T2) can produce a different result. Isn't surprising? Furthermore how to tell now which equation is the correct one?

andi-huber commented 5 years ago

@desruisseaux, thanks for the examples. These could be key to understanding, what the discussion is all about.

I suggest we translate your examples into Mathematica syntax for having concrete cases we can focus the discussion. Otherwise I feel the subject is too abstract to comprehend. (At least for me.)

Best outcome would be, that we all come to an agreement, what we mean by 'consistent' or 'surprising' and/or identify cases, where we agree Mathematica does something 'wrong' and we can do better.

desruisseaux commented 5 years ago

There is my try:

k = Quantity[1.3806504E−23, "JoulePerKelvin"]
T = Quantity[10, "DegreeCelsius"]

I'm not sure what would be Mathematica behavior with 1.5 * k * T - maybe it would raises an error. #95 proposal is to convert T to Kelvin automatically. Let suppose that Mathematica can do that too, then the following produce different results in Mathematica:

See "Multiplication and unit conversions are non-commutative for absolute temperatures" section in their examples. See also the following example:

UnitConvert[Quantity[3., "DegreesCelsius"] *18.2, "Kelvins"] = 327.75 K

That example computed 3°C * 18.2 = 54.6°C, then added 273.15 K. Note that they did that despite the unit being "DegreesCelsius", not "DegreesCelsiusDifference" (see also the other inconsistencies reported in above comment). Their calculation is in contradiction with other source saying that "The common belief, that twice 10°C (or °F) equals 20°C (or °F), is wrong."

keilw commented 5 years ago

@andi-huber Thanks for the input also on other quantities. Also the clarification, that Difference, Relativeor whatever we may call applies only to a small fraction, most notably Temperature.

@desruisseaux "...- which is also consistent with thermodynamic laws". This shows, the entire discussion is about an extremely narrow case. Everywhere else operations like 10m + 5ft -> 37.8084 ft (the only difference is, JSR 363 or rather most of its implementations interpret that in the first unit m, not the second one, but that is a detail and either one makes perfect sense)

We should focus on concrete solutions for these narrow cases without breaking everything for 99% of all other cases or making things overly complicated for the common use case.

Java won't allow us to identify the Q in Quantity<Q> add(Quantity<Q> addend); Other languages like Kotlin, C# or F# may be better but unless the JDK team and Oracle thinks this was an important feature to add to Java 13 or 20 we can't do anything about it. So looking at a Quantity type like Temperature won't work, even if a child element like TemperatureDifference was created unless we gave it some kind of meta-information, but then this would have to be done everyhere. We could "blacklist" or specially treat certain units, this is done in a limited form e.g. for GRAM vs. KILOGRAM, etc. when formatting and parsing. In the end it does not matter much, if LevelOfMeasurement was applied to Unit or Quantity, it is a property we could more easily use to prevent certain operations than on the quantity type itself. Forcing addition, subtraction or any other operation only in a system unit also makes no sense for most types (the only one that is commonly discussed being Temperature) the given Wolfram example of 10m + 5ft -> 37.8084 ft shows that. It is not force-converted into m first, here the last unit counts instead of the first, but there is no need to show the result in m unless you really want to.

Btw, I prefer these kinds of discussions and decision-making, even if end up rolling something back here. The Wiki only allows to track the author and who changed what, but so far GitHub does not offer proper interaction like the issue tickets do. Or to assign certain tasks.

desruisseaux commented 5 years ago

@keilw: I'm not advocating for complicating the 99% of cases, and ensuring that A * B = B * A is not a narrow case. My points are:

keilw commented 5 years ago

@desruisseaux We currently have that with Quantities.create(5, Units.CELSIUS, INTERVAL) using the LevelOfMeasurement. If everyone agrees we must use an Absolute/Relative flag instead, why not, it's already there as well (in a "draft" state), but then do we still need the LevelOfMeasurement at all or can we just use isAbsolute() or isDifference() or (if the other enum is worthless) then even an enum with a reasonable name might work. The enum is there, I even started applying it to @andi-huber's PoC based on the Mathematica case, so please let's use that enum for now (RATIO vs. INTERVAL) and demonstrate that it solves all the problems where needed and has no negative side-effects or adds too much complication for all the others. Actually the flag is also there in both places, someone just has to wire it to certain places like the Quantities class, but to demonstrate what should behave how, using the enum would be just as good. We can change it if the outcome is satisfactory.

andi-huber commented 5 years ago

There is my try:

k = Quantity[1.3806504E−23, "JoulePerKelvin"]
T = Quantity[10, "DegreeCelsius"]

For the sake of demonstration I redefined k ... k = Quantity[1, "Joule"]/Quantity[1, "Kelvins"]; tc = Quantity[10, "DegreesCelsius"];

Correct answer to calculating 1.5 * toKelvin(tc) * k is 424.725 J. With Mathematica (correct) ... 1.5 * UnitConvert[tc, "Kelvins"] * k -> 424.725 J

I'm not sure what would be Mathematica behavior with 1.5 * k * T - maybe it would raises an error. #95 proposal is to convert T to Kelvin automatically. Let suppose that Mathematica can do that too, then the following produce different results in Mathematica:

* `E = 1.5 * k * T` if executed as `1.5 * k * UnitConvert[T, "Kelvin"]`

* `E = 1.5 * T * k` if executed as `UnitConvert[1.5 * T, "Kelvin"] * k`

Surprise with Mathematica (both wrong) ...

(1.5 * tc ) * k -> 4112.25 J
(1.5 * k ) * tc -> 4112.25 J

Root reason for this seems that Mathematica calculates ... Quantity[10, "DegreesCelsius"] / Quantity[2, "Kelvins"] -> 5 which is a surprise to me.

See "Multiplication and unit conversions are non-commutative for absolute temperatures" section in their examples. See also the following example:

UnitConvert[Quantity[3., "DegreesCelsius"] *18.2, "Kelvins"] = 327.75 K

This does not surprise me, as long as a + a = 2 * a (given a is a quantity, regardless of unit). eg ...

Quantity[3, "DegreesCelsius"] + Quantity[3, "DegreesCelsius"]  -> 6°C
2 * Quantity[3, "DegreesCelsius"] -> 6°C

agreed?

That example computed 3°C * 18.2 = 54.6°C, then added 273.15 K. Note that they did that despite the unit being "DegreesCelsius", not "DegreesCelsiusDifference" (see also the other inconsistencies reported in above comment). Their calculation is in contradiction with other source saying that "The common belief, that twice 10°C (or °F) equals 20°C (or °F), is wrong."

I'd like to emphasize that Quantity[10, "DegreesCelsius"] / Quantity[2, "Kelvins"] -> 5 certainly is something, we can do better. This calculation should not be allowed, in Mathematica's context.

desruisseaux commented 5 years ago

This does not surprise me, as long as a + a = 2 * a (given a is a quantity, regardless of unit).

Yes, we agree that this relationship must be kept. However the result depends if a is a temperature or a temperature difference: 2°C + 2°C = 4°C in the later case, while 2°C + 2°C = 277.15 °C in the former case. Mathematica surprise in my opinion is to have two different units for distinguishing those two cases ("DegreeCelsius" and "DegreeCelsiusDifference"), but still ignore the user unit ("DegreeCelsius") by silently replacing it with another unit ("DegreeCelsiusDifference") which does not have the same meaning.

andi-huber commented 5 years ago

@desruisseaux, I remember we discussed this earlier this year: Assume, you can leave the physical interpretation of any quantity aside, than the calculation results should not be surprising. Conceptually this is just one possible (yet simple) way to do it. The consequences being, that certain operations are not (or should not be) allowed, as well as unit-conversions are not commuting with other transformations.

To recap on

Quantity[10, "DegreesCelsius"] / Quantity[2, "Kelvins"] -> 5

I do accept, that with UOM, we do things differently, such that we bring in the physical interpretation of Quantities (convert to Kelvin before calculation). I also do accept, that as a consequence we can not just copy Mathematica's way.

So at least that seems clear to me now. I hope that's also clear to everyone else.

andi-huber commented 5 years ago

Question to you guys, suppose one needs to calculate the difference quantity of 2 absolute temperatures, how to do this with UOM? (Let's for a moment not worry about the naming ABSOLUTE/DIFFERENCE)

t1 = Quantities.create(5., Units.CELSIUS, ABSOLUTE);
t2 = Quantities.create(3., Units.CELSIUS, ABSOLUTE);
deltaT = ??? // t1 - t2 = 2°C (DIFFERENCE)

I guess t1.subtract(t2) should be the same as t1.add(t2.multiply(-1)) which is an absolute temperature, so that does not work.

keilw commented 5 years ago

Would we judge by both the Unit (because Temperature is not so easy to grasp at runtime) and the absolute/relative distinction?

dautelle commented 5 years ago

t1 = Quantities.create(5., Units.CELSIUS, ABSOLUTE); t2 = Quantities.create(3., Units.CELSIUS, ABSOLUTE); deltaT = ??? // t1 - t2 = 2°C (DIFFERENCE) Either it returns a Quantity with the DIFFERENCE flag set or if it returns a general Quantity stated in CELSIUS_DIFFERENCE

My personal feeling is that a quantity is an "absolute amount" (conceptually), but units can be offset (could even be more complicated, e.g. Decibels units based on Log function). For the cornercase of Température, the simplest solution would be CELSIUS_DIFFERENCE and FAHRENHEIT_DIFFERENCE units (with Unit.add and Unit.subtract operations). KELVIN_DIFFERENCE is unnecessary (as for all non-offset units).

andi-huber commented 5 years ago

t1 = Quantities.create(5., Units.CELSIUS, ABSOLUTE); t2 = Quantities.create(3., Units.CELSIUS, ABSOLUTE);

Sorry, in case my question was badly phrased, to be more clear: I'd like to know what code a user has to write (utilizing UOM API) in order to get from given t1 and t2 to a result deltaT (t1-t2) which is a Quantity equivalent to Quantities.create(2., Units.CELSIUS, DIFFERENCE)

I'm asking this, because it is not clear to me, whether we have any operators yet (other then the 'Quantities.create(...)' above), that would yield such a difference quantity as a calculation result.

keilw commented 5 years ago

Wouldn't ABSOLUTE be the default case if nothing is given? (the 1.0 case) As none of the code snippets mention the LevelOfMeasurement, I take it we could replace it with another enum. Which IMO whether it goes with Quantity or Unit should probably just be a static nested enum along the lines of SimpleUnitFormat.Flavor in the RI. Let's keep LevelOfMeasurement only if there really is a value for either operation that needs tweaking, otherwise one flag/enum would do.

andi-huber commented 5 years ago

@keilw It seems there is no use-case yet for LevelOfMeasurement.

To extend my question regarding the API design:

Given

t1 = Quantities.getQuantity(5., Units.CELSIUS, ABSOLUTE);
t2 = Quantities.getQuantity(3., Units.CELSIUS, ABSOLUTE);
d = Quantities.getQuantity(1., Units.CELSIUS, DIFFERENCE);

Expected API behavior:

t1.subtract(t2) -> -271.15 °C (ABSOLUTE)
t1.add(d) -> 6 °C (ABSOLUTE)
d.add(d) -> 2 °C (DIFFERENCE)
d.multiply(2.) -> 2 °C (DIFFERENCE)

but what operators to use in order to calculate t1 - t2 such that the result is 2 °C (DIFFERENCE)

Correct me if I'm wrong, but right now we can only do this like so ...

deltaT = Quantities.getQuantity(
    t1.getValue().doubleValue() - t2.getValue().doubleValue(), 
Units.CELSIUS, DIFFERENCE);

Clearly that cannot be the solution we provide.

desruisseaux commented 5 years ago

@keilw: ABSOLUTE would indeed be the default for QuantityFactory.create methods, but my understanding of Andy's question is what should be the result of Quantity arithmetic operations, which involve other considerations. More on it below.

@dautelle: Quantity is more easily conceived as an absolute measurement for Temperature and Mass for instance, but more difficult to conceive as an absolute measurement for Length and Time for example. Most spatio-temporal measurements are relative to some position, and despite that we still qualify them as "quantity". If nevertheless we put the "absolute versus difference" flag in Unit, then the following code which was used to succeed in JSR-363 would fail in JSR-385 if b is Units.CELSIUS_DIFFERENCE:

Unit<Temperature> a = Units.CELSIUS;
Unit<Temperature> b = ...;
UnitConverter c = a.getConverterTo(b);

Remember that Unit has two methods: getConverterTo which enforce compatible quantity at compile time and throw no checked exception, and getConverterToAny which does not enforce anything at compile time and throws the checked IncommensurableException if quantities are not compatible. We lost this compile-time safety if we introduce units like Units.CELCIUS_DIFFERENCE. Another consequence if that users can not rely anymore on the assumption that if A and B are in the same unit, then A - B is in the same unit.

@andi-huber: intuitively, with t1 and t2 as ABSOLUTE quantities, the behavior of t1 - t2 should be to produce a DIFFERENCE quantities. However I agree that it introduces an apparent inconsistency with the requirement that t1.subtract(t2) is equal to t1.add(t2.multiply(-1)). By symmetry, we should also require that t1.add(t2) is equal to t1.subtract(t2.multiply(-1)) and we can imagine yet more combinations. The only way I can see at this time to get fully consistent behavior would be to state the following rules:

If at least one of t1 or t2 is DIFFERENCE, then the rules are as you described in your previous comment. It may look odd that all operations depend only on the "absolute versus difference" flag except the ABSOLUTE ± ABSOLUTE operation which depends also on the sign of the values, but it may be because having two flag values ("absolute" or "difference") is not as accurate as it could be (does "negative absolute value" really make sense?). However I don't think it is worth to complicate the API with more flags than "absolute" and "difference", so having to look at the sign of absolute values may be a good compromise.

dautelle commented 5 years ago

Length, Time and Temperature are absolute measurement/quantity (in any non-accelerated referential according to relativity). But is a temperature increment (e.g. difference between two absolute temperatures) of the same 'kind' as an absolute temperature? I don't think so. Hence different units and no converter between both (despite they have the same dimensions).

andi-huber commented 5 years ago

@desruisseaux instead of making the quantity's sign relevant, can we not rather have an API extension (see below)?

Regardless of where we store the information ABSOLUTE/DIFFERENCE (either in Unit or Quantity), we also need to get arithmetic rules right!

1) Quantity operations on eg. quantities of Unit.CELSIUS

Assuming commutativity of addition and multiplication:

ABSOLUTE ± ABSOLUTE -> ABSOLUTE ABSOLUTE ± DIFFERENCE -> ABSOLUTE DIFFERENCE ± DIFFERENCE -> DIFFERENCE

ABSOLUTE scalar -> ABSOLUTE DIFFERENCE scalar -> DIFFERENCE

ABSOLUTE ABSOLUTE -> yields unit °C² ... ambiguous, what to do? DIFFERENCE DIFFERENCE -> yields unit °C² ... ambiguous, what to do? ABSOLUTE * DIFFERENCE -> yields unit °C² ... ambiguous, what to do?

ABSOLUTE / ABSOLUTE -> scalar DIFFERENCE / DIFFERENCE -> scalar

ABSOLUTE / DIFFERENCE -> NaN DIFFERENCE / ABSOLUTE -> NaN

The API could be extended to also provide for the special case delta( ABSOLUTE , ABSOLUTE) -> DIFFERENCE eg. by introducing a new method ...

t1 = Quantities.getQuantity(5., Units.CELSIUS, ABSOLUTE);
t2 = Quantities.getQuantity(3., Units.CELSIUS, ABSOLUTE);
deltaT = Quantities.getAsDifference(t1, t2); // new API !!!

2) Unit Conversion

ABSOLUTE.getConverterTo(ABSOLUTE) -> allowed DIFFERENCE.getConverterTo(DIFFERENCE) -> allowed

ABSOLUTE.getConverterTo(DIFFERENCE) -> what to do? DIFFERENCE.getConverterTo(ABSOLUTE) -> what to do?


I'm not sure if that's all rules, but we should be aware that we need to specify these.

keilw commented 5 years ago

What would scalar be, a number or something else? The idea of deltaT = Quantities.getAsDifference(t1, t2); could be a little tricky because the API should be able to mirror it somewhere.

Quantities is a convenience facade, but all its methods are either used by SPI elements like QuantityFactory or wrap calls toQuantityFormat.parse().

andi-huber commented 5 years ago

What would scalar be, a number or something else?

Yes a number, or more generally speaking a dimensionless quantity.

The idea of deltaT = Quantities.getAsDifference(t1, t2); could be a little tricky because the API should >be able to mirror it somewhere.

Clearly I'm lacking insight as to where to put such an extension, but point being, we should consider extending the API, rather than having rules which involve the sign of quantities.

keilw commented 5 years ago

So if we assume two ABSOLUTE quantities like 10 °C / 3 °C it would result in a number or Dimensionless Quantity?

Any sort of rule could not be restricted to CELSIUS, Other unit system extension modules like uom-systems-* etc. shall be able to add FAHRENHEIT or others like Reaumur although it is a bit historical, but it does not matter. Although it is nearly impossible to create an independent implementation of the java.time "low level" API implementing small parts like Chronology seems easier. And one could add ancient calendar systems like the Maya calendar, an Ancient Egyptian or Babylonian calendar for certain purposes. The same way it should be possible to add further units to a pre-defined rule set that may only contain CELSIUS out of the box in the RI.

desruisseaux commented 5 years ago

@dautelle: given that lengths are always the distance between two points and that there is no natural zero spatio-temporal position (ignoring big bang), we could debate about whether lengths are absolute measurements, regardless if classical physics or relativity. But anyway this is not this topic. A question for this JSR topic would be: why a temperature increment should be considered different than an absolute temperature, but a mass increment should not be considered different than an absolute mass?

@andi-huber: I agree with the rules you listed with 3 dampers:

I suggest to focus on ABSOLUTE ± ABSOLUTE for now, which seems to be the core of the issue we are trying to solve. I think the 2 other points are relatively minor in comparison. I will try to elaborate more on my sign proposal in the next days, and we can explore alternatives in parallel to see what fit best.

keilw commented 5 years ago

We could have both on Quantity, but I would not spawn off two enums that are more confusing. Either scrap LevelOfMeasurement entirely and invent something "Absoludity", maybe not, I just can't think of anything other than isAbsolute() vs. isRelative() or maybe isDifference().
Very vaguely comparable to say BigDecimal having attributes like scale and precision. The LevelOfMeasurement is also called "scale" in many cases, but I would stick to "level" if we keep the enum at all?

andi-huber commented 5 years ago

Regarding isAbsolute() vs. isRelative() or maybe isDifference():

I suggest to use an enum like this to 'put' on Quantity or Unit:

public enum ArithmeticCharacteristics {

  /** no arithmetic distinction is made between absolute or difference on quantities having 
   * UNIFIED characteristics, eg. any quantity of unit KELVIN
   **/
  UNIFIED,

  /** special arithmetic rules apply to quantities having ABSOLUTE characteristics
   * eg. an absolute quantity of unit CELSIUS
   **/
  ABSOLUTE,

  /** special arithmetic rules apply to quantities having DIFFERENCE characteristics
   * eg. a difference quantity of unit CELSIUS
   */
  DIFFERENCE
}

Idea being, that for most of our Quantities/Units this characteristics is UNIFIED and can not be something else, like for eg KELVIN. While for CELSIUS it can either be ABSOLUTE or DIFFERENCE but never UNIFIED.

My thoughts on solving

ABSOLUTE ABSOLUTE -> yields unit °C² ... ambiguous, what to do? DIFFERENCE DIFFERENCE -> yields unit °C² ... ambiguous, what to do? ABSOLUTE * DIFFERENCE -> yields unit °C² ... ambiguous, what to do?

Lets say we have 2 quantities a and b then we can have arithmetic rules to resolve the ambiguity problem for multiplication by (pseudo-code)

a * b -> convertToUnified(a) * convertToUnified(b) -> yields unit K²

provided, we do have rules for converting non-unified to unified, example (pseudo-code):

a = Quantities.getQuantity(5., Units.CELSIUS, ABSOLUTE);
b = Quantities.getQuantity(1., Units.CELSIUS, DIFFERENCE);

a * b  -> toKelvin(a) * toKelvin(b) -> yields unit K² ... no longer ambiguous

I conclude we need to allow for quantities of CELSIUS (either difference or absolute) to be converted to KELVIN (always unified). We need to specify how to do this generally (not only Temperature).

andi-huber commented 5 years ago

I created a wiki page summarizing the rules at Indriya [1]. Feel free to copy/move it over to here. I don't have editing rights on this wiki.

[1] https://github.com/unitsofmeasurement/indriya/wiki/Arithmetic-Rules-for-Difference-vs-Absolute-Quantities-(DRAFT)

desruisseaux commented 5 years ago

Thanks Andy for the wiki page. I think it is really useful. I will try to copy to unit-API during the weekend and elaborate on alternatives regarding ABSOLUTE - ABSOLUTE (sign comparison versus fixing the result to ABSOLUTE).

desruisseaux commented 5 years ago

@keilw: we could have the "absolute versus difference" flag or enum in Quantity, and LevelOfMeasurement in Unit. It would be a natural place since:

Unit.getConverterTo(Unit.getSystemUnit()).isLinear() implies LevelOfMeasurement.RATIO

As said before, technically LevelOfMeasurement is not strictly necessary. But conceptually it may help to bring clarity. I do not have a strong opinion about whether to keep it or not, but if we keep it I think it should not be in the same class than the "absolute versus difference" flag for avoiding confusion.

keilw commented 5 years ago

If it is just absolute vs. relative/difference, I would leave a boolean flag. It feels natural with the UnitConverter.isLinear() we already got also mentioned here ;-) Since the other thread was already resolved (and the majority voted for using Quantity) @andi-huber @dautelle since you were the most active/only participants other than @desruisseaux with arguments like

LevelOfMeasurement fits well in Unit but is not exactly what we need for

or

I would suggest not changing current units definition ( "scaled dimensions")

How do you feel about LevelOfMeasurement either in Unit, Quantity (where a majority of all participants found it would be better) or completely drop it because an absolute/different attribute in Quantity is totally sufficient for our current needs?

I would not oppose refactoring it into Unit, but there it should be a constant unique to each Unit, so in theory we could have a KELVIN_INTERVAL or METRE_INTERVAL somewhere, but the units defined in Units or other places have one distinct immutable LevelOfMeasurement.

andi-huber commented 5 years ago

@keilw I suggest to drop LevelOfMeasurement, simply because we don't use it. Why worry about introducing a feature, which has no evident use-case within Indriya. We can always reintroduce it later, should we find a use-case. Also please note that I'm currently suggesting [1] an enum instead of a boolean regarding absolute vs different.

[1] https://github.com/unitsofmeasurement/indriya/wiki/Arithmetic-Rules-for-Difference-vs-Absolute-Quantities-(DRAFT)

keilw commented 5 years ago

@andi-huber If there really can be more than one even an "Unknown" value, why not, but what about simply calling it Scale?

andi-huber commented 5 years ago

@keilw My thought process from early morning, following the idea of having 3 instead of 2 values for this arithmetic characteristics, is not set in stone at all. As Martin has offered, he'll think this through next weekend. I'll also think this through again. If it makes sense to stick with my proposal of 3 values ABSOLUTE, DIFFERENCE and UNIFIED then I'd rather stick with the term 'Characteristics'. If it turns out that only 2 values ABSOLUTE, DIFFERENCE are needed, the term 'Scale' seems a good choice.

keilw commented 5 years ago

Either way I think we should keep it as a static inner enum with the affected type (Quantity for all we know). And if we ever find a need for LevelOfMeasurement, we can recover it. Indriya has at least one or two research branches for CompoundUnit, etc. I think I'll take a branch of the API to preserve a few discussion items. and then at least @deprecate LevelOfMeasurementso it can be removed if no further use is found for it.

keilw commented 5 years ago

Sure, think about it over the weekend. I deprecated LevelOfMeasurement, not yet removing it from master. If there is no use case, then it could be removed in favor of another definition. Tomorrow the CGPM in Versailles will decide on the redefined SI units. Hope it goes through, although it has no direct impact on this issue. Even in the unlikely event they rejected or postponed it again we shall add such new features or improvements. At most a MR or future release may have to deal with more SI changes if it did not happen by May 20 next year.