hgrecco / pint-pandas

Pandas support for pint
Other
166 stars 41 forks source link

No module named pint.quantity #145

Closed zobac closed 1 year ago

zobac commented 1 year ago

python: 3.11.0 OS: Windows 10 pint: 0.20 pandas: 1.5.1 pint_pandas: 0.2

It looks like pint.quantity was removed after version 0.17. Pint_pandas 0.2 crashes on import.

import pandas
import pint
import pint_pandas

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "E:\Envs\pintPandasTest\Lib\site-packages\pint_pandas\__init__.py", line 3, in <module>
    from .pint_array import PintArray, PintType
  File "E:\Envs\pintPandasTest\Lib\site-packages\pint_pandas\pint_array.py", line 21, in <module>
    from pint.quantity import _Quantity
ModuleNotFoundError: No module named 'pint.quantity'
hgrecco commented 1 year ago

We are about to release a new version of pint-pandas that makes use of ndim. We will fix that here as well

ping: @andrewgsavage

zobac commented 1 year ago

Awesome. Thanks.

I've built a package with objects that extend Quantity and Unit, which took some doing. Of course, they no longer work with pint .20. I guess switching to composition is the way to solve this going forward?

hgrecco commented 1 year ago

If there is something that we can add in Pint to ease the transition, let us know. Is there is a part of the code you can share?

zobac commented 1 year ago

The reason we've extended pint is to add the concept of (what we call) ResultTypes to Units so that physical properties that can have the same units are recognized as different in context. For example Pressure and Vacuum can both be measured in Pascals, but need to be differentiated for our purposes. Adding a way to do that to pint Units would be extremely helpful.

That said, I'll show you how I've extended Quantity and Unit up until now. There's a fair bit of punching involved...

from pint.quantity import Quantity as _Quantity, eq, DimensionalityError
from pint.unit import Unit as _Unit

class CMTQuantity(_Quantity):
    """ An extention of pint Quantity with additional methods and overrides
    """

    def __str__(self):
        """ overrides the pint Quantity.__str__ to provide common, user-facing unit strings.
        """
        if self.units == UREG.dimensionless:
            return '{magnitude}'.format(magnitude=self.magnitude)
        try:
            return '{magnitude} {units}'.format(magnitude=self.magnitude, units=str(self.units))
        except KeyError:
            return '{magnitude} {units}'.format(magnitude=self.magnitude, units=super(CMTQuantity, self).units)

    @property
    def units(self):
        """ wrap the ureg.Unit in a CMTUnit
        """
        units = super(CMTQuantity, self).units
        return UREG.Unit(units)

    def __eq__(self, other):

        # We compare to the base class of Quantity because
        # each Quantity class is unique.
        if not isinstance(other, _Quantity):
            if eq(other, 0, True):
                # Handle the special case in which we compare to zero
                # (or an array of zeros)
                if self._is_multiplicative:
                    # compare magnitude
                    return eq(self._magnitude, other, False)
                else:
                    # compare the magnitude after converting the
                    # non-multiplicative quantity to base units
                    if self._REGISTRY.autoconvert_offset_to_baseunit:
                        return eq(self.to_base_units()._magnitude, other, False)
                    else:
                        raise OffsetUnitCalculusError(self._units)

            return (self.dimensionless and
                        eq(self._convert_magnitude(UnitsContainer()), other, False))

        if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True):
            return self.dimensionality == other.dimensionality

        if self.units.isEquivilant(other.units):
            return eq(self._magnitude, other._magnitude, False)
        try:
            return eq(self._convert_magnitude_not_inplace(other._units),
                           other._magnitude, False)
        except DimensionalityError:
            return False

class CMTUnit(_Unit):
    """ An extention of pint Unit with additional methods and overrides
    """

    def __str__(self):
        try:
            return UNIT_DISPLAY_NAMES[super(CMTUnit, self).__str__()]
        except KeyError:
            return super(CMTUnit, self).__str__()

    @property
    def displayName(self):
        return str(self)

    @property
    def name(self):
        """ return the name of this unit as defined in the convergent_units file
        """
        return self.format_babel()

    @property
    def isGauge(self):
        return 'gauge' in repr(self).lower()

    @property
    def isDelta(self):
        return self.name in DELTA_UNITS

    @property
    def hasDelta(self):
        return self.name in DELTA_MAPPING

    def isEquivilant(self, other):

        if self._check(other):
            if isinstance(other, self.__class__):
                return self.dimensionality == other.dimensionality and \
                           self.isGauge == other.isGauge and \
                           self.isDelta == other.isDelta
            else:
                return other == self._REGISTRY.Quantity(1, self._units)

        #elif isinstance(other, NUMERIC_TYPES):
            #return other == self._REGISTRY.Quantity(1, self._units)

        return False

    def __eq__(self, other):

        return self.isEquivilant(other) and str(self) == str(other)

    def __hash__(self):
        return self._units.__hash__()

    def compatible_units(self, *contexts):
        if contexts:
            with self._REGISTRY.context(*contexts):
                return self._REGISTRY.get_compatible_units(self)

        units = self._REGISTRY.get_compatible_units(self)
        return frozenset([u for u in units if \
                          u.isDelta == self.isDelta and \
                          u.isGauge == self.isGauge])

