General processing functions#

FEAT general-purpose workflow utility functions.

Provides spatial, interpolation, and data-manipulation helpers used across FEAT survey workflows, including transect extent extraction, kriging mesh preparation, and adaptive nearest-neighbor search utilities.

echopop.utils.feat_functions.convert_afsc_nasc_to_feat(nasc_data: DataFrame, default_interval_distance: float = 0.5, default_transect_spacing: float = 10.0, inclusion_filter: dict[str, Any] = None, exclusion_filter: dict[str, Any] = None) DataFrame#

Convert AFSC-MACE to NWFSC-FEAT transect NASC format.

Parameters:
nasc_datapd.DataFrame

DataFrame containing AFSC NASC data.

default_interval_distancefloat, optional

Default distance interval for transects, by default 0.5.

default_transect_spacingfloat, optional

Default transect spacing, by default 10.0.

inclusion_filterDict[str, Any], optional

Filter to include specific rows based on column values, by default {}.

exclusion_filterDict[str, Any], optional

Filter to exclude specific rows based on column values, by default {}.

Returns:
pd.DataFrame

Transformed DataFrame corresponding to the expected NWFSC-FEAT format.

echopop.utils.feat_functions.filter_transect_intervals(nasc_data: DataFrame, transect_filter: DataFrame | Path, survey_filter: str | None = None, transect_filter_sheet: str | None = None) DataFrame#

Filter transect intervals based on log start and end values.

Parameters:
nasc_datapandas.DataFrame

DataFrame containing NASC data with columns ‘transect_num’, ‘distance_s’, and ‘distance_e’

transect_filterUnion[pandas.DataFrame, Path]

DataFrame containing transect filter data with columns ‘transect_num’, ‘log_start’, and ‘log_end’, or a filepath that reads in a file.

survey_filterstr, optional

Query survey string to filter transect_filter (e.g., “region_id == ‘A’”)

transect_filter_sheetstr, optional

Optional sheetname if a filename is input

Returns:
pandas.DataFrame

Filtered DataFrame containing only rows that overlap with the specified transect intervals

Examples

>>> nasc_data = pd.DataFrame({
...     'transect_num': [1, 1, 2, 2],
...     'distance_s': [0, 1, 0, 1],
...     'distance_e': [1, 2, 1, 2],
...     'nasc': [10, 20, 30, 40]
... })
>>> filter_data = pd.DataFrame({
...     'transect_num': [1, 2],
...     'log_start': [0.5, 0.5],
...     'log_end': [1.5, 1.5],
...     'region_id': ['A', 'B']
... })
>>> result = filter_transect_intervals(nasc_data, filter_data)
echopop.utils.feat_functions.get_survey_western_extents(transects: DataFrame, coordinate_names: tuple[str, str], latitude_threshold: float) DataFrame#

Get the western extents of each survey transect line.

Get the western extents of each survey transect that can be used to constrain the adaptive nearest neighbors search algorithm incorporated into the kriging interpolation algorithm.

Parameters:
transectspd.DataFrame

A dataframe containing georeferenced coordinates associated with a particular variable (e.g. biomass). This DataFrame must have at least two valid columns comprising the overall 2D coordinates (e.g. ‘x’ and ‘y’). Furthermore, this function requires that transects also contain a column called ‘latitude’.

coordinates_namesTuple[str, str], default = (‘x’, ‘y’)

A tuple containing the ‘transects’ column names defining the coordinates. The order of this input matters where they should be defined as the (horizontal axis, vertical axis).

latitude_thresholdfloat

A threshold that is applied to the georeferenced coordinates that further constrains any extrapolation that occurs during the kriging analysis.

Returns:
pd.DataFrame

A DataFrame comprising three columns: ‘transect_num’, and the column names supplied by the argument coordinate_names.

echopop.utils.feat_functions.region_13_conditions(x, boundary_column: str, position: str)#

Map mesh regions 1 and 3 boundaries based on encoded transect boundary values.

