Skip to content

Commit 4b397f7

Browse files
authored
Merge pull request #212 from ocefpaf/modernize
Modernize and apply ruff
2 parents 1088315 + 375d3d1 commit 4b397f7

18 files changed

Lines changed: 542 additions & 408 deletions

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ${{ matrix.os }}
1111
strategy:
1212
matrix:
13-
python-version: ["3.9", "3.10", "3.11"]
13+
python-version: [ "3.11", "3.12" ]
1414
os: [windows-latest, ubuntu-latest, macos-latest]
1515
fail-fast: false
1616

.pre-commit-config.yaml

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ repos:
1313
- id: file-contents-sorter
1414
files: requirements-dev.txt
1515

16-
- repo: https://github.com/psf/black
17-
rev: 24.4.2
18-
hooks:
19-
- id: black
20-
language_version: python3
21-
2216
- repo: https://github.com/keewis/blackdoc
2317
rev: v0.3.9
2418
hooks:
@@ -45,9 +39,27 @@ repos:
4539
- id: add-trailing-comma
4640

4741
- repo: https://github.com/astral-sh/ruff-pre-commit
48-
rev: v0.4.7
42+
rev: v0.5.0
4943
hooks:
5044
- id: ruff
45+
args: ["--fix", "--show-fixes"]
46+
- id: ruff-format
47+
48+
- repo: https://github.com/nbQA-dev/nbQA
49+
rev: 1.8.5
50+
hooks:
51+
- id: nbqa-check-ast
52+
- id: nbqa-black
53+
- id: nbqa-ruff
54+
args: [
55+
--fix,
56+
--config=ruff.toml,
57+
]
58+
59+
- repo: https://github.com/bdice/nb-strip-paths
60+
rev: v0.1.0
61+
hooks:
62+
- id: nb-strip-paths
5163

5264
- repo: https://github.com/tox-dev/pyproject-fmt
5365
rev: 2.1.3

ctd/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
"""
2-
Tools to load hydrographic data as pandas DataFrame with some handy methods for
3-
data pre-processing and analysis.
1+
"""Tools to load hydrographic data as pandas DataFrame with some handy methods
2+
for data pre-processing and analysis.
43
"""
54

65
from .plotting import plot_cast

ctd/extras.py

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
"""
2-
Extra functionality for plotting and post-processing.
3-
"""
1+
"""Extra functionality for plotting and post-processing."""
42

53
import matplotlib.pyplot as plt
64
import numpy as np
7-
import numpy.ma as ma
8-
from pandas import Series
5+
import pandas as pd
6+
from numpy import ma
97

108

119
def _extrap1d(interpolator):
12-
"""
13-
How to make scipy.interpolate return an extrapolated result beyond the
10+
"""How to make scipy.interpolate return an extrapolated result beyond the
1411
input range.
1512
1613
This is usually bad interpolation! But sometimes useful for pretty pictures,
@@ -25,10 +22,9 @@ def pointwise(x):
2522
"""Pointwise interpolation."""
2623
if x < xs[0]:
2724
return ys[0] + (x - xs[0]) * (ys[1] - ys[0]) / (xs[1] - xs[0])
28-
elif x > xs[-1]:
25+
if x > xs[-1]:
2926
return ys[-1] + (x - xs[-1]) * (ys[-1] - ys[-2]) / (xs[-1] - xs[-2])
30-
else:
31-
return interpolator(x)
27+
return interpolator(x)
3228

3329
def ufunclike(xs):
3430
"""Return an interpolation ufunc."""
@@ -39,30 +35,34 @@ def ufunclike(xs):
3935

4036
def get_maxdepth(self):
4137
"""Return the maximum depth/pressure of a cast."""
42-
valid_last_depth = self.apply(Series.notnull).values.T
43-
return np.float_(self.index.values * valid_last_depth).max(axis=1)
44-
45-
46-
def extrap_sec(data, dist, depth, w1=1.0, w2=0):
47-
"""
48-
Extrapolates `data` to zones where the shallow stations are shadowed by
38+
valid_last_depth = self.apply(pd.Series.notnull).to_numpy().T
39+
return np.float64(self.index.to_numpy() * valid_last_depth).max(axis=1)
40+
41+
42+
def extrap_sec(
43+
data: np.ndarray,
44+
dist: np.ndarray,
45+
depth: np.ndarray,
46+
w1: float = 1.0,
47+
w2: float = 0,
48+
) -> np.ndarray:
49+
"""Extrapolate `data` to zones where the shallow stations are shadowed by
4950
the deep stations. The shadow region usually cannot be extrapolates via
5051
linear interpolation.
5152
5253
The extrapolation is applied using the gradients of the `data` at a certain
5354
level.
5455
55-
Parameters
56-
----------
57-
data : array_like
58-
Data to be extrapolated
59-
dist : array_like
60-
Stations distance
61-
fd : float
62-
Decay factor [0-1]
56+
Inputs
57+
------
58+
data : Data to be extrapolated
59+
dist : Stations distance
60+
depth : Depth of the profile
61+
w1 : weights [0-1]
62+
w2 : weights [0-1]
6363
6464
65-
Returns
65+
Outputs
6666
-------
6767
Sec_extrap : array_like
6868
Extrapolated variable
@@ -72,39 +72,45 @@ def extrap_sec(data, dist, depth, w1=1.0, w2=0):
7272

7373
new_data1 = []
7474
for row in data:
75+
new_row = row.copy()
7576
mask = ~np.isnan(row)
7677
if mask.any():
7778
y = row[mask]
7879
if y.size == 1:
79-
row = np.repeat(y, len(mask))
80+
new_row = np.repeat(y, len(mask))
8081
else:
8182
x = dist[mask]
8283
f_i = interp1d(x, y)
8384
f_x = _extrap1d(f_i)
84-
row = f_x(dist)
85-
new_data1.append(row)
85+
new_row = f_x(dist)
86+
new_data1.append(new_row)
8687

8788
new_data2 = []
8889
for col in data.T:
90+
new_col = col.copy()
8991
mask = ~np.isnan(col)
9092
if mask.any():
9193
y = col[mask]
9294
if y.size == 1:
93-
col = np.repeat(y, len(mask))
95+
new_col = np.repeat(y, len(mask))
9496
else:
9597
z = depth[mask]
9698
f_i = interp1d(z, y)
9799
f_z = _extrap1d(f_i)
98-
col = f_z(depth)
99-
new_data2.append(col)
100+
new_col = f_z(depth)
101+
new_data2.append(new_col)
100102

101-
new_data = np.array(new_data1) * w1 + np.array(new_data2).T * w2
102-
return new_data
103+
return np.array(new_data1) * w1 + np.array(new_data2).T * w2
103104

104105

105-
def gen_topomask(h, lon, lat, dx=1.0, kind="linear", plot=False):
106-
"""
107-
Generates a topography mask from an oceanographic transect taking the
106+
def gen_topomask(
107+
h: np.ndarray,
108+
lon: np.ndarray,
109+
lat: np.ndarray,
110+
dx: float = 1.0,
111+
kind: str = "linear",
112+
) -> tuple:
113+
"""Generate a topography mask from an oceanographic transect taking the
108114
deepest CTD scan as the depth of each station.
109115
110116
Inputs
@@ -119,8 +125,6 @@ def gen_topomask(h, lon, lat, dx=1.0, kind="linear", plot=False):
119125
kind : string, optional
120126
Type of the interpolation to be performed.
121127
See scipy.interpolate.interp1d documentation for details.
122-
plot : bool
123-
Whether to plot mask for visualization.
124128
125129
Outputs
126130
-------
@@ -134,26 +138,33 @@ def gen_topomask(h, lon, lat, dx=1.0, kind="linear", plot=False):
134138
André Palóczy Filho (paloczy@gmail.com) -- October/2012
135139
136140
"""
137-
138141
import gsw
139142
from scipy.interpolate import interp1d
140143

