MetPy icon indicating copy to clipboard operation
MetPy copied to clipboard

MetPy needs a general solution for returning scalars when provided scalars

Open akrherz opened this issue 6 years ago • 7 comments

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?

akrherz avatar Oct 17 '19 16:10 akrherz

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.

dopplershift avatar Oct 21 '19 16:10 dopplershift

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.

wholmgren avatar Oct 21 '19 18:10 wholmgren

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?

goekce avatar Jun 21 '23 16:06 goekce

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.

dopplershift avatar Jun 29 '23 23:06 dopplershift

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 avatar Feb 21 '24 14:02 akrherz

@akrherz Is this with pint 0.23?

dopplershift avatar Feb 21 '24 18:02 dopplershift

@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.

akrherz avatar Feb 21 '24 18:02 akrherz