This helper function interprets the encoded boundary values for regions 1 and 3, which use latitude-parallel transects. The encoding uses decimal fractions to indicate spatial positions (e.g., .1 for west, .4 for east, .6 for south, .9 for north).

Parameters:
xpd.DataFrame

DataFrame subset containing transect data for a specific transect number

boundary_columnstr

Name of the column containing the encoded boundary values

positionstr

Position identifier (‘east’, ‘west’, ‘north’, ‘south’) used for output column naming

Returns:
pd.Series

Series containing longitude and latitude coordinates for the specified position, with column names formatted as ‘longitude_{position}’ and ‘latitude_{position}’

Notes

The encoding scheme: - x.1: Western boundary (minimum longitude) - x.4: Eastern boundary (maximum longitude) - x.6: Southern boundary (minimum latitude) - x.9: Northern boundary (maximum latitude)

echopop.utils.feat_functions.region_2_conditions(x, boundary_column: str, position: str)#

Map mesh region 2 boundaries based on encoded transect boundary values.

This helper function interprets the encoded boundary values for region 2, which uses longitude-parallel transects. The encoding uses decimal fractions to indicate spatial positions.

Parameters:
xpd.DataFrame

DataFrame subset containing transect data for a specific transect number

boundary_columnstr

Name of the column containing the encoded boundary values

positionstr

Position identifier (‘north’, ‘south’) used for output column naming

Returns:
pd.Series

Series containing longitude and latitude coordinates for the specified position, with column names formatted as ‘longitude_{position}’ and ‘latitude_{position}’

Notes

The encoding scheme for region 2: - x.1: Western boundary (minimum longitude) - x.4: Eastern boundary (maximum longitude) - x.6: Southern boundary (minimum latitude) - x.9: Northern boundary (maximum latitude)

Region 2 transects run parallel to longitudes (north-south orientation).

echopop.utils.feat_functions.transect_ends_crop(transects: DataFrame, mesh: DataFrame, latitude_resolution: float, transect_mesh_region_function: Callable) tuple[DataFrame, DataFrame]#

Crop the kriging mesh by interpolating the eastern and western transect extents.

Crop the kriging mesh by interpolating the eastern and western extents of survey transects partitioned into discrete regions via user-defined sorting functions. This function processes survey transect data to define spatial boundaries for mesh cropping. It divides transects into three regions, interpolates their boundaries, and identifies mesh cells that fall within the survey extent.

Parameters:
transectspd.DataFrame

Georeferenced survey transect data used for defining the spatial extent for the kriging mesh grid. Must contain columns: ‘transect_num’, ‘longitude’, ‘latitude’.

meshpd.DataFrame

Complete kriging mesh DataFrame that is subsequently cropped. Must contain columns: ‘longitude’, ‘latitude’.

latitude_resolutionfloat

The latitudinal resolution (in degrees) used for the interpolation. This determines the spacing between interpolation points and affects the precision of boundary detection.

transect_mesh_region_functionCallable

A sorting function that maps specific transect numbers to their respective discretized mesh regions. The outputs of this function are expected to be:

  • ‘transect_start’ (np.number): the first transect number of a particular region,

  • ‘transect_end’ (np.number): the last transect number of a particular region,

  • ‘transect_lower_bound’ (List[np.number]): a list of encoded transect numbers indicating whether the transect is north, south, west, or east,

  • ‘transect_upper_bound’ (List[np.number]): a list of encoded transect numbers indicating whether the transect is north, south, west, or east.

Returns:
Tuple[pd.DataFrame, pd.DataFrame]

A tuple comprising:

  • Cropped kriging mesh DataFrame containing only cells within the survey extent

  • Annotated transect data with each transect number’s respective mesh region assignment

Examples

