Skip to content

Commit 3e6682e

Browse files
committed
Add bucket and orbit utilities
2 parents 1a255cd + 84bcbd1 commit 3e6682e

File tree

4 files changed

+77
-54
lines changed

4 files changed

+77
-54
lines changed

Diff for: gpm/bucket/analysis.py

-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ def get_swath_indices(df):
9898
df_along.columns = ["granule_id", "along_track_id"]
9999

100100
# We will assign new x indices so that each granule's along-track block is contiguous.
101-
# We also build a full list of x indices.
102101
x_index_list = []
103102
# Allocate an array to hold the new x value for each row in df.
104103
x_values = np.empty(len(df), dtype=int)
+21-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
---
22
colormap_type: ListedColormap
33
color_space: hex
4-
color_palette: ["#1f78b4", "#E0FFFF", "#006400", "#228B22", "#32CD32", "#98FB98", "#F4A460", "#7D7F80", "#9E9E9E", "#BDBDBD", "#E0E0E0", "#87CEFA", "#5F9EA0", "#ff4c00", "#F5DEB3", "#B0E0E6", "#708090", "#B0BEC5"]
4+
color_palette:
5+
[
6+
"#1f78b4",
7+
"#E0FFFF",
8+
"#006400",
9+
"#228B22",
10+
"#32CD32",
11+
"#98FB98",
12+
"#F4A460",
13+
"#7D7F80",
14+
"#9E9E9E",
15+
"#BDBDBD",
16+
"#E0E0E0",
17+
"#87CEFA",
18+
"#5F9EA0",
19+
"#ff4c00",
20+
"#F5DEB3",
21+
"#B0E0E6",
22+
"#708090",
23+
"#B0BEC5",
24+
]
525
auxiliary:
626
category: [surfaceTypeIndex]

Diff for: gpm/tests/test_utils/test_orbit.py

+21-17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"""This module tests the dataframe utilities functions."""
2828
import numpy as np
2929
import pytest
30+
3031
from gpm.utils.orbit import adjust_short_sequences
3132

3233

@@ -67,36 +68,39 @@ def test_replace_short_sequence_end(self):
6768
"""Replace a short ending sequence with the previous sequence value."""
6869
# The ending sequence [2] is short (< min_size=2) and should be replaced by 1.
6970
arr = [1, 1, 1, 2]
70-
result = adjust_short_sequences(arr, min_size=2)
71+
result = adjust_short_sequences(arr, min_size=2)
7172
expected = np.array([1, 1, 1, 1])
7273
np.testing.assert_array_equal(result, expected)
7374

7475
def test_min_size_one(self):
7576
"""Return unchanged array when min_size is 1 (all sequences allowed)."""
7677
arr = [1, 2, 3, 4]
77-
result = adjust_short_sequences(arr, min_size=1)
78+
result = adjust_short_sequences(arr, min_size=1)
7879
np.testing.assert_array_equal(result, arr)
7980

8081
def test_non_1d_input(self):
8182
"""Raise ValueError for non 1D array input."""
8283
arr = [[1, 1, 1], [2, 2, 2]]
8384
with pytest.raises(ValueError):
8485
adjust_short_sequences(arr, min_size=2)
85-
86-
@pytest.mark.parametrize("arr,expected", [
87-
(
88-
[1, -1, -1, -1, 1, 1, 1, 1, -1], # Short sequences at the edges
89-
[-1, -1, -1, -1, 1, 1, 1, 1, 1]
90-
),
91-
(
92-
[1, -1, -1, -1, 1, 1, 1, 1, -1, -1], # Short sequence at start
93-
[-1, -1, -1, -1, 1, 1, 1, 1, -1, -1]
94-
),
95-
(
96-
[1, 1, -1, 1, 1, 1, 1, 1, -1], #Short sequence in the middle and at the end
97-
[1, 1, 1, 1, 1, 1, 1, 1, 1]
98-
),
99-
])
86+
87+
@pytest.mark.parametrize(
88+
("arr", "expected"),
89+
[
90+
(
91+
[1, -1, -1, -1, 1, 1, 1, 1, -1], # Short sequences at the edges
92+
[-1, -1, -1, -1, 1, 1, 1, 1, 1],
93+
),
94+
(
95+
[1, -1, -1, -1, 1, 1, 1, 1, -1, -1], # Short sequence at start
96+
[-1, -1, -1, -1, 1, 1, 1, 1, -1, -1],
97+
),
98+
(
99+
[1, 1, -1, 1, 1, 1, 1, 1, -1], # Short sequence in the middle and at the end
100+
[1, 1, 1, 1, 1, 1, 1, 1, 1],
101+
),
102+
],
103+
)
100104
def test_edge_cases(self, arr, expected):
101105
"""Test various edge cases with short sequences at the edges."""
102106
result = adjust_short_sequences(arr, min_size=2)