141144
h, lon, lat = list(map(np.asanyarray, (h, lon, lat)))
142145
# Distance in km.
143146
x = np.append(0, np.cumsum(gsw.distance(lon, lat)[0] / 1e3))
144147
h = -gsw.z_from_p(h, lat.mean())
145-
Ih = interp1d(x, h, kind=kind, bounds_error=False, fill_value=h[-1])
148+
ih = interp1d(x, h, kind=kind, bounds_error=False, fill_value=h[-1])
146149
xm = np.arange(0, x.max() + dx, dx)
147-
hm = Ih(xm)
150+
hm = ih(xm)
148151

149152
return xm, hm
150153

151154

152-
def plot_section(self, reverse=False, filled=False, **kw):
155+
def plot_section( # noqa: PLR0915
156+
self: pd.DataFrame,
157+
*,
158+
reverse: bool = False,
159+
filled: bool = False,
160+
**kw: dict,
161+
) -> tuple:
153162
"""Plot a sequence of CTD casts as a section."""
154163
import gsw
155164

156-
lon, lat, data = list(map(np.asanyarray, (self.lon, self.lat, self.values)))
165+
lon, lat, data = list(
166+
map(np.asanyarray, (self.lon, self.lat, self.to_numpy())),
167+
)
157168
data = ma.masked_invalid(data)
158169
h = self.get_maxdepth()
159170
if reverse:
@@ -163,7 +174,7 @@ def plot_section(self, reverse=False, filled=False, **kw):
163174
h = h[::-1]
164175
lon, lat = map(np.atleast_2d, (lon, lat))
165176
x = np.append(0, np.cumsum(gsw.distance(lon, lat)[0] / 1e3))
166-
z = self.index.values.astype(float)
177+
z = self.index.to_numpy().astype(float)
167178

168179
if filled: # CAVEAT: this method cause discontinuities.
169180
data = data.filled(fill_value=np.nan)
@@ -248,51 +259,53 @@ def plot_section(self, reverse=False, filled=False, **kw):
248259
return fig, ax, cb
249260

250261

