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

Need better handling of temperatures #137

Closed
MasonProtter opened this issue Apr 27, 2018 · 25 comments
Closed

Need better handling of temperatures #137

MasonProtter opened this issue Apr 27, 2018 · 25 comments

Comments

@MasonProtter
Copy link

MasonProtter commented Apr 27, 2018

Right now, it seems that temperature conversions have been made correct on a case by case basis, but its easy to find cases where things go wrong. For instance,

julia> uconvert(u"K", 1.0u"°C")
274.15 K

julia> uconvert(u"K^-1", 1.0u"°C^"^-1)
1.0 K^-1

if 1°C is 274.15 K, then it should not be the case that 1°C^-1 is 1 K^-1.

It seems that the handling of temperature conversions needs to be made more general. However, this is a really fraught area. I think this package needs to choose a philosophy and stick to it consistently for offset units. This is a hard choice to make though. For instance, what does it mean to multiply an offset unit by a number? Is 5°C really 5 times warmer than 1°C, or is it only 1.015 times warmer? If one thinks that it should be treated as 5 times warmer, then one then has to commit to the position that 1°C is infinitely warmer than 0°C which soon becomes a problematic position. It's really not obvious what the optimal choice is to make here, but I think that whatever choice is made should at least be well documented and consistent.

My suggestion would be to always internally represent offset units on an absolute scale but then display them as offset units such that 5°C is only 1.015 times warmer than 1°C but provide an opt-in option to treat them as if they were absolute if that's what a user needs.

@rafaqz
Copy link
Contributor

rafaqz commented May 27, 2018

+1. I've been bitten by this a few times recently. But I also have a few awful empirical equations that actually need to multiply by degrees C, so the answer isn't totally clear cut. But I would lean to defaulting to Kelvins by default with a display in C.

@MartinOtter
Copy link

The basic problem is that whenever a unit has an offset (as for a Temperature), then it must be defined whether the variable is an absolute or a relative quantity and the unit conversion has to take this additional information into account. For example, assume that T = 10.0u"°C" and T shall be converted to Kelvin, then the result is T=10.0u"K" if T is a relative quantity and T=283.15u"K" if T is an absolute quantity.

@burnpanck
Copy link

I agree with @MartinOtter: absolute and relative temperatures are fundamentally different things, and guessing which is which is very dangerous.
Adding two absolute quantities is ill-defined: What temperature is 5 °C + 9 °F? If both are considered temperature offsets, then the result is 10 °C or 18 °F, which are consistent as temperature offsets. However, if the result is to be considered an absolute temperature, these two are not equivalent: 10 °C (or 50 °F) is the correct answer if only the first summand is considered absolute, while 18 °F (or approx. -7.8 °C) is the correct answer if only the second summand is considered absolute. If both temperatures are considered absolute, then addition has no physical meaning at all.
Because of that, the only simple policy that a package can assume if it wants to support all standard mathematical operations is to consider all temperatures relative. This is what most unit-packages do that I am familiar with (mostly from other languages such as python). However, given the many meta-programming features of Julia, one should probably seek inspiration from languages like C++, where the Boost.Units library chose a very interesting path: They define a template type absolute<Unit> (in Julia, that would be a parametric type absolute{U}, I believe), which models a numeric type very similar to the underlying (relative) Unit, but disallows all ill-defined operations (compile-time error) and performs the expected conversions where well-defined. The important part is to realise that absolute and relative units have different behaviours and therefore need a different type representation in any strongly typed system. I would love to see that in Julia!

@dnadlinger
Copy link
Contributor

dnadlinger commented Oct 13, 2018