Diff for: gpm/utils/orbit.py

+35-35
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,27 @@
2626
# -----------------------------------------------------------------------------.
2727
"""This module contains utilities for orbit processing."""
2828

29-
import numpy as np
30-
import pandas as pd
29+
import numpy as np
30+
import pandas as pd
3131
import xarray as xr
3232

3333

3434
def adjust_short_sequences(arr, min_size):
3535
"""
3636
Replace value of short sequences of consecutive identical values.
37-
37+
3838
The function examines contiguous sequences of identical elements in the input
3939
array.
4040
If a sequence is shorter than `min_size`, its values are replaced with
41-
the value of the adjacent longer sequence, working outward from the first
41+
the value of the adjacent longer sequence, working outward from the first
4242
valid sequence.
43-
43+
4444
Parameters
4545
----------
4646
arr : array_like
4747
The input array of values.
4848
min_size : int
49-
The minimum number of consecutive identical elements to not be modified.
49+
The minimum number of consecutive identical elements to not be modified.
5050
Shorter sequences will be replaced with the previous sequence value.
5151
5252
Returns
@@ -59,33 +59,33 @@ def adjust_short_sequences(arr, min_size):
5959
arr = np.asarray(arr)
6060
if arr.ndim != 1:
6161
raise ValueError("Input must be a 1D array.")
62-
62+
6363
# If array is empty or has only one element, return as is
6464
if len(arr) <= 1:
6565
return arr
66-
66+
6767
# Create a copy to modify
6868
result = arr.copy()
69-
69+
7070
# Find boundaries of sequences (where values change)
7171
change_indices = np.where(np.diff(result) != 0)[0]
7272
sequence_starts = np.concatenate(([0], change_indices + 1))
7373
sequence_ends = np.concatenate((change_indices + 1, [len(result)]))
74-
74+
7575
# Find the first valid sequence (length >= min_size)
7676
first_valid_idx = None
7777
valid_value = None
78-
79-
for i, (start, end) in enumerate(zip(sequence_starts, sequence_ends)):
78+
79+
for i, (start, end) in enumerate(zip(sequence_starts, sequence_ends, strict=False)):
8080
if end - start >= min_size:
8181
first_valid_idx = i
8282
valid_value = result[start]
8383
break
84-
84+
8585
if first_valid_idx is None:
8686
# If no valid sequence found, return original array
8787
return result
88-
88+
8989
# Process sequences after the first valid sequence
9090
for i in range(first_valid_idx + 1, len(sequence_starts)):
9191
start = sequence_starts[i]
@@ -94,15 +94,15 @@ def adjust_short_sequences(arr, min_size):
9494
result[start:end] = valid_value
9595
else:
9696
valid_value = result[start]
97-
97+
9898
# Process sequences before the first valid sequence (in reverse)
9999
valid_value = result[sequence_starts[first_valid_idx]] # Reset to first valid sequence value
100100
for i in range(first_valid_idx - 1, -1, -1):
101101
start = sequence_starts[i]
102102
end = sequence_ends[i]
103103
if end - start < min_size:
104104
result[start:end] = valid_value
105-
105+
106106
# Return array with replaced values
107107
return result
108108

@@ -111,31 +111,31 @@ def get_orbit_direction(lats, n_tol=1):
111111
"""
112112
Infer the satellite orbit direction from latitude values.
113113
114-
This function determines the orbit direction by computing the sign
115-
of the differences between consecutive latitude values.
116-
117-
A positive sign (+1) indicates an ascending orbit (increasing latitude),
118-
while a negative sign (-1) indicates a descending orbit (decreasing latitude).
119-
114+
This function determines the orbit direction by computing the sign
115+
of the differences between consecutive latitude values.
116+
117+
A positive sign (+1) indicates an ascending orbit (increasing latitude),
118+
while a negative sign (-1) indicates a descending orbit (decreasing latitude).
119+
120120
Any zero differences are replaced by the nearest nonzero direction.
121-
Additionally, short sequences of direction changes - those lasting fewer
122-
than `n_tol` consecutive data points - are adjusted to reduce
121+
Additionally, short sequences of direction changes - those lasting fewer
122+
than `n_tol` consecutive data points - are adjusted to reduce
123123
the influence of geolocation errors.
124124
125125
Parameters
126126
----------
127127
lats : array_like
128128
1-dimensional array of latitude values corresponding to the satellite's orbit.
129129
n_tol : int, optional
130-
The minimum number of consecutive data points required to confirm a change
130+
The minimum number of consecutive data points required to confirm a change
131131
in direction.
132132
Sequences shorter than this threshold will be smoothed. Default is 1.
133133
134134
Returns
135135
-------
136136
numpy.ndarray
137-
A 1-dimensional array of the same length as `lats` containing
138-
the inferred orbit direction.
137+
A 1-dimensional array of the same length as `lats` containing
138+
the inferred orbit direction.
139139
A value of +1 denotes an ascending orbit and -1 denotes a descending orbit.
140140
141141
Examples
@@ -147,11 +147,11 @@ def get_orbit_direction(lats, n_tol=1):
147147
# Get direction (1 for ascending, -1 for descending)
148148
directions = np.sign(np.diff(lats))
149149
directions = np.append(directions[0], directions) # Include starting point
150-
# Set 0 to NaN and infill values
150+
# Set 0 to NaN and infill values
151151
directions = directions.astype(float)
152152
directions[directions == 0] = np.nan
153153
if np.all(np.isnan(directions)):
154-
raise ValueError("Invalid orbit.")
154+
raise ValueError("Invalid orbit.")
155155
directions = pd.Series(directions).ffill().bfill().to_numpy()
156156
# Remove short consecutive changes in direction to account for geolocation errors
157157
directions = adjust_short_sequences(directions, min_size=n_tol)
@@ -162,19 +162,19 @@ def get_orbit_direction(lats, n_tol=1):
162162

163163
def get_orbit_mode(ds):
164164
# Retrieve latitude coordinates
165-
if "SClatitude" in ds:
165+
if "SClatitude" in ds:
166166
lats = ds["SClatitude"]
167-
elif "scLat" in ds:
167+
elif "scLat" in ds:
168168
lats = ds["scLat"]
169-
else:
169+
else:
170170
# Define cross_track idx defining the orbit coordinates
171-
# - TODO: conically scanning vs cross-track scanning
171+
# - TODO: conically scanning vs cross-track scanning
172172
# - TODO: Use SClatitude ?
173-
# Remove invalid outer cross-track_id if selecting 0 or -1
173+
# Remove invalid outer cross-track_id if selecting 0 or -1
174174
# from gpm.visualization.orbit import remove_invalid_outer_cross_track
175175
# ds, _ = remove_invalid_outer_cross_track(ds)
176176
idx = int(ds["cross_track"].shape[0] / 2)
177177
lats = ds["lat"].isel({"cross_track": idx}).to_numpy()
178178
directions = get_orbit_direction(lats, n_tol=10)
179179
orbit_mode = xr.DataArray(directions, dims=["along_track"])
180-
return orbit_mode
180+
return orbit_mode

0 commit comments

Comments
 (0)