Skip to content

Commit 17a6bef

Browse files
authored
Merge pull request #122 from GeoOcean/clean/binwaves
Clean/binwaves
2 parents 0aa5359 + 4a5ced0 commit 17a6bef

File tree

5 files changed

+414
-85
lines changed

5 files changed

+414
-85
lines changed

bluemath_tk/topo_bathy/swan_grid.py

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,141 @@
22
import xarray as xr
33

44

5-
def generate_grid_parameters(bathy_data: xr.DataArray) -> dict:
5+
def generate_grid_parameters(
6+
bathy_data: xr.DataArray,
7+
alpc: float = 0,
8+
xpc: float = None,
9+
ypc: float = None,
10+
xlenc: float = None,
11+
ylenc: float = None,
12+
buffer_distance: float = None,
13+
) -> dict:
614
"""
7-
Generate the grid parameters for the SWAN model.
15+
Generate grid parameters for the SWAN model based on bathymetry.
816
917
Parameters
1018
----------
1119
bathy_data : xr.DataArray
12-
Bathymetry data.
20+
Bathymetry data.
1321
Must have the following dimensions:
14-
- lon: longitude
15-
- lat: latitude
22+
- lon/x: longitude or x coordinate
23+
- lat/y: latitude or y coordinate
24+
alpc: float
25+
Computational Grid Rotation angle in degrees.
26+
xpc: float
27+
X origin.
28+
ypc: float
29+
Y origin.
1630
1731
Returns
1832
-------
1933
dict
20-
Grid parameters for the SWAN model.
21-
22-
Contact @bellidog on GitHub for more information.
34+
Dictionary with grid configuration for SWAN input.
2335
"""
2436