Yep, this is analogous to the difference between vector spaces and affine spaces. I implemented something like this in my std.units draft for D some years ago (docs, source – this is ancient code, so not reflective of D's current capabilities).

When working on this, I debated whether the added complexity of properly supporting such affine units was worth it for quite a while, but came to the conclusion that if units such as °C are to be supported at all, they should be represented properly. Otherwise, the unit system might actually end up increasing the likelihood of unit conversion mistakes, as it instills a false sense of security (immunity to mistakes) in users, while it doesn't help with what is otherwise a favourite source of unit conversion mistakes already.

@MartinOtter
Copy link

This is interesting because it could also solve a dangerous situation in Media descriptions: Quantities such as the specific enthalpy h (with unit J/kg) are defined with respect to a reference point. For example, h=0 for T=298.15 K and p=1 bar. Operations on specific enthalpies make only sense, if all are defined with respect to the same reference point:

delta_h = h2 - h1

is fine, if both h2 and h1 are defined with respect to the same reference point. However, if they have different reference points, the result is wrong. Unfortunately, such a situation can occur because tables of specific enthalpies might be defined with respect to different reference points (e.g. reference point is 0K or 293.15K, and 1bar or 1atm).

So, to summarize, it would be nice if "affine" units could be defined with respect to 1 or more reference points and addition/subtraction would be only allowed if the involved variables are defined with respect to the same reference points.

@cstjean
Copy link
Contributor

cstjean commented Oct 19, 2018

Another example of problematic behavior: addition is non-associative.

julia> u"1.3°C+1°C+1K"
276.45 K

julia> u"1.3°C+(1°C+1K)"
549.5999999999999 K

@cstjean
Copy link
Contributor

cstjean commented Oct 22, 2018

Here's a puzzle if we have both absolute and relative temperature. The law of perfect gases states:

PV = nRT      # T in "absolute" Kelvin

and thus

PΔV = nRΔT  #  ΔT in "relative" Kelvin/Celsius

Should R be expressed in per-absolute-temperature-unit, or in per-relative-temperature-unit? One solution is to use relative temperature, but that implies writing the first version of the law as

PV = n*R*(T - 0absoluteK)

It's nicely explicit, in a way, but it's not so aesthetically pleasing.

@cstjean
Copy link
Contributor

cstjean commented Oct 22, 2018

Some substance initially at 10°C is heated up at a rate of 2°C/hour, for 60 minutes, then is heated up once more at a rate of 3°C/hour for one hour. What is its final temperature?

julia> using Unitful: °C, hr, minute

julia> 10°C + 2.0°C/hr * 60minute + 3.0°C/hr * hr
565.8525 K

julia> 10°C + 2.0°C/hr * 60minute + 3.0°C/hr * 60minute
297.255 K

Using °C everywhere is no safeguard.

@rafaqz
Copy link
Contributor

rafaqz commented Oct 25, 2018

That last example is truly frightening.

@ajkeller34
Copy link
Collaborator

Many of the issues brought up here are resolved in #177. I'm sure it is not perfect, but I welcome feedback.

@dnadlinger
Copy link
Contributor

@cstjean: Regarding your ideal gas law example, the crucial point is to realise that "absolute" Kelvin and "relative" Kelvin are the same thing. Writing pV = nRT only makes sense because there is a well-defined reference point for all the quantities involved. Conversely, one could also use conformance to the ideal gas law as the distinguishing property for proper (thermodynamic) temperature scales. In other words, both the above and ∆(pV) = n R ∆T are correct for unit(T) == unit(∆T) == K.


A small tangent: Incidentally, this nicely illustrates why many equations in the sciences are customarily written in differential form. For instance, one would have to pick an arbitrary point in time as reference in order to be able to write Newton's second law F = m * x'' as an integral equation. Since no natural choice exists (in this case as a direct consequence of the symmetries of the wider theory), explicitly dealing with the initial conditions only complicates the notation without benefits. The same applies to many other systems which are described in terms of time derivatives, as there is usually no natural reference point for the time coordinate.


In a Julia version of the D library I mentioned above, your second example would read:

julia> using Unitful: °C, K, hr, minute
julia> 10°C + 2.0K/hr * 60minute + 3.0K/hr * 60minute
15 °C

@cstjean
Copy link
Contributor

cstjean commented Oct 29, 2018

@klickverbot Do you mean by unit(T) == unit(∆T) == K that K and absK should be one and the same? That's not what #177 does:

For consistency, even temperature scales that have zero scale offset (e.g. Kelvin) have two
types of units (K and absK).

I like the idea that K == absK conceptually, but doesn't it have issues? What is 0abs°C - 0K? is it 273°C, or is it 0abs°C?

@dnadlinger
Copy link
Contributor

@cstjean:

Do you mean by unit(T) == unit(∆T) == K that K and absK should be one and the same?

Yes, and #177 in it's current state indeed differs from what I'd propose.

What is 0abs°C - 0K? is it 273°C, or is it 0abs°C?

There is only a single unit °C, which is the affine unit that conceptually describes the unit/offset tuple (K, 273.15K). 0°C + 0K == 0°C, 25°C - 10°C == 15K, etc.

@ajkeller34
Copy link
Collaborator

I like your proposal and have mostly implemented it already on my end, just editing the docs to reflect the changes.

@cstjean
Copy link
Contributor

cstjean commented Oct 29, 2018

I like it too! Very neat. Some questions

  • Presumably there's no unit conversion between celsius and kelvin, then? How would one go from 10°C to 283K, and vice versa? Maybe we'd need an absolute_zero constant, in celsius?
  • PV = n*R*T with T in celsius would be an error?

@ajkeller34
Copy link
Collaborator

ajkeller34 commented Oct 29, 2018

Presumably there's no unit conversion between celsius and kelvin, then? How would one go from 10°C to 283K, and vice versa?

You can unit convert between them. I don't think it is actually an issue since some operations on affine quantities are restricted. If you want to try it out, let me know if you have any problems.

PV = n*R*T with T in celsius would be an error?

That would be an error, yes. There's no constant R that could be chosen to make that equation valid if T is on a relative scale. It could be made to work for any temperature unit by writing n*R*(T-0K). This is because promotion will convert T and 0K to the preferred unit, K, which is a thermodynamic scale. It may be good for me to add this as an example to the docs.

@cstjean
Copy link
Contributor

cstjean commented Oct 29, 2018

You can unit convert between them.

For us a big part of the appeal of Unitful is to prevent stupid mistakes. Some users might think about "an increase of 10°C", and have it converted to "an increase of 283K".

struct Explosion
    temperature_increase::typeof(K)
end
ex = Explosion(10°C)

Plus, 10°C + 2K === 12°C is nice. That said, this PR is a huge improvement over the current state of affairs, so thank you for working on it!

@ajkeller34
Copy link
Collaborator

I guess it is ultimately a question of which mistakes you think are most likely. If 10°C referred to a temperature difference, then users expecting 0°C == 32°F would be in for a surprise. I'm not sure it is possible to have it both ways without lots of special casing, if at all.

Thank you for taking the initiative with your PR, which was very helpful in getting things going.

@cstjean
Copy link
Contributor

cstjean commented Oct 30, 2018

I guess it is ultimately a question of which mistakes you think are most likely.

I wasn't clear enough. I was suggesting not having any uconvert at all between celsius and kelvin:

const abs_zero = -273.15°C
10°C + 2K --> 12°C
10°C - 2°C --> 8K
10°C - abs_zero --> 283.15K   # effectively this is how conversion is done
283.15K + abs_zero --> 10°C
uconvert(°C, 12K) --> DimensionError
uconvert(K, 12°C) --> DimensionError

If there was an agreed upon symbol for absolute zero, it would be more satisfying, but unfortunately 0K doesn't work for this purpose.

@klickverbot How did it work in the D library?

@ajkeller34
Copy link
Collaborator

@cstjean, some responses:

  1. I'm not willing to have Unitful consider °C and K as dimensionally different. A remark from Tim Holy comes to mind, when the philosophy of this package was set long ago: "unitful numbers correspond to physical reality. That's very different from the artificial world in which computers work, and it's important not to let the computer world infringe unnecessarily on the actual meaning of quantities."

  2. uconvert is a well-defined operation if you decide on having °C be an affine unit. I think we all agree that °C must be either an affine unit or represent a temperature difference, and not both. A way to address your example is to difference with respect to the scale zero in the constructor for that type, though I know your point was that users may expect what you wrote to just work.

  3. Right now, addition of temperatures happens in the usual way, where quantities are promoted to a common type before addition; if the common type is °C, this is problematic because we restrict addition of two affine quantities. I could define +(::AffineQuantity, ::Quantity) and vice-versa, which would enable 10°C + 2K === 12°C, rather than a result in K. I think the way to do this properly would be to have these methods work by first subtracting absolute zero from each operand to get on an absolute scale, then add, then convert to the preferred promotion type. I would still restrict addition of two affine quantities (10°C + 2°C would error). The output unit of addition could then be user configurable through the usual promotion mechanisms. I'll try this out.

@ajkeller34
Copy link
Collaborator

Alright, I've updated the PR, now the following is possible:

julia> using Unitful

julia> using Unitful:ContextUnits, K

julia> °C = ContextUnits(u"°C", u"°C")
°C

julia> 10°C + 2K
12//1 °C

This allows users to decide if they'd prefer K or °C for the result of such addition within a given package that uses Unitful. Note that the difference is still in K, as is required:

julia> 10°C - 2K
5623//20 K

@cstjean
Copy link
Contributor

cstjean commented Oct 31, 2018

Thank you for the detailed and considerate reply. I understand the tradeoffs, it's a reasonable position. With the latest changes to your PR, it should work great for our purpose.

@cstjean
Copy link
Contributor

cstjean commented Oct 31, 2018

I wanted to add that I'm impressed with the design of this package. It made me rethink how I structure type relationships in my own work.

@ajkeller34
Copy link
Collaborator

Closed by #177. Feel free to open new issues to address remaining specific concerns.

@cstjean
Copy link
Contributor

cstjean commented Aug 12, 2020

We've used temperatures extensively since #177 was merged. It works great, and the K vs °C distinction is very elegant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants