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

initial Quantity protocol #1

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

lucascolley
Copy link
Member

No description provided.

src/quantity_api/__init__.py Outdated Show resolved Hide resolved
src/quantity_api/__init__.py Outdated Show resolved Hide resolved

def __init__(self, value: V, unit: U, **kw: Any) -> Quantity[V, U]: ...

def to_unit(self, unit: U, /, **kw: Any) -> Quantity[V, U]: ...
Copy link

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!

Copy link

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?

Copy link

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__

Copy link
Member Author

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?

Can't the object of type U in the return value be different to the object of type U exposed by the unit property?

Copy link

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 a Q. Currently it and unit 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.

Copy link

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] vs Quantity[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" 🤔

Copy link

@jorenham jorenham Feb 9, 2025

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.

Copy link

@nstarman nstarman Feb 9, 2025

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?

Copy link
Member Author

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

Copy link
Member Author

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.

lucascolley and others added 4 commits February 5, 2025 15:27
Co-authored-by: Joren Hammudoglu <jhammudoglu@gmail.com>
Copy link

@nstarman nstarman left a 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:

  1. How is this parametrized
  2. Attribute names: value, unit
  3. 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.

Copy link

@mhvk mhvk left a 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...

@andrewgsavage
Copy link
Collaborator

Thanks @lucascolley! IMO the aspects we are discussing are ready to be voted on by all stakeholders:

  1. How is this parametrized
  2. Attribute names: value, unit
  3. 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.

Could we also vote on whether to include a to_unit method, potentially in a subsequent version? or alternatively a function.

This changes how to perform one of the most common operations, and would need a bit more than a find/replace to change code.

@mhvk
Copy link

mhvk commented Feb 6, 2025

Could we also vote on whether to include a to_unit method, potentially in a subsequent version? or alternatively a function.

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., to_unit to me suggests, in analogy with to_string, that one converts the quantity to a unit (which is fine for a scalar quantity) rather than to a quantity in a new unit. I also think that if it is not very short, there is little benefit over a function (but of course I'm biased by astropy's .to(new_unit)...). The [C++library](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html) you linked to elsewhere used.in(new_unit)which reads equally well, but in python would have to be.in_which is less nice. (I'm still somewhat partial toq >> new_unit`, but know it is unlikely I'll get everyone to agree on that...)

Anyway, my sense would be to vote first on this limited subset with just the value and unit attributes, next add the other dunder methods and merge, and then open a new issue to discuss function vs method.

@nstarman
Copy link

nstarman commented Feb 6, 2025

Anyway, my sense would be to vote first on this limited subset with just the value and unit attributes, next add the other dunder methods and merge, and then open a new issue to discuss function vs method.

SGTM. Not bad to get the pieces in 1 at a time. particularly when there's ongoing discussion.

@andrewgsavage
Copy link
Collaborator

Anyway, my sense would be to vote first on this limited subset with just the value and unit attributes, next add the other dunder methods and merge, and then open a new issue to discuss function vs method.

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.

@andrewgsavage
Copy link
Collaborator

Something else worth mentioning when putting .units or .units to a vote is that we should follow the same convention for other attributes like dimension (although that could be .dimensionality too!)

@lucascolley
Copy link
Member Author

lucascolley commented Feb 7, 2025

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:

  • Quantity.value (as opposed to .magnitude or any other suggestions)
  • Quantity.unit (as opposed to .units or any other suggestions)

Important things to follow in further issues/PRs:

@lucascolley lucascolley marked this pull request as ready for review February 7, 2025 12:15
@lucascolley lucascolley mentioned this pull request Feb 8, 2025
@andrewgsavage
Copy link
Collaborator

Regarding what is in this PR, it sounds like we want to vote to check that we are happy with:

  • Quantity.value (as opposed to .magnitude or any other suggestions)
  • Quantity.unit (as opposed to .units or any other suggestions)

Alright, what's the plan for voting? Shall we do something here?

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

Successfully merging this pull request may close these issues.

7 participants