xopr.opr_tools

  1import warnings
  2from typing import Iterable, Union
  3
  4import geopandas as gpd
  5import xarray as xr
  6
  7from xopr.util import get_ror_display_name, merge_dicts_no_conflicts
  8
  9
 10def merge_frames(frames: Iterable[xr.Dataset]) -> Union[list[xr.Dataset], xr.Dataset]:
 11    """
 12    Merge a set of radar frames into a list of merged xarray Datasets. Frames from the
 13    same segment (typically a flight) are concatenated along the 'slow_time' dimension.
 14
 15    Parameters
 16    ----------
 17    frames : Iterable[xr.Dataset]
 18        An iterable of xarray Datasets representing radar frames.
 19
 20    Returns
 21    -------
 22    list[xr.Dataset] or xr.Dataset
 23        List of merged xarray Datasets or a single merged Dataset if there is only one segment.
 24    """
 25    segments = {}
 26
 27    mergable_keys = ['source_url', 'collection', 'data_product', 'granule']
 28
 29    for frame in frames:
 30        # Get segment path from frame attributes
 31        granule = frame.attrs.get('granule')
 32        date, segment_id, frame_id = granule.split('_')
 33        segment_path = f"{date}_{segment_id}"
 34
 35        if not segment_path:
 36            warnings.warn("Frame missing 'granule' attribute or it was not in the expected format, skipping.", UserWarning)
 37            continue
 38
 39        if segment_path not in segments:
 40            segments[segment_path] = []
 41
 42        segments[segment_path].append(frame)
 43
 44    # Merge frames for each segment
 45    merged_segments = []
 46    for segment_path, segment_frames in segments.items():
 47        merged_segment = xr.concat(segment_frames, dim='slow_time', combine_attrs=merge_dicts_no_conflicts).sortby('slow_time')
 48
 49        for k in mergable_keys:
 50            if k not in merged_segment.attrs:
 51                merged_segment.attrs[k] = set([v for v in {f.attrs.get(k) for f in segment_frames} if v is not None])
 52
 53        merged_segments.append(merged_segment)
 54
 55    if len(merged_segments) == 1:
 56        return merged_segments[0]
 57    else:
 58        return merged_segments
 59
 60def generate_citation(ds : xr.Dataset) -> str:
 61        """
 62        Generate a citation string for the dataset based on its attributes.
 63
 64        Parameters
 65        ----------
 66        ds : xr.Dataset
 67            The xarray Dataset containing metadata.
 68
 69        Returns
 70        -------
 71        str
 72            A formatted citation string.
 73        """
 74
 75        citation_string = ""
 76        any_citation_info = False
 77
 78        citation_string += "== Data Citation ==\n"
 79
 80        if 'ror' in ds.attrs and ds.attrs['ror']:
 81            any_citation_info = True
 82            if isinstance(ds.attrs['ror'], (set, list)):
 83                institution_name = ', '.join([get_ror_display_name(ror) for ror in ds.attrs['ror']])
 84            else:
 85                institution_name = get_ror_display_name(ds.attrs['ror'])
 86
 87            citation_string += f"This data was collected by {institution_name}.\n"
 88
 89        if 'doi' in ds.attrs and ds.attrs['doi']:
 90            any_citation_info = True
 91            citation_string += f"Please cite the dataset DOI: https://doi.org/{ds.attrs['doi']}\n"
 92
 93        if 'funder_text' in ds.attrs and ds.attrs['funder_text']:
 94            any_citation_info = True
 95            citation_string += f"Please include the following funder acknowledgment:\n{ds.attrs['funder_text']}\n"
 96
 97        if not any_citation_info:
 98            citation_string += "No specific citation information was retrieved for this dataset. By default, please cite:\n"
 99            citation_string += "CReSIS. 2024. REPLACE_WITH_RADAR_NAME Data, Lawrence, Kansas, USA. Digital Media. http://data.cresis.ku.edu/."
