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