update meteolib functions

This commit is contained in:
wyq 2025-08-12 12:01:59 +08:00
parent bcfd50f1e3
commit 9dd3368e25
21 changed files with 2870 additions and 291 deletions

View File

@ -18,7 +18,7 @@ import org.meteoinfo.common.util.BigDecimalUtil;
import java.awt.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.*;
import java.util.List;
/**
@ -1420,4 +1420,74 @@ public class MIMath {
return new double[]{u, v};
}
/**
* Sort List A, and rearrange List B according to the sorting result of List A.
* @param listA The list to be sorted
* @param listB The list to be rearranged according to the sorting result of List A
*/
public static <T extends Comparable<T>, U> void sortAndRearrange(List<T> listA, List<U> listB) {
// Check the size of the two list
if (listA.size() != listB.size()) {
throw new IllegalArgumentException("The two lists must be of the same size");
}
// Create a list containing values and their original indices
List<Map.Entry<T, Integer>> entries = new ArrayList<>();
for (int i = 0; i < listA.size(); i++) {
entries.add(new AbstractMap.SimpleEntry<>(listA.get(i), i));
}
// Sort according to the natural order of values.
Collections.sort(entries, Map.Entry.comparingByKey());
// Save sorted A values
List<T> sortedA = new ArrayList<>();
// Save sorted B values
List<U> sortedB = new ArrayList<>();
for (Map.Entry<T, Integer> entry : entries) {
sortedA.add(entry.getKey());
sortedB.add(listB.get(entry.getValue()));
}
// Update origin lists
listA.clear();
listA.addAll(sortedA);
listB.clear();
listB.addAll(sortedB);
}
/**
* Sort List, and return the sorted index.
* @param list The list to be sorted
* @return Sorted index list
*/
public static <T extends Comparable<T>> List<Integer> sort(List<T> list) {
// Create a list containing values and their original indices
List<Map.Entry<T, Integer>> entries = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
entries.add(new AbstractMap.SimpleEntry<>(list.get(i), i));
}
// Sort according to the natural order of values.
Collections.sort(entries, Map.Entry.comparingByKey());
// Save sorted values
List<T> sorted = new ArrayList<>();
// Save sorted index values
List<Integer> sortIndex = new ArrayList<>();
for (Map.Entry<T, Integer> entry : entries) {
sorted.add(entry.getKey());
sortIndex.add(entry.getValue());
}
// Update origin list
list.clear();
list.addAll(sorted);
return sortIndex;
}
}

View File

