MetPy needs a general solution for returning scalars when provided scalars
Depending on the function, some MetPy calculations will return scalars when provided scalars or return numpy scalar array when provided a scalar. For example:
from metpy.units import units
from metpy.calc import wind_direction, wind_components
wdir = wind_direction(5. * units('m/s'), 0. * units('m/s'))
print("wdir.m type is: %s" % (type(wdir.m),))
# wdir.m type is: <class 'numpy.ndarray'>
u, v = wind_components(5. * units('m/s'), 0. * units('degree'))
print("u.m type is: %s" % (type(u.m),))
# u.m type is: <class 'numpy.float64'>
Functions like apparent_temperature got a is_not_scalar hack in #838 that attempts to keep track of the inbound Quantity magnitude dimensionality and then return the result with that same dimensionality.
I started down the path of writing a helper for this:
def match_dimensionality(quantity_to_return, quantity_to_match):
"""Helper returning `pint.Quantity` that matches dimensionality.
Various MetPy methods take both array-like and scalar `pint.Quantity`
magnitudes as arguments. The API user likely assumes that the returned
value matches the provided dimensionality. For example, when provided a
scalar, the method should return a scalar.
Parameters
----------
quantity_to_return : `pint.Quantity`
The `pint.Quantity` whose magnitude dimensionality should be modified.
quantity_to_match : `pint.Quantity`
The `pint.Quantity` whose magnitude dimensionality should be copied.
Returns
-------
`pint.Quantity` with matched dimensionality.
"""
if not isinstance(quantity_to_match.m, (list, tuple, np.ndarray)):
# Our match quantity is a scalar, generally convert to scalar
if isinstance(quantity_to_return.m, (list, tuple, np.ndarray)):
return quantity_to_return.m.item() * quantity_to_return.units
return quantity_to_return
But this approach quickly broke down as some functions modify their arguments and thus can no longer be checked at function return time. I also found some surprising/scary pint.Quantity in-place modification when using np.asarray.
>>> from metpy.units import units
>>> import numpy as np
>>> u = 5. * units('m')
>>> type(u.m)
<class 'float'>
>>> np.asarray(u)
array(5.)
>>> type(u.m)
<class 'numpy.ndarray'>
Any thoughts here on what a general solution to this could be?
It would be good to have something to do this--of course it would be better if we didn't have to care and things just worked with scalars transparently, but alas. I wonder if a decorator would be enough--and if we could get away with only applying to functions that need the help?
MetPy should NOT be modifying any arguments in-place. If you see places where we're doing something like that, please open an issue (or even better another PR :wink:).
Your issue with pint I think is related to an issue I opened ages ago in Pint: hgrecco/pint#481.
Same as https://github.com/numpy/numpy/issues/12636? I'm guessing numpy doesn't change anything anytime soon, but it would be nice if there was a recommended solution available for package maintainers.
Today I wanted to return a scalar from heat_index after providing a scalar but failed:
from metpy.units import units
from metpy.calc import heat_index
hi = heat_index(30 * units.degC, 0.65)
print("hi.m type is: %s" % (type(hi.m),))
# hi.m type is: <class 'numpy.ndarray'>
Additionally I tried apparent_temperature() which checks for:
https://github.com/Unidata/MetPy/blob/eb3fa007b7e7a0be95ea209867b07a4fe121cadc/src/metpy/calc/basic.py#L379
but fails too:
from metpy.calc import apparent_temperature
from metpy.units import units
at = apparent_temperature(90 * units.degF, .6, 5 * units.mph)
print("at.m type is: %s" % (type(at.m),))
# at.m type is: <class 'numpy.ndarray'>
Does these behaviors also relate to this issue?
Certainly the heat_index one does. The apparent_temperature one apparently is a bug in Pint where:
hasattr(5 * units.m, '__len__')
returns True. 😒
It looks like doing is_not_scalar = hasattr(temperature, 'shape') and getattr(temperature, 'shape') gets it right, but I'm currently debating whether it's worth bothering fixing for this one case. The original hack I believe got added to help Dask support along.
This issue continues to be a bit of trouble with Numpy now warning about using single element arrays as if they were floats.
Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)
@akrherz Is this with pint 0.23?
@akrherz Is this with pint 0.23?
yes , I was efforting some code in this space earlier this morning while it worked on python 3.11, it failed on python 3.9. I haven't taken a chance to dig further, sorry.