>>> from echopop.nwfsc_feat.FEAT import transect_mesh_region_2019
>>> cropped_mesh, annotated_transects = transect_ends_crop(
...     transects, mesh, 0.05, transect_mesh_region_2019
... )
>>> print(f"Original mesh size: {len(mesh)}")
>>> print(f"Cropped mesh size: {len(cropped_mesh)}")
echopop.utils.feat_functions.western_boundary_search_strategy(kriging_mesh: DataFrame, western_extent: DataFrame, coordinate_names: tuple[str, str], sparse_radii: ndarray[int], valid_distances: ndarray[int], local_points: ndarray[float], distance_matrix_masked: ndarray[float], nearby_indices: ndarray[int], k_min: int, k_max: int, search_radius: float, wr_indices: ndarray[int], oos_indices: ndarray[number], oos_weights: ndarray[float], **kwargs) tuple[ndarray[number], ndarray[number], ndarray[number]]#

Search strategy that applies western boundary constraints for transect-based surveys.

Parameters:
kriging_meshpd.DataFrame

Kriging mesh used for interpolated data values via geostatistics.

western_extentpd.DataFrame

DataFrame with the western-most extent of each transect line used for re-weighting the out-of-sample/extrapolated kriged values.

coordinate_namesTuple[str, str]

Names of the coordinate columns when using DataFrames. Expected format: (x_col, y_col).

sparse_radiinp.ndarray[int]

Indices where there are fewer than k_min nearest neighbors.

valid_distancesnp.ndarray[int]

The number of masked distance matrix values where extrapolation is required.

local_pointsnp.ndarray[float]

An array with the sorted distances (from nearest to furthest) relative to each point.

distance_matrix_maskednp.ndarray[float]

An array with the search-radius-masked nearest neighbor distances.

nearby_indicesnp.ndarray[int]

Indices of points that require extrapolation.

k_minint

The minimum number of nearest neighbors required for including values for kriging within the search radius.

k_maxint

The maximum number of nearest neighbors required for including values for kriging detected within the search radius.

search_radiusfloat

The adaptive search radius that identifies the k-nearest neighbors around each georeferenced value that are subsequently kriged.

wr_indicesnp.ndarray[int]

Indices of within-radius (WR) (i.e. < k_max) points.

oos_indicesnp.ndarray[np.number]

Template array based on the size of the data input and k_min that will contain indices where extrapolation is required where there are fewer than k_min nearest neighbors.

oos_weightsnp.ndarray[float]

Weights applied to extraplolated values.

Returns:
Tuple[np.ndarray[np.number], np.ndarray[np.number], np.ndarray[np.number]]

A tuple with updated values for wr_indices, oos_indices, and oos_weights via a search strategy that uses an extrapolation re-weighting based on transect extents.

Source code#

Function: convert_afsc_nasc_to_feat

def convert_afsc_nasc_to_feat(nasc_data: pd.DataFrame, default_interval_distance: float=0.5, default_transect_spacing: float=10.0, inclusion_filter: dict[str, Any]=None, exclusion_filter: dict[str, Any]=None) -> pd.DataFrame:
    if exclusion_filter is None:
        exclusion_filter = {}
    if inclusion_filter is None:
        inclusion_filter = {}
    df = utils.apply_filters(nasc_data, include_filter=inclusion_filter, exclude_filter=exclusion_filter)
    df.rename(columns={'distance': 'distance_s'}, inplace=True)
    df['distance_e'] = df['distance_s'] + default_interval_distance
    df['transect_spacing'] = df['transect_spacing'].fillna(default_transect_spacing)
    return df

Function: filter_transect_intervals

