-
Notifications
You must be signed in to change notification settings - Fork 1
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
initial Quantity
protocol
#1
base: main
Are you sure you want to change the base?
Conversation
src/quantity_api/__init__.py
Outdated
|
||
def __init__(self, value: V, unit: U, **kw: Any) -> Quantity[V, U]: ... | ||
|
||
def to_unit(self, unit: U, /, **kw: Any) -> Quantity[V, U]: ... |
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.
I think there's a fair argument to be made that a minimal implementation does not have convenience methods, i.e., that conversion should be left to the unit class.
p.s. Fun fact, with this, every suggested method already has a comment!
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.
The U
type is bound to the Quantity
type, so this is a no-op?
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, using the U
in an input position like here will cause U
to be inferred as invariant, which I don't think is what you want here. The same can be said about __eq__
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.
The
U
type is bound to theQuantity
type, so this is a no-op?
Can't the object of type U
in the return value be different to the object of type U
exposed by the unit
property?
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.
I think there's a fair argument to be made that a minimal implementation does not have convenience methods, i.e., that conversion should be left to the unit class.
I definitely think this is something to vote on!
On one hand:
- Most (all?) current Q classes have a unit conversion method.
- Having
to_unit
may be useful to statically tell if something is aQ
. Currently it andunit
are the only really unique properties of the class. - Users kind of expect it.
OTOH: - All stakeholder Q libraries may agree we don't want to do this.
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.
The question is, are we able to do both here, by simply varying the signatures of functions to involve
Quantity[V, BaseUnit]
vsQuantity[V, CrapTon]
? Or is there some limitation stopping that from being possible?
I'm not sure what you mean with "by simplfy varying the signatures" 🤔
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 keep in mind that it's a lot easier to add a type parameter to a generic type, then to remove one. So if at this point there's no idea what Unit
is allowed to be (i.e. something that isn't Any
or object
), then it might be worth considering to just leave it out of the static typing for now.
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.
Interesting stuff.!To put my own summary of goals, we want to be able to type Quantity s.t. it can say that it works with X unit library or Y unit library. For example
def multiply_float_qs(
q1: Quantity[float, AstropyUnit], q2: Quantity[float, AstropyUnit]
) -> Quantity[float, AstropyUnit]:
...
W/out being able to parametrize by the unit we lose the important ability to distinguish between unit libraries, so
def multiply_float_qs(q1: Quantity[float], q2: Quantity[float]) -> Quantity[float]: ...
could not tell that the following is problematic multiply_float_qs(Quantity[AstropyUnit], Quantity[PintUnit])
A nice side effect of the unit type parameter is that we also have the ability to type functions that can convert between two unit libraries. The Quantity API, as a set of Protocols not concrete implementations, does NOT provide any methods to do this. But if someone had a function, e.g.
def my_custom_conversion_function(q1: Quantity[float, AstropyUnit]) -> Quantity[float, PintUnit]: ...
it could be typed. This constrains unit type (not the Quantity type, which can be anything satisfying the Quantity API protocol). If you wanted to constrain the quantity type you'd just import the relevant libraries. The advantage here is that this function also supports wrappers around a quantity library, e.g. mylibrary.MaskedPintQuantity[V]
since it would satisfy Quantity[float, PintUnit]
.
Another nice side effect is that regardless whether a unit library has a common base class we can type functions that change the unit type, e.g.
class Log10Unit: ...
class NormalUnit: ...
def power10(q: Quantity[float, Log10Unit]) -> Quantity[float, NormalUnit]: ...
If a library doesn't have a common base class (IMO a bad choice) then using a Protocol object will still always work for more general functions
class AstropyUnitAPI(Protocol):
physical_type: ...
def multiply_float_qs(
q1: Quantity[float, AstropyUnitAPI], q2: Quantity[float, AstropyUnitAPI]
) -> Quantity[float, AstropyUnitAPI]:
...
If this feels too verbose then a simple TypeAlias (or py3.13 equivalent) makes this easy.
AstropyQuantity: TypeAlias = Quantity[V, AstropyUnitAPI]
def multiply_float_qs(q1: AstropyQuantity[float], q2: AstropyQuantity[float]) -> AstropyQuantity[float]:
...
TL;DR I think we need at least one unit type parameter in Quantity. Otherwise we're saying all units work with all other units; and that's just wrong.
@jorenham I like the idea of making "Unit generic on its own base unit type", but IMO the limited support by static typecheckers makes it a no-go.
@jorenham what does adding the 2nd unit type get us?
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.
I'm not sure what you mean with "by simplfy varying the signatures" 🤔
def f1(q: Quantity[V, BaseUnit]):
q1 = q.to_unit(SubUnit1('meter')) # must work
q2 = q.to_unit(SubUnit2('km')) # must also work
def f2(q: Quantity[V, SubUnit1]):
q1 = q.to_unit(SubUnit1('meter')) # must work
q1 = q.to_unit(SubUnit2('km')) # undefined
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.
I think I see now the need for the second unit type - in my f1
example, it allows us to specify that q1.unit
be not just an instance of BaseUnit
, but specifically an instance of SubUnit1
.
Co-authored-by: Joren Hammudoglu <jhammudoglu@gmail.com>
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.
Thanks @lucascolley!
IMO the aspects we are discussing are ready to be voted on by all stakeholders:
- How is this parametrized
- Attribute names: value, unit
- other dunder methods
We should ensure all the libraries vote on this so we can get commitment that the API will be adopted! I think the 3 largest are pint, astropy, and unyt, but everyone should be included.
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.
Since this uses .value
and .unit
, which matches what astropy has, I'm of course very happy. But that may not go for libraries that made a different choice...
Could we also vote on whether to include a This changes how to perform one of the most common operations, and would need a bit more than a find/replace to change code. |
Beyond my general sense that it may be good to follow the array API and not have explicit methods that return new instances, I worry this is asking for bike shedding about the name. E.g., Anyway, my sense would be to vote first on this limited subset with just the |
SGTM. Not bad to get the pieces in 1 at a time. particularly when there's ongoing discussion. |
alright that sounds good. I think its important to make the future plans clear as I'd expect people not following this pull to ask about that. Similar for the constructor. |
Something else worth mentioning when putting |
It looks like the only ongoing discussion is #1 (comment), where we seem to be hitting the limitation of lacking higher kinded typing support. But that is about a method which is not currently in this PR, so that discussion can be moved elsewhere. Regarding what is in this PR, it sounds like we want to vote to check that we are happy with:
Important things to follow in further issues/PRs: |
Alright, what's the plan for voting? Shall we do something here? |
No description provided.