100
101        # Add general OPR Toolbox citation
102        citation_string += "\n== Processing Citation ==\n"
103        citation_string += "Data was processed using the Open Polar Radar (OPR) Toolbox: https://doi.org/10.5281/zenodo.5683959\n"
104        citation_string += "Please cite the OPR Toolbox as:\n"
105        citation_string += "Open Polar Radar. (2024). opr (Version 3.0.1) [Computer software]. https://gitlab.com/openpolarradar/opr/. https://doi.org/10.5281/zenodo.5683959\n"
106        citation_string += "And include the following acknowledgment:\n"
107        citation_string += "We acknowledge the use of software from Open Polar Radar generated with support from the University of Kansas, NASA grants 80NSSC20K1242 and 80NSSC21K0753, and NSF grants OPP-2027615, OPP-2019719, OPP-1739003, IIS-1838230, RISE-2126503, RISE-2127606, and RISE-2126468.\n"
108
109        return citation_string if citation_string else "No citation information available for this dataset."
110
111
112def find_intersections(gdf : gpd.GeoDataFrame, remove_self_intersections: bool = True,
113                        remove_adjacent_intersections: bool = True,
114                        calculate_crossing_angles: bool = False) -> gpd.GeoDataFrame:
115    """
116    Find intersections between geometries in a GeoDataFrame.
117
118    Parameters
119    ----------
120    gdf : gpd.GeoDataFrame
121        A GeoDataFrame containing geometries to check for intersections.
122    remove_self_intersections : bool, optional
123        If True, remove self-intersections (a geometry intersecting with itself), by default True.
124    remove_adjacent_intersections : bool, optional
125        If True, remove intersections between adjacent geometries (e.g., consecutive frames in a flight line), by default True.
126
127    Returns
128    -------
129    gpd.GeoDataFrame
130        A GeoDataFrame containing pairs of intersecting geometries.
131    """
132
133    tmp_df = gdf.reset_index()
134    tmp_df['geom'] = tmp_df.geometry
135    intersections = gpd.sjoin(tmp_df, tmp_df, how='inner', predicate='intersects', lsuffix='1', rsuffix='2')
136
137    if remove_self_intersections:
138        intersections = intersections[intersections['id_1'] != intersections['id_2']]
139
140    intersections['intersection_geometry'] = intersections.apply(lambda row: row['geom_1'].intersection(row['geom_2']), axis=1)
141    intersections.set_geometry('intersection_geometry', inplace=True, crs=gdf.crs)
142    intersections = intersections.drop_duplicates(subset=['intersection_geometry'])
143    intersections = intersections.explode(index_parts=True).reset_index(drop=True)
144
145    intersections_tmp = intersections[['id_1', 'id_2', 'intersection_geometry', 'collection_1', 'collection_2', 'geom_1', 'geom_2']].copy()
146
147    for k in ['opr:date', 'opr:segment', 'opr:frame']:
148        intersections_tmp[f'{k}_1'] = intersections['properties_1'].apply(lambda x: x[k])
149        intersections_tmp[f'{k}_2'] = intersections['properties_2'].apply(lambda x: x[k])
150
151    intersections = intersections_tmp
152
153    if remove_adjacent_intersections:
154        intersections = intersections[
155            (intersections['opr:date_1'] != intersections['opr:date_2']) |
156            (intersections['opr:segment_1'] != intersections['opr:segment_2']) |
157            ((intersections['opr:frame_1'] != (intersections['opr:frame_2'] + 1)) &
158            (intersections['opr:frame_1'] != (intersections['opr:frame_2'] - 1)))
159        ]
160
161    if calculate_crossing_angles:
162        intersections['crossing_angle'] = intersections.apply(
163            lambda row: _calculate_crossing_angle(row['geom_1'], row['geom_2'], row['intersection_geometry']),
164            axis=1
165        )
166
167    return intersections
168
169def _calculate_crossing_angle(line1, line2, intersection_point):
170    """
171    Calculate the crossing angle between two lines at their intersection point.
172
173    Parameters
174    ----------
175    line1 : shapely.geometry.LineString
176        The first line.
177    line2 : shapely.geometry.LineString
178        The second line.
179    intersection_point : shapely.geometry.Point
180        The point of intersection between the two lines.
181
182    Returns
183    -------
184    float
185        The crossing angle in degrees.
186    """
187    import numpy as np
188
189    def get_line_angle(line, point):
190        # Get the nearest point on the line to the intersection point
191        nearest_point = line.interpolate(line.project(point))
192        # Get a small segment of the line around the nearest point to calculate the angle
193        buffer_distance = 1e-6  # Small distance to create a segment
194        start_point = line.interpolate(max(0, line.project(nearest_point) - buffer_distance))
195        end_point = line.interpolate(min(line.length, line.project(nearest_point) + buffer_distance))
196
197        # Calculate the angle of the segment
198        delta_x = end_point.x - start_point.x
199        delta_y = end_point.y - start_point.y
200        angle = np.arctan2(delta_y, delta_x)
201        return angle
202
203    angle1 = get_line_angle(line1, intersection_point)
204    angle2 = get_line_angle(line2, intersection_point)
205
206    # Calculate the absolute difference in angles and convert to degrees
207    angle_diff = np.abs(angle1 - angle2)
208    crossing_angle = np.degrees(angle_diff)
209
210    # Ensure the angle is between 0 and 180 degrees
211    if crossing_angle > 180:
212        crossing_angle = 360 - crossing_angle
213
214    if crossing_angle > 90:
215        crossing_angle = 180 - crossing_angle
216
217    return crossing_angle
def merge_frames( frames: Iterable[xarray.core.dataset.Dataset]) -> Union[list[xarray.core.dataset.Dataset], xarray.core.dataset.Dataset]:
11def merge_frames(frames: Iterable[xr.Dataset]) -> Union[list[xr.Dataset], xr.Dataset]:
12    """
13    Merge a set of radar frames into a list of merged xarray Datasets. Frames from the
14    same segment (typically a flight) are concatenated along the 'slow_time' dimension.
15
16    Parameters
17    ----------
18    frames : Iterable[xr.Dataset]
19        An iterable of xarray Datasets representing radar frames.
20
21    Returns
22    -------
23    list[xr.Dataset] or xr.Dataset
24        List of merged xarray Datasets or a single merged Dataset if there is only one segment.
25    """
26    segments = {}
27
28    mergable_keys = ['source_url', 'collection', 'data_product', 'granule']
29
30    for frame in frames:
31        # Get segment path from frame attributes
32        granule = frame.attrs.get('granule')
33        date, segment_id, frame_id = granule.split('_')
34        segment_path = f"{date}_{segment_id}"
35
36        if not segment_path:
37            warnings.warn("Frame missing 'granule' attribute or it was not in the expected format, skipping.", UserWarning)
38            continue
39
40        if segment_path not in segments:
41            segments[segment_path] = []
42
43        segments[segment_path].append(frame)
44
45    # Merge frames for each segment
46    merged_segments = []
47    for segment_path, segment_frames in segments.items():
48        merged_segment = xr.concat(segment_frames, dim='slow_time', combine_attrs=merge_dicts_no_conflicts).sortby('slow_time')
49
50        for k in mergable_keys:
51            if k not in merged_segment.attrs:
52                merged_segment.attrs[k] = set([v for v in {f.attrs.get(k) for f in segment_frames} if v is not None])
53
54        merged_segments.append(merged_segment)
55
56    if len(merged_segments) == 1:
57        return merged_segments[0]
58    else:
59        return merged_segments