def filter_transect_intervals(nasc_data: pd.DataFrame, transect_filter: pd.DataFrame | Path, survey_filter: str | None=None, transect_filter_sheet: str | None=None) -> pd.DataFrame:
    nasc_df = nasc_data.copy()
    if isinstance(transect_filter, Path):
        transect_filter = pd.read_excel(transect_filter, sheet_name=transect_filter_sheet, index_col=None, header=0)
    transect_filter.columns = transect_filter.columns.str.lower()
    if 'transect' in transect_filter.columns and 'transect_num' not in transect_filter.columns:
        transect_filter.rename(columns={'transect': 'transect_num'}, inplace=True)
    if not set(transect_filter.columns) >= set(['transect_num', 'log_start', 'log_end']):
        missing = list(set(['transect_num', 'log_start', 'log_end']) - set(transect_filter.columns))
        missing_str = [f"'{v}'" for v in missing]
        raise KeyError(f"The following columns could not be formatted from 'transect_filter': {', '.join(missing_str)}.")
    if survey_filter is not None:
        tokens = re.findall('\\b[a-zA-Z_][a-zA-Z0-9_]*\\b', survey_filter)
        keywords = {'and', 'or', 'not', 'in', 'notin', 'True', 'False'}
        column_names = [t for t in tokens if t not in keywords and (not t.isnumeric()) and (t in transect_filter.columns)]
        missing = [col for col in column_names if col not in transect_filter.columns]
        if missing:
            raise ValueError(f"Invalid column(s): {', '.join(missing)}")
        else:
            transect_filter = transect_filter.query(survey_filter).sort_values(['transect_num'])
    transect_filter = transect_filter.sort_values('log_start').reset_index(drop=True)
    filter_transect_nums = transect_filter['transect_num'].values
    filter_log_starts = transect_filter['log_start'].values
    filter_log_ends = transect_filter['log_end'].values
    unique_filter_transects = np.unique(filter_transect_nums)
    nasc_transect_nums = nasc_df['transect_num'].values
    nasc_distance_starts = nasc_df['distance_s'].values
    nasc_distance_ends = nasc_df['distance_e'].values
    indices_to_remove = []
    for transect in unique_filter_transects:
        filter_interval_indices = np.where(filter_transect_nums == transect)[0]
        num_intervals = len(filter_interval_indices)
        nasc_data_indices = np.where(nasc_transect_nums == transect)[0]
        if len(nasc_data_indices) == 0:
            continue
        current_removal_indices = []
        if num_intervals > 1:
            for interval in range(num_intervals):
                if interval == 0:
                    condition_indices = np.where(nasc_distance_ends[nasc_data_indices] < filter_log_starts[filter_interval_indices[0]])[0]
                elif interval == num_intervals - 1:
                    condition_indices = np.where((nasc_distance_starts[nasc_data_indices] > filter_log_ends[filter_interval_indices[num_intervals - 1]]) | (nasc_distance_starts[nasc_data_indices] > filter_log_ends[filter_interval_indices[interval - 1]]) & (nasc_distance_ends[nasc_data_indices] < filter_log_starts[filter_interval_indices[interval]]))[0]
                else:
                    condition_indices = np.where((nasc_distance_starts[nasc_data_indices] > filter_log_ends[filter_interval_indices[interval - 1]]) & (nasc_distance_ends[nasc_data_indices] < filter_log_starts[filter_interval_indices[interval]]))[0]
                if len(condition_indices) > 0:
                    current_removal_indices.extend(nasc_data_indices[condition_indices])
        else:
            condition_indices = np.where((nasc_distance_ends[nasc_data_indices] < filter_log_starts[filter_interval_indices[0]]) | (nasc_distance_starts[nasc_data_indices] > filter_log_ends[filter_interval_indices[0]]))[0]
            if len(condition_indices) > 0:
                current_removal_indices.extend(nasc_data_indices[condition_indices])
        indices_to_remove.extend(current_removal_indices)
    keep_mask = np.ones(len(nasc_df), dtype=bool)
    keep_mask[indices_to_remove] = False
    return nasc_df[keep_mask].reset_index(drop=True)

Function: get_survey_western_extents