@ -1430,7 +1430,7 @@ public class LegendManage {
LegendScheme ls;
if (isUnique) {
try {
Array ua = ArrayUtil.unique(array, null);
Array ua = ArrayUtil.unique(array, null)[0];
List<Number> values = new ArrayList<>();
IndexIterator iter = ua.getIndexIterator();
while (iter.hasNext()) {

View File

@ -1,32 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<MeteoInfo File="milconfig.xml" Type="configurefile">
<Path OpenPath="D:\Working\MIScript\Jython\mis\common_math\special">
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\plot_types\3d\jogl"/>
<Path OpenPath="D:\Working\MIScript\Jython\mis\array">
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\plot_types\3d\jogl\plot"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\meteo"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\dataset"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\io\web"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\io"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\others"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\test"/>
<RecentFolder Folder="D:\Working\MIScript\mywork\music"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\array"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\meteo\calc"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\common_math\integrate"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\common_math"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\common_math\special"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\meteo"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\array\slice"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\meteo\calc"/>
<RecentFolder Folder="D:\Working\MIScript\Jython\mis\array"/>
</Path>
<File>
<OpenedFiles>
<OpenedFile File="D:\Working\MIScript\Jython\mis\meteo\calc\mixing_ratio_from_relative_humidity.py"/>
<OpenedFile File="D:\Working\MIScript\Jython\mis\common_math\special\airy.py"/>
<OpenedFile File="D:\Working\MIScript\Jython\mis\common_math\special\lambertw.py"/>
<OpenedFile File="D:\Working\MIScript\Jython\mis\meteo\calc\parcel_profile_test.py"/>
<OpenedFile File="D:\Working\MIScript\Jython\mis\meteo\calc\parcel_profile_with_lcl.py"/>
</OpenedFiles>
<RecentFiles>
<RecentFile File="D:\Working\MIScript\Jython\mis\meteo\calc\mixing_ratio_from_relative_humidity.py"/>
<RecentFile File="D:\Working\MIScript\Jython\mis\common_math\special\airy.py"/>
<RecentFile File="D:\Working\MIScript\Jython\mis\common_math\special\lambertw.py"/>
<RecentFile File="D:\Working\MIScript\Jython\mis\meteo\calc\parcel_profile_test.py"/>
<RecentFile File="D:\Working\MIScript\Jython\mis\meteo\calc\parcel_profile_with_lcl.py"/>
</RecentFiles>
</File>
<Font>

View File

@ -10,7 +10,42 @@ from itertools import product
import mipylib.numeric as np
from .. import constants
__all__ = ['coriolis_parameter','smooth_window','smooth_n_point']
__all__ = ['coriolis_parameter','height_to_pressure_std','pressure_to_height_std',
'smooth_window','smooth_n_point','wind_speed']
# The following variables are constants for a standard atmosphere
t0 = 288. #'kelvin'
p0 = 1013.25 #'hPa'
gamma = 6.5 #'K/km'
def wind_speed(u, v):
r"""Compute the wind speed from u and v-components.
Parameters
----------
u : `m/s`
Wind component in the X (East-West) direction
v : `m/s`
Wind component in the Y (North-South) direction
Returns
-------
wind speed: `m/s`
Speed of the wind
See Also
--------
wind_components
Examples
--------
>>> from mipylib.meteolib.calc import wind_speed
>>> wind_speed(10., 10.)
<(14.1421356, 'meter / second')>
"""
return np.hypot(u, v)
def coriolis_parameter(latitude):
r"""Calculate the Coriolis parameter at each point.
@ -181,4 +216,52 @@ def smooth_n_point(scalar_grid, n=5, passes=1):
raise ValueError('The number of points to use in the smoothing '
'calculation must be either 5 or 9.')
return smooth_window(scalar_grid, window=weights, passes=passes, normalize_weights=False)
return smooth_window(scalar_grid, window=weights, passes=passes, normalize_weights=False)
def height_to_pressure_std(height):
r"""Convert height data to pressures using the U.S. standard atmosphere [NOAA1976]_.
The implementation inverts the formula outlined in [Hobbs1977]_ pg.60-61.
Parameters
----------
height : `meters`
Atmospheric height
Returns
-------
`hPa`
Corresponding pressure value(s)
Notes
-----
.. math:: p = p_0 e^{\frac{g}{R \Gamma} \text{ln}(1-\frac{Z \Gamma}{T_0})}
"""
gamma_1 = gamma * 1e-3 #to K/m
return p0 * (1 - (gamma_1 / t0) * height) ** (constants.g / (constants.Rd * gamma_1))
def pressure_to_height_std(pressure):
r"""Convert pressure data to height using the U.S. standard atmosphere [NOAA1976]_.
The implementation uses the formula outlined in [Hobbs1977]_ pg.60-61.
Parameters
----------
pressure : `hPa`
Atmospheric pressure
Returns
-------
`meters`
Corresponding height value(s)
Notes
-----
.. math:: Z = \frac{T_0}{\Gamma}[1-\frac{p}{p_0}^\frac{R\Gamma}{g}]
"""
gamma_1 = gamma * 1e-3 #to K/m
return (t0 / gamma_1) * (1 - (pressure / p0)**(
constants.Rd * gamma_1 / constants.g))

View File

@ -0,0 +1,5 @@
"""Contains specific exceptions raised by calculations."""
class InvalidSoundingError(ValueError):
"""Raise when a sounding does not meet thermodynamic calculation expectations."""

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,12 @@ Ported from MetPy.
"""
import mipylib.numeric as np
import mipylib.numeric.ma as ma
from mipylib.geolib import Geod
from ..interpolate import interpolate_1d
from ..cbook import broadcast_indices
__all__ = ['resample_nn_1d', 'nearest_intersection_idx', 'first_derivative', 'gradient',
__all__ = ['resample_nn_1d', 'nearest_intersection_idx', 'first_derivative', 'find_intersections', 'gradient',
'lat_lon_grid_deltas', 'get_layer_heights', 'find_bounding_indices', 'geospatial_gradient']
@ -58,6 +59,125 @@ def nearest_intersection_idx(a, b):
return sign_change_idx
def find_intersections(x, a, b, direction='all', log_x=False):
"""Calculate the best estimate of intersection.
Calculates the best estimates of the intersection of two y-value
data sets that share a common x-value set.
Parameters
----------
x : array-like
1-dimensional array of numeric x-values
a : array-like
1-dimensional array of y-values for line 1
b : array-like
1-dimensional array of y-values for line 2
direction : str, optional
specifies direction of crossing. 'all', 'increasing' (a becoming greater than b),
or 'decreasing' (b becoming greater than a). Defaults to 'all'.
log_x : bool, optional
Use logarithmic interpolation along the `x` axis (i.e. for finding intersections
in pressure coordinates). Default is False.
Returns
-------
A tuple (x, y) of array-like with the x and y coordinates of the
intersections of the lines.
Notes
-----
This function implicitly converts `xarray.DataArray` to `pint.Quantity`, with the results
given as `pint.Quantity`.
"""
# Change x to logarithmic if log_x=True
if log_x is True:
x = np.log(x)
# Find the index of the points just before the intersection(s)
nearest_idx = nearest_intersection_idx(a, b)
next_idx = nearest_idx + 1
# Determine the sign of the change
sign_change = np.sign(a[next_idx] - b[next_idx])
# x-values around each intersection
_, x0 = _next_non_masked_element(x, nearest_idx)
_, x1 = _next_non_masked_element(x, next_idx)
# y-values around each intersection for the first line
_, a0 = _next_non_masked_element(a, nearest_idx)
_, a1 = _next_non_masked_element(a, next_idx)
# y-values around each intersection for the second line
_, b0 = _next_non_masked_element(b, nearest_idx)
_, b1 = _next_non_masked_element(b, next_idx)
# Calculate the x-intersection. This comes from finding the equations of the two lines,
# one through (x0, a0) and (x1, a1) and the other through (x0, b0) and (x1, b1),
# finding their intersection, and reducing with a bunch of algebra.
delta_y0 = a0 - b0
delta_y1 = a1 - b1
intersect_x = (delta_y1 * x0 - delta_y0 * x1) / (delta_y1 - delta_y0)
# Calculate the y-intersection of the lines. Just plug the x above into the equation
# for the line through the a points. One could solve for y like x above, but this
# causes weirder unit behavior and seems a little less good numerically.
intersect_y = ((intersect_x - x0) / (x1 - x0)) * (a1 - a0) + a0
# If there's no intersections, return
if len(intersect_x) == 0:
return intersect_x, intersect_y
# Return x to linear if log_x is True
if log_x is True:
intersect_x = np.exp(intersect_x)
# Check for duplicates
duplicate_mask = (np.ediff1d(intersect_x, to_end=1) != 0)
# Make a mask based on the direction of sign change desired
if direction == 'increasing':
mask = sign_change > 0
elif direction == 'decreasing':
mask = sign_change < 0
elif direction == 'all':
return intersect_x[duplicate_mask], intersect_y[duplicate_mask]
else:
raise ValueError('Unknown option for direction: {}'.format(direction))
return intersect_x[mask & duplicate_mask], intersect_y[mask & duplicate_mask]
def _next_non_masked_element(a, idx):
"""Return the next non masked element of a masked array.
If an array is masked, return the next non-masked element (if the given index is masked).
If no other unmasked points are after the given masked point, returns none.
Parameters
----------
a : array-like
1-dimensional array of numeric values
idx : integer
Index of requested element
Returns
-------
Index of next non-masked element and next non-masked element
"""
try:
next_idx = idx + a[idx:].mask.argmin()
if ma.is_masked(a[next_idx]):
return None, None
else:
return next_idx, a[next_idx]
except (AttributeError, TypeError, IndexError):
return idx, a[idx]
def _remove_nans(*variables):
"""Remove NaNs from arrays that cause issues with calculations.
Takes a variable number of arguments and returns masked arrays in the same
@ -73,10 +193,120 @@ def _remove_nans(*variables):
# Mask everyone with that joint mask
ret = []
for v in variables:
v = np.asarray(v)
ret.append(v[~mask])
return ret
def _get_bound_pressure_height(pressure, bound, height=None, interpolate=True):
"""Calculate the bounding pressure and height in a layer.
Given pressure, optional heights and a bound, return either the closest pressure/height
or interpolated pressure/height. If no heights are provided, a standard atmosphere
([NOAA1976]_) is assumed.
Parameters
----------
pressure : `pint.Quantity`
Atmospheric pressures
bound : `pint.Quantity`
Bound to retrieve (in pressure or height)
height : `pint.Quantity`, optional
Atmospheric heights associated with the pressure levels. Defaults to using
heights calculated from ``pressure`` assuming a standard atmosphere.
interpolate : boolean, optional
Interpolate the bound or return the nearest. Defaults to True.
Returns
-------
`pint.Quantity`
The bound pressure and height
"""
# avoid circular import if basic.py ever imports something from tools.py
from .basic import height_to_pressure_std, pressure_to_height_std
# Make sure pressure is monotonically decreasing
sort_inds = np.argsort(pressure)[::-1]
pressure = pressure[sort_inds]
if height is not None:
height = height[sort_inds]
# Bound is given in pressure
if bound.check('[length]**-1 * [mass] * [time]**-2'):
# If the bound is in the pressure data, we know the pressure bound exactly
if bound in pressure:
# By making sure this is at least a 1D array we avoid the behavior in numpy
# (at least up to 1.19.4) that float32 scalar * Python float -> float64, which
# can wreak havok with floating point comparisons.
bound_pressure = np.atleast_1d(bound)
# If we have heights, we know the exact height value, otherwise return standard
# atmosphere height for the pressure
if height is not None:
bound_height = height[pressure == bound_pressure]
else:
bound_height = pressure_to_height_std(bound_pressure)
# If bound is not in the data, return the nearest or interpolated values
else:
if interpolate:
bound_pressure = bound # Use the user specified bound
if height is not None: # Interpolate heights from the height data
bound_height = log_interpolate_1d(bound_pressure, pressure, height)
else: # If not heights given, use the standard atmosphere
bound_height = pressure_to_height_std(bound_pressure)
else: # No interpolation, find the closest values
idx = (np.abs(pressure - bound)).argmin()
bound_pressure = pressure[idx]
if height is not None:
bound_height = height[idx]
else:
bound_height = pressure_to_height_std(bound_pressure)
# Bound is given in height
elif bound.check('[length]'):
# If there is height data, see if we have the bound or need to interpolate/find nearest
if height is not None:
if bound in height: # Bound is in the height data
bound_height = bound
bound_pressure = pressure[height == bound]
else: # Bound is not in the data
if interpolate:
bound_height = bound
# Need to cast back to the input type since interp (up to at least numpy
# 1.13 always returns float64. This can cause upstream users problems,
# resulting in something like np.append() to upcast.
bound_pressure = np.interp(np.atleast_1d(bound),
height, pressure).astype(np.result_type(bound))
else:
idx = (np.abs(height - bound)).argmin()
bound_pressure = pressure[idx]
bound_height = height[idx]
else: # Don't have heights, so assume a standard atmosphere
bound_height = bound
bound_pressure = height_to_pressure_std(bound)
# If interpolation is on, this is all we need, if not, we need to go back and
# find the pressure closest to this and refigure the bounds
if not interpolate:
idx = (np.abs(pressure - bound_pressure)).argmin()
bound_pressure = pressure[idx]
bound_height = pressure_to_height_std(bound_pressure)
# Bound has invalid units
else:
raise ValueError('Bound must be specified in units of length or pressure.')
# If the bound is out of the range of the data, we shouldn't extrapolate
if not (_greater_or_close(bound_pressure, np.nanmin(pressure))
and _less_or_close(bound_pressure, np.nanmax(pressure))):
raise ValueError('Specified bound is outside pressure range.')
if height is not None and not (_less_or_close(bound_height, np.nanmax(height))
and _greater_or_close(bound_height, np.nanmin(height))):
raise ValueError('Specified bound is outside height range.')
return bound_pressure, bound_height
def get_layer_heights(height, depth, *args, **kwargs):
"""Return an atmospheric layer from upper air data with the requested bottom and depth.
@ -167,6 +397,109 @@ def get_layer_heights(height, depth, *args, **kwargs):
return ret
def get_layer(pressure, *args, **kwargs):
r"""Return an atmospheric layer from upper air data with the requested bottom and depth.
This function will subset an upper air dataset to contain only the specified layer. The
bottom of the layer can be specified with a pressure or height above the surface
pressure. The bottom defaults to the surface pressure. The depth of the layer can be
specified in terms of pressure or height above the bottom of the layer. If the top and
bottom of the layer are not in the data, they are interpolated by default.
Parameters
----------
pressure : array-like
Atmospheric pressure profile
args : array-like
Atmospheric variable(s) measured at the given pressures
height: array-like, optional
Atmospheric heights corresponding to the given pressures. Defaults to using
heights calculated from ``pressure`` assuming a standard atmosphere [NOAA1976]_.
bottom : `pint.Quantity`, optional
Bottom of the layer as a pressure or height above the surface pressure. Defaults
to the highest pressure or lowest height given.
depth : `pint.Quantity`, optional
Thickness of the layer as a pressure or height above the bottom of the layer.
Defaults to 100 hPa.
interpolate : bool, optional
Interpolate the top and bottom points if they are not in the given data. Defaults
to True.
Returns
-------
`pint.Quantity, pint.Quantity`
The pressure and data variables of the layer
Notes
-----
Only functions on 1D profiles (not higher-dimension vertical cross sections or grids).
Also, this will return Pint Quantities even when given xarray DataArray profiles.
"""
height = kwargs.pop('height', None)
bottom = kwargs.pop('bottom', None)
depth = kwargs.pop('depth', 100) #'hPa'
interpolate = kwargs.pop('interpolate', True)
# Make sure pressure and datavars are the same length
for datavar in args:
if len(pressure) != len(datavar):
raise ValueError('Pressure and data variables must have the same length.')
# If the bottom is not specified, make it the surface pressure
if bottom is None:
bottom = np.max(pressure)
bottom_pressure, bottom_height = _get_bound_pressure_height(pressure, bottom,
height=height,
interpolate=interpolate)
# Calculate the top in whatever units depth is in
if depth.check('[length]**-1 * [mass] * [time]**-2'):
top = bottom_pressure - depth
elif depth.check('[length]'):
top = bottom_height + depth
else:
raise ValueError('Depth must be specified in units of length or pressure')
top_pressure, _ = _get_bound_pressure_height(pressure, top, height=height,
interpolate=interpolate)
ret = [] # returned data variables in layer
# Ensure pressures are sorted in ascending order
sort_inds = np.argsort(pressure)
pressure = pressure[sort_inds]
# Mask based on top and bottom pressure
inds = (_less_or_close(pressure, bottom_pressure)
& _greater_or_close(pressure, top_pressure))
p_interp = pressure[inds]
# Interpolate pressures at bounds if necessary and sort
if interpolate:
# If we don't have the bottom or top requested, append them
if not np.any(np.isclose(top_pressure, p_interp)):
p_interp = np.sort(np.append(p_interp.m, top_pressure.m))
if not np.any(np.isclose(bottom_pressure, p_interp)):
p_interp = np.sort(np.append(p_interp.m, bottom_pressure.m))
ret.append(p_interp[::-1])
for datavar in args:
# Ensure that things are sorted in ascending order
datavar = datavar[sort_inds]
if interpolate:
# Interpolate for the possibly missing bottom/top values
datavar_interp = log_interpolate_1d(p_interp, pressure, datavar)
datavar = datavar_interp
else:
datavar = datavar[inds]
ret.append(datavar[::-1])
return ret
def find_bounding_indices(arr, values, axis, from_below=True):
"""Find the indices surrounding the values within arr along axis.

View File

@ -1,7 +1,7 @@
import mipylib.numeric as np
__all__ = ['broadcast_indices']
__all__ = ['broadcast_indices', 'validate_choice']
def broadcast_indices(x, minv, ndim, axis):
"""Calculate index values to properly broadcast index array within data array.
@ -17,4 +17,36 @@ def broadcast_indices(x, minv, ndim, axis):
broadcast_slice[dim] = slice(None)
dim_inds = np.arange(x.shape[dim])
ret.append(dim_inds[tuple(broadcast_slice)])
return tuple(ret)
return tuple(ret)
def validate_choice(options, **kwargs):
r"""Confirm that a choice is contained within a set of options.
Parameters
----------
options : iterable
\*\*kwargs
Function kwarg names (keys) and their passed-in values (values) for validation
Raises
------
ValueError
If the choice is not contained within any of these options, present valid options
Examples
--------
>>> from mipylib.meteolib.cbook import validate_choice
>>> def try_wrong_choice(color=None):
... validate_choice({'blue', 'green', 'yellow'}, color=color)
>>> try_wrong_choice(color='red') # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ValueError: 'red' is not a valid option for color.
Valid options are 'yellow', 'green', 'blue'.
"""
for kw, choice in kwargs.items():
if choice not in options:
raise ValueError(
'{} is not a valid option for {}. '
'Valid options are {}.'.format(choice, kw,", ".join(map(repr, options)))
)

View File

@ -1,15 +1,47 @@
P0 = 1000. #reference pressure for potential temperature (hPa)
R = 8.3144598 #molar gas constant (J / K / mol)
Mw = 18.01528 #Molecular weight of water (g / mol)
Md = 28.9644 #Nominal molecular weight of dry air at the surface of th Earth (g / mol)
Rd = R / Md #Gas constant for dry air at the surface of the Earth (J (K kg)^-1)
Lv = 2.501e6 #Latent heat of vaporization for liquid water at 0C (J kg^-1)
Cp_d = 1005 #Specific heat at constant pressure for dry air (J kg^-1)
epsilon = Mw / Md
kappa = 0.286
degCtoK = 273.15 # Temperature offset between K and C (deg C)
# Earth
Re = earth_avg_radius = 6371008.7714 # Earth average radius with meters
g = 9.8 # Gravitational acceleration (m / s^2)
earth_gravity = g = 9.80665 #Gravitational acceleration (m / s^2)
G = gravitational_constant = 6.67430e-11 #(m^3 / kg / s^2)
GM = geocentric_gravitational_constant = 3986005e8 #(m^3 / s^2)
omega = 7292115e-11 # Avg. angular velocity of Earth (rad / s)
d = earth_sfc_avg_dist_sun = 149597870700. #(m)
S = earth_solar_irradiance = 1360.8 #(W / m^2)
delta = earth_max_declination = 23.45 #(degrees)
earth_orbit_eccentricity = 0.0167 #(dimensionless)
earth_mass = me = geocentric_gravitational_constant / gravitational_constant
# molar gas constant
R = 8.314462618 #molar gas constant (J / K / mol)
# Water
Mw = 18.015268 #Molecular weight of water (g / mol)
Rv = water_gas_constant = R / Mw * 1000 #(J / kg / K)
rho_l = density_water = 999.97495 #(kg / m^3)
wv_specific_heat_ratio = 1.330 #(dimensionless)
Cp_v = wv_specific_heat_press = wv_specific_heat_ratio * Rv / (wv_specific_heat_ratio - 1)
Cv_v = wv_specific_heat_vol = Cp_v / wv_specific_heat_ratio
Cp_l = water_specific_heat = 4219.4 #(J / kg / K)
Lv = water_heat_vaporization = 2.50084e6 #Latent heat of vaporization for liquid water at 0C (J kg^-1)
Lf = water_heat_fusion = 3.337e5 #(J / kg)
Ls = water_heat_sublimation = Lv + Lf
Cp_i = ice_specific_heat = 2090 #(J / kg / K)
rho_i = density_ice = 917 #(kg / m^3)
sat_pressure_0c = 6.112 #Saturation pressure at 0 degree (hPa)
T0 = water_triple_point_temperature = 273.16 #(K)
# Dry air
Md = dry_air_molecular_weight = 28.96546e-3 #(kg / mol)
Rd = dry_air_gas_constant = R / Md #Gas constant for dry air at the surface of the Earth (J (K kg)^-1)
dry_air_spec_heat_ratio = 1.4 #(dimensionless)
Cp_d = dry_air_spec_heat_press = dry_air_spec_heat_ratio * Rd / (dry_air_spec_heat_ratio - 1)
Cv_d = dry_air_spec_heat_vol = Cp_d / dry_air_spec_heat_ratio
degCtoK = 273.15 # Temperature offset between K and C (deg C)
rho_d = dry_air_density_stp = 1000. / (Rd * degCtoK)
# General meteorology constants
P0 = 1000. #reference pressure for potential temperature (hPa)
kappa = poisson_exponent = Rd / Cp_d
gamma_d = dry_adiabatic_lapse_rate = g / Cp_d
epsilon = Mw / Md / 1000.
T_BASE = 300.
omega = 7292115e-11 # Avg. angular velocity of Earth (rad / s)

View File

@ -14,12 +14,12 @@ import constants as constants
from .calc.thermo import saturation_vapor_pressure, saturation_mixing_ratio
__all__ = [
'cumsimp','dewpoint2rh','dewpoint_rh','dry_lapse','ds2uv',
'cumsimp','dewpoint2rh','dewpoint_rh','ds2uv',
'flowfun','h2p',
'moist_lapse','p2h','qair2rh','rh2dewpoint',
'p2h','qair2rh','rh2dewpoint',
'sigma_to_pressure','tc2tf',
'tf2tc','uv2ds','pressure_to_height_std',
'height_to_pressure_std','eof','vapor_pressure'
'tf2tc','uv2ds',
'eof'
]
def uv2ds(u, v):
@ -67,20 +67,7 @@ def p2h(press):
return press.array_wrap(r)
else:
return MeteoMath.press2Height(press)
def pressure_to_height_std(press):
"""
Convert pressure data to heights using the U.S. standard atmosphere.
:param press: (*float*) Pressure - hPa.
:returns: (*float*) Height - meter.
"""
t0 = 288.
gamma = 6.5
p0 = 1013.25
h = (t0 / gamma) * (1 - (press / p0)**(constants.Rd * gamma / constants.g)) * 1000
return h
def h2p(height):
"""
@ -95,21 +82,7 @@ def h2p(height):
return height.array_wrap(r)
else:
return MeteoMath.height2Press(height)
def height_to_pressure_std(height):
"""
Convert height data to pressures using the U.S. standard atmosphere.
:param height: (*float*) Height - meter.
:returns: (*float*) Height - meter.
"""
t0 = 288.
gamma = 6.5
p0 = 1013.25
height = height * 0.001
p = p0 * (1 - (gamma / t0) * height) ** (constants.g / (constants.Rd * gamma))
return p
def sigma_to_pressure(sigma, psfc, ptop):
r"""Calculate pressure from sigma values.
@ -237,6 +210,7 @@ def rh2dewpoint(rh, temp):
else:
return MeteoMath.rh2dewpoint(rh, temp)
def dewpoint_rh(temperature, rh):
r"""Calculate the ambient dewpoint given air temperature and relative humidity.
@ -260,74 +234,6 @@ def dewpoint_rh(temperature, rh):
# warnings.warn('Relative humidity >120%, ensure proper units.')
return dewpoint(rh * saturation_vapor_pressure(temperature))
def dry_lapse(pressure, temperature):
"""
Calculate the temperature at a level assuming only dry processes
operating from the starting point.
This function lifts a parcel starting at `temperature`, conserving
potential temperature. The starting pressure should be the first item in
the `pressure` array.
Parameters
----------
pressure : array_like
The atmospheric pressure level(s) of interest
temperature : array_like
The starting temperature
Returns
-------
array_like
The resulting parcel temperature at levels given by `pressure`
See Also
--------
moist_lapse : Calculate parcel temperature assuming liquid saturation
processes
parcel_profile : Calculate complete parcel profile
potential_temperature
"""
return temperature * (pressure / pressure[0])**constants.kappa
def moist_lapse(pressure, temperature):
"""
Calculate the temperature at a level assuming liquid saturation processes
operating from the starting point.
This function lifts a parcel starting at `temperature`. The starting
pressure should be the first item in the `pressure` array. Essentially,
this function is calculating moist pseudo-adiabats.
Parameters
----------
pressure : array_like
The atmospheric pressure level(s) of interest
temperature : array_like
The starting temperature
Returns
-------
array_like
The temperature corresponding to the the starting temperature and
pressure levels.
See Also
--------
dry_lapse : Calculate parcel temperature assuming dry adiabatic processes
parcel_profile : Calculate complete parcel profile
Notes
-----
This function is implemented by integrating the following differential
equation:
.. math:: \frac{dT}{dP} = \frac{1}{P} \frac{R_d T + L_v r_s}
{C_{pd} + \frac{L_v^2 r_s \epsilon}{R_d T^2}}
This equation comes from [1]_.
References
----------
.. [1] Bakhshaii, A. and R. Stull, 2013: Saturated Pseudoadiabats--A
Noniterative Approximation. J. Appl. Meteor. Clim., 52, 5-15.
"""
def dt(t, p):
rs = saturation_mixing_ratio(p, t)
frac = ((constants.Rd * t + constants.Lv * rs) /
(constants.Cp_d + (constants.Lv * constants.Lv * rs * constants.epsilon / (constants.Rd * t * t)))).to('kelvin')
return frac / p
return dt
def vapor_pressure(pressure, mixing):
r"""Calculate water vapor (partial) pressure.
@ -355,6 +261,7 @@ def vapor_pressure(pressure, mixing):
"""
return pressure * mixing / (constants.epsilon + mixing)
def cumsimp(y):
"""
Simpson-rule column-wise cumulative summation.
@ -418,6 +325,7 @@ def cumsimp(y):
return f
def flowfun(u, v):
"""
Computes the potential PHI and the streamfunction PSI
@ -471,7 +379,8 @@ def flowfun(u, v):
psi = (psi+cumsimp(v.T).T - np.tile(cy, [1,lx]))/2
return psi, phi
def eof(x, svd=True, transform=False, return_index=False):
"""
Empirical Orthogonal Function (EOF) analysis to finds both time series and spatial patterns.
@ -537,7 +446,8 @@ def eof(x, svd=True, transform=False, return_index=False):
return EOF, E, PC, np.arange(x.shape[0])
else:
return EOF, E, PC
def varimax(x, norm=True, tol=1e-10, it_max=1000):
"""
Rotate EOFs according to varimax algorithm
@ -588,4 +498,4 @@ def varimax(x, norm=True, tol=1e-10, it_max=1000):
rr[valid_idx,:] = r
r = rr
return r, TT
return r, TT

View File

@ -840,6 +840,40 @@ class NDArray(object):
"""
self._array = ArrayUtil.sort(self._array, axis)
def searchsorted(self, v, side='left', sorter=None):
"""
Find indices where elements should be inserted to maintain order.
Find the indices into a sorted array a such that, if the corresponding elements in v were inserted
before the indices, the order of a would be preserved.
Parameters
----------
v : array_like
Input array. If sorter is None, then it must be sorted in ascending order, otherwise sorter must
be an array of indices that sort it.
side : {left, right}, optional
If left, the index of the first suitable location found is given. If right, return the last
such index. If there is no suitable index, return either 0 or N (where N is the length of a).
sorter : 1-D array_like, optional
Optional array of integer indices that sort array a into ascending order. They are typically
the result of argsort.
Returns
-------
indices : int or array of ints
Array of insertion points with the same shape as v, or an integer if v is a scalar.
"""
v = NDArray(v)
left = True if side == 'left' else False
r = ArrayUtil.searchSorted(self._array, v._array, left)
if isinstance(r, int):
return r
else:
return self.array_wrap(r)
def max(self, axis=None):
"""
Get maximum value along an axis.

View File

@ -75,7 +75,7 @@ def nonzero(a):
a = array(a)
ra = ArrayUtil.nonzero(a.asarray())
if ra is None:
return None
return tuple([NDArray([]).astype('int')])
r = []
for aa in ra:

View File

@ -354,25 +354,35 @@ def zeros_like(a, dtype=None):
dtype = _dtype.fromjava(a.dtype)
elif isinstance(dtype, basestring):
dtype = _dtype.DataType(dtype)
return NDArray(ArrayUtil.zeros(shape, dtype._dtype))
def empty_like(a, dtype=None):
def empty_like(a, dtype=None, shape=None):
"""
Return a new array with the same shape and type as a given array.
:param a: (*array*) The shape and data-type of a define these same attributes of the returned array.
:param dtype: (*string*) Overrides the data type of the result. Default is ``None``, keep the data
type of array ``a``.
:param shape: (*list of int*) Overrides the shape of the result. Default is ``None``.
:returns: Array of uninitialized (arbitrary) data with the same shape and type as prototype.
"""
shape = a.shape
if shape is None:
shape = a.shape
if isinstance(shape, int):
shape = [shape]
if dtype is None:
dtype = a.dtype
elif isinstance(dtype, basestring):
dtype = _dtype.DataType(dtype)
return NDArray(ArrayUtil.empty(shape, dtype._dtype))
def ones_like(a, dtype=None):
"""
Return an array of ones with the same shape and type as a given array.
@ -389,7 +399,8 @@ def ones_like(a, dtype=None):
elif isinstance(dtype, basestring):
dtype = _dtype.DataType(dtype)
return NDArray(ArrayUtil.ones(shape, dtype._dtype))
def ones(shape, dtype='float'):
"""
Create a new aray of given shape and type, filled with ones.
@ -415,10 +426,13 @@ def ones(shape, dtype='float'):
shapelist.append(shape)
else:
shapelist = shape
if isinstance(dtype, basestring):
dtype = _dtype.DataType(dtype)
return NDArray(ArrayUtil.ones(shapelist, dtype._dtype))
def full(shape, fill_value, dtype=None):
"""
Return a new array of given shape and type, filled with fill_value.
@ -555,15 +569,16 @@ def repeat(a, repeats, axis=None):
"""
if isinstance(repeats, int):
repeats = [repeats]
if isinstance(a, (list, tuple)):
a = array(a)
if isinstance(a, NDArray):
a = a.asarray()
repeats = asarray(repeats)
a = asarray(a)
if axis is None:
r = ArrayUtil.repeat(a, repeats)
r = ArrayUtil.repeat(a._array, repeats._array)
else:
r = ArrayUtil.repeat(a, repeats, axis)
r = ArrayUtil.repeat(a._array, repeats._array, axis)
return NDArray(r)
def tile(a, repeats):
"""
@ -2031,7 +2046,10 @@ def concatenate(arrays, axis=0):
"""
ars = []
for a in arrays:
if isinstance(a, numbers.Number):
a = [a]
ars.append(asanyarray(a).asarray())
r = ArrayUtil.concatenate(ars, axis)
return NDArray(r)

View File

@ -1,7 +1,10 @@
from org.meteoinfo.ndarray.math import ArrayUtil
from .. import core as np
__all__ = ['unique', 'intersect1d']
__all__ = ['unique', 'ediff1d', 'intersect1d']
def _unique1d(ar, return_index=False, return_inverse=False,
return_counts=False):
@ -46,28 +49,78 @@ def _unique1d(ar, return_index=False, return_inverse=False,
ret += (np.diff(idx),)
return ret
def unique(a, return_index=False, axis=None):
def unique(a, return_index=False, return_inverse=False, return_counts=False, axis=None):
"""
Find the unique elements of an array.
Returns the sorted unique elements of an array.
Returns the sorted unique elements of an array. There are three optional
outputs in addition to the unique elements:
:param a: (*array_like*) Array to be sorted.
:param return_index: (*bool*) Optional, If True, also return the indices of ar (along the specified
axis, if provided, or in the flattened array) that result in the unique array.
:param axis: (*int or None*) Optional. Axis along which to operate on. If None, the array is
flattened.
* the indices of the input array that give the unique values
* the indices of the unique array that reconstruct the input array
* the number of times each unique value comes up in the input array
:returns: (*NDArray*) Sorted unique elements of input array.
Parameters
----------
ar : array_like
Input array. Unless `axis` is specified, this will be flattened if it
is not already 1-D.
return_index : bool, optional
If True, also return the indices of `ar` (along the specified axis,
if provided, or in the flattened array) that result in the unique array.
return_inverse : bool, optional
If True, also return the indices of the unique array (for the specified
axis, if provided) that can be used to reconstruct `ar`.
return_counts : bool, optional
If True, also return the number of times each unique item appears
in `ar`.
axis : int or None, optional
The axis to operate on. If None, `ar` will be flattened. If an integer,
the subarrays indexed by the given axis will be flattened and treated
as the elements of a 1-D array with the dimension of the given axis,
see the notes for more details. Object arrays or structured arrays
that contain objects are not supported if the `axis` kwarg is used. The
default is None.
Returns
-------
unique : ndarray
The sorted unique values.
unique_indices : ndarray, optional
The indices of the first occurrences of the unique values in the
original array. Only provided if `return_index` is True.
unique_inverse : ndarray, optional
The indices to reconstruct the original array from the
unique array. Only provided if `return_inverse` is True.
unique_counts : ndarray, optional
The number of times each of the unique values comes up in the
original array. Only provided if `return_counts` is True.
See Also
--------
repeat : Repeat elements of an array.
sort : Return a sorted copy of an array.
Notes
-----
When an axis is specified the subarrays indexed by the axis are sorted.
This is done by making the specified axis the first dimension of the array
(move the axis to the first dimension to keep the order of the other axes)
and then flattening the subarrays in C order. The flattened subarrays are
then viewed as a structured type with each element given a label, with the
effect that we end up with a 1-D array of structured types that can be
treated in the same way as any other 1-D array. The result is that the
flattened subarrays are sorted in lexicographic order starting with the
first element.
"""
if isinstance(a, list):
if isinstance(a, (tuple, list)):
a = np.array(a)
if return_index:
r = ArrayUtil.uniqueIndex(a.asarray(), axis)
return np.NDArray(r[0]), np.NDArray(r[1])
else:
r = ArrayUtil.unique(a.asarray(), axis)
return np.NDArray(r)
r = ArrayUtil.unique(a._array, axis, return_index, return_inverse, return_counts)
return np.NDArray(r[0]) if len(r) == 1 else [np.NDArray(rr) for rr in r]
def intersect1d(ar1, ar2, assume_unique=False, return_indices=False):
"""
@ -155,4 +208,86 @@ def intersect1d(ar1, ar2, assume_unique=False, return_indices=False):
return int1d, ar1_indices, ar2_indices
else:
return int1d
return int1d
def ediff1d(ary, to_end=None, to_begin=None):
"""
The differences between consecutive elements of an array.
Parameters
----------
ary : array_like
If necessary, will be flattened before the differences are taken.
to_end : array_like, optional
Number(s) to append at the end of the returned differences.
to_begin : array_like, optional
Number(s) to prepend at the beginning of the returned differences.
Returns
-------
ediff1d : ndarray
The differences. Loosely, this is ``ary.flat[1:] - ary.flat[:-1]``.
See Also
--------
diff, gradient
Notes
-----
When applied to masked arrays, this function drops the mask information
if the `to_begin` and/or `to_end` parameters are used.
Examples
--------
>>> x = np.array([1, 2, 4, 7, 0])
>>> np.ediff1d(x)
array([ 1, 2, 3, -7])
>>> np.ediff1d(x, to_begin=-99, to_end=np.array([88, 99]))
array([-99, 1, 2, ..., -7, 88, 99])
The returned array is always 1D.
>>> y = [[1, 2, 4], [1, 6, 24]]
>>> np.ediff1d(y)
array([ 1, 2, -3, 5, 18])
"""
conv = np.asanyarray(ary)
# Convert to (any) array and ravel:
ary = conv.ravel()
# enforce that the dtype of `ary` is used for the output
dtype_req = ary.dtype
# fast track default case
if to_begin is None and to_end is None:
return ary[1:] - ary[:-1]
if to_begin is None:
l_begin = 0
else:
to_begin = np.asanyarray(to_begin)
to_begin = to_begin.ravel()
l_begin = len(to_begin)
if to_end is None:
l_end = 0
else:
to_end = np.asanyarray(to_end)
to_end = to_end.ravel()
l_end = len(to_end)
# do the calculation in place and copy to_begin and to_end
l_diff = max(len(ary) - 1, 0)
result = np.empty_like(ary, shape=l_diff + l_begin + l_end)
if l_begin > 0:
result[:l_begin] = to_begin
if l_end > 0:
result[l_begin + l_diff:] = to_end
result[l_begin:l_begin+l_diff] = ary[1:] - ary[:-1]
return result

View File

@ -4,7 +4,7 @@ from org.meteoinfo.ndarray.math import ArrayUtil
nomask = False
__all__ = ['MaskedArray','masked_array','masked_invalid','getdata']
__all__ = ['MaskedArray','masked_array','masked_invalid','getdata','getmask','is_masked']
class MaskedArray(NDArray):
"""
@ -69,6 +69,92 @@ class MaskedArray(NDArray):
def __repr__(self):
return self.__str__()
def __setmask__(self, mask, copy=False):
"""
Set the mask.
"""
idtype = self.dtype
current_mask = self._mask
if mask is masked:
mask = True
if current_mask is nomask:
# Make sure the mask is set
# Just don't do anything if there's nothing to do.
if mask is nomask:
return
current_mask = self._mask = make_mask_none(self.shape, idtype)
if idtype.names is None:
# No named fields.
# Hardmask: don't unmask the data
if self._hardmask:
current_mask |= mask
# Softmask: set everything to False
# If it's obviously a compatible scalar, use a quick update
# method.
elif isinstance(mask, (int, float, np.bool, np.number)):
current_mask[...] = mask
# Otherwise fall back to the slower, general purpose way.
else:
current_mask.flat = mask
else:
# Named fields w/
mdtype = current_mask.dtype
mask = np.asarray(mask)
# Mask is a singleton
if not mask.ndim:
# It's a boolean : make a record
if mask.dtype.kind == 'b':
mask = np.array(tuple([mask.item()] * len(mdtype)),
dtype=mdtype)
# It's a record: make sure the dtype is correct
else:
mask = mask.astype(mdtype)
# Mask is a sequence
else:
# Make sure the new mask is a ndarray with the proper dtype
try:
copy = None if not copy else True
mask = np.array(mask, copy=copy, dtype=mdtype)
# Or assume it's a sequence of bool/int
except TypeError:
mask = np.array([tuple([m] * len(mdtype)) for m in mask],
dtype=mdtype)
# Hardmask: don't unmask the data
if self._hardmask:
for n in idtype.names:
current_mask[n] |= mask[n]
# Softmask: set everything to False
# If it's obviously a compatible scalar, use a quick update
# method.
elif isinstance(mask, (int, float, np.dtyp.bool, np.dtype.number)):
current_mask[...] = mask
# Otherwise fall back to the slower, general purpose way.
else:
current_mask.flat = mask
# Reshape if needed
if current_mask.shape:
current_mask.shape = self.shape
return
_set_mask = __setmask__
@property
def mask(self):
""" Current mask. """
# We could try to force a reshape, but that wouldn't work in some
# cases.
# Return a view so that the dtype and shape cannot be changed in place
# This still preserves nomask by identity
return self._mask.view()
@mask.setter
def mask(self, value):
self.__setmask__(value)
def filled(self, fill_value=None):
"""
Return a copy of self, with masked values filled with a given value.
@ -164,8 +250,10 @@ class MaskedArray(NDArray):
cnt = self.count(axis=axis)
return dsum * 1. / cnt
masked_array = MaskedArray
def getdata(a, subok=True):
"""
Return the data of a masked array as an ndarray.
@ -208,6 +296,68 @@ def getdata(a, subok=True):
return data
def getmask(a):
"""
Return the mask of a masked array, or nomask.
Return the mask of `a` as an ndarray if `a` is a `MaskedArray` and the
mask is not `nomask`, else return `nomask`. To guarantee a full array
of booleans of the same shape as a, use `getmaskarray`.
Parameters
----------
a : array_like
Input `MaskedArray` for which the mask is required.
See Also
--------
getdata : Return the data of a masked array as an ndarray.
getmaskarray : Return the mask of a masked array, or full array of False.
Examples
--------
>>> a = np.ma.masked_equal([[1,2],[3,4]], 2)
>>> a
masked_array(
data=[[1, --],
[3, 4]],
mask=[[False, True],
[False, False]],
fill_value=2)
>>> ma.getmask(a)
array([[False, True],
[False, False]])
Equivalently use the `MaskedArray` `mask` attribute.
>>> a.mask
array([[False, True],
[False, False]])
Result when mask == `nomask`
>>> b = np.ma.masked_array([[1,2],[3,4]])
>>> b
masked_array(
data=[[1, 2],
[3, 4]],
mask=False,
fill_value=999999)
>>> np.ma.nomask
False
>>> np.ma.getmask(b) == np.ma.nomask
True
>>> b.mask == np.ma.nomask
True
"""
return getattr(a, '_mask', nomask)
get_mask = getmask
def masked_invalid(a, copy=True):
"""
Mask an array where invalid values occur (NaNs or infs).
@ -244,4 +394,58 @@ def masked_invalid(a, copy=True):
else:
result = MaskedArray(a)
result._mask = condition
return result
return result
def is_masked(x):
"""
Determine whether input has masked values.
Accepts any object as input, but always returns False unless the
input is a MaskedArray containing masked values.
Parameters
----------
x : array_like
Array to check for masked values.
Returns
-------
result : bool
True if `x` is a MaskedArray with masked values, False otherwise.
Examples
--------
>>> import numpy as np
>>> import numpy.ma as ma
>>> x = ma.masked_equal([0, 1, 0, 2, 3], 0)
>>> x
masked_array(data=[--, 1, --, 2, 3],
mask=[ True, False, True, False, False],
fill_value=0)
>>> ma.is_masked(x)
True
>>> x = ma.masked_equal([0, 1, 0, 2, 3], 42)
>>> x
masked_array(data=[0, 1, 0, 2, 3],
mask=False,
fill_value=42)
>>> ma.is_masked(x)
False
Always returns False if `x` isn't a MaskedArray.
>>> x = [False, True, False]
>>> ma.is_masked(x)
False
>>> x = 'a string'
>>> ma.is_masked(x)
False
"""
m = getmask(x)
if m is nomask:
return False
elif m.any():
return True
return False

View File

@ -1623,4 +1623,24 @@ public abstract class Array {
public void resetLocalIterator() {
ii = null;
}
@Override
public boolean equals(Object obj) {
Array o = (Array) obj;
if (!Arrays.equals(this.getShape(), o.getShape()))
return false;
if (this.getDataType() != o.getDataType())
return false;
IndexIterator iter = this.getIndexIterator();
IndexIterator iterO = o.getIndexIterator();
while (iter.hasNext()) {
if (!iter.getObjectNext().equals(iterO.getObjectNext())) {
return false;
}
}
return true;
}
}

View File

@ -1566,6 +1566,105 @@ public class ArrayUtil {
return r;
}
/**
* Repeat elements of an array.
*
* @param a The value
* @param repeats The number of repetitions for each element
* @return Repeated array
*/
public static Array repeat(Array a, Array repeats) {
repeats = repeats.copyIfView();
Array r;
IndexIterator iterA = a.getIndexIterator();
Object o;
if (repeats.getSize() == 1) {
int n = repeats.getInt(0);
r = Array.factory(a.getDataType(), new int[]{(int) a.getSize() * n});
int i = 0;
while(iterA.hasNext()) {
o = iterA.getObjectNext();
for (int j = 0; j < n; j++) {
r.setObject(i * n + j, o);
}
i += 1;
}
} else {
int n = 0;
for (int i = 0; i < repeats.getSize(); i++) {
n += repeats.getInt(i);
}
r = Array.factory(a.getDataType(), new int[]{n});
int idx = 0;
int i = 0;
while(iterA.hasNext()) {
o = iterA.getObjectNext();
for (int j = 0; j < repeats.getInt(i); j++) {
r.setObject(idx, o);
idx += 1;
}
i += 1;
}
}
return r;
}
/**
* Repeat elements of an array.
*
* @param a The value
* @param repeats The number of repetitions for each element
* @param axis The axis
* @return Repeated array
*/
public static Array repeat(Array a, Array repeats, int axis) {
repeats = repeats.copyIfView();
Array r;
if (repeats.getSize() == 1) {
int n = repeats.getInt(0);
int[] shape = a.getShape();
shape[axis] = shape[axis] * n;
r = Array.factory(a.getDataType(), shape);
Index aindex = a.getIndex();
Index index = r.getIndex();
int[] current;
for (int i = 0; i < r.getSize(); i++) {
current = index.getCurrentCounter();
current[axis] = current[axis] / n;
aindex.set(current);
r.setObject(index, a.getObject(aindex));
index.incr();
}
} else {
int n = 0;
int[] rsum = new int[(int) repeats.getSize()];
for (int i = 0; i < repeats.getSize(); i++) {
rsum[i] = n;
n += repeats.getInt(i);
}
int[] shape = a.getShape();
shape[axis] = n;
r = Array.factory(a.getDataType(), shape);
Index aindex = a.getIndex();
Index index = r.getIndex();
int[] current;
int idx;
for (int i = 0; i < a.getSize(); i++) {
current = aindex.getCurrentCounter();
idx = current[axis];
for (int j = 0; j < repeats.getInt(idx); j++) {
current[axis] = rsum[idx] + j;
index.set(current);
r.setObject(index, a.getObject(aindex));
}
aindex.incr();
}
}
return r;
}
/**
* Repeat a value n times
*
@ -2580,7 +2679,7 @@ public class ArrayUtil {
* @return The sorted unique elements of an array
* @throws InvalidRangeException
*/
public static Array unique(Array a, Integer axis) throws InvalidRangeException {
public static Array[] unique(Array a, Integer axis) throws InvalidRangeException {
int n = a.getRank();
int[] shape = a.getShape();
if (axis == null) {
@ -2599,7 +2698,7 @@ public class ArrayUtil {
r.setObject(i, tList.get(i));
}
return r;
return new Array[]{r};
} else {
if (axis == -1) {
axis = n - 1;
@ -2628,7 +2727,7 @@ public class ArrayUtil {
ranges.set(axis, range);
ArrayMath.setSection(r, ranges, tList.get(i));
}
return r;
return new Array[]{r};
}
}
@ -2657,82 +2756,165 @@ public class ArrayUtil {
}
/**
* Find the unique elements and index of an array.
* Find the unique elements and index/inverse/counts.
*
* @param a Array a
* @param axis The axis
* @return The sorted unique elements and index of an array
* @param returnIndex Return index or not
* @param returnInverse Return inverse or not
* @param returnCounts Return counts or not
* @return The sorted unique elements and index/inverse/counts
* @throws InvalidRangeException
*/
public static Array[] uniqueIndex(Array a, Integer axis) throws InvalidRangeException {
public static Array[] unique(Array a, Integer axis, boolean returnIndex, boolean returnInverse,
boolean returnCounts) throws InvalidRangeException {
int n = a.getRank();
int[] shape = a.getShape();
List<Array> result = new ArrayList<>();
if (axis == null) {
TreeMap<Object, Integer> map = new TreeMap<>();
List tList = new ArrayList<>();
List<Integer> idxList = new ArrayList<>();
List<Integer> countList = new ArrayList<>();
IndexIterator ii = a.getIndexIterator();
Object o;
int idx = 0;
while (ii.hasNext()) {
o = ii.getObjectNext();
if (!tList.contains(o)) {
tList.add(o);
map.put(o, idx);
if (returnCounts) {
while (ii.hasNext()) {
o = ii.getObjectNext();
int i = tList.indexOf(o);
if (i < 0) {
tList.add(o);
idxList.add(idx);
countList.add(1);
} else {
countList.set(i, countList.get(i) + 1);
}
idx += 1;
}
} else {
while (ii.hasNext()) {
o = ii.getObjectNext();
if (!tList.contains(o)) {
tList.add(o);
idxList.add(idx);
}
idx += 1;
}
idx += 1;
}
int[] nShape = new int[]{map.size()};
Array r = Array.factory(a.getDataType(), nShape);
Array index = Array.factory(DataType.INT, nShape);
Iterator iter = map.entrySet().iterator();
idx = 0;
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
r.setObject(idx, entry.getKey());
index.setInt(idx, (Integer) entry.getValue());
idx += 1;
}
return new Array[]{r, index};
List<Integer> sortIndex = MIMath.sort(tList);
int nv = tList.size();
int[] nShape = new int[]{nv};
Array r = Array.factory(a.getDataType(), nShape);
for (int i = 0; i < nv; i++) {
r.setObject(i, tList.get(i));
}
result.add(r);
if (returnIndex) {
Array index = Array.factory(DataType.INT, nShape);
for (int i = 0; i < nv; i++) {
index.setInt(i, idxList.get(sortIndex.get(i)));
}
result.add(index);
}
if (returnInverse) {
int na = (int) a.getSize();
Array inverse = Array.factory(DataType.INT, new int[]{na});
ii = a.getIndexIterator();
IndexIterator iterInverse = inverse.getIndexIterator();
while (ii.hasNext()) {
o = ii.getObjectNext();
iterInverse.setObjectNext(tList.indexOf(o));
}
result.add(inverse);
}
if (returnCounts) {
Array counts = Array.factory(DataType.INT, nShape);
for (int i = 0; i < nv; i++) {
counts.setInt(i, countList.get(sortIndex.get(i)));
}
result.add(counts);
}
} else {
if (axis == -1) {
axis = n - 1;
}
List<Array> tList = new ArrayList();
TreeMap<Array, Integer> map = new TreeMap<>();
List<Integer> idxList = new ArrayList<>();
List<Integer> countList = new ArrayList<>();
Array ta;
int nn = shape[axis];
shape[axis] = 1;
int[] origin = new int[shape.length];
for (int i = 0; i < nn; i++) {
origin[axis] = i;
ta = a.section(origin, shape);
if (!tList.contains(ta)) {
tList.add(ta);
map.put(ta, i);
if (returnCounts) {
for (int i = 0; i < nn; i++) {
origin[axis] = i;
ta = a.section(origin, shape);
int idx = tList.indexOf(ta);
if (idx < 0) {
tList.add(ta);
idxList.add(i);
countList.add(1);
} else {
countList.set(idx, countList.get(idx) + 1);
}
}
} else {
for (int i = 0; i < nn; i++) {
origin[axis] = i;
ta = a.section(origin, shape);
if (!tList.contains(ta)) {
tList.add(ta);
idxList.add(i);
}
}
}
shape[axis] = map.size();
int nv = tList.size();
shape[axis] = nv;
Array r = Array.factory(a.getDataType(), shape);
Array index = Array.factory(DataType.INT, shape);
Array index = Array.factory(DataType.INT, new int[]{nv});
List<Range> ranges = new ArrayList<>();
for (int i = 0; i < shape.length; i++) {
ranges.add(new Range(0, shape[i] - 1, 1));
}
Range range = ranges.get(axis);
Iterator iter = map.keySet().iterator();
Iterator iter = tList.iterator();
int i = 0;
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Array arr = (Array) iter.next();
range = new Range(i, i, 1);
ranges.set(axis, range);
ArrayMath.setSection(r, ranges, entry.getKey());
index.setInt(i, (Integer) entry.getValue());
ArrayMath.setSection(r, ranges, arr);
index.setInt(i, idxList.get(i));
i += 1;
}
result.add(r);
if (returnIndex) {
result.add(index);
}
if (returnInverse) {
int na = (int) a.getSize();
Array inverse = Array.factory(DataType.INT, new int[]{na});
IndexIterator ii = a.getIndexIterator();
IndexIterator iterInverse = inverse.getIndexIterator();
while (ii.hasNext()) {
Object o = ii.getObjectNext();
iterInverse.setObjectNext(tList.indexOf(o));
}
result.add(inverse);
}
return new Array[]{r, index};
if (returnCounts) {
Array counts = Array.factory(DataType.INT, shape);
for (i = 0; i < nv; i++) {
counts.setInt(i, countList.get(i));
}
result.add(counts);
}
}
return result.stream().toArray(Array[]::new);
}
/**