25-
return {
26-
"xpc": int(np.nanmin(bathy_data.lon)), # x origin
27-
"ypc": int(np.nanmin(bathy_data.lat)), # y origin
28-
"alpc": 0, # x-axis direction
29-
"xlenc": int(
30-
np.nanmax(bathy_data.lon) - np.nanmin(bathy_data.lon)
31-
), # grid length in x
32-
"ylenc": int(
33-
np.nanmax(bathy_data.lat) - np.nanmin(bathy_data.lat)
34-
), # grid length in y
35-
"mxc": len(bathy_data.lon) - 1, # number mesh x, una menos pq si no SWAN peta
36-
"myc": len(bathy_data.lat) - 1, # number mesh y, una menos pq si no SWAN peta
37-
"xpinp": np.nanmin(bathy_data.lon), # x origin
38-
"ypinp": np.nanmin(bathy_data.lat), # y origin
39-
"alpinp": 0, # x-axis direction
40-
"mxinp": len(bathy_data.lon) - 1, # number mesh x
41-
"myinp": len(bathy_data.lat) - 1, # number mesh y
42-
"dxinp": abs(
43-
bathy_data.lon[1].values - bathy_data.lon[0].values
44-
), # size mesh x (resolution in x)
45-
"dyinp": abs(
46-
bathy_data.lat[1].values - bathy_data.lat[0].values
47-
), # size mesh y (resolution in y)
48-
}
37+
# Determine coordinate system based on coordinate names
38+
coord_names = list(bathy_data.coords)
39+
40+
# Get coordinate variables
41+
if any(name in ["lon", "longitude"] for name in coord_names):
42+
x_coord = next(name for name in coord_names if name in ["lon", "longitude"])
43+
y_coord = next(name for name in coord_names if name in ["lat", "latitude"])
44+
else:
45+
x_coord = next(
46+
name for name in coord_names if name in ["x", "X", "cx", "easting"]
47+
)
48+
y_coord = next(
49+
name for name in coord_names if name in ["y", "Y", "cy", "northing"]
50+
)
51+
52+
# Get resolution from cropped data
53+
grid_resolution_x = abs(
54+
bathy_data[x_coord][1].values - bathy_data[x_coord][0].values
55+
)
56+
grid_resolution_y = abs(
57+
bathy_data[y_coord][1].values - bathy_data[y_coord][0].values
58+
)
59+
60+
if alpc != 0:
61+
angle_rad = np.radians(alpc)
62+
# Create rotation matrix
63+
R = np.array(
64+
[
65+
[np.cos(angle_rad), -np.sin(angle_rad)],
66+
[np.sin(angle_rad), np.cos(angle_rad)],
67+
]
68+
)
69+
70+
# Create unrotated rectangle corners
71+
dx = np.array([0, xlenc, xlenc, 0, 0])
72+
dy = np.array([0, 0, ylenc, ylenc, 0])
73+
points = np.column_stack([dx, dy])
74+
75+
# Rotate points
76+
rotated = np.dot(points, R.T)
77+
78+
# Translate to corner position
79+
x = rotated[:, 0] + xpc
80+
y = rotated[:, 1] + ypc
81+
corners = np.column_stack([x, y])
82+
83+
x_min = np.min(x) - buffer_distance
84+
x_max = np.max(x) + buffer_distance
85+
y_min = np.min(y) - buffer_distance
86+
y_max = np.max(y) + buffer_distance
87+
88+
print(f"Cropping bounds: x=[{x_min}, {x_max}], y=[{y_min}, {y_max}]")
89+
90+
# Crop bathymetry
91+
cropped = bathy_data.sel(
92+
{
93+
x_coord: slice(x_min, x_max),
94+
y_coord: slice(y_max, y_min),
95+
} # Note: slice from max to min for descending coordinates
96+
)
97+
98+
grid_parameters = {
99+
"xpc": xpc,
100+
"ypc": ypc,
101+
"alpc": alpc,
102+
"xlenc": xlenc,
103+
"ylenc": ylenc,
104+
"mxc": int(np.round(xlenc / grid_resolution_x) - 1),
105+
"myc": int(np.round(ylenc / grid_resolution_y) - 1),
106+
"xpinp": np.nanmin(cropped[x_coord]), # x origin from cropped data
107+
"ypinp": np.nanmin(cropped[y_coord]), # y origin from cropped data
108+
"alpinp": 0, # x-axis direction
109+
"mxinp": len(cropped[x_coord]) - 1, # number mesh x from cropped data
110+
"myinp": len(cropped[y_coord]) - 1, # number mesh y from cropped data
111+
"dxinp": grid_resolution_x, # resolution from cropped data
112+
"dyinp": grid_resolution_y, # resolution from cropped data
113+
}
114+
return grid_parameters, cropped, corners
115+
116+
else:
117+
# Compute parameters from full bathymetry
118+
grid_parameters = {
119+
"xpc": float(np.nanmin(bathy_data[x_coord])), # origin x
120+
"ypc": float(np.nanmin(bathy_data[y_coord])), # origin y
121+
"alpc": alpc, # x-axis direction
122+
"xlenc": float(
123+
np.nanmax(bathy_data[x_coord]) - np.nanmin(bathy_data[x_coord])
124+
), # grid length x
125+
"ylenc": float(
126+
np.nanmax(bathy_data[y_coord]) - np.nanmin(bathy_data[y_coord])
127+
), # grid length y
128+
"mxc": len(bathy_data[x_coord]) - 1, # num mesh x
129+
"myc": len(bathy_data[y_coord]) - 1, # num mesh y
130+
"xpinp": float(np.nanmin(bathy_data[x_coord])), # origin x
131+
"ypinp": float(np.nanmin(bathy_data[y_coord])), # origin y
132+
"alpinp": 0, # x-axis direction
133+
"mxinp": len(bathy_data[x_coord]) - 1, # num mesh x
134+
"myinp": len(bathy_data[y_coord]) - 1, # num mesh y
135+
"dxinp": float(
136+
abs(bathy_data[x_coord][1].values - bathy_data[x_coord][0].values)
137+
), # resolution x
138+
"dyinp": float(
139+
abs(bathy_data[y_coord][1].values - bathy_data[y_coord][0].values)
140+
), # resolution y
141+
}
142+
return grid_parameters

bluemath_tk/waves/binwaves.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,67 @@
1616

1717