def get_survey_western_extents(transects: pd.DataFrame, coordinate_names: tuple[str, str], latitude_threshold: float) -> pd.DataFrame:
    transect_thresholded = transects.loc[transects['latitude'] < latitude_threshold]
    western_extent_idx = transect_thresholded.groupby(['transect_num'])[coordinate_names[0]].idxmin()
    transect_western_extent = transect_thresholded.loc[western_extent_idx].reset_index(drop=True)
    return transect_western_extent.filter(['transect_num', *coordinate_names])

Function: region_13_conditions

def region_13_conditions(x, boundary_column: str, position: str):
    x_floor = np.round(x[boundary_column].iloc[0] % 1 * 10)
    if x_floor == 1.0:
        return pd.Series({f'longitude_{position}': x['longitude'].min(), f'latitude_{position}': x['latitude'].iloc[x['longitude'].argmin()]})
    elif x_floor == 4.0:
        return pd.Series({f'longitude_{position}': x['longitude'].max(), f'latitude_{position}': x['latitude'].iloc[x['longitude'].argmax()]})
    elif x_floor == 6.0:
        return pd.Series({f'longitude_{position}': x['longitude'].iloc[x['latitude'].argmin(), f'latitude_{position}':x['latitude'].min()]})
    elif x_floor == 9.0:
        return pd.Series({f'longitude_{position}': x['longitude'].iloc[x['latitude'].argmax(), f'latitude_{position}':x['latitude'].max()]})

Function: region_2_conditions

def region_2_conditions(x, boundary_column: str, position: str):
    x_floor = np.round(x[boundary_column].iloc[0] % 1 * 10)
    if x_floor == 1.0:
        return pd.Series({f'latitude_{position}': x['latitude'].iloc[x['longitude'].argmin()], f'longitude_{position}': x['longitude'].min()})
    elif x_floor == 4.0:
        return pd.Series({f'latitude_{position}': x['latitude'].iloc[x['longitude'].argmax()], f'longitude_{position}': x['longitude'].max()})
    elif x_floor == 6.0:
        return pd.Series({f'latitude_{position}': x['latitude'].min(), f'longitude_{position}': x['longitude'].iloc[x['latitude'].argmin()]})
    elif x_floor == 9.0:
        return pd.Series({f'latitude_{position}': x['latitude'].max(), f'longitude_{position}': x['longitude'].iloc[x['latitude'].argmax()]})

Function: transect_ends_crop

