Closed zobac closed 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
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?
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?
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,
]
First ... awesome! Really nice example to see how we can make Pint more extensible.
I was just about to drop a file in with the code cleaned up a bit. Is what I had above ok?
I've attached a file with the same code above, minus the broken imports.
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.
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?
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.
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.
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.
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.
Yep, much better. Thanks again!
@andrewgsavage is there anything else that should be fixed before 0.20.1 so that pint-pandas can build upon?
I don't see anything. My full test suite is passing now.
@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.
I added a test for subclassing already.
I added a test for subclassing already.
I mean in pint-pandas there is no test for using a custom ureg
closed by #133
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.
Just waiting on @hgrecco to make a 0.3 release...
I will do it right now. Anything else to merge?
Nope
done!
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.