-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: DimensionSystem and decomposition #16
base: main
Are you sure you want to change the base?
Conversation
609af76
to
2788bd9
Compare
I like the decompose method, but am less sure about the need for a A bit more generally, I think it is really good to have the standard bases listed as an example, but even better would be to have a different example worked out in a bit more detail. How would a Now on to something a bit more abstract - should there be an easy way to associate base units with dimensions? In particular, I guess a larger question would be whether it makes sense to have some form of reference implementation of all this... With, arguably more importantly, a reference test suite. |
Yes! It's along the "parse, not validate" model, where instead of having
A
|
src/metrology_apis/__init__.py
Outdated
@@ -17,6 +17,70 @@ def __pow__(self, other: int, /) -> Self: ... | |||
def __rmul__(self, other: Self, /) -> Self: ... | |||
def __rtruediv__(self, other: Self, /) -> Self: ... | |||
|
|||
def decompose(self, dimension_system: "DimensionSystem", /) -> "dict[Dimension, float | int]": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to stick to int | Fraction
no? Don't know if float makes sense in this context.
Any examples ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also it will ease manipulation when going from one system to another manipulating only integers all the way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would this be expected to return all the base units, or just the ones with nonzero exponents?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also what would be the expected decomposition of something like dBmW, or other equation based units. For example in the library I work there is also mechanics for completely new units, internally these are represented by specific bit sequences, which could be translated to the existing base units, but would be nonsensical and not translatable outside the string representation if the other libraries happened to represent them.
Also if this is to be a method in an implemented dimension class in a library, does it make sense to require it take as an argument a class defined in a higher level library. Wouldn't it be better to have it take a set(or Tuple, or list) of Dimensions so as to not define a separate class for that concept. That would seem more portable. Or not have an argument to that method and just return the dictionary of dimensions and exponents as defined by a library.
I guess I am not seeing the compelling case for DimensionSystem as of yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would this be expected to return all the base units, or just the ones with nonzero exponents?
I think that would be up to implementing libraries. We just specify the types.
Might be good to stick to int | Fraction no? Don't know if float makes sense in this context.
Any examples ?
A "natural" unit system taking h-bar, c, and G to be one and to define the base dimensions (a little strange, but not unreasonable) then
So Fraction
would work in this case but int
is insufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
I feel like this is fairly niche, I don't think any existing libraries have this functionality ? You can do Planck units in pint, but it doesn't change the base dimensions; it'll still use length (but scaled to Could we set this as optional? |
I guess I agree with @andrewgsavage - certainly, for It may be most fruitful to define the behaviour of units first, and then use the more exotic cases as examples we want to be sure to keep possible. Eg., decomposing to SI is likely available in all units packages, so that should be supported, but everything else would not seem essential. Maybe just like the Array API defines a couple of standard data types that have to be supported, we should similarly define a couple of unit (strings) that have to be supported, and define how units interact with each other? p.s. Classes are nearly over, so I hope to have a bit more time to contribute instead of just commenting from the sidelines... |
I'm very happy to strip the DimensionSystem from this PR and approach it at a later date! In general, dimension systems can be useful for:
Also, there is prior art: https://docs.sympy.org/latest/modules/physics/units/dimensions.html#sympy.physics.units.dimensions.DimensionSystem All that being said, it's very easy to start with the |
I think this would be a bit easier to think about if we did the analogous unit method first. There's a few questions that come to mind:
this leads into the concept of quantity kinds. |
The power of concrete examples... If we are having dimensions, I'd like 'torque' to be different from 'energy and, my personal bugbear, 'pressure' not to be the same as 'energy density' even if their units decompose to the same (N/m2 and J/m3 both decompose to kg/(m s2)). But I'd also like 'velocity' to be 'length' / 'time'... Aside: I like trying to think through how to deal with physical quantities as best as possible, but worry a bit that it distracts from getting us to a minimal API that we can all agree on... |
50f3d4d
to
9195af7
Compare
Signed-off-by: Nathaniel Starkman <[email protected]>
9195af7
to
9f5b897
Compare
@andrewgsavage As I understood it from https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html there's a difference between Dimension and QuantityKind which is that
For |
@nstarman - thanks for pointing back at that. I guess the conclusion is that both types of logic have their place, and that they are best represented by different classes. |
Agreed. @andrewgsavage I'm a huge fan for the QuantityKind idea and think we should find some way to support it in the API at a level where Python libraries can still choose whether to use it. I think Dimensions, Units, and Quantities are the fundamental building blocks but that QuantityKinds provide an incredibly useful means to distinguish units and sensible math with Quantities, preventing incompatibly-kinded operations. |
As for the name of the method, I'd love to get some inputs from other libraries about what they've implemented that's similar. Is there a common method name? Or is there a method name we'd all be willing to support? |
|
Could you point to which parts of the protocol would be problematic? |
the units library I work with is primary in C++. The unit is stored as a 32 bit integer with bitfields representing the standard SI base units, along with count, radians, and currency. In addition it has 4 bit flags representing(iFlag - imaginary, eFlag-extra, perUnit, and equation). The unit object also includes a floating point scalar. The closest thing to a dimension is the base_unit which just includes the bitfield portion and no scalar. What I am doing in python is to just have the Dimension class be a unit, and force the scalar to be 1. Then for string interaction use a defaultunit method which can take any "dimension" or "measurementType" and get the default unit for it. As well as the reverse. I will probably add some ability to add the math operations in that as well to handle more complex strings. I haven't added a decompose method yet but will probably just have the method output the values for the base units as a dictionary. Not clear what should be done with the flags as in some cases those could be used as a discriminator of the "Kinds" mentioned earlier, and I doubt they have equivalents in other libraries. |
I should also mention that the python portion is pretty new so I can match any naming conventions pretty easily since everything in python is still pretty experimental and easy to tweak. |
all of it, potentially, since we don't have our own |
While I like the idea of a |
It sounds sensible to me to leave out |
Indeed, I think a close look at the API for unit conversion makes sense. I think it should start with a function, though, not a method on |
we have unit conversion using |
That seems reasonable. We should have a way to denote optional classes |
Did we decide that |
I guess so, although we haven't yet spelled out the semantics. At https://github.com/quantity-dev/quantity-array/blob/ff14ef6e3e8925ce6e8fd44d79ae44d681190ccb/src/quantity_array/__init__.py#L745-L755, def multiply(x1, x2, /, *args, **kwargs):
x1 = asarray(x1)
x2 = asarray(x2)
units = x1.units * x2.units
x1_magnitude = xp.asarray(x1.magnitude, copy=True)
x2_magnitude = x2.m_as(x1.units)
magnitude = xp.multiply(x1_magnitude, x2_magnitude, *args, **kwargs)
return ArrayUnitQuantity(magnitude, units) could then look like def multiply(x1, x2, /, *args, **kwargs):
x1 = as_quantity_array(x1) # interpret `x1` as an `ArrayQuantity`
x2 = as_quantity_array(x2) # interpret `x2` as an `ArrayQuantity`
unit = x1.unit * x2.unit
x1_value = xp.asarray(x1.value, copy=True)
x2_value = mn.asquantity(x2, x1.unit).value # use `mn.asquantity` for conversion
value = xp.multiply(x1_value, x2_value, *args, **kwargs)
return as_quantity_array(value, unit) where Side note: is |
See #9 (comment) for discussion about |
This comment was marked as off-topic.
This comment was marked as off-topic.
@mhvk I strongly agree! Also we should move this discussion elsewhere. |
unyt's dimension is recognised as a dimension with the current api: @runtime_checkable
class Dimension(Protocol):
def __mul__(self, other: Self, /) -> Self: ...
def __truediv__(self, other: Self, /) -> Self: ...
def __pow__(self, other: int, /) -> Self: ...
def __rmul__(self, other: Self, /) -> Self: ...
def __rtruediv__(self, other: Self, /) -> Self: ...
isinstance(unyt.dimensions.jerk, Dimension)
True We could leave the decompose method to a future version |
While I agree with leaving decomposition for later, isn't a problem with the definition you gavethat |
Yes. This was part of the subject of #4 |
yes - but you've still standardised on .dimension and asdimension('length'), which I think is preferable to not having these features. |
@neutrinoceros what are the prospects for |
Growing out of discussion in #4.
This definitely needs the consensus of all stakeholder libraries as implementing this on Dimension(-adjacent) objects will require adding a new class: the dimension system.
There is no rush to get this in, just thought I'd formalize the ideas in code.
Method name in other libraries:
decompose
decompose