def transect_ends_crop(transects: pd.DataFrame, mesh: pd.DataFrame, latitude_resolution: float, transect_mesh_region_function: Callable) -> tuple[pd.DataFrame, pd.DataFrame]:
    mesh_region_dict = {region: {name: value for name, value in zip(['start', 'end', 'upper', 'lower'], transect_mesh_region_function(region), strict=False)} for region in [1, 2, 3]}
    mesh_region_df = pd.concat([pd.DataFrame({'transect_num': np.arange(values['start'], values['end'] + 1), 'mesh_region': region, 'transect_lower_bound': values['lower'], 'transect_upper_bound': values['upper']}) for region, values in mesh_region_dict.items()])
    transect_df = transects.merge(mesh_region_df, on='transect_num', how='inner')
    transect_df.set_index(['mesh_region'], inplace=True)
    mesh_region_df.set_index(['mesh_region'], inplace=True)
    region_1_latitude = np.arange(transect_df.loc[1, 'latitude'].min(), transect_df.loc[1, 'latitude'].max(), latitude_resolution)
    longitude_resolution_region_2 = latitude_resolution * np.cos(np.radians(transect_df.loc[2, 'latitude'].mean()))
    region_2_longitude = np.arange(transect_df.loc[2, 'longitude'].min(), transect_df.loc[2, 'longitude'].max(), longitude_resolution_region_2)
    t3_start = mesh_region_dict[3]['start']
    t3_end = mesh_region_dict[3]['end']
    region_3_latitude = np.arange(transect_df.loc[3].loc[lambda x: x['transect_num'].isin([t3_start, t3_end]), 'latitude'].min(), transect_df.loc[3].loc[lambda x: x['transect_num'].isin([t3_start, t3_end]), 'latitude'].max(), latitude_resolution)
    region_1_extents = pd.concat([transect_df.loc[1].groupby(['transect_num']).apply(region_13_conditions, 'transect_lower_bound', 'east', include_groups=False), transect_df.loc[1].groupby(['transect_num']).apply(region_13_conditions, 'transect_upper_bound', 'west', include_groups=False)], axis=1)
    region_1_interp = pd.DataFrame({'longitude_west': interpolate.interp1d(region_1_extents['latitude_west'], region_1_extents['longitude_west'], kind='linear', bounds_error=False)(region_1_latitude), 'longitude_east': interpolate.interp1d(region_1_extents['latitude_east'], region_1_extents['longitude_east'], kind='linear', bounds_error=False)(region_1_latitude)})
    delta_longitude_region_1 = latitude_resolution * np.cos(np.radians(region_1_latitude))
    region_1_index = []
    for i in range(len(region_1_latitude)):
        idx = np.where((mesh['longitude'] >= region_1_interp.loc[i, 'longitude_west'] - delta_longitude_region_1[i]) & (mesh['longitude'] <= region_1_interp.loc[i, 'longitude_east'] + delta_longitude_region_1[i]) & (mesh['latitude'] >= region_1_latitude[i] - latitude_resolution) & (mesh['latitude'] < region_1_latitude[i] + latitude_resolution))
        region_1_index.append(idx[0])
    region_1_index_unique = np.unique(np.concatenate(region_1_index))
    region_2_extents = pd.concat([transect_df.loc[2].groupby(['transect_num']).apply(region_2_conditions, 'transect_upper_bound', 'south', include_groups=False), transect_df.loc[2].groupby(['transect_num']).apply(region_2_conditions, 'transect_lower_bound', 'north', include_groups=False)], axis=1)
    region_2_interp = pd.DataFrame({'latitude_south': interpolate.interp1d(region_2_extents['longitude_south'], region_2_extents['latitude_south'], kind='linear', bounds_error=False)(region_2_longitude), 'latitude_north': interpolate.interp1d(region_2_extents['longitude_north'], region_2_extents['latitude_north'], kind='linear', bounds_error=False)(region_2_longitude)})
    delta_longitude_region_2 = latitude_resolution * np.cos(np.radians(transect_df.loc[2, 'latitude'].mean()))
    region_2_index = []
    for i in range(len(region_2_longitude)):
        if ~np.isnan(region_2_interp.loc[i, 'latitude_south']) or ~np.isnan(region_2_interp.loc[i, 'latitude_north']):
            lon_n_min = np.argmin(np.abs(region_2_longitude[i] - region_2_extents['longitude_north']))
            lon_s_min = np.argmin(np.abs(region_2_longitude[i] - region_2_extents['longitude_south']))
            slope = (region_2_extents['latitude_north'].iloc[lon_n_min] - region_2_extents['latitude_south'].iloc[lon_s_min]) / (region_2_extents['longitude_north'].iloc[lon_n_min] - region_2_extents['longitude_south'].iloc[lon_s_min])
            latitude_slope_i = slope * (region_2_longitude[i] - region_2_extents['longitude_south'].iloc[lon_s_min]) + region_2_extents['latitude_south'].iloc[lon_s_min]
            if np.isnan(region_2_interp.loc[i, 'latitude_south']):
                idx = np.where((mesh['longitude'] >= region_2_longitude[i] - delta_longitude_region_2) & (mesh['longitude'] <= region_2_longitude[i] + delta_longitude_region_2) & (mesh['latitude'] >= latitude_slope_i - latitude_resolution) & (mesh['latitude'] < region_2_interp.loc[i, 'latitude_north'] + latitude_resolution))
            elif np.isnan(region_2_interp.loc[i, 'latitude_north']):
                idx = np.where((mesh['longitude'] >= region_2_longitude[i] - delta_longitude_region_2) & (mesh['longitude'] <= region_2_longitude[i] + delta_longitude_region_2) & (mesh['latitude'] >= region_2_interp.loc[i, 'latitude_south'] - latitude_resolution) & (mesh['latitude'] < latitude_slope_i + latitude_resolution))
            else:
                idx = np.where((mesh['longitude'] >= region_2_longitude[i] - delta_longitude_region_2) & (mesh['longitude'] <= region_2_longitude[i] + delta_longitude_region_2) & (mesh['latitude'] >= region_2_interp.loc[i, 'latitude_south'] - latitude_resolution) & (mesh['latitude'] < region_2_interp.loc[i, 'latitude_north'] + latitude_resolution))
            region_2_index.append(idx[0])
    region_2_index_unique = np.unique(np.concatenate(region_2_index))
    region_3_extents = pd.concat([transect_df.loc[3].groupby(['transect_num']).apply(region_13_conditions, 'transect_upper_bound', 'west', include_groups=False), transect_df.loc[3].groupby(['transect_num']).apply(region_13_conditions, 'transect_lower_bound', 'east', include_groups=False)], axis=1)
    region_3_interp = pd.DataFrame({'longitude_west': interpolate.interp1d(region_3_extents['latitude_west'], region_3_extents['longitude_west'], kind='linear', bounds_error=False)(region_3_latitude), 'longitude_east': interpolate.interp1d(region_3_extents['latitude_east'], region_3_extents['longitude_east'], kind='linear', bounds_error=False)(region_3_latitude)})
    delta_longitude_region_3 = latitude_resolution * np.cos(np.radians(region_3_latitude))
    region_3_index = []
    lat_w_max = np.argmax(region_3_extents['latitude_west'])
    lat_e_max = np.argmax(region_3_extents['latitude_east'])
    slope = (region_3_extents.iloc[lat_w_max]['longitude_west'] - region_3_extents.iloc[lat_e_max]['longitude_east']) / (region_3_extents.iloc[lat_w_max]['latitude_west'] - region_3_extents.iloc[lat_e_max]['latitude_east'])
    for i in range(len(region_3_latitude)):
        if ~np.isnan(region_3_interp.loc[i, 'longitude_west']) or ~np.isnan(region_3_interp.loc[i, 'longitude_east']):
            longitude_slope_i = slope * (region_3_latitude[i] - region_3_extents.iloc[lat_e_max]['latitude_east']) + region_3_extents.iloc[lat_w_max]['longitude_east']
            if np.isnan(region_3_interp.loc[i, 'longitude_west']):
                idx = np.where((mesh['longitude'] >= longitude_slope_i - delta_longitude_region_3[i]) & (mesh['longitude'] <= region_3_interp.loc[i, 'longitude_east'] + delta_longitude_region_3[i]) & (mesh['latitude'] >= region_3_latitude[i] - latitude_resolution) & (mesh['latitude'] < region_3_latitude[i] + latitude_resolution))
            elif np.isnan(region_3_interp.loc[i, 'longitude_east']):
                idx = np.where((mesh['longitude'] >= region_3_interp.loc[i, 'longitude_west'] - delta_longitude_region_3[i]) & (mesh['longitude'] <= longitude_slope_i + delta_longitude_region_3[i]) & (mesh['latitude'] >= region_3_latitude[i] - latitude_resolution) & (mesh['latitude'] < region_3_latitude[i] + latitude_resolution))
            else:
                idx = np.where((mesh['longitude'] >= region_3_interp.loc[i, 'longitude_west'] - delta_longitude_region_3[i]) & (mesh['longitude'] <= region_3_interp.loc[i, 'longitude_east'] + delta_longitude_region_3[i]) & (mesh['latitude'] >= region_3_latitude[i] - latitude_resolution) & (mesh['latitude'] < region_3_latitude[i] + latitude_resolution))
            region_3_index.append(idx[0])
    region_3_index_unique = np.unique(np.concatenate(region_3_index))
    mesh_indices = np.concatenate([region_1_index_unique, region_2_index_unique, region_3_index_unique])
    return (mesh.loc[mesh_indices], transect_df.reset_index())