Merge a set of radar frames into a list of merged xarray Datasets. Frames from the same segment (typically a flight) are concatenated along the 'slow_time' dimension.

Parameters
  • frames (Iterable[xr.Dataset]): An iterable of xarray Datasets representing radar frames.
Returns
  • list[xr.Dataset] or xr.Dataset: List of merged xarray Datasets or a single merged Dataset if there is only one segment.
def generate_citation(ds: xarray.core.dataset.Dataset) -> str:
 61def generate_citation(ds : xr.Dataset) -> str:
 62        """
 63        Generate a citation string for the dataset based on its attributes.
 64
 65        Parameters
 66        ----------
 67        ds : xr.Dataset
 68            The xarray Dataset containing metadata.
 69
 70        Returns
 71        -------
 72        str
 73            A formatted citation string.
 74        """
 75
 76        citation_string = ""
 77        any_citation_info = False
 78
 79        citation_string += "== Data Citation ==\n"
 80
 81        if 'ror' in ds.attrs and ds.attrs['ror']:
 82            any_citation_info = True
 83            if isinstance(ds.attrs['ror'], (set, list)):
 84                institution_name = ', '.join([get_ror_display_name(ror) for ror in ds.attrs['ror']])
 85            else:
 86                institution_name = get_ror_display_name(ds.attrs['ror'])
 87
 88            citation_string += f"This data was collected by {institution_name}.\n"
 89
 90        if 'doi' in ds.attrs and ds.attrs['doi']:
 91            any_citation_info = True
 92            citation_string += f"Please cite the dataset DOI: https://doi.org/{ds.attrs['doi']}\n"
 93
 94        if 'funder_text' in ds.attrs and ds.attrs['funder_text']:
 95            any_citation_info = True
 96            citation_string += f"Please include the following funder acknowledgment:\n{ds.attrs['funder_text']}\n"
 97
 98        if not any_citation_info:
 99            citation_string += "No specific citation information was retrieved for this dataset. By default, please cite:\n"
