def partition(self, wsp_darr, wdir_darr, dep_darr, agefac=1.7, wscut=0.3333, hs_min=0.001, nearest=False, max_swells=5): """Partition wave spectra using WW3 watershed algorithm. Args: - wsp_darr (DataArray): wind speed (m/s). - wdir_darr (DataArray): Wind direction (degree). - dep_darr (DataArray): Water depth (m). - agefac (float): Age factor. - wscut (float): Wind speed cutoff. - hs_min (float): minimum Hs for assigning swell partition. - nearest (bool): if True, wsp, wdir and dep are allowed to be taken from the. nearest point if not matching positions in SpecArray (slower). - max_swells: maximum number of swells to extract Returns: - part_spec (SpecArray): partitioned spectra with one extra dimension representig partition number. Note: - All input DataArray objects must have same non-spectral dimensions as SpecArray. References: - Hanson, Jeffrey L., et al. "Pacific hindcast performance of three numerical wave models." Journal of Atmospheric and Oceanic Technology 26.8 (2009): 1614-1633. TODO: - We currently loop through each spectrum to calculate the partitions which is slow. Ideally we should handle the problem in a multi-dimensional way. """ # Assert spectral dims are present in spectra and non-spectral dims are present in winds and depths assert attrs.FREQNAME in self._obj.dims and attrs.DIRNAME in self._obj.dims, ( 'partition requires E(freq,dir) but freq|dir ' 'dimensions not in SpecArray dimensions (%s)' % (self._obj.dims)) for darr in (wsp_darr, wdir_darr, dep_darr): # Conditional below aims at allowing wsp, wdir, dep to be DataArrays within the SpecArray. not working yet if isinstance(darr, str): darr = getattr(self, darr) assert set(darr.dims) == self._non_spec_dims, ( '%s dimensions (%s) need matching non-spectral dimensions ' 'in SpecArray (%s) for consistent slicing' % (darr.name, set(darr.dims), self._non_spec_dims)) from wavespectra.specpart import specpart # Initialise output - one SpecArray for each partition all_parts = [0 * self._obj] # Predefine these for speed dirs = self.dir.values freqs = self.freq.values ndir = len(dirs) nfreq = len(freqs) # Slice each possible 2D, freq-dir array out of the full data array slice_ids = { dim: range(self._obj[dim].size) for dim in self._non_spec_dims } for slice_dict in self._product(slice_ids): specarr = self._obj[slice_dict] if nearest: slice_dict_nearest = { key: self._obj[key][val].values for key, val in slice_dict.items() } wsp = float( wsp_darr.sel(method='nearest', **slice_dict_nearest)) wdir = float( wdir_darr.sel(method='nearest', **slice_dict_nearest)) dep = float( dep_darr.sel(method='nearest', **slice_dict_nearest)) else: wsp = float(wsp_darr[slice_dict]) wdir = float(wdir_darr[slice_dict]) dep = float(dep_darr[slice_dict]) Up = agefac * wsp * np.cos(D2R * (dirs - wdir)) windbool = np.tile(Up, (nfreq, 1)) > np.tile( self.celerity(dep, freqs)[:, _], (1, ndir)) spectrum = specarr.values part_array = specpart.partition(spectrum) part_array_max = part_array.max() #TODO: join the two loops in a while loop # Assign new partition if multiple valleys and satisfying some conditions for part in range(1, part_array_max + 1): part_spec = np.where(part_array == part, spectrum, 0.) # Current partition imax, imin = self._inflection(part_spec, dfres=0.01, fmin=0.05) if len(imin) > 0: part_spec[imin[0].squeeze():, :] = 0 newpart = part_spec > 0 if newpart.sum() > 20: part_array_max += 1 part_array[newpart] = part_array_max # Group sea partitions and sort swells by hs swell_hs_parts = np.zeros(part_array_max + 1) # +1 because of sea for part in range(1, part_array_max + 1): part_spec = np.where(part_array == part, spectrum, 0.) # Current partition W = part_spec[windbool].sum() / part_spec.sum() if W > wscut: part_array[part_array == part] = 0 swell_hs_parts[part] = 0 # not really needed else: swell_hs_parts[part] = hs(part_spec, freqs, dirs) sortedparts = np.flipud(swell_hs_parts[1:].argsort() + 1) num_swells = min(max_swells, sum(swell_hs_parts[1:] > hs_min)) parts = np.concatenate(([0], sortedparts[:num_swells])) # Extend partitions list if any extra one has been detected for dummy in range(1 + num_swells - len(all_parts)): all_parts.append(0 * self._obj) for ind, part in enumerate(parts): all_parts[ind][slice_dict] = np.where(part_array == part, spectrum, 0.) # Concatenate partitions along new axis part_coord = xr.DataArray(data=range(len(all_parts)), coords={'part': range(len(all_parts))}, dims=('part', ), name='part', attrs=OrderedDict( (('standard_name', 'spectral_partition_number'), ('units', '')))) return xr.concat(all_parts, dim=part_coord)
def nppart(spectrum, freq, dir, wspd, wdir, dpt, swells=3, agefac=1.7, wscut=0.3333): """Watershed partition on a numpy array. Args: - spectrum (2darray): Wave spectrum array with shape (nf, nd). - freq (1darray): Wave frequency array with shape (nf). - dir (1darray): Wave direction array with shape (nd). - wspd (float): Wind speed. - wdir (float): Wind direction. - dpt (float): Water depth. - swells (int): Number of swell partitions to compute. - agefac (float): Age factor. - wscut (float): Wind speed cutoff. Returns: - specpart (3darray): Wave spectrum partitions with shape (np, nf, nd). """ part_array = specpart.partition(spectrum) Up = agefac * wspd * np.cos(D2R * (dir - wdir)) windbool = np.tile(Up, (freq.size, 1)) > np.tile( celerity(freq, dpt)[:, np.newaxis], (1, dir.size)) ipeak = 1 # values from specpart.partition start at 1 part_array_max = part_array.max() # partitions_hs_swell = np.zeros(part_array_max + 1) # zero is used for sea partitions_hs_swell = np.zeros(part_array_max + 1) # zero is used for sea while ipeak <= part_array_max: part_spec = np.where(part_array == ipeak, spectrum, 0.0) # Assign new partition if multiple valleys and satisfying conditions __, imin = inflection(part_spec, freq, dfres=0.01, fmin=0.05) if len(imin) > 0: part_spec_new = part_spec.copy() part_spec_new[imin[0].squeeze():, :] = 0 newpart = part_spec_new > 0 if newpart.sum() > 20: part_spec[newpart] = 0 part_array_max += 1 part_array[newpart] = part_array_max partitions_hs_swell = np.append(partitions_hs_swell, 0) # Assign sea partition W = part_spec[windbool].sum() / part_spec.sum() if W > wscut: part_array[part_array == ipeak] = 0 else: partitions_hs_swell[ipeak] = hs(part_spec, freq, dir) ipeak += 1 sorted_swells = np.flipud(partitions_hs_swell[1:].argsort() + 1) parts = np.concatenate(([0], sorted_swells[:swells])) all_parts = [] for part in parts: all_parts.append(np.where(part_array == part, spectrum, 0.0)) # Extend partitions list if it is less than swells if len(all_parts) < swells + 1: nullspec = 0 * spectrum nmiss = (swells + 1) - len(all_parts) for i in range(nmiss): all_parts.append(nullspec) return np.array(all_parts)
def partition( self, wsp_darr=None, wdir_darr=None, dep_darr=None, agefac=1.7, wscut=0.3333, hs_min=0.001, nearest=False, max_swells=5, fortran_code=True, ): """Partition wave spectra using WW3 watershed algorithm. Args: - wsp_darr (DataArray): wind speed (m/s). - wdir_darr (DataArray): Wind direction (degree). - dep_darr (DataArray): Water depth (m). - agefac (float): Age factor. - wscut (float): Wind speed cutoff. - hs_min (float): minimum Hs for assigning swell partition. - nearest (bool): if True, wsp, wdir and dep are allowed to be taken from the. nearest point if not matching positions in SpecArray (slower). - max_swells (int): maximum number of swells to extract - fortran_code (bool): use fortran partition code or (~30x slower) pure python Returns: - part_spec (SpecArray): partitioned spectra with one extra dimension representig partition number. Note: - All input DataArray objects must have same non-spectral dimensions as SpecArray. References: - Hanson, Jeffrey L., et al. "Pacific hindcast performance of three numerical wave models." Journal of Atmospheric and Oceanic Technology 26.8 (2009): 1614-1633. TODO: - We currently loop through each spectrum to calculate the partitions which is slow. Ideally we should handle the problem in a multi-dimensional way. """ # Assert spectral dims are present in spectra and non-spectral dims are present in winds and depths assert attrs.FREQNAME in self._obj.dims and attrs.DIRNAME in self._obj.dims, ( "partition requires E(freq,dir) but freq|dir " "dimensions not in SpecArray dimensions (%s)" % (self._obj.dims)) for darr in (wsp_darr, wdir_darr, dep_darr): # Conditional below aims at allowing wsp, wdir, dep to be DataArrays within the SpecArray. not working yet if isinstance(darr, str): darr = getattr(self, darr) assert set(darr.dims) == self._non_spec_dims, ( "%s dimensions (%s) need matching non-spectral dimensions " "in SpecArray (%s) for consistent slicing" % (darr.name, set(darr.dims), self._non_spec_dims)) if fortran_code: from wavespectra.specpart import specpart else: from wavespectra.core import specpartpy as specpart # Initialise output - one SpecArray for each partition all_parts = [0 * self._obj.load() for i in range(1 + max_swells)] # Predefine these for speed dirs = self.dir.values freqs = self.freq.values dfarr = self.dfarr.values ndir = len(dirs) nfreq = len(freqs) wsp_darr.load() wdir_darr.load() dep_darr.load() # Slice each possible 2D, freq-dir array out of the full data array slice_ids = { dim: range(self._obj[dim].size) for dim in self._non_spec_dims } for slice_dict in self._product(slice_ids): spectrum = self._obj[slice_dict].values if nearest: slice_dict_nearest = { key: self._obj[key][val].values for key, val in slice_dict.items() } wsp = float( wsp_darr.sel(method="nearest", **slice_dict_nearest)) wdir = float( wdir_darr.sel(method="nearest", **slice_dict_nearest)) dep = float( dep_darr.sel(method="nearest", **slice_dict_nearest)) else: wsp = float(wsp_darr[slice_dict]) wdir = float(wdir_darr[slice_dict]) dep = float(dep_darr[slice_dict]) Up = agefac * wsp * np.cos(D2R * (dirs - wdir)) windbool = np.tile(Up, (nfreq, 1)) > np.tile( self.celerity(dep, freqs)[:, _], (1, ndir)) part_array = specpart.partition(spectrum) ipeak = 1 # values from specpart.partition start at 1 part_array_max = part_array.max() partitions_hs_swell = np.zeros( part_array_max + 1) # +1 because zeroth index is used for windseas while ipeak <= part_array_max: part_spec = np.where(part_array == ipeak, spectrum, 0.0) # Current partition # Assign new partition if multiple valleys and satisfying some conditions imax, imin = self._inflection(part_spec, dfres=0.01, fmin=0.05) if len(imin) > 0: # TODO: swap part_spec and part_spec_new and deal with more than one imin value ? part_spec_new = part_spec.copy() part_spec_new[imin[0].squeeze():, :] = 0 newpart = part_spec_new > 0 if newpart.sum() > 20: part_spec[newpart] = 0 part_array_max += 1 part_array[newpart] = part_array_max partitions_hs_swell = np.append(partitions_hs_swell, 0) # Group sea partitions and sort swells by hs W = np.dot((part_spec * windbool).sum(1), dfarr) / np.dot( part_spec.sum(1), dfarr) if W > wscut: part_array[part_array == ipeak] = 0 # mark as windsea else: partitions_hs_swell[ipeak] = hs(part_spec, freqs, dirs) ipeak += 1 num_swells = min(max_swells, sum(partitions_hs_swell > hs_min)) sorted_swells = np.flipud(partitions_hs_swell[1:].argsort() + 1) parts = np.concatenate(([0], sorted_swells[:num_swells])) for ind, part in enumerate(parts): all_parts[ind][slice_dict] = np.where(part_array == part, spectrum, 0.0) # Concatenate partitions along new axis part_coord = xr.DataArray( data=range(len(all_parts)), coords={"part": range(len(all_parts))}, dims=("part", ), name="part", attrs=OrderedDict((("standard_name", "spectral_partition_number"), ("units", ""))), ) return xr.concat(all_parts, dim=part_coord)
def partition(self, wsp_darr, wdir_darr, dep_darr, agefac=1.7, wscut=0.3333, hs_min=0.001, nearest=False): """Partition wave spectra using WW3 watershed algorithm. Args: - wsp_darr (DataArray): wind speed (m/s). - wdir_darr (DataArray): Wind direction (degree). - dep_darr (DataArray): Water depth (m). - agefac (float): Age factor. - wscut (float): Wind speed cutoff. - hs_min (float): minimum Hs for assigning swell partition. - nearest (bool): if True, wsp, wdir and dep are allowed to be taken from the. nearest point if not matching positions in SpecArray (slower). Returns: - part_spec (SpecArray): partitioned spectra with one extra dimension representig partition number. Note: - All input DataArray objects must have same non-spectral dimensions as SpecArray. TODO: - We currently loop through each spectrum to calculate the partitions which is slow. Ideally we should handle the problem in a multi-dimensional way. """ # Assert spectral dims are present in spectra and non-spectral dims are present in winds and depths assert attrs.FREQNAME in self._obj.dims and attrs.DIRNAME in self._obj.dims, ( 'partition requires E(freq,dir) but freq|dir ' 'dimensions not in SpecArray dimensions (%s)' % (self._obj.dims)) for darr in (wsp_darr, wdir_darr, dep_darr): # Conditional below aims at allowing wsp, wdir, dep to be DataArrays within the SpecArray. not working yet if isinstance(darr, str): darr = getattr(self, darr) assert set(darr.dims) == self._non_spec_dims, ( '%s dimensions (%s) need matching non-spectral dimensions ' 'in SpecArray (%s) for consistent slicing' % (darr.name, set(darr.dims), self._non_spec_dims)) from wavespectra.specpart import specpart # Initialise output - one SpecArray for each partition all_parts = [0 * self._obj] # Predefine these for speed dirs = self.dir.values freqs = self.freq.values ndir = len(dirs) nfreq = len(freqs) # Slice each possible 2D, freq-dir array out of the full data array slice_ids = { dim: range(self._obj[dim].size) for dim in self._non_spec_dims } for slice_dict in self._product(slice_ids): specarr = self._obj[slice_dict] if nearest: slice_dict_nearest = { key: self._obj[key][val].values for key, val in slice_dict.items() } wsp = float( wsp_darr.sel(method='nearest', **slice_dict_nearest)) wdir = float( wdir_darr.sel(method='nearest', **slice_dict_nearest)) dep = float( dep_darr.sel(method='nearest', **slice_dict_nearest)) else: wsp = float(wsp_darr[slice_dict]) wdir = float(wdir_darr[slice_dict]) dep = float(dep_darr[slice_dict]) spectrum = specarr.values part_array = specpart.partition(spectrum) nparts = part_array.max() # Assign new partition if multiple valleys and satisfying some conditions for part in range(1, nparts + 1): part_spec = np.where(part_array == part, spectrum, 0.) # Current partition only imax, imin = self._inflection(part_spec, dfres=0.01, fmin=0.05) if len(imin) > 0: part_spec[imin[0].squeeze():, :] = 0 newpart = part_spec > 0 if newpart.sum() > 20: nparts += 1 part_array[newpart] = nparts # Extend partitions list if any extra one has been detected (+1 because of sea) if len(all_parts) < nparts + 1: for new_part_number in set(range(nparts + 1)).difference( range(len(all_parts))): all_parts.append(0 * self._obj) # Assign sea and swells partitions based on wind and wave properties sea = 0 * spectrum swells = list() hs_swell = list() Up = agefac * wsp * np.cos(D2R * (dirs - wdir)) for part in range(1, nparts + 1): part_spec = np.where(part_array == part, spectrum, 0.) # Current partition only W = part_spec[np.tile(Up, (nfreq, 1)) > \ np.tile(self.celerity(dep, freqs)[:,_], (1, ndir))].sum() / part_spec.sum() if W > wscut: sea += part_spec else: _hs = hs(part_spec, freqs, dirs) if _hs > hs_min: swells.append(part_spec) hs_swell.append(_hs) hs_swell = [ h + np.random.rand() * 1e-10 for h in hs_swell ] # If two or more partitions with same Hs if len(swells) > 1: swells = [ x for (y, x) in sorted(zip(hs_swell, swells), reverse=True) ] # Updating partition SpecArrays for current slice all_parts[0][slice_dict] = sea for ind, swell in enumerate(swells): all_parts[ind + 1][slice_dict] = swell # Concatenate partitions along new axis part_coord = xr.DataArray(data=range(len(all_parts)), coords={'part': range(len(all_parts))}, dims=('part', ), name='part', attrs=OrderedDict( (('standard_name', 'spectral_partition_number'), ('units', '')))) return xr.concat(all_parts, dim=part_coord)