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 +34 -0
- xopr/_version.py +34 -0
- xopr/cf_units.py +148 -0
- xopr/example.py +36 -0
- xopr/geometry.py +104 -0
- xopr/opr_access.py +1277 -0
- xopr/ops_api.py +201 -0
- xopr/stac/__init__.py +24 -0
- xopr/stac/catalog.py +263 -0
- xopr/stac/metadata.py +228 -0
- xopr-0.1.2.dist-info/METADATA +60 -0
- xopr-0.1.2.dist-info/RECORD +14 -0
- xopr-0.1.2.dist-info/WHEEL +4 -0
- xopr-0.1.2.dist-info/licenses/LICENSE +21 -0
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
|