1818
def generate_swan_cases(
19-
frequencies_array: np.ndarray,
20-
directions_array: np.ndarray,
19+
frequencies_array: np.ndarray = None,
20+
directions_array: np.ndarray = None,
21+
direction_range: tuple = (0, 360),
22+
direction_divisions: int = 24,
23+
direction_sector: tuple = None,
24+
frequency_range: tuple = (0.035, 0.5),
25+
frequency_divisions: int = 29,
26+
gamma: float = 50,
27+
spr: float = 2,
2128
) -> xr.Dataset:
2229
"""
2330
Generate the SWAN cases monocromatic wave parameters.
2431
2532
Parameters
2633
----------
27-
directions_array : np.ndarray
28-
The directions array.
29-
frequencies_array : np.ndarray
30-
The frequencies array.
34+
frequencies_array : np.ndarray, optional
35+
The frequencies array. If None, it is generated using frequency_range and frequency_divisions.
36+
directions_array : np.ndarray, optional
37+
The directions array. If None, it is generated using direction_range and direction_divisions.
38+
direction_range : tuple
39+
(min, max) range for directions in degrees.
40+
direction_divisions : int
41+
Number of directional divisions.
42+
frequency_range : tuple
43+
(min, max) range for frequencies in Hz.
44+
frequency_divisions : int
45+
Number of frequency divisions.
3146
3247
Returns
3348
-------
3449
xr.Dataset
3550
The SWAN monocromatic cases Dataset with coordinates freq and dir.
3651
"""
3752

38-
# Wave parameters
39-
gamma = 50 # waves gamma
40-
spr = 2 # waves directional spread
53+
# Auto-generate directions if not provided
54+
if directions_array is None:
55+
step = (direction_range[1] - direction_range[0]) / direction_divisions
56+
directions_array = np.arange(
57+
direction_range[0] + step / 2, direction_range[1], step
58+
)
59+
60+
if direction_sector is not None:
61+
start, end = direction_sector
62+
if start < end:
63+
directions_array = directions_array[
64+
(directions_array >= start) & (directions_array <= end)
65+
]
66+
else: # caso circular, ej. 270–90
67+
directions_array = directions_array[
68+
(directions_array >= start) | (directions_array <= end)
69+
]
70+
71+
# Auto-generate frequencies if not provided
72+
if frequencies_array is None:
73+
frequencies_array = np.geomspace(
74+
frequency_range[0], frequency_range[1], frequency_divisions
75+
)
76+
77+
# Constants for SWAN
78+
gamma = gamma # waves gamma
79+
spr = spr # waves directional spread
4180

