Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MetPy needs a general solution for returning scalars when provided scalars #1209

Open
akrherz opened this issue Oct 17, 2019 · 7 comments
Open
Labels
Area: Calc Pertains to calculations Type: Feature New functionality

Comments

@akrherz
Copy link
Contributor

akrherz commented Oct 17, 2019

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?

@dopplershift
Copy link
Member

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 😉).

Your issue with pint I think is related to an issue I opened ages ago in Pint: hgrecco/pint#481.

@wholmgren
Copy link
Contributor

Same as numpy/numpy#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.

@goekce
Copy link

goekce commented Jun 21, 2023

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:

is_not_scalar = hasattr(temperature, '__len__')

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?

@dopplershift
Copy link
Member

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.

@akrherz
Copy link
Contributor Author

akrherz commented Feb 21, 2024

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

@dopplershift
Copy link
Member

@akrherz Is this with pint 0.23?

@akrherz
Copy link
Contributor Author

akrherz commented Feb 21, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Calc Pertains to calculations Type: Feature New functionality
Projects
None yet
Development

No branches or pull requests

5 participants