pint icon indicating copy to clipboard operation
pint copied to clipboard

Issues with 'percent' units

Open daniel-fink opened this issue 4 months ago • 6 comments

I'm not sure why this occurs, but:

foo = pint.Quantity(20, "%") * pint.Quantity(5, "%") / pint.Quantity(1, "%")
foo
<Quantity(100.0, 'percent')>

Is not the same as:

foo = 20 * ureg.percent * 5 * ureg.percent / 1 * ureg.percent
foo
<Quantity(100.0, 'percent ** 3')>

In addition, .to_reduced_units() scales and reduces to 'dimensionless' when not combining with other units:

ratio = 20 * ureg.percent * 5
ratio
<Quantity(100, 'percent')>

ratio.to_reduced_units()
<Quantity(1.0, 'dimensionless')>

But when combining they don't 'scale' or reduce to dimensionless:

foo = ratio * 1 * ureg.meter
foo
<Quantity(100.0, 'percent * meter')>

foo.to_reduced_units()
<Quantity(100.0, 'percent * meter')>

Finally, if I define my own 'percent' units like: ureg.define("percent = 0.01 * dimensionless = % = pct")

I get errors on accessing the dimensionality of the result:

foo = 20 * ureg.percent * 5 * ureg.percent * 1 * ureg.meter
foo
<Quantity(100, 'percent ** 2 * meter')>

foo.dimensionality
Traceback (most recent call last):
  File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/IPython/core/interactiveshell.py", line 3579, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-42-6e4a081873fb>", line 1, in <module>
    foo.dimensionality
  File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/quantity.py", line 354, in dimensionality
    self._dimensionality = self._REGISTRY._get_dimensionality(self._units)
  File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 722, in _get_dimensionality
    self._get_dimensionality_recurse(input_units, 1, accumulator)
  File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 754, in _get_dimensionality_recurse
    self._get_dimensionality_recurse(reg.reference, exp2, accumulator)
  File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 752, in _get_dimensionality_recurse
    reg = self._units[self.get_name(key)]
  File "/Users/daniel/.pyenv/versions/3.10.15/lib/python3.10/collections/__init__.py", line 986, in __getitem__
    return self.__missing__(key)            # support subclasses that define __missing__
  File "/Users/daniel/.pyenv/versions/3.10.15/lib/python3.10/collections/__init__.py", line 978, in __missing__
    raise KeyError(key)
KeyError: ''

What would be the best way to have 'percent' units act as proper 1/100s of dimensionless units?

daniel-fink avatar Aug 11 '25 05:08 daniel-fink

AFAICT, YOU are missing a parenthesis grouping everything after the "/" operator

On 11 August 2025 13:04:23 GMT+08:00, Daniel Fink @.***> wrote:

daniel-fink created an issue (hgrecco/pint#2205)

I'm not sure why this occurs, but:

foo = pint.Quantity(20, "%") * pint.Quantity(5, "%") / pint.Quantity(1, "%")
foo
<Quantity(100.0, 'percent')>

Is not the same as:

foo = 20 * ureg.percent * 5 * ureg.percent / 1 * ureg.percent
foo
<Quantity(100.0, 'percent ** 3')>

In addition, .to_reduced_units() scales and reduces to 'dimensionless' when not combining with other units:

ratio = 20 * ureg.percent * 5
ratio
<Quantity(100, 'percent')>

ratio.to_reduced_units()
<Quantity(1.0, 'dimensionless')>

But when combining they don't 'scale' or reduce to dimensionless:

foo = ratio * 1 * ureg.meter
foo
<Quantity(100.0, 'percent * meter')>

foo.to_reduced_units()
<Quantity(100.0, 'percent * meter')>

Finally, if I define my own 'percent' units like: ureg.define("percent = 0.01 * dimensionless = % = pct")

I get errors on accessing the dimensionality of the result:

foo = 20 * ureg.percent * 5 * ureg.percent * 1 * ureg.meter
foo
<Quantity(100, 'percent ** 2 * meter')>

foo.dimensionality
Traceback (most recent call last):
 File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/IPython/core/interactiveshell.py", line 3579, in run_code
   exec(code_obj, self.user_global_ns, self.user_ns)
 File "<ipython-input-42-6e4a081873fb>", line 1, in <module>
   foo.dimensionality
 File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/quantity.py", line 354, in dimensionality
   self._dimensionality = self._REGISTRY._get_dimensionality(self._units)
 File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 722, in _get_dimensionality
   self._get_dimensionality_recurse(input_units, 1, accumulator)
 File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 754, in _get_dimensionality_recurse
   self._get_dimensionality_recurse(reg.reference, exp2, accumulator)
 File "/Volumes/Data/Projects/Rangekeeper/src/.venv/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 752, in _get_dimensionality_recurse
   reg = self._units[self.get_name(key)]
 File "/Users/daniel/.pyenv/versions/3.10.15/lib/python3.10/collections/__init__.py", line 986, in __getitem__
   return self.__missing__(key)            # support subclasses that define __missing__
 File "/Users/daniel/.pyenv/versions/3.10.15/lib/python3.10/collections/__init__.py", line 978, in __missing__
   raise KeyError(key)
KeyError: ''

What would be the best way to have 'percent' units act as proper 1/100s of dimensionless units?

-- Reply to this email directly or view it on GitHub: https://github.com/hgrecco/pint/issues/2205 You are receiving this because you are subscribed to this thread.

Message ID: @.***>

cpascual avatar Aug 11 '25 05:08 cpascual

Apologies; you are correct for that first example. But I don't think that error is necessarily connected to the other issue (re: .to_reduced_units())?

daniel-fink avatar Aug 11 '25 05:08 daniel-fink

Hi, regarding the behaviour of to_reduced_units when multiplying percent (dimensionless) and meter,  I cannot help ...

But regarding the KeyError exception , I remember that using % (which happens to be a builtin operator in python) as the symbol in custom defined units was problematic in early versions of pint. Maybe it still is.

On 11 August 2025 13:47:21 GMT+08:00, Daniel Fink @.***> wrote:

daniel-fink left a comment (hgrecco/pint#2205)

Apologies; you are correct for that first example. But I don't think that error is necessarily connected to the other issue (re: .to_reduced_units())?

-- Reply to this email directly or view it on GitHub: https://github.com/hgrecco/pint/issues/2205#issuecomment-3173341003 You are receiving this because you commented.

Message ID: @.***>

cpascual avatar Aug 11 '25 07:08 cpascual

percent is already defined https://github.com/hgrecco/pint/blob/88f2ff02921b6fae82185c966a865a6fa592ed12/pint/default_en.txt#L151C9-L152C19 percent = 0.01 = % you can modify that file locally if you wish i suspect it does not like you redefining units

the reduce units issue is difficult. there are many times you'd want to keep the dimensionless unit, eg if it were radians. Here you may want to_base_units to get meters. Any help documenting the to...units functions or improvments to them would be appreciated

andrewgsavage avatar Aug 11 '25 08:08 andrewgsavage

Thanks @andrewgsavage Understood re: keeping dimensionless units for domain-specific calculations.

Could a solution (hack) could be defining a function to 'filter'/'exclude' specific units in a reduction...? e.g.

def to_filtered(
    quantity: pint.Quantity,
    exclude=("percent",),
) -> pint.Quantity:
    units = quantity._units
    excluded = [name for name in units if name in set(exclude)]
    new_units = units.remove(excluded) if excluded else units
    return quantity.to(new_units)

daniel-fink avatar Aug 12 '25 02:08 daniel-fink

I think (not 100% sure) this only applies to dimensionless units in to_reduce_units. So you could add a parameter to to_reduce_units for how to deal with them - whether to remove them or keep them

To get a result that is percent * meter, you must previously have multiplied percent and meter. Instead of calling to_reduce_units on percent * meter, you could call it in a previous step, so percent becomes dimensionless, or you the auto_reduce_units option

andrewgsavage avatar Aug 13 '25 08:08 andrewgsavage