Skip to content
This repository was archived by the owner on Mar 12, 2025. It is now read-only.

initial Dimension protocol #1

Merged
merged 5 commits into from
Feb 11, 2025
Merged

Conversation

lucascolley
Copy link
Member

@nstarman @jorenham @andrewgsavage does this look about right?


@runtime_checkable
class Dimension(Protocol):
def __eq__(self, other: Self, /) -> bool: ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure: requiring __eq__ to work with other: Self seems like a more restrictive requirement (though, I'm often unsure how to interpret object used as a type annotation). Unresolving this thread for now.

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 guess object in an annotation within the object class is effectively doing the same as what Self is doing?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might, but in any other context I take it as meaning "this variable cannot be assume to conform to any interface", which is radically different.
I suspect maybe @nstarman knows whether they are truly equivalent here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparison dunder methods are complicated. They are meant to work with all input, returning NotImplemented if they don't know how to perform the comparison, and bool if they do (obv numpy extends this to Array[bool]).
What we want is approximately

@overload
def __eq__(self, other: object) -> NotImplemented: ...
@overload
def __eq__(self, other: Self) -> bool: ...
def __eq__(self, other: object) -> NotImplemented | bool

A problem in Python is that type checkers really don't like if you annotate the return type as NotImplemented | bool!
@jorenham, I'm looking at optype for how to type this precisely, but AFAIK it should just be

def __eq__(self, other: object) -> bool

Which is the builtin, but here and elsewhere we should add some docs!

def __eq__(self, other: object) -> bool
    """Equality comparison.
     
     Comparison between two Dimension objects (of the same type) must return a `bool`.
     Comparison with other objects is outside the scope of this API.

     """

@nstarman
Copy link

nstarman commented Feb 5, 2025

It looks good, the major issue is that Dimension essentially has no distinguishing attribute! Many things will pass isinstance checks on this protocol. What can we add?

@jorenham
Copy link
Contributor

jorenham commented Feb 5, 2025

It looks good, the major issue is that Dimension essentially has no distinguishing attribute! Many things will pass isinstance checks on this protocol. What can we add?

.name 🤔 ?

@jorenham
Copy link
Contributor

jorenham commented Feb 5, 2025

Should dimensions also be orderable, i.e. implement __gt__ & co. ?

@jorenham
Copy link
Contributor

jorenham commented Feb 5, 2025

It looks good, the major issue is that Dimension essentially has no distinguishing attribute! Many things will pass isinstance checks on this protocol. What can we add?

Or perhaps something like .finite: bool 🤔 ?

@lucascolley
Copy link
Member Author

lucascolley commented Feb 5, 2025

Looking at what the existing implementations have:

  • unyt uses a sympy.Symbol
  • pint uses a dict of base_dimension: power elements

Does astropy.units have a similar (perhaps private) object @nstarman ?

@nstarman
Copy link

nstarman commented Feb 5, 2025

.name 🤔 ?

.name is good to have, but still not differentiating enough.
What about properties like is_dimensionless, good for composite dimensions, or methods like equivalent_to(other_dimension)?
Ping @mhvk

@mhvk
Copy link

mhvk commented Feb 5, 2025

Sorry, just reacting to a ping and short of time, so really just a quaestion: what is a Dimension meant to be physically!? Is it what we call the physical type? (length, area, speed, etc.; https://docs.astropy.org/en/latest/units/physical_types.html) Or what in a composite units are the bases? If the latter, then we're similar to pint, except that for us we have a two lists, of bases and powers (each unit has attributes scale, bases, and powers).

@nstarman
Copy link

nstarman commented Feb 5, 2025

The former, I think.
Thinking of units as vectors in a space of dimensions, these are the direction vectors (not unit vectors, which implies a scale, which is defined by the unit system, which is the set of directions (a dimension system) and their base scalings).

@lucascolley
Copy link
Member Author

lucascolley commented Feb 5, 2025

what is a Dimension meant to be physically!?

I'm pretty sure we're implementing this (from wikipedia):

The SI standard selects the following dimensions and corresponding dimension symbols:

time (T), length (L), mass (M), electric current (I), absolute temperature (Θ), amount of substance (N) and luminous intensity (J).

Mathematically, the dimension of the quantity Q is given by ${\displaystyle \text{dim}\ Q={\mathsf {T}}^{a}{\mathsf {L}}^{b}{\mathsf {M}}^{c}{\mathsf {I}}^{d}{\mathsf {\Theta }}^{e}{\mathsf {N}}^{f}{\mathsf {J}}^{g}}$

where their dim Q is our Q.unit.dimension. Of course, we don't need to restrict it to SI.

@mhvk
Copy link

mhvk commented Feb 5, 2025

OK, I see. Seems a bit secondary -- at least in astropy we've had physical types for a while now but I don't know that I've seen anybody actually using it.

Not sure that it needs a new class/protocol. Wouldn't it be fine if we simply required that a unit class has the ability to produce a composite unit that uses only a given set of bases?

Anyway, maybe best to try to nail down the unit API first?

@andrewgsavage
Copy link

I think it is worth adding a docstring for the class as it's not completely obvious what its purpose is.

The SI standard selects the following dimensions and corresponding dimension symbols:
time (T), length (L), mass (M), electric current (I), absolute temperature (Θ), amount of substance (N) and luminous intensity (J).

Yes, it is these base dimensions. Although there may be more or fewer dimensions; some libraries use angle or information as a dimension, someone may wish to use currency.

astropy's physical type is more similar to quantitykind in other libraries/databases. However quantitykind is a property of a Quantity and is stricter, preventing a torque quantity from being added to an energy.

The purpose of this class is to allow a user to check the dimensions of a Unit or Quantity in a consistent manner. Without this the dimension property could return anything, so not relied on when writing code to support multiple libraries.

Not sure that it needs a new class/protocol. Wouldn't it be fine if we simply required that a unit class has the ability to produce a composite unit that uses only a given set of bases?

in pint that would be
q.to_base_units().units == Unit("m kg").to_base_units().units

compared to using dimensionality:
q.dimensionality == Dimension("[length]*[mass]")

note that the base units could be any unit system which is why calls to to_base_units is needed.

is_dimensionless or dimensionless would be useful yes

@nstarman
Copy link

nstarman commented Feb 5, 2025

astropy's physical type is more similar to quantitykind in other libraries/databases. However quantitykind is a property of a Quantity and is stricter, preventing a torque quantity from being added to an energy.

I'm a big fan of this idea and would like to hear more about how this would be implemented API-wise. How does it fit into UAPI vs DAPI? Is it an intermediate level?

@mhvk
Copy link

mhvk commented Feb 5, 2025

OK, so astropy's equivalent is unit1.is_equivalent(unit2) - which goes through _physical_type_id, which decomposes the unit in bases and then returns a tuple of a list of unit names and a list of powers.

Note that at least for us, .dimension is not quite good enough for testing equivalency, since we support so-called equivalencies. But clearly it would be equivalent to making our current _physical_type_id public, so that's something we definitely can support (and which makes sense now, thanks!)

@andrewgsavage
Copy link

andrewgsavage commented Feb 5, 2025

astropy's physical type is more similar to quantitykind in other libraries/databases. However quantitykind is a property of a Quantity and is stricter, preventing a torque quantity from being added to an energy.

I'm a big fan of this idea and would like to hear more about how this would be implemented API-wise. How does it fit into UAPI vs DAPI? Is it an intermediate level?

it's similar to units, and can also be heirachial, see https://github.com/qudt/qudt-public-repo/wiki/Advanced-User-Guide#4-computing-applicable-units-for-a-quantitykind

I made a start at an implementation hgrecco/pint#1967
I think pint should wait till it moves to a new unit defintion file format as there's the quantitykind would ideally have more attributes than can fit on one line!

the only released implementation I have seen is described in
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html

e: there's many more quantitykinds than units so the plan in my head is for pint to move to a new unit file format, then parse the QUDT ontology database using the new file format

@lucascolley lucascolley marked this pull request as ready for review February 6, 2025 10:16
@lucascolley
Copy link
Member Author

I think this API is ready!

@nstarman
Copy link

nstarman commented Feb 6, 2025

@mhvk another use of dimensions is for dimensional analysis e.g solving Buckingham Pi type problems. Physics-wise this is important for analytics (eg Sympy's quantities), symbolic regression, etc, and then just expedited quantity checking more broadly.

@mhvk
Copy link

mhvk commented Feb 6, 2025

@andrewgsavage - thanks for those links - the open-std documentation is really nice and thorough. It may make sense to explicitly refer to that in our various documents, no point reinventing the wheel!

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.

OK, if int for the power is fine typing wise in leaving room for fractions in implementing libraries, I'm happy with this.

@lucascolley
Copy link
Member Author

@neutrinoceros how does this look to you?

Copy link
Member Author

@lucascolley lucascolley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed is_dimensionless from this PR and opened https://github.com/quantity-dev/dimension-api/issues/2.

I think there is nothing to vote on for this PR now. I plan to merge it soon if there are no objections.

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.

I agree nothing feels controversial in this PR.
Thoughts:

  1. Don't love the Pixi stuff. I have the sense more people use uv. But we can easily support both without conflict so 🤷 SGTM. Which we used is a discussion for later when we set up CI; and even then doesn't impact whether there's config for both.
  2. dynamic versioning. For future PR.
  3. this can't be released without #2 being resolved, but that's irrelevant to whether this should be merged.

Thanks @lucascolley for this PR!

@lucascolley
Copy link
Member Author

thanks all, in it goes

@lucascolley lucascolley merged commit 9f4cd9f into quantity-dev:main Feb 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants