xopr 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xopr/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2025 Thomas Teisberg, Shane Grigsby
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice (including the next
13
+ # paragraph) shall be included in all copies or substantial portions of the
14
+ # Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+
24
+ """
25
+ Add a docstring here for the init module.
26
+
27
+ This might include a very brief description of the package,
28
+ its purpose, and any important notes.
29
+ """
30
+
31
+ try:
32
+ from ._version import __version__
33
+ except ImportError:
34
+ __version__ = "unknown"
xopr/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.2'
32
+ __version_tuple__ = version_tuple = (0, 1, 2)
33
+
34
+ __commit_id__ = commit_id = None
xopr/cf_units.py ADDED
@@ -0,0 +1,148 @@
1
+ # TODO: This is entirely AI generated and unchecked.
2
+ # I just wanted something to quickly demonstrate what units might looks like.
3
+ # Should be carefully reviewed.
4
+
5
+ import xarray as xr
6
+ import numpy as np
7
+
8
+ def apply_cf_compliant_attrs(ds):
9
+ """
10
+ Apply CF-compliant units and comments to radar echogram dataset variables.
11
+
12
+ Parameters:
13
+ -----------
14
+ ds : xarray.Dataset
15
+ The input radar echogram dataset
16
+
17
+ Returns:
18
+ --------
19
+ xarray.Dataset
20
+ Dataset with CF-compliant attributes applied
21
+ """
22
+
23
+ # Create a copy to avoid modifying the original dataset
24
+ ds_cf = ds.copy()
25
+
26
+ # Define CF-compliant attributes for coordinates
27
+ coordinate_attrs = {
28
+ 'slow_time': {
29
+ #'units': 'seconds since 1970-01-01T00:00:00Z',
30
+ 'standard_name': 'time',
31
+ 'long_name': 'slow time',
32
+ 'comment': 'Time coordinate for radar pulse transmission along flight track'
33
+ },
34
+ 'twtt': {
35
+ 'units': 's',
36
+ 'standard_name': 'time',
37
+ 'long_name': 'two-way travel time',
38
+ 'comment': 'Two-way travel time from radar to target and back'
39
+ }
40
+ }
41
+
42
+ # Define CF-compliant attributes for data variables
43
+ data_var_attrs = {
44
+ 'Bottom': {
45
+ 'units': 's',
46
+ 'long_name': 'bottom surface two-way travel time',
47
+ 'comment': 'Two-way travel time to detected bottom surface. NaN where bottom not detected.',
48
+ '_FillValue': np.nan
49
+ },
50
+ 'Data': {
51
+ 'units': '1', # TODO: Appropriate units for radar data -- can we calibrate to watts?
52
+ 'long_name': 'radar echo power',
53
+ 'comment': 'Radar echo power in linear scale',
54
+ 'coordinates': 'slow_time twtt'
55
+ },
56
+ 'Elevation': {
57
+ 'units': 'm',
58
+ 'standard_name': 'height_above_reference_ellipsoid',
59
+ 'long_name': 'platform elevation above WGS84 ellipsoid',
60
+ 'comment': 'GPS-derived elevation of radar platform above WGS84 reference ellipsoid'
61
+ },
62
+ 'Heading': {
63
+ 'units': 'radians',
64
+ 'standard_name': 'platform_orientation',
65
+ 'long_name': 'platform heading angle',
66
+ 'comment': 'Platform heading angle in radians from north, clockwise positive',
67
+ 'valid_min': -np.pi,
68
+ 'valid_max': np.pi,
69
+ 'valid_range': [-np.pi, np.pi],
70
+ },
71
+ 'Latitude': {
72
+ 'units': 'degrees_north',
73
+ 'standard_name': 'latitude',
74
+ 'long_name': 'platform latitude',
75
+ 'comment': 'GPS-derived latitude of radar platform in WGS84 coordinate system',
76
+ 'valid_min': -90.0,
77
+ 'valid_max': 90.0,
78
+ 'valid_range': [-90.0, 90.0]
79
+ },
80
+ 'Longitude': {
81
+ 'units': 'degrees_east',
82
+ 'standard_name': 'longitude',
83
+ 'long_name': 'platform longitude',
84
+ 'comment': 'GPS-derived longitude of radar platform in WGS84 coordinate system',
85
+ 'valid_min': -180.0,
86
+ 'valid_max': 180.0,
87
+ 'valid_range': [-180.0, 180.0]
88
+ },
89
+ 'Pitch': {
90
+ 'units': 'radians',
91
+ 'standard_name': 'platform_pitch_angle',
92
+ 'long_name': 'platform pitch angle',
93
+ 'comment': 'Platform pitch angle in radians, positive nose up',
94
+ 'valid_min': -np.pi/2,
95
+ 'valid_max': np.pi/2,
96
+ 'valid_range': [-np.pi/2, np.pi/2]
97
+ },
98
+ 'Roll': {
99
+ 'units': 'radians',
100
+ 'standard_name': 'platform_roll_angle',
101
+ 'long_name': 'platform roll angle',
102
+ 'comment': 'Platform roll angle in radians, positive right wing down',
103
+ 'valid_min': -np.pi,
104
+ 'valid_max': np.pi,
105
+ 'valid_range': [-np.pi, np.pi]
106
+ },
107
+ 'Surface': {
108
+ 'units': 's',
109
+ 'long_name': 'surface two-way travel time',
110
+ 'comment': 'Two-way travel time to detected surface. Zero indicates surface at platform level.',
111
+ 'valid_min': 0.0
112
+ }
113
+ }
114
+
115
+ # Apply coordinate attributes
116
+ for coord_name, attrs in coordinate_attrs.items():
117
+ if coord_name in ds_cf.coords:
118
+ ds_cf[coord_name].attrs.update(attrs)
119
+
120
+ # Apply data variable attributes
121
+ for var_name, attrs in data_var_attrs.items():
122
+ if var_name in ds_cf.data_vars:
123
+ ds_cf[var_name].attrs.update(attrs)
124
+
125
+ # Add global attributes for CF compliance
126
+ global_attrs = {
127
+ 'Conventions': 'CF-1.8',
128
+ 'title': 'Radar Echogram Data',
129
+ 'institution': 'Open Polar Radar (OPR)',
130
+ 'source': 'Airborne/ground-based radar sounder',
131
+ 'history': f'Converted to CF-compliant format on {np.datetime64("now").astype(str)}',
132
+ 'references': 'https://gitlab.com/openpolarradar/opr',
133
+ 'comment': 'Polar radar echogram data with CF-compliant metadata',
134
+ 'geospatial_lat_min': float(ds_cf.Latitude.min()) if 'Latitude' in ds_cf else None,
135
+ 'geospatial_lat_max': float(ds_cf.Latitude.max()) if 'Latitude' in ds_cf else None,
136
+ 'geospatial_lon_min': float(ds_cf.Longitude.min()) if 'Longitude' in ds_cf else None,
137
+ 'geospatial_lon_max': float(ds_cf.Longitude.max()) if 'Longitude' in ds_cf else None,
138
+ 'time_coverage_start': str(ds_cf.slow_time.min().values) if 'slow_time' in ds_cf else None,
139
+ 'time_coverage_end': str(ds_cf.slow_time.max().values) if 'slow_time' in ds_cf else None
140
+ }
141
+
142
+ # Remove None values from global attributes
143
+ global_attrs = {k: v for k, v in global_attrs.items() if v is not None}
144
+
145
+ # Update global attributes
146
+ ds_cf.attrs.update(global_attrs)
147
+
148
+ return ds_cf
xopr/example.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ A module that adds numbers together.
3
+
4
+ You may want to delete this module or modify it for your package.
5
+ It's generally good practice to have a docstring
6
+ that explains the purpose of the module, at the top.
7
+ """
8
+
9
+ def add_numbers(a: float, b: float) -> float:
10
+ """
11
+ Add two numbers together and return the result.
12
+
13
+ This is an example function with a numpy style docstring.
14
+ We recommend using this style for consistency and readability.
15
+
16
+ Parameters
17
+ ----------
18
+ a : float
19
+ The first number to add.
20
+ b : float
21
+ The second number to add.
22
+
23
+ Returns
24
+ -------
25
+ float
26
+ The sum of the two numbers.
27
+
28
+ Examples
29
+ --------
30
+ >>> add_numbers(3, 5)
31
+ 8
32
+ >>> add_numbers(-2, 7)
33
+ 5
34
+
35
+ """
36
+ return a + b
xopr/geometry.py ADDED
@@ -0,0 +1,104 @@
1
+ import geopandas as gpd
2
+ import json
3
+ from cartopy import crs
4
+ from pyproj import Transformer
5
+ import shapely.ops
6
+
7
+
8
+ def get_antarctic_regions(
9
+ name=None,
10
+ regions=None,
11
+ subregions=None,
12
+ type=None,
13
+ merge_regions=True,
14
+ measures_boundaries_url = "https://storage.googleapis.com/opr_stac/reference_geometry/measures_boundaries_4326.geojson"
15
+ ):
16
+ """
17
+ Load and filter Antarctic regional boundaries from the MEASURES dataset.
18
+
19
+ Parameters
20
+ ----------
21
+ name : str or list, optional
22
+ NAME field value(s) to filter by
23
+ regions : str or list, optional
24
+ REGIONS field value(s) to filter by
25
+ subregions : str or list, optional
26
+ SUBREGION field value(s) to filter by
27
+ type : str or list, optional
28
+ TYPE field value(s) to filter by
29
+ merge_regions : bool, default True
30
+ If True, return a single merged geometry; if False, return list of geometries
31
+ measures_boundaries_url : str, default "https://storage.googleapis.com/opr_stac/reference_geometry/measures_boundaries_4326.geojson"
32
+ URL to the GeoJSON file containing Antarctic region boundaries
33
+
34
+ Returns
35
+ -------
36
+ list or dict
37
+ If merge_regions=False: List of GeoJSON geometry dicts
38
+ If merge_regions=True: Single GeoJSON geometry dict of merged regions
39
+
40
+ Examples
41
+ --------
42
+ # Get George VI ice shelf
43
+ >>> george_vi = get_antarctic_regions(name="George_VI", type="FL")
44
+
45
+ # Get all ice shelves, merged into one geometry
46
+ >>> all_shelves = get_antarctic_regions(type="FL", merge_regions=True)
47
+
48
+ # Get multiple regions by name
49
+ >>> regions = get_antarctic_regions(name=["George_VI", "LarsenC"])
50
+ """
51
+
52
+
53
+ # Load the boundaries GeoJSON from the reference URL
54
+ filtered = gpd.read_file(measures_boundaries_url)
55
+
56
+ # Apply filters based on provided parameters
57
+ if name is not None:
58
+ if isinstance(name, str):
59
+ name = [name]
60
+ filtered = filtered[filtered['NAME'].isin(name)]
61
+
62
+ if regions is not None:
63
+ if isinstance(regions, str):
64
+ regions = [regions]
65
+ filtered = filtered[filtered['Regions'].isin(regions)]
66
+
67
+ if subregions is not None:
68
+ if isinstance(subregions, str):
69
+ subregions = [subregions]
70
+ filtered = filtered[filtered['Subregions'].isin(subregions)]
71
+
72
+ if type is not None:
73
+ if isinstance(type, str):
74
+ type = [type]
75
+ filtered = filtered[filtered['TYPE'].isin(type)]
76
+
77
+ # Filter out invalid geometries
78
+ filtered = filtered[filtered.is_valid]
79
+
80
+ if len(filtered) == 0:
81
+ return [] if not merge_regions else None
82
+
83
+ if merge_regions:
84
+ merged = filtered.iloc[0].geometry
85
+ for i in range(1, len(filtered)):
86
+ merged = merged.union(filtered.iloc[i].geometry)
87
+ return merged
88
+ else:
89
+ return filtered
90
+
91
+ def project_dataset(ds, target_crs):
92
+ projected_coords = target_crs.transform_points(
93
+ crs.PlateCarree(), ds['Longitude'].values, ds['Latitude'].values
94
+ ).T
95
+ ds = ds.assign_coords({
96
+ 'x': (('slow_time'), projected_coords[0]),
97
+ 'y': (('slow_time'), projected_coords[1])
98
+ })
99
+ return ds
100
+
101
+ def project_geojson(geometry, source_crs="EPSG:4326", target_crs="EPSG:3031"):
102
+ transformer = Transformer.from_crs(source_crs, target_crs, always_xy=True)
103
+ projected_geometry = shapely.ops.transform(transformer.transform, geometry)
104
+ return projected_geometry