Function: western_boundary_search_strategy

def western_boundary_search_strategy(kriging_mesh: pd.DataFrame, western_extent: pd.DataFrame, coordinate_names: tuple[str, str], sparse_radii: np.ndarray[int], valid_distances: np.ndarray[int], local_points: np.ndarray[float], distance_matrix_masked: np.ndarray[float], nearby_indices: np.ndarray[int], k_min: int, k_max: int, search_radius: float, wr_indices: np.ndarray[int], oos_indices: np.ndarray[np.number], oos_weights: np.ndarray[float], **kwargs) -> tuple[np.ndarray[np.number], np.ndarray[np.number], np.ndarray[np.number]]:
    x_name, y_name = coordinate_names
    mesh_y = kriging_mesh[y_name].to_numpy()[sparse_radii].reshape(-1, 1)
    mesh_x = kriging_mesh[x_name].to_numpy()[sparse_radii]
    mesh_western_distance = np.abs(mesh_y - western_extent['y'].to_numpy()).argmin(axis=1)
    western_limit = western_extent.iloc[np.ravel(mesh_western_distance)][x_name]
    western_threshold = western_limit - search_radius
    western_limit_mask = mesh_x < western_threshold
    if np.any(~western_limit_mask):
        soft_extrapolation_index = sparse_radii[~western_limit_mask]
        partial_indices = soft_extrapolation_index[valid_distances[soft_extrapolation_index] > 0]
        if len(partial_indices) > 0:
            wr_mask = local_points[partial_indices, :k_max] < search_radius
            wr_tmp = wr_indices[partial_indices].copy()
            oos_tmp = wr_indices[partial_indices].copy()
            wr_tmp[~wr_mask] = np.nan
            oos_tmp[wr_mask] = np.nan
            oos_indices[partial_indices] = np.sort(oos_tmp[:, :k_min])
            wr_indices[partial_indices] = np.sort(wr_tmp[:, :k_max])
        full_extrap_indices = soft_extrapolation_index[valid_distances[soft_extrapolation_index] == 0]
        if len(full_extrap_indices) > 0:
            oos_indices[full_extrap_indices] = wr_indices[full_extrap_indices, :k_min]
            wr_indices[full_extrap_indices] = np.nan
    if np.any(western_limit_mask):
        extrapolation_index = sparse_radii[western_limit_mask]
        oos_mean = np.apply_along_axis(np.nanmean, 1, local_points[extrapolation_index, :k_min])
        oos_exp = np.exp(-oos_mean / search_radius)
        oos_weights[extrapolation_index] = oos_exp
        sparse_extrapolation_index = nearby_indices[western_limit_mask].astype(float)
        extrapolated_distance = np.take_along_axis(distance_matrix_masked[sparse_radii][western_limit_mask], sparse_extrapolation_index.astype(int), axis=1)
        extrapolated_nan_mask = ~np.isnan(extrapolated_distance)
        sparse_extrapolation_index_nan = sparse_extrapolation_index.copy()
        sparse_extrapolation_index_nan[extrapolated_nan_mask] = np.nan
        oos_indices[extrapolation_index] = np.sort(sparse_extrapolation_index_nan)
        interpolated_nan_mask = np.isnan(extrapolated_distance)
        sparse_interpolation_index_nan = sparse_extrapolation_index.copy()
        sparse_interpolation_index_nan[interpolated_nan_mask] = np.nan
        sparse_interpolation_pad = np.pad(sparse_interpolation_index_nan, [(0, 0), (0, k_max - k_min)], mode='constant', constant_values=np.nan)
        wr_indices[extrapolation_index] = np.sort(sparse_interpolation_pad)
    return (wr_indices, oos_indices, oos_weights)