100            citation_string += "CReSIS. 2024. REPLACE_WITH_RADAR_NAME Data, Lawrence, Kansas, USA. Digital Media. http://data.cresis.ku.edu/."
101
102        # Add general OPR Toolbox citation
103        citation_string += "\n== Processing Citation ==\n"
104        citation_string += "Data was processed using the Open Polar Radar (OPR) Toolbox: https://doi.org/10.5281/zenodo.5683959\n"
105        citation_string += "Please cite the OPR Toolbox as:\n"
106        citation_string += "Open Polar Radar. (2024). opr (Version 3.0.1) [Computer software]. https://gitlab.com/openpolarradar/opr/. https://doi.org/10.5281/zenodo.5683959\n"
107        citation_string += "And include the following acknowledgment:\n"
108        citation_string += "We acknowledge the use of software from Open Polar Radar generated with support from the University of Kansas, NASA grants 80NSSC20K1242 and 80NSSC21K0753, and NSF grants OPP-2027615, OPP-2019719, OPP-1739003, IIS-1838230, RISE-2126503, RISE-2127606, and RISE-2126468.\n"
109
110        return citation_string if citation_string else "No citation information available for this dataset."

Generate a citation string for the dataset based on its attributes.

Parameters
  • ds (xr.Dataset): The xarray Dataset containing metadata.
Returns
  • str: A formatted citation string.
def find_intersections( gdf: geopandas.geodataframe.GeoDataFrame, remove_self_intersections: bool = True, remove_adjacent_intersections: bool = True, calculate_crossing_angles: bool = False) -> geopandas.geodataframe.GeoDataFrame:
113def find_intersections(gdf : gpd.GeoDataFrame, remove_self_intersections: bool = True,
114                        remove_adjacent_intersections: bool = True,
115                        calculate_crossing_angles: bool = False) -> gpd.GeoDataFrame:
116    """
117    Find intersections between geometries in a GeoDataFrame.
118
119    Parameters
120    ----------
121    gdf : gpd.GeoDataFrame
122        A GeoDataFrame containing geometries to check for intersections.
123    remove_self_intersections : bool, optional
124        If True, remove self-intersections (a geometry intersecting with itself), by default True.
125    remove_adjacent_intersections : bool, optional
126        If True, remove intersections between adjacent geometries (e.g., consecutive frames in a flight line), by default True.
127
128    Returns
129    -------
130    gpd.GeoDataFrame
131        A GeoDataFrame containing pairs of intersecting geometries.
132    """
133
134    tmp_df = gdf.reset_index()
135    tmp_df['geom'] = tmp_df.geometry
136    intersections = gpd.sjoin(tmp_df, tmp_df, how='inner', predicate='intersects', lsuffix='1', rsuffix='2')
137
138    if remove_self_intersections:
139        intersections = intersections[intersections['id_1'] != intersections['id_2']]
140
141    intersections['intersection_geometry'] = intersections.apply(lambda row: row['geom_1'].intersection(row['geom_2']), axis=1)
142    intersections.set_geometry('intersection_geometry', inplace=True, crs=gdf.crs)
143    intersections = intersections.drop_duplicates(subset=['intersection_geometry'])
144    intersections = intersections.explode(index_parts=True).reset_index(drop=True)
145
146    intersections_tmp = intersections[['id_1', 'id_2', 'intersection_geometry', 'collection_1', 'collection_2', 'geom_1', 'geom_2']].copy()
147
148    for k in ['opr:date', 'opr:segment', 'opr:frame']:
149        intersections_tmp[f'{k}_1'] = intersections['properties_1'].apply(lambda x: x[k])
150        intersections_tmp[f'{k}_2'] = intersections['properties_2'].apply(lambda x: x[k])
151
152    intersections = intersections_tmp
153
154    if remove_adjacent_intersections:
155        intersections = intersections[
156            (intersections['opr:date_1'] != intersections['opr:date_2']) |
157            (intersections['opr:segment_1'] != intersections['opr:segment_2']) |
158            ((intersections['opr:frame_1'] != (intersections['opr:frame_2'] + 1)) &
159            (intersections['opr:frame_1'] != (intersections['opr:frame_2'] - 1)))
160        ]
161
162    if calculate_crossing_angles:
163        intersections['crossing_angle'] = intersections.apply(
164            lambda row: _calculate_crossing_angle(row['geom_1'], row['geom_2'], row['intersection_geometry']),
165            axis=1
166        )
167
168    return intersections

Find intersections between geometries in a GeoDataFrame.

Parameters
  • gdf (gpd.GeoDataFrame): A GeoDataFrame containing geometries to check for intersections.
  • remove_self_intersections (bool, optional): If True, remove self-intersections (a geometry intersecting with itself), by default True.
  • remove_adjacent_intersections (bool, optional): If True, remove intersections between adjacent geometries (e.g., consecutive frames in a flight line), by default True.
Returns
  • gpd.GeoDataFrame: A GeoDataFrame containing pairs of intersecting geometries.