def build_quantity_class(registry, force_ndarray=False, force_ndarray_like=False):
    """ Force pint to use our Quantity definition

    This function matches the one defined in pint.quantity, but uses our _Quantity extension class
    instead.  This function is then punched onto pint.quantity, which results in the unit registry using
    our CMTQuantity objects.
    """

    class Quantity(CMTQuantity):
        pass

    Quantity._REGISTRY = registry
    Quantity.force_ndarray = force_ndarray
    Quantity.force_ndarray_like = force_ndarray_like

    return Quantity

def build_unit_class(registry):
    """ Force pint to use our Unit definition

    This function matches the one defined in pint.unit, but uses our _Unit extension class
    instead.  This function is then punched onto pint.unit, which results in the unit registry using
    our CMTUnit objects.
    """

    class Unit(CMTUnit):
        pass

    Unit._REGISTRY = registry
    return Unit

pint.quantity.build_quantity_class = build_quantity_class
pint.unit.build_unit_class = build_unit_class

UREG = pint.UnitRegistry(fmt_locale='sys')
pint.set_application_registry(UREG)

pint.UnitRegistry = lambda : UREG

try:
    # make pint_pandas use our UREG and Quantity objects
    from pint_pandas import PintType
    PintType.ureg = UREG
    PintType.type = CMTQuantity
    # Ensure temperature/pressure/vacuum rates convert as expected
    UREG.autoconvert_offset_to_baseunit = True
except ImportError:
    pass

DELTA_UNITS =[UREG.deltaC.name,
              UREG.delta_degC.name,
              UREG.deltaF.name,
              UREG.delta_degF.name,

              UREG.delta_Pa_gauge.name,
              UREG.delta_atm_gauge.name,
              UREG.psia.name,
              UREG.delta_inHg_gauge.name,
              UREG.delta_in_Hg_NTP.name,
             ]
hgrecco commented 1 year ago

First ... awesome! Really nice example to see how we can make Pint more extensible.

zobac commented 1 year ago

I was just about to drop a file in with the code cleaned up a bit. Is what I had above ok?

zobac commented 1 year ago

pintExtension.txt

I've attached a file with the same code above, minus the broken imports.

hgrecco commented 1 year ago

can you try the following:

class CTMRegistry(UnitRegistry):

    _quantity_class = CTMQuantity
    _unit_class = CTMUnit

ureg = CTMRegistry(fmt_locale='sys')
# or maybe 
# ureg = CTMRegistry(fmt_locale='sys', autoconvert_offset_to_baseunit=True)
pint.set_application_registry(ureg)

I would be very happy if this works, because it means that the facets refactoring (while creating certain hiccups in the transition) simplified subclassing.

zobac commented 1 year ago

can you try the following:

class CTMRegistry(UnitRegistry):

    _quantity_class = CTMQuantity
    _unit_class = CTMUnit

ureg = CTMRegistry(fmt_locale='sys')
# or maybe 
# ureg = CTMRegistry(fmt_locale='sys', autoconvert_offset_to_baseunit=True)
pint.set_application_registry(ureg)

I would be very happy if this works, because it means that the facets refactoring (while creating certain hiccups in the transition) simplified subclassing.

Yes, this works! It looks like spaces in the dimension names in the .txt aren't tolerated anymore, so I had to deal with that, but once that's done it works.

This is great. The only change I had to make was to change the class declarations to

class CMTQuantity(pint.Quantity) and class CMTUnit(pint.Unit)

and I was able to ditch the build_quantity_class and build_unit_class functions. Thank you!

Now the only thing I'm missing is the eq function that was previously defined in pint.quantity. Is that still accessible somewhere?

hgrecco commented 1 year ago

It looks like spaces in the dimension names in the .txt aren't tolerated anymore, so I had to deal with that, but once that's done it works.

That is correct. They should never have been allowed in the first place. When we improved the parser, we made it more consistent.

