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

Make physical constants (numerically) more easily accessible #1078

Open
DuaneKaufman opened this issue Apr 13, 2020 · 20 comments
Open

Make physical constants (numerically) more easily accessible #1078

DuaneKaufman opened this issue Apr 13, 2020 · 20 comments

Comments

@DuaneKaufman
Copy link

Hi,
I would like to easily be able to retrieve the numerical value of a constant in Pint.

I believe this is a follow-on to "Make physical constants more easily accessible #159", and forgive me if my request has been answered and I have just been unable to find it

If I try to use the .to() method with constants, I get an error.

c = ureg.speed_of_light
print(c.to(ureg.m/ureg.s))
"AttributeError: 'Unit' object has no attribute 'to'"

Right now I am converting constants to Quantities by multiplying by 1, like this:

c = ureg.speed_of_light
print((c*1).to(ureg.m/ureg.s))
299792458.0 meter / second

Is this the preferred way to get a numerical representation of a constant?

Thanks,
Duane

@hgrecco
Copy link
Owner

hgrecco commented Apr 16, 2020

Right now, physical are just quantities, which is very useful but not complete. I been looking forward to create better support for constants. But I want to have better support for measurements first (so we can parse uncertainties from the definition file).

@DuaneKaufman
Copy link
Author

DuaneKaufman commented Apr 16, 2020 via email

@codejager
Copy link

codejager commented May 28, 2020

Constants can be considered a unit; like g0, gn for default gravity acceleration, or g_(yourlocation) for the gravitational accelleration at your measurement facility. At ours we used to use "g" which was not a problem until we started scaling values using pint. Of course "1 g" for 9.81 m/s^2 conflicts with 1 gram.
What is a standard for computer interpretable units that also support constants, either be it common ones or industry specific? I found an elegant solution at http://unitsofmeasure.org/ucum.html#const but I understood that pint does not recognize the notation of this standard and I also don't know if this standard is broadly adopted.

@DuaneKaufman
Copy link
Author

DuaneKaufman commented May 28, 2020 via email

@saroad2
Copy link

saroad2 commented Jun 1, 2020

Really looking forward to this issue closure. I think it is a much-needed ability that can really elevate my code.

@dr-shorthair
Copy link

@codejager asked

I found an elegant solution at http://unitsofmeasure.org/ucum.html#const but I understood that pint does not recognize the notation of this standard and I also don't know if this standard is broadly adopted.

Indeed it is. UCUM is built in to LOINC and FHIR, which gets you much of the health and clinical community. So there is a practical benefit to adopting the UCUM style of unit codes/symbols.

FWIW there is an API for testing and converting UCUM here: https://ucum.nlm.nih.gov/ucum-service.html
and a cute little UI here https://ucum.nlm.nih.gov/ucum-lhc/demo.html

@TomNicholas
Copy link
Contributor

TomNicholas commented Oct 20, 2021

I've just come across this issue whilst work on pint-xarray, and I'm confused as to
(a) what the status is,
(b) whether it's possible to solve this issue by having physical constants be listed as both pint.Quantities and pint.Units?

Right now, physical [constants] are just quantities

I don't understand what is meant by this - they are pint.Unit instances right now, not pint.Quantity instances.

In [1]: from pint import UnitRegistry

In [2]: ureg = UnitRegistry()

In [3]: ureg.speed_of_light
Out[3]: <Unit('speed_of_light')>

It seems to me that physical constants are regularly used as both units and like quantities. If I say "we measure the speed of the galaxy in units of c" then I'm using it like a unit, and if I say "E = m * c**2" then I'm using it like a quantity.

The argument for having constants represented as a Quantity is that the speed of light is a single real-world data point with a specific value (so has a magnitude), that is commonly expressed with certain units (and those units may be different in a different unit system). Isn't that simply just a pint.Quantity(2.99792458 * 10**8, units='metres / second')? In this sense it is no different from any data that we would get from any other source (which we would represent as a Quantity).