4281
# Initialize data arrays for each variable
4382
hs = np.zeros((len(directions_array), len(frequencies_array)))
@@ -54,11 +93,11 @@ def generate_swan_cases(
5493

5594
# Create xarray Dataset
5695
ds = xr.Dataset(
57-
data_vars={
58-
"hs": (["dir", "freq"], hs),
59-
"tp": (["dir", "freq"], tp),
60-
"spr": (["dir", "freq"], spr_arr),
61-
"gamma": (["dir", "freq"], gamma_arr),
96+
{
97+
"hs": (("dir", "freq"), hs),
98+
"tp": (("dir", "freq"), tp),
99+
"spr": (("dir", "freq"), spr_arr),
100+
"gamma": (("dir", "freq"), gamma_arr),
62101
},
63102
coords={
64103
"dir": directions_array,
@@ -115,6 +154,7 @@ def process_kp_coefficients(
115154
concatened_kp = output_kp_list[0]
116155
for file in output_kp_list[1:]:
117156
concatened_kp = xr.concat([concatened_kp, file], dim="case_num")
157+
118158
return concatened_kp.fillna(0.0).sortby("freq").sortby("dir")
119159

120160

bluemath_tk/waves/calibration.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,11 @@ def _create_vec_direc(self, waves: np.ndarray, direcs: np.ndarray) -> np.ndarray
348348
if direcs[i] < 0:
349349
direcs[i] = direcs[i] + 360
350350
if direcs[i] > 0 and waves[i] > 0:
351-
bin_idx = int(direcs[i] / self.direction_bin_size)
351+
# Handle direction = 360° case by mapping to the first bin (0-22.5°)
352+
if direcs[i] >= 360:
353+
bin_idx = 0
354+
else:
355+
bin_idx = int(direcs[i] / self.direction_bin_size)
352356
data[i, bin_idx] = waves[i]
353357

354358
return data
@@ -558,6 +562,8 @@ def correct(
558562
[
559563
self.calibration_params["sea_correction"][
560564
int(peak_direction / self.direction_bin_size)
565+
if peak_direction < 360
566+
else 0 # TODO: Check if this with Javi
561567
]
562568
for peak_direction in peak_directions.isel(
563569
part=n_part
@@ -569,6 +575,8 @@ def correct(
569575
[
570576
self.calibration_params["swell_correction"][
571577
int(peak_direction / self.direction_bin_size)
578+
if peak_direction < 360
579+
else 0 # TODO: Check if this with Javi
572580
]
573581
for peak_direction in peak_directions.isel(
574582
part=n_part
@@ -595,6 +603,8 @@ def correct(
595603
[
596604
self.calibration_params["sea_correction"][
597605
int(peak_direction / self.direction_bin_size)
606+
if peak_direction < 360
607+
else 0
598608
]
599609
for peak_direction in corrected_data["Dirsea"]
600610
]
@@ -609,6 +619,8 @@ def correct(
609619
[
610620
self.calibration_params["swell_correction"][
611621
int(peak_direction / self.direction_bin_size)
622+
if peak_direction < 360
623+
else 0
612624
]
613625
for peak_direction in corrected_data[f"Dirswell{n_part}"]
614626
]
@@ -753,11 +765,16 @@ def plot_calibration_results(self) -> Tuple[Figure, list]:
753765
)
754766

755767
# Plot sea wave climate
756-
x, y, z = density_scatter(
757-
self._data["Dirsea"].iloc[::10] * np.pi / 180,
758-
self._data["Hsea"].iloc[::10],
759-
)
760-
ax5.scatter(x, y, c=z, s=3, cmap="jet")
768+
sea_dirs = self._data["Dirsea"].iloc[::10] * np.pi / 180
769+
sea_heights = self._data["Hsea"].iloc[::10]
770+
# Filter out NaN and infinite values
771+
valid_mask = np.isfinite(sea_dirs) & np.isfinite(sea_heights)
772+
sea_dirs_valid = sea_dirs[valid_mask]
773+
sea_heights_valid = sea_heights[valid_mask]
774+
775+
if len(sea_dirs_valid) > 0:
776+
x, y, z = density_scatter(sea_dirs_valid, sea_heights_valid)
777+
ax5.scatter(x, y, c=z, s=3, cmap="jet")
761778
ax5.set_theta_zero_location("N", offset=0)
762779
ax5.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"])
763780
ax5.xaxis.grid(True, color="lavender", linestyle="-")
@@ -768,11 +785,16 @@ def plot_calibration_results(self) -> Tuple[Figure, list]:
768785
ax5.set_title("SEA $Wave$ $Climate$", pad=35, fontweight="bold")
769786

770787
# Plot swell wave climate
771-
x, y, z = density_scatter(
772-
self._data["Dirswell1"].iloc[::10] * np.pi / 180,
773-
self._data["Hswell1"].iloc[::10],
774-
)
775-
ax6.scatter(x, y, c=z, s=3, cmap="jet")
788+
swell_dirs = self._data["Dirswell1"].iloc[::10] * np.pi / 180
789+
swell_heights = self._data["Hswell1"].iloc[::10]
790+
# Filter out NaN and infinite values
791+
valid_mask = np.isfinite(swell_dirs) & np.isfinite(swell_heights)
792+
swell_dirs_valid = swell_dirs[valid_mask]
793+
swell_heights_valid = swell_heights[valid_mask]
794+
795+
if len(swell_dirs_valid) > 0:
796+
x, y, z = density_scatter(swell_dirs_valid, swell_heights_valid)
797+
ax6.scatter(x, y, c=z, s=3, cmap="jet")
776798
ax6.set_theta_zero_location("N", offset=0)
777799
ax6.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"])
778800
ax6.xaxis.grid(True, color="lavender", linestyle="-")

0 commit comments

Comments
 (0)