and I was able to ditch the build_quantity_class and build_unit_class functions. Thank you!

Indeed, ditching custom build_quantity_class and build_unit_class was one of the goals. I think your code is cleaner now, right?

Now the only thing I'm missing is the eq function that was previously defined in pint.quantity. Is that still accessible somewhere?

Yes. It was actually never defined in pint.quantity (just imported there) but in pint.compat. It is still there. Can you describe how are you using it? Because I never had to use it directly in my user code as it is used by Pint internally.

zobac commented 1 year ago

I use it in the __eq__ method for my CMTQuantity class. I wrote the code years ago and perhaps it's unnecessary?

def __eq__(self, other):

        # We compare to the base class of Quantity because
        # each Quantity class is unique.
        if not isinstance(other, pint.Quantity):
            if eq(other, 0, True):
                # Handle the special case in which we compare to zero
                # (or an array of zeros)
                if self._is_multiplicative:
                    # compare magnitude
                    return eq(self._magnitude, other, False)
                else:
                    # compare the magnitude after converting the
                    # non-multiplicative quantity to base units
                    if self._REGISTRY.autoconvert_offset_to_baseunit:
                        return eq(self.to_base_units()._magnitude, other, False)
                    else:
                        raise OffsetUnitCalculusError(self._units)

            return (self.dimensionless and
                        eq(self._convert_magnitude(UnitsContainer()), other, False))

        if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True):
            return self.dimensionality == other.dimensionality

        if self.units.isEquivilant(other.units):
            return eq(self._magnitude, other._magnitude, False)
        try:
            return eq(self._convert_magnitude_not_inplace(other._units),
                           other._magnitude, False)
        except DimensionalityError:
            return False

If there's a better way, please let me know.

zobac commented 1 year ago

Hernan,

Thank you! All my tests are passing again, and my code is much nicer and will be easier to manage going forward. I'm eagerly awaiting the next release of pint_pandas, but in the meantime I'll test my code that uses pint_xarray.

hgrecco commented 1 year ago

My understanding of your __eq__ code is that you are just adding in between:

if self.units.isEquivilant(other.units):
            return eq(self._magnitude, other._magnitude, False)

I wonder if you can just do something like this:

def __eq__(self, other):
    if self.units.isEquivilant(other.units):
        return eq(self._magnitude, other._magnitude, False)
    return super().__eq__(other)

I understand that this is not exactly the same now, but maybe by tweaking isEquivilant it can work. My suggestion is always make the super classes do as much work as possible.

zobac commented 1 year ago

Yep, much better. Thanks again!

hgrecco commented 1 year ago

@andrewgsavage is there anything else that should be fixed before 0.20.1 so that pint-pandas can build upon?

zobac commented 1 year ago

I don't see anything. My full test suite is passing now.

andrewgsavage commented 1 year ago

@andrewgsavage is there anything else that should be fixed before 0.20.1 so that pint-pandas can build upon?

I don't think so. This is an issue only when the ureg is non default - we should add a test for this.

hgrecco commented 1 year ago

I added a test for subclassing already.

andrewgsavage commented 1 year ago

I added a test for subclassing already.

I mean in pint-pandas there is no test for using a custom ureg

andrewgsavage commented 1 year ago

closed by #133

burnpanck commented 1 year ago

I think we urgently need a fix for this issue in a released version of pint_pandas (either a dedicated 0.2.1 or the full 0.3).

With the release of pint 0.20, all libraries that depend on pint_pandas are essentially broken. Furthermore, there is no good fix available to them:

Applications can either pin pint to <0.20, or they can install a bleeding edge version of pint_pandas, Neither option is really acceptable for a library: Bleeding edge require explicit repository paths, which libraries should never use. But if a library pins pint to fix compatibility with the current pint_pandas, it prevents downstream applications from using the new pint together with a bleeding edge version of pint_pandas. Furthermore, it risks having that pin in place longer than necessary, as it might not be able to release immediately when pint_pandas releases the fix.

If a release of 0.3 or a backport of the fix in #133 to a hypotetical 0.2.1 are not feasible, could we maybe release a very simple 0.2.1 which is exactly the same as 0.2 except for a pin pint<0.20. Contrary to downstream libraries, this neither prevents applications from installing bleeding edge pint_pandas, nor does it pin pint longer than necessary.

andrewgsavage commented 1 year ago

Just waiting on @hgrecco to make a 0.3 release...

hgrecco commented 1 year ago

I will do it right now. Anything else to merge?

andrewgsavage commented 1 year ago

Nope

hgrecco commented 1 year ago

done!