(For what it's worth the uynt package just has two lists, one for Units and one for Physical Constants, and they both have the speed of light in them.)

Can we have physical constants listed as both Units and Quantities? Presumably that would mean the UnitRegistry would need an additional list of physical constants as well as unit definitions, but is that a problem? The only issue I can see is of potential name collisions, but you could just have the default import be the Unit, and have the quantity version be hidden behind ureg.constants for use when desired. I'm happy to have a go at implementing this.

But I want to have better support for measurements first (so we can parse uncertainties from the definition file).

Why do we need support for uncertainties first? Can we not have functionality for physical constants without uncertainties to begin with and with uncertainties later?

@hgrecco
Copy link
Owner

hgrecco commented Oct 21, 2021

I am 100% in favor of restructuring the constants API to include your suggestion. The reason of having the measurement API overhaul is to include constants with their uncertainties. But if someone wants to work on the constants first, go for it!

@TomNicholas
Copy link
Contributor

Okay, so thinking about this more, I think I need to:

  • Add a .constants property to UnitRegistry, which contains a list of Quantity objects,
  • Populate that list upon initialising the UnitRegistry object,
  • Ideally get all the descriptions of the constants from the constants_en.txt file, so that every constant is automatically defined as both a Unit and a Quantity,
  • Which means I should add an additional constants_filepath argument to UnitRegistry, which is read from to create both the Units we already have and the new Quantities,
  • Problem is I don't think there is currently a function for reading those definition files and creating Quantities from them.
  • And to make such a function, what would I do with the aliases listed? They can't be attached to Quantities right?

@OrangeChannel
Copy link

So is the idea that "constants" can be accessed as quantities in system-specific base units?

Right now, 2 * ureg.c returns <Quantity(2, 'speed_of_light')> but what we'd want is 2 * ureg.constants.c giving 2 * ureg.Quantity(1, ureg.c).to_base_units() == <Quantity(5.99584916e+08, 'meter / second')> if the SI system is the default system? And <Quantity(6.55714038e+08, 'yard / second')> if imperial is used etc.

What should dimensionless constants return? Will ureg.constants.pi support the following inherently?

>>> import decimal
>>> decimal.getcontext().prec = 55
>>> import pint
>>> ureg = pint.UnitRegistry(non_int_type=decimal.Decimal)
>>> ureg.Quantity(1, ureg.pi)
<Quantity(1, 'pi')>
>>> _.to_base_units()
<Quantity(3.1415926535897932384626433832795028841971693993751, 'dimensionless')>
>>> _.m
Decimal('3.1415926535897932384626433832795028841971693993751')  # is this what ureg.constants.pi should return?

If a user has a custom list of constants, will there be a new way to load the definitions file? Can we maybe implement a kwarg like ureg.load_definitions('/your/path/to/my_consts.txt', constants=True)?

Will there be a way to programmatically define new constants once the registry is initiated? Maybe ureg.define('euler_mascheroni_constant = 0.57721566490153286060 = gamma_e', constant=True)

Personally, I think it will be more of a hassle to support this instead of just telling users in the documentation they can just use c = Q_(1, ureg.c).to_base_units().

The big problem I see is that the whole unit definition process right now is designed to be easily expanded and that prevents us from separating constants and units in the definition files. The constants are currently just explicitly imported into default_en.txt with no differentiation, and it's definitely useful to be able to represent values in terms of pi etc. so them being units as default just works IMO.

Although this should definitely be talked about in another issue, the easiest way to allow much more flexible unit/constant definitions (with uncertainties too) would be to define them as dicts or some other python structure in a default_en.py file, and I could see this system easily supporting an idea of certain definitions being accessible in their base units form. This has the obvious disadvantage of making it more difficult for users to add custom unit definitions and the entire definition parsing logic would need to be redone, including ureg.define etc.

@hgrecco
Copy link
Owner

hgrecco commented Oct 23, 2021

Indeed constants are just units right now. There is on difference in terms of definition in a text file or internal usage or API. This was great in pint early stages as it allowed us to expand pint capabilities without making the code harder nor longer. Another nice thing is that many time you want an "algebraic" usage (i.e delta_x = ureg.speed_of_light * (4 * ureg.seconds) and the way we do it keeps the equation clear.

The main complain that I have seen is that constants are "different" and we should provide by default the "explicity value".

I think that @OrangeChannel makes a great point that might allow use to satisfy both user cases without changing the code to much.

So is the idea that "constants" can be accessed as quantities in system-specific base units?

What if we provide

  1. A way to get the value in the base units of a system:
    ureg.sys.imperial.speed_of_light -> <Quantity(3.27857019e+08, 'yard / second')>
  2. A pseudo system that returns the constant in the current default system (for example if the internationl is the defaultt)
    ureg.constants.speed_of_light -> <Quantity(2.99792458e+08, 'meter / second')>

This requires minor changes to the codebase, no change to the definitions and just some documentation.

It does not solve the uncertainty problem which is an important one but could be a nice way in the right direction as I do not see any drawback of this API in the future.

@TomNicholas
Copy link
Contributor

You raise a lot of good points @OrangeChannel .

Personally, I think it will be more of a hassle to support this instead of just telling users in the documentation they can just use c = Q_(1, ureg.c).to_base_units().

You might well be right about this.

What if we provide

  1. A way to get the value in the base units of a system:
    ureg.sys.imperial.speed_of_light -> <Quantity(3.27857019e+08, 'yard / second')>
  2. A pseudo system that returns the constant in the current default system (for example if the internationl is the defaultt)
    ureg.constants.speed_of_light -> <Quantity(2.99792458e+08, 'meter / second')>

I do quite like this idea of just providing more functional ways to access things as constants instead of defining entirely new lists.
Users mostly just want a quick way to import the physical constant as a quantity - could we perhaps just make ureg.constants.X (or even ureg.constant(X)) be a shorthand for ureg.Quantity(1, ureg.X).to_base_units()? Then we don't have to maintain a special list of physical constants, because we just allow the user to do this to any unit definition they like. And it still makes the quantity version of the speed of light easy to import: c = ureg.constant('speed_of_light').

@hgrecco
Copy link
Owner

hgrecco commented Oct 23, 2021

Let me describe my suggestion in more detail:

  1. We create a Group named constants and put every constant inside it.
  2. We change System.__getattr__ to be like this:
    def __getattr__(self, item):
        getattr_maybe_raise(self, item)
        u = getattr(self._REGISTRY, self.name + "_" + item, None)
        if u is not None:
            return u
         if u in self._REGISTRY.get_group("constants"):
            return 1 * self._REGISTRY.get_base_units(u)
        return getattr(self._REGISTRY, item)
  1. We add Group.__getattr__to be like ths:
    def __getattr__(self, item):
         if u in self._REGISTRY.constants.members:
            return 1 * self._REGISTRY.get_base_units(u)
        return getattr(self._REGISTRY, item)
  1. We add in the base regitry the following property:
    @property
    def constants(self):
        return self._REGISTRY.get_group("constants")

(1) provides a way to declare constants without any additional syntax
(2) provides a way to obtain a constant in a given unit system
(3) and (4) provides a way to obtan a constant in the current system used
and this is fully backwards compatible.

In use

>>> ureg.speed_of_light
<Unit('speed_of_light')>
>>> 1 * ureg.speed_of_light
<Quantity(1, 'speed_of_light')>
>>> ureg.sys.imperial.speed_of_light
<Quantity(3.27857019e+08, 'yard / second')>
>>> ureg.constants.speed_of_light # if the current system is 'mks'
<Quantity(2.99792458e+08, 'meter / second')>

If I get your comment 3 days ago as a list of requirements:

  • Add a .constants property to UnitRegistry, which contains a list of Quantity objects,
  • Populate that list upon initialising the UnitRegistry object,
  • Ideally get all the descriptions of the constants from the constants_en.txt file, so that every constant is automatically defined as both a Unit and a Quantity,

And then

Which means I should add an additional constants_filepath argument to UnitRegistry, which is read from to create both the Units we already have and the new Quantities.

Not needed as we used the Group API

Problem is I don't think there is currently a function for reading those definition files and creating Quantities from them.

Not needed.

And to make such a function, what would I do with the aliases listed? They can't be attached to Quantities right?

Not needed .

What do you think?

@OrangeChannel
Copy link

Oh, I was working on my own solution and just finished as well. I didn't know about the group api and the way I did it is definitely not the most elegant 😄 .
OrangeChannel@dfd0a37

Here's how my solution works (I lazily just used the UnitRegistry in places and I think that's wrong)

>>>import pint
>>>ureg = pint.UnitRegistry()
>>>ureg.constants.speed_of_light
<Quantity(2.99792458e+08, 'meter / second')>
>>>ureg.constants.US.speed_of_light
<Quantity(3.27857019e+08, 'yard / second')>
>>>ureg.constants.US('c')
<Quantity(3.27857019e+08, 'yard / second')>
>>>ureg.constants['c']
<Quantity(2.99792458e+08, 'meter / second')>

What do you think?

@hgrecco That looks great, I think this part (shown below) should be changed though. It should probably be ureg.sys.imperial.constants.speed_of_light (which is really long but only longer by a .sys than my solution).

>>> ureg.sys.imperial.speed_of_light
<Quantity(3.27857019e+08, 'yard / second')>

hgrecco added a commit that referenced this issue Oct 26, 2021
1. A new property `contants` is added to the registry.
2. This property is just a reference to the `constants` Group.
3. This group is populated using the @group directive in the definitions file.
4. Minor changes to Group and System __getattr__

This change is fully backwards compatible:
```python
>>> ureg.speed_of_light
<Unit('speed_of_light')>
>>> 1 * ureg.speed_of_light
<Quantity(1, 'speed_of_light')>
>>> ureg.sys.imperial.speed_of_light
<Quantity(3.27857019e+08, 'yard / second')>
>>> ureg.constants.speed_of_light # if the current system is 'mks'
<Quantity(2.99792458e+08, 'meter / second')>
```

Close #1078
hgrecco added a commit that referenced this issue Oct 26, 2021
1. A new property `contants` is added to the registry.
2. This property is just a reference to the `constants` Group.
3. This group is populated using the @group directive in the definitions file.
4. Minor changes to Group and System __getattr__

This change is fully backwards compatible:
```python
>>> ureg.speed_of_light
<Unit('speed_of_light')>
>>> 1 * ureg.speed_of_light
<Quantity(1, 'speed_of_light')>
>>> ureg.sys.imperial.speed_of_light
<Quantity(3.27857019e+08, 'yard / second')>
>>> ureg.constants.speed_of_light # if the current system is 'mks'
<Quantity(2.99792458e+08, 'meter / second')>
```

Close #1078
@hgrecco
Copy link
Owner

hgrecco commented Oct 26, 2021

Hi @OrangeChannel and the rest of the people in this thread. I have pushed to my develop branch an implementation of my proposed constants API. AFAIK is fully backwards compatible and does not change Pint too much. It is also future compatible (I think) as any upgrades to the constants (ie. adding uncertainties) could be added.

The only thing that I do not like is that ureg.sys.imperial.speed_of_light returns a Quantity and ureg.sys.imperial.yard returns a Unit. This could be fixed by doing the proposed API change by @OrangeChannel: ureg.sys.imperial.constants.speed_of_light (which indeed is long but I guess as long as the alternative)

I am open to ideas, suggestions and criticisms.

@OrangeChannel
Copy link

I was experimenting with some stuff and found some possibly unwanted behavior in pint.systems.Group:

>>> ureg.c, ureg.speed_of_light
(<Unit('speed_of_light')>, <Unit('speed_of_light')>)
>>> ureg.constants.c, ureg.constants.speed_of_light
(<Unit('speed_of_light')>, <Quantity(2.99792458e+08, 'meter / second')>)
# why is c returning a Unit but speed_of_light a Quantity

Let's say I want to add my own constant, testing to the constants group in my current unit registry:

>>> ureg.define('testing = 0.8 * meter')
>>> ureg.constants.members
frozenset({'neutron_mass', 'K_alpha_W_d_220', 'coulomb_constant', ...})
>>> 'testing' in _
False
>>> ureg.constants.testing  # should not be accessible
<Unit('testing')>  # how?
# correct behavior
>>> ureg.define('testing = 0.8 * meter')
>>> ureg.constants.add_units('testing')
>>> ureg.testing, ureg.constants.testing
(<Unit('testing')>, <Quantity(0.8, 'meter')>)  # expected

>>> 'testing' in ureg.constants
TypeError: argument of type 'Group' is not iterable  # should this work?
>>> ureg.constants.members
frozenset({'boltzmann_constant', 'elementary_charge', ...})  # 'testing' is in this
>>> 'testing' in ureg.constants.members
True  # expected

Now if I load my own definitions with this file: testing.txt:

@group constants
    testing_in_file = 0.6 inch
@end
>>> ureg.load_definitions(r'testing.txt')
>>> ureg.constants.members
frozenset({'testing_in_file'})  # did we overwrite the group?
>>> ureg.constants.testing_in_file
<Quantity(0.01524, 'meter')>  # correct unit conversion
>>> ureg.constants.c, ureg.constants.speed_of_light
(<Unit('speed_of_light')>, <Unit('speed_of_light')>)
# !!! this is now using the wrong behavior again

Now if I load my own definitions with this file: testing_groups.txt:

@group my_constants
    testing_in_file = 0.6 inch
@end
# correct behavior
>>> ureg.get_group('my_constants').members
frozenset({'testing_in_file'})  # correct behavior
>>> ureg.constants.add_groups('my_constants')  # is this way the only way?
>>> ureg.constants.speed_of_light, ureg.constants.testing_in_file
(<Quantity(2.99792458e+08, 'meter / second')>, <Quantity(0.01524, 'meter')>)

I also think the @group directive (?) thing should be mentioned somewhere in the user-level docs, and if the second method there should be the only way to do this (i.e. overwriting is expected from defintion files), that should definitely be mentioned in both user-level and developer reference.

Also, would be nice to have a __dir__ defined for pint.systems.Group so we get autocompletion like in pint/registry.py:347

@hgrecco
Copy link
Owner

hgrecco commented Oct 31, 2021

@OrangeChannel Regarding c/speed_of_light good catch, membership is not checked agains cannonical names, not alias. I should fix this.

Also regarding overwriting and extending a group, I think is something to discuss. I think both API could be useful and we can provide a syntax for that along the following lines

@group EXISTING_GROUP
@end 

raises an error

@group GROUP extends
@end 

warns if GROUP does not exists
concatenates content if GROUP exists

@group GROUP replace
@end 

warns if GROUP does not exists
replaces content if GROUP exists

I like the __dir__ idea

@OrangeChannel
Copy link

Is this also desired behavior?

>>> ureg.define('testing = 0.8 * meter')
>>> 'testing' in ureg.constants.members
False
>>> ureg.constants.testing  # should not be accessible
<Unit('testing')>  # how?

Seems anything defined in the UnitRegistry is fetched even if it's not in the group, but it ignores the new constants behavior of returning a Quantity. It also happens with other groups, and not just members.

>>> ureg.get_group('USCSLengthSurvey').testing
<Unit('testing')>
>>> ureg.get_group('USCSLengthSurvey').constants
<pint.systems.build_group_class.<locals>.Group object at 0x0000020EE55A9070>
>>> ureg.get_group('USCSLengthSurvey').define
<bound method BaseRegistry.define of <pint.registry.UnitRegistry object at 0x0000020EDD8FB8B0>>

@TraceBivens
Copy link

Any progress on this? Alternatively, can the workaround that is mentioned at the beginning of this thread be documented? I love using pint for quick calculations but I often need constants and it's a pain to define them manually using Scipy and look up the dimensionality every time.

@the-vindicar
Copy link

What is the current status of this issue?
For the reference, I just wasted a week because (Quantity(1, ureg['kg']) * ureg.g0 * Quantity(1, ureg['m'])).magnitude turned out to be 1 instead of 9.81. Sure, adding a m_as('newton * meter') call fixes the problem, but it's a really unexpected behaviour.
If there is something like constants property mentioned above, then perhaps the current approach could be deprecated?

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

9 participants