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

Distinguishing affine and vector quantities #289

Open
jacg opened this issue Mar 18, 2022 · 9 comments
Open

Distinguishing affine and vector quantities #289

jacg opened this issue Mar 18, 2022 · 9 comments

Comments

@jacg
Copy link
Contributor

jacg commented Mar 18, 2022

Edit: I am now pretty much convinced that there should be an affine and a vector version of every Quantity, as described in this comment further down in the thread.


I'm not sure if this is an appropriate place to ask this question. If not, I apologize and do feel free to close the issue.

In trying to use uom to get the Rust type system to help me understand my simulation code better and to reduce the bugs in it, I'm wondering to what extent I should distinguish between 3D Points and 3D Vectors. For example

  • Point - Point should give Vector
  • Point + Point should not be allowed
  • Vector - Vector should give Vector
  • Vector + Vector should give Vector
  • Point + Vector should give Point
  • Scalar * Vector should give Vector
  • Scalar * Point should not be allowed
  • etc.

and I'd like the type system to be aware of it. This can be done without much trouble (beyond verbosity) by defining separate Vector and Point types and implementing the relevant std::ops traits accordingly.

But it's not clear to me whether these Vectors and Points should have the same uom types representing their components: should the above rules apply to the components of Point and Vector at the type level, too? For example, should

  • point.x be some affine version of Length,
  • point1.x - point2.x return some vector version of Length,
  • point1.x + point2.x be disallowed by the type system,
  • the whole idea be applied to abstract affine and vector spaces, ones whose components are not necessarily Lengths?

This, in turn, makes me wonder about the distinction when it comes to other scalar quantities that appear in my code, and that leads me to wonder more generally about the distinction of affine and vector quantities in physical units.

It seems that the distinction has been recognized as important by the software development community in the context of dates and times: most languages' date/time libraries distinguish between (affine) dates, and (vector) durations, tough they don't use the affine/vector nomenclature explicitly.

But I don't see any evidence of this distinction being used much in scientific code.

Do you have any thoughts or relevant experience? Would it be worthwhile to explain these differences to the type system? Would it be more trouble than it is worth? How would it fit in with uom?

@iliekturtles
Copy link
Owner

My initial thoughts are that this should be a higher level than uom. e.g. your Point and Vector types. In my limited experience I think I've only seen the explicit distinction around length. What is a mass point vs. a mass vector?

Thermodynamic temperature/temperature interval is another case similar to dates/durations and uom does support the distinction. This support is done through the Kind marker type and I suppose uom could have a new affine Kind that limits operations. While this is possible I think it would be more confusing and less ergonomic than Point and Vector types.

uom/src/lib.rs

Lines 479 to 497 in 66a0546

/// Default [kind][kind] of quantities to allow addition, subtraction, multiplication, division,
/// remainder, negation, and saturating addition/subtraction.
///
/// [kind]: https://jcgm.bipm.org/vim/en/1.2.html
pub trait Kind:
marker::Add
+ marker::AddAssign
+ marker::Sub
+ marker::SubAssign
+ marker::Mul
+ marker::MulAssign
+ marker::Div
+ marker::DivAssign
+ marker::Rem
+ marker::RemAssign
+ marker::Neg
+ marker::Saturating
{
}

Do you see use for affine/vector quantities beyond length? Are there cases in your simulation where Point and Vector are not sufficient?

@jacg
Copy link
Contributor Author

jacg commented Mar 19, 2022

I think that there are two separate concerns here, which both use the word 'vector', with different meanings, and we should be careful not to confuse:

  1. Scalar vs vector: or 1-D vs N-D
  2. Affine vs vector: or absolute vs relative

Temperature difference and duration, both form vector rather than affine spaces, but those spaces have only 1 dimension, and hence are scalars rather than vectors: vector not affine, but scalar not vector!

I don't think that the N-D stuff belongs in uom, but I suspect that the affine-vs-vector (or absolute-vs-relative) probably does.

The difference between absolute time and duration seems obvious to everyone, because you don't need to be a scientist to be aware, at least implicitly, that 2:45 pm + 1:25 am makes no sense, while 2:45 pm + 1h25m and 2h45m + 1h25m both do make sense. Some of our natural languages distinguish between them, which helps with the intuition: "3 o' clock" vs "3 hours"; "3 Uhr" vs "3 Stunden".

The difference between thermodynamic temperature and temperature interval seems to be the next most obvious thing. It feels right to me that it should, but I can't quite put my finger on the reason why. Oddly enough,uom is explicitly aware of the distinction for temperature, but, as far as I can tell after a quick look, not for time.

I suspect that if you start being sufficiently pedantic (or precise or careful) about units, the distinction between absolute and relative quantities will emerge everywhere, not just in a small selection of quantities.

Do you see use for affine/vector quantities beyond length? Are there cases in your simulation where Point and Vector are not sufficient?