251-
def cell_thermal_mass(temperature, conductivity):
252-
"""
253-
Sample interval is measured in seconds.
262+
def cell_thermal_mass(
263+
temperature: pd.Series,
264+
conductivity: pd.Series,
265+
) -> pd.Series:
266+
"""Sample interval is measured in seconds.
254267
Temperature in degrees.
255268
CTM is calculated in S/m.
256269
257270
"""
258-
259271
alpha = 0.03 # Thermal anomaly amplitude.
260272
beta = 1.0 / 7 # Thermal anomaly time constant (1/beta).
261273

262274
sample_interval = 1 / 15.0
263275
a = 2 * alpha / (sample_interval * beta + 2)
264276
b = 1 - (2 * a / alpha)
265-
dCodT = 0.1 * (1 + 0.006 * [temperature - 20])
266-
dT = np.diff(temperature)
267-
ctm = -1.0 * b * conductivity + a * (dCodT) * dT # [S/m]
268-
return ctm
277+
dc_o_dt = 0.1 * (1 + 0.006 * [temperature - 20])
278+
dt = np.diff(temperature)
279+
return -1.0 * b * conductivity + a * (dc_o_dt) * dt # [S/m]
269280

270281

271-
def mixed_layer_depth(CT, method="half degree"):
282+
def mixed_layer_depth(ct: pd.Series, method: str = "half degree") -> pd.Series:
272283
"""Return the mixed layer depth based on the "half degree" criteria."""
273-
if method == "half degree":
274-
mask = CT[0] - CT < 0.5
275-
else:
276-
mask = np.zeros_like(CT)
277-
return Series(mask, index=CT.index, name="MLD")
284+
half_degree = 0.5
285+
mask = (
286+
ct[0] - ct < half_degree
287+
if method == "half degree"
288+
else np.zeros_like(ct)
289+
)
290+
return pd.Series(mask, index=ct.index, name="MLD")
278291

279292

280-
def barrier_layer_thickness(SA, CT):
281-
"""
282-
Compute the thickness of water separating the mixed surface layer from the
283-
thermocline. A more precise definition would be the difference between
284-
mixed layer depth (MLD) calculated from temperature minus the mixed layer
285-
depth calculated using density.
293+
def barrier_layer_thickness(sa: pd.Series, ct: pd.Series) -> pd.Series:
294+
"""Compute the thickness of water separating the mixed surface layer from
295+
the thermocline.
296+
A more precise definition would be the difference between mixed layer depth
297+
(MLD) calculated from temperature minus the mixed layer depth calculated
298+
using density.
286299
287300
"""
288301
import gsw
289302

290-
sigma_theta = gsw.sigma0(SA, CT)
291-
mask = mixed_layer_depth(CT)
303+
sigma_theta = gsw.sigma0(sa, ct)
304+
mask = mixed_layer_depth(ct)
292305
mld = np.where(mask)[0][-1]
293306
sig_surface = sigma_theta[0]
294-
sig_bottom_mld = gsw.sigma0(SA[0], CT[mld])
307+
sig_bottom_mld = gsw.sigma0(sa[0], ct[mld])
295308
d_sig_t = sig_surface - sig_bottom_mld
296309
d_sig = sigma_theta - sig_bottom_mld
297310
mask = d_sig < d_sig_t # Barrier layer.
298-
return Series(mask, index=SA.index, name="BLT")
311+
return pd.Series(mask, index=sa.index, name="BLT")

ctd/plotting.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
"""
2-
Plotting module
3-
"""
1+
"""Plotting module."""
2+
3+
from __future__ import annotations
44

55
import matplotlib.pyplot as plt
66
import pandas as pd
77
from pandas_flavor import register_dataframe_method, register_series_method
88

9+
cast = pd.DataFrame | pd.Series
10+
911

1012
@register_series_method
1113
@register_dataframe_method
12-
def plot_cast(df, secondary_y=False, label=None, ax=None, *args, **kwargs):
13-
"""
14-
Plot a CTD variable with the index in the y-axis instead of x-axis.
15-
16-
"""
17-
14+
def plot_cast(
15+
df: cast,
16+
*,
17+
secondary_y: bool = False,
18+
label: str | None = None,
19+
ax: plt.Axes | None = None,
20+
**kwargs: dict,
21+
) -> cast:
22+
"""Plot a CTD variable with the index in the y-axis instead of x-axis."""
1823
fignums = plt.get_fignums()
1924
if ax is None and not fignums:
2025
ax = plt.axes()
@@ -44,7 +49,7 @@ def plot_cast(df, secondary_y=False, label=None, ax=None, *args, **kwargs):
4449
ax.plot(series, series.index, label=labels[k])
4550
elif isinstance(df, pd.Series):
4651
label = label if label else str(df.name)
47-
ax.plot(df.values, df.index, *args, label=label, **kwargs)
52+
ax.plot(df.values, df.index, label=label, **kwargs)
4853

4954
ax.set_ylabel(ylabel)
5055
ax.set_xlabel(xlabel)

0 commit comments

Comments
 (0)