If you are talking about the N-D aspect, no. If you are talking about the relative-absolute aspect, yes.

  • The time (relative to some arbitrary start-of-event, hence affine/absolute) at which a particle is detected, is a different beast from the difference in times at which two different particles were detected (independent of arbitrary start-of-event, hence vector/relative). And I would like the type system to help me prevent confusing the two.
  • The position where a particle was detected (relative to some arbitrary spacial origin, hence affine/absolute), is a different beast from the separation between the places where two particles were detected (independent of choice of origin, hence vector/relative). And I would like the type system to help me prevent confusing the two.
  • The previous point also applies when I extract components from the 3D Points and Vectors, giving me Lengths: some of these are absolute/affine while others are relative/vector, and I'd like the type system to help me remember which is which.

In this particular part of our code I care predominantly about time and space, so these examples are most obvious to me. But there are other parts of the code (which I haven't looked at yet) where we will have to deal with charges a voltages. Voltages strike me as something else that will obviously benefit from the affine-absolute vs vector-relative distinction: any quantity which has 'potential' in its name (gravitational potential) is begging for this distinction!

The part of the code that has to deal with the charges is really not my area, but I imagine that it might be useful to have the type system distinguish absolute charges from differences between absolute charges. In brief, I suspect that having an absolute version and a relative version of any quantity, would make sense.

Thermodynamic temperature/temperature interval is another case similar to dates/durations and uom does support the distinction. This support is done through the Kind marker type and I suppose uom could have a new affine Kind that limits operations.

Yes, I came across Kind when reading other issues, but didn't really manage to understand it with little else beyond the API docs to go on, in what little time I had. Similarly, in some issue, you mention Frame, which I completely failed to find or understand. Is it referring to a frame of reference?

Is the design or structure of uom described somewhere? Is there a more high-level overview, or do I simply have to trawl through the API docs and the source, trying to infer the big picture?

I suppose uom could have a new affine Kind that limits operations. While this is possible I think it would be more confusing and less ergonomic than Point and Vector types.

Reiterating what I said at the top: it looks like this statement mixes two orthogonal concerns: vector-vs-scalar and vector-vs-affine, in other words dimensionality and relative-vs-absolute. I don't think that the dimensionality aspect is uom's business, but the relative-vs-absolute aspect is.

@adamreichold
Copy link
Contributor

adamreichold commented Mar 19, 2022

One problem I see for expressing the absolute/relative distinction at the type level is that having numerical values for absolute values requires a choice of origin as the numeric values are always based on the vector space elements via the free and transitive action of it on the point set. For example, there are multiple absolute temperature scales based on the absolute zero, the triple point of water or the average human body temperature. Similarly, our dates are basically durations w.r.t. a certain calendar of which multiple are in use.

So tracking absolutes in the type system would also require tracking the references in the type system which seems feasible when a few "official" ones exist but would require dependent types AFAIU for arbitrary references like the detector geometry of an experimental setup.

@jacg
Copy link
Contributor Author

jacg commented Mar 19, 2022

Which is why I was hoping that the Frame mentioned in another issue here does indeed refer to frames of reference, and somehow caters for this.

BTW, I ended up asking the same question on the Rust forum, and this reply makes a similar point.

I wonder whether there's a perfect-is-enemy-of-good effect here. Isn't it better to have protection against confusing relative for absolute (even if it comes without protection against confusing different absolutes), than having no such protection at all? Do we lose anything by adding this incomplete solution? Does adding this protection remove some pre-existing protection? In the case where an official absolute already exists (such as thermodynamic temperature) perhaps we do.

In the case of thermodynamic temperature, there is an objective origin. There can't be many other absolute quantities for which this is the case (none come to mind immediately [edit: there are plenty: mass, amount of substance, resistance, conductivity ... in fact, I'd now say these are in the majority!]), which probably explains why thermodynamic temperature is the only absolute quantity implemented in uom.

But maybe I'm overlooking some details which make it impossible to implement an absolute at all, without making a choice of origin, i.e. your original point. Can uom not say "this is an absolute value, not a relative one, but I know nothing about its origin"? Isn't this better than uom having to say "I don't even know whether this is an absolute or relative value"?

would require dependent types AFAIU for arbitrary references

Hmm.

@iliekturtles
Copy link
Owner

I've been thinking about this more the last couple days. My initial thinking was to just add an AffineKind with the correct bounds. This would be easy and could let you start testing to see if it works out in practice. Once I reviewed in more detail I don't think it is that trivial since as currently implemented Add/Sub trait impls will maintain the dimension's kind. Mul/Div trait impls will change back to crate::Kind (see code block linked below).

My next thought is to add trait bounds, similar to how typenum works, that transform the output kind based on the input kind(s). Some operations would be blocked while others would transform the kind when performed. Something like the following:

type Output = Quantity<
    $quantities<$($crate::typenum::$AddSubAlias<Dl::$symbol, Dr::$symbol>,)+, $MulDivAlias<Dl::Kind, Dr::Kind>>,
        Ul, V>;

My next thought is that overloading kind is not the appropriate method and there should instead be AffineQuantity and ????Quantity. Or perhaps Quantity could be left alone and another layer of wrapping could be used.

Another thought would be to add an additional sibling type parameter next to Kind for absolute/relative constraints.

@jacg Do you have any kind of intuitive sense at this point the right point to add this abstraction? I think I need to spend more time studying the subject which may take a while given the limited time I'm able to dedicate to uom right now.

Current code:

uom/src/system.rs

Lines 466 to 491 in 66a0546

impl<Dl, Dr, Ul, Ur, V> $crate::lib::ops::$MulDivTrait<Quantity<Dr, Ur, V>>
for Quantity<Dl, Ul, V>
where
Dl: Dimension + ?Sized,
$(Dl::$symbol: $crate::lib::ops::$AddSubTrait<Dr::$symbol>,
<Dl::$symbol as $crate::lib::ops::$AddSubTrait<Dr::$symbol>>::Output: $crate::typenum::Integer,)+
Dl::Kind: $crate::marker::$MulDivTrait,
Dr: Dimension + ?Sized,
Dr::Kind: $crate::marker::$MulDivTrait,
Ul: Units<V> + ?Sized,
Ur: Units<V> + ?Sized,
V: $crate::num::Num + $crate::Conversion<V> + $crate::lib::ops::$MulDivTrait<V>,
{
type Output = Quantity<
$quantities<$($crate::typenum::$AddSubAlias<Dl::$symbol, Dr::$symbol>,)+>,
Ul, V>;
#[inline(always)]
fn $muldiv_fun(self, rhs: Quantity<Dr, Ur, V>) -> Self::Output {
Quantity {
dimension: $crate::lib::marker::PhantomData,
units: $crate::lib::marker::PhantomData,
value: self.value $muldiv_op change_base::<Dr, Ul, Ur, V>(&rhs.value),
}
}
}}

@jacg
Copy link
Contributor Author

jacg commented Mar 30, 2022

Do you have any kind of intuitive sense at this point the right point to add this abstraction?

I'm afraid that I'm not yet sufficiently familiar with the structure of uom to be able to evaluate the pros and cons of the various approaches. Intuitively? I'd say that the AffineQuantity + Quantity option resonates most with me, but that is probably because it raises far fewer questions that I can't answer, than the other two. Which brings us back the the fact that I'm simply too ignorant about uom's internals to be of much use in this particular case. I'm working on diminishing my ignorance, but have very limited time.

@iliekturtles
Copy link
Owner

@jacg I'll start with that avenue of investigation when I eventually get to this!

@jacg
Copy link
Contributor Author

jacg commented Apr 7, 2023

This comment has helped me focus my thoughts. In summary,

I believe that every quantity should have a vector and an affine version. They should admit different operations.

Using the shorthand

  • V: vector
  • A: affine

vector quantities should allow

  1. V + V = V
  2. V - V = V
  3. V1 * V2 = V3
  4. V + A = A

while for affine quantities we have

  1. A + A impossible
  2. A - A = V
  3. A * ... impossible
  4. A + V = A

Additionally, as discussed in #403, both affine and vector quantities should support mean or average. It is trivial for the user to implement these for vectors, but for affines the user would have to transform to vectors, perform the computation there and transform back. Such operations could be implemented more directly by the implementer of the affine type.

Note that

  • the operations for vectors are closed on the set of vectors
  • the operations on affines always involve a vector quantity, either as input or output

Additionally

  • Some people might not care about the distinction
  • Units (such as Mass) in which there is an objective zero, blur the distinction between vector and affine and allow us to get away with performing vector operations on affine quantities and get the right answer in most cases.

The closure property of the vector operations is important, because it means that the vector side of things is self-contained: you can continue using it and completely ignore the existence of affines, which is useful in the two cases listed just above. On the other hand, in situations where you care about the affine/vector distinction, you can opt into using the affine machinery.

Put another way, uom already supports the vector operations 1-3 in the list above. Adding

  • affine versions of all quantities
  • vector operation 4
  • all the affine operations

need not interfere with any of the already existing uom machinery. At least not on a philosophical level. Maybe some implementation detail of uom can throw a spanner in the works.

The notable exception is ThermodynamicTemperature, which, I believe, is fundamentally broken, as #380 bears witness. In terms of the model I set out above, the problem is that all current uom quantities implement the vector side of things, so ThermodynamicTemperature is behaving like a vector, even though it is affine. I'd say that ThermodynamicTemperature should be removed and replaced with an affine Temperature; TemperatureInterval should be renamed to Temperature on the vector side.

We shouldn't forget @adamreichold's pertinent comment about the choice of origin in affine quantities. Ideally the affine side would support distinct choices of origin, and either prevent mixing of affine quantities with different origins, or automatically convert between them.

This ties in with the ThermodynamicTemperature issue. In principle the affine Temperature should support not only thermodynamic temperatures, but also non-thermodynamic ones such as Celsius.

@ryanolf-tb
Copy link

I'd be a huge fan of an implementation of @jacg's vector and affine quantities.

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

4 participants