Coverage for python / aubellhop / pyplot.py: 62%

231 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-24 14:11 +0000

1"""Plotting functions using Matplotlib for aubellhop. 

2""" 

3 

4from __future__ import annotations 

5 

6from typing import Any 

7from sys import float_info as _fi 

8 

9import numpy as np 

10import scipy.interpolate as _interp 

11import pandas as pd 

12 

13import matplotlib.pyplot as _pyplt 

14import matplotlib.colors as _mplc 

15from matplotlib.axes import Axes 

16 

17from .constants import BHStrings 

18from .environment import Environment 

19 

20 

21def pyplot_env2d( 

22 env: Environment, 

23 surface_color: str = 'dodgerblue', 

24 bottom_color: str = 'peru', 

25 source_color: str = 'orangered', 

26 receiver_color: str = 'midnightblue', 

27 receiver_plot: bool | None = None, 

28 fill: bool | None = None, 

29 ax: Any | None = None, 

30 **kwargs: Any 

31 ) -> None: 

32 """Plots a visual representation of the environment with matplotlib. 

33 

34 Parameters 

35 ---------- 

36 env : dict 

37 Environment description 

38 surface_color : str, default='dodgerblue' 

39 Color of the surface (see `Bokeh colors <https://bokeh.pydata.org/en/latest/docs/reference/colors.html>`_) 

40 bottom_color : str, default='peru' 

41 Color of the bottom (see `Bokeh colors <https://bokeh.pydata.org/en/latest/docs/reference/colors.html>`_) 

42 source_color : str, default='orangered' 

43 Color of transmitters (see `Bokeh colors <https://bokeh.pydata.org/en/latest/docs/reference/colors.html>`_) 

44 receiver_color : str, default='midnightblue' 

45 Color of receivers (see `Bokeh colors <https://bokeh.pydata.org/en/latest/docs/reference/colors.html>`_) 

46 receiver_plot : bool, optional 

47 True to plot all receivers, False to not plot any receivers, None to automatically decide 

48 **kwargs 

49 Other keyword arguments applicable for `bellhop.plot.plot()` are also supported 

50 

51 Notes 

52 ----- 

53 The surface, bottom, transmitters (marker: '*') and receivers (marker: 'o') 

54 are plotted in the environment. If `receiver_plot` is set to None and there are 

55 more than 2000 receivers, they are not plotted. 

56 

57 Examples 

58 -------- 

59 >>> import aubellhop as bh 

60 >>> env = bh.Environment(bottom_depth=[[0, 40], [100, 30], [500, 35], [700, 20], [1000,45]]) 

61 >>> bh.plot_env(env) 

62 """ 

63 

64 env.check() 

65 

66 if ax is None: 

67 fig = _pyplt.figure() 

68 ax = fig.add_subplot() 

69 

70 if np.array(env['receiver_range']).size > 1: 

71 min_x = np.min(env['receiver_range']) 

72 else: 

73 min_x = 0 

74 max_x = np.max(env['receiver_range']) 

75 if max_x - min_x > 10000: 

76 divisor = 1000 

77 min_x /= divisor 

78 max_x /= divisor 

79 range_unit = ' (km)' 

80 else: 

81 divisor = 1 

82 range_unit = ' (m)' 

83 if np.size(env['surface_depth']) == 1: 

84 min_y = 0 

85 else: 

86 min_y = np.min(env['surface_depth'][:, 1]) 

87 max_y = env['_depth_max'] 

88 mgn_x = 0.01 * (max_x - min_x) 

89 mgn_y = 0.1 * (max_y - min_y) 

90 

91 if np.size(env['surface_depth']) == 1: 

92 surface_x = [min_x, max_x] 

93 surface_y = [0, 0] 

94 else: 

95 surface_x = env['surface_depth'][:, 0] / divisor 

96 surface_y = env['surface_depth'][:, 1] 

97 _pyplt.plot(surface_x, surface_y, color=surface_color, **kwargs) 

98 

99 if np.size(env['bottom_depth']) == 1: 

100 _pyplt.plot([min_x, max_x], [env['bottom_depth'], env['bottom_depth']], color=bottom_color, **kwargs) 

101 else: 

102 _pyplt.plot(env['bottom_depth'][:, 0] / divisor, env['bottom_depth'][:, 1], color=bottom_color, **kwargs) 

103 

104 txd = env['source_depth'] 

105 _pyplt.plot([0] * np.size(txd), txd, marker='*', markersize=6, color=source_color, **kwargs) 

106 

107 if receiver_plot is None: 

108 receiver_plot = np.size(env['receiver_depth']) * np.size(env['receiver_range']) < 2000 

109 if receiver_plot: 

110 rxr = env['receiver_range'] 

111 if np.size(rxr) == 1: 

112 rxr = [rxr] 

113 for r in np.array(rxr): 

114 rxd = env['receiver_depth'] 

115 _pyplt.plot([r / divisor] * np.size(rxd), rxd, marker='o', color=receiver_color, **kwargs) 

116 

117 if fill: 

118 y0 = 0.0 

119 _pyplt.axhline(y0, color="w", linestyle="-") 

120 _pyplt.fill_between(surface_x, surface_y, y0, color="w") 

121 

122 _pyplt.xlabel('Range'+range_unit) 

123 _pyplt.ylabel('Depth (m)') 

124 ax.yaxis.set_inverted(True) 

125 _pyplt.xlim([min_x - mgn_x, max_x + mgn_x]) 

126 _pyplt.ylim([max_y + mgn_y, min_y - mgn_y]) 

127 

128def pyplot_env3d(env: Environment, surface_color: str = 'dodgerblue', bottom_color: str = 'peru', source_color: str = 'orangered', receiver_color: str = 'midnightblue', 

129 receiver_plot: bool | None = None, ax: Any | None = None, **kwargs: Any) -> None: 

130 """Plots a visual representation of the environment with matplotlib. 

131 """ 

132 

133 env.check() 

134 

135 if ax is None: 

136 fig = _pyplt.figure() 

137 ax = fig.add_subplot(projection='3d') 

138 

139 if np.array(env['receiver_range']).size > 1: 

140 min_x = np.min(env['receiver_range']) 

141 else: 

142 min_x = 0 

143 max_x = env['simulation_range'] 

144 min_y = -env['simulation_cross_range'] 

145 max_y = +env['simulation_cross_range'] 

146 xdivisor = 1 

147 ydivisor = 1 

148 xrange_unit = ' (m)' 

149 yrange_unit = ' (m)' 

150 if max_x - min_x > 10000: 

151 xdivisor = 1000 

152 min_x /= xdivisor 

153 max_x /= xdivisor 

154 xrange_unit = ' (km)' 

155 if max_y - min_y > 10000: 

156 ydivisor = 1000 

157 min_y /= ydivisor 

158 max_y /= ydivisor 

159 yrange_unit = ' (km)' 

160 if np.size(env['surface_depth']) == 1: 

161 min_z = 0 

162 else: 

163 min_z = np.min(env['surface_depth'][:, 1]) 

164 max_z = env['simulation_depth'] 

165 mgn_x = 0.01 * (max_x - min_x) 

166 mgn_z = 0.1 * (max_z - min_z) 

167 

168 if np.size(env['surface_depth']) == 1: 

169 z = float(env['surface_depth']) 

170 X, Y = np.meshgrid([min_x, max_x], [min_y, max_y]) 

171 Z = np.full_like(X, z) 

172 ax.plot_surface(X, Y, Z, color=surface_color, alpha=0.3, **kwargs) 

173 else: 

174 _pyplt.plot(env['surface_depth'][:, 0] / xdivisor, env['surface_depth'][:, 1], color=surface_color, **kwargs) 

175 

176 if np.size(env['bottom_depth']) == 1: 

177 z = float(env['bottom_depth']) 

178 X, Y = np.meshgrid([min_x, max_x], [min_y, max_y]) 

179 Z = np.full_like(X, z) 

180 ax.plot_surface(X, Y, Z, color=bottom_color, alpha=0.3, **kwargs) 

181 else: 

182 _pyplt.plot(env['bottom_depth'][:, 0] / xdivisor, env['bottom_depth'][:, 1], color=bottom_color, **kwargs) 

183 

184 if env._source_num == 1: 

185 _pyplt.plot( 

186 env['source_range'] / xdivisor, 

187 env['source_cross_range'] / ydivisor, 

188 env['source_depth'], 

189 marker='*', 

190 markersize=6, 

191 color=source_color, 

192 **kwargs, 

193 ) 

194 else: 

195 print("MULTIPLE SOURCES NOT IMPLEMENTED YET") 

196 

197 if env._source_num == 1: 

198 _pyplt.plot( 

199 env['receiver_range'] * np.cos(env['receiver_bearing']) / xdivisor, 

200 env['receiver_range'] * np.sin(env['receiver_bearing']) / ydivisor, 

201 env['receiver_depth'], 

202 marker='o', 

203 markersize=6, 

204 color=receiver_color, 

205 **kwargs, 

206 ) 

207 else: 

208 print("MULTIPLE RECEIVERS NOT IMPLEMENTED YET") 

209 

210 ax.set_xlabel('Range'+xrange_unit) 

211 ax.set_ylabel('Cross range'+yrange_unit) 

212 ax.set_zlabel('Depth (m)') 

213 ax.yaxis.set_inverted(True) 

214 ax.set_xlim([min_x - mgn_x, max_x + mgn_x]) 

215 ax.set_ylim([min_y, max_y]) 

216 ax.set_zlim([max_z + mgn_z, min_z - mgn_z]) 

217 

218def pyplot_ssp(env: Environment, ax: Any | None = None, **kwargs: Any) -> None: 

219 """Plots the sound speed profile with matplotlib. 

220 

221 Parameters 

222 ---------- 

223 env : Environment 

224 Environment description 

225 **kwargs 

226 Other keyword arguments applicable for `bellhop.plot.plot()` are also supported 

227 

228 Notes 

229 ----- 

230 If the sound speed profile is range-dependent, this function only plots the first profile. 

231 

232 Examples 

233 -------- 

234 >>> import aubellhop as bh 

235 >>> env = bh.Environment(soundspeed=[[ 0, 1540], [10, 1530], [20, 1532], [25, 1533], [30, 1535]]) 

236 >>> bh.plot_ssp(env) 

237 """ 

238 

239 if ax is None: 

240 fig = _pyplt.figure() 

241 ax = fig.add_subplot() 

242 

243 assert(isinstance(ax, Axes)) 

244 

245 env.check() 

246 svp = env['soundspeed'] 

247 if isinstance(svp, pd.DataFrame): 

248 svp = np.hstack((np.array([svp.index]).T, np.asarray(svp))) 

249 if np.size(svp) == 1: 

250 if np.size(env['bottom_depth']) > 1: 

251 max_y = np.max(env['bottom_depth'][:, 1]) 

252 else: 

253 max_y = env['bottom_depth'] 

254 _pyplt.plot([svp, svp], [0, -max_y], **kwargs) 

255 _pyplt.xlabel('Soundspeed (m/s)') 

256 _pyplt.ylabel('Depth (m)') 

257 elif env['soundspeed_interp'] == BHStrings.spline: 

258 ynew = np.linspace(np.min(svp[:, 0]), np.max(svp[:, 0]), 100) 

259 tck = _interp.splrep(svp[:, 0], svp[:, 1], s=0) 

260 xnew = _interp.splev(ynew, tck, der=0) 

261 _pyplt.plot(xnew, -ynew, **kwargs) 

262 _pyplt.xlabel('Soundspeed (m/s)') 

263 _pyplt.ylabel('Depth (m)') 

264 _pyplt.plot(svp[:, 1], -svp[:, 0], marker='.', **kwargs) 

265 else: 

266 for i in range(svp.shape[1]-1): 

267 _pyplt.plot(svp[:, i+1], -svp[:, 0], **kwargs) 

268 _pyplt.xlabel('Soundspeed (m/s)') 

269 _pyplt.ylabel('Depth (m)') 

270 

271def pyplot_arrivals(arrivals: Any, dB: bool = False, color: str = 'blue', **kwargs: Any) -> None: 

272 """Plots the arrival times and amplitudes with matplotlib. 

273 

274 Parameters 

275 ---------- 

276 arrivals : pandas.DataFrame 

277 Arrivals times (s) and coefficients 

278 dB : bool, default=False 

279 True to plot in dB, False for linear scale 

280 color : str, default='blue' 

281 Line color (see `Bokeh colors <https://bokeh.pydata.org/en/latest/docs/reference/colors.html>`_) 

282 **kwargs 

283 Other keyword arguments applicable for `bellhop.plot.plot()` are also supported 

284 

285 Examples 

286 -------- 

287 >>> import aubellhop as bh 

288 >>> env = bh.Environment() 

289 >>> arrivals = bh.compute_arrivals(env) 

290 >>> bh.plot_arrivals(arrivals) 

291 """ 

292 t0 = min(arrivals.time_of_arrival) 

293 t1 = max(arrivals.time_of_arrival) 

294 if dB: 

295 min_y = 20 * np.log10(np.max(np.abs(arrivals.arrival_amplitude))) - 60 

296 ylabel = 'Amplitude (dB)' 

297 else: 

298 ylabel = 'Amplitude' 

299 _pyplt.plot([t0, t1], [0, 0], color=color, **kwargs) 

300 _pyplt.xlabel('Arrival time (s)') 

301 _pyplt.ylabel(ylabel) 

302 min_y = 0 

303 for _, row in arrivals.iterrows(): 

304 t = row.time_of_arrival.real 

305 y = np.abs(row.arrival_amplitude) 

306 if dB: 

307 y = max(20 * np.log10(_fi.epsilon + y), min_y) 

308 _pyplt.plot([t, t], [min_y, y], color=color, **kwargs) 

309 _pyplt.xlabel('Arrival time (s)') 

310 _pyplt.ylabel(ylabel) 

311 

312def pyplot_rays( 

313 rays: Any, 

314 env: Environment | None = None, 

315 invert_colors: bool = False, 

316 ax: Any | None = None, 

317 **kwargs: Any 

318 ) -> Axes: 

319 """Plots ray paths with matplotlib 

320 

321 Parameters 

322 ---------- 

323 rays : pandas.DataFrame 

324 Ray paths 

325 env : Environment, optional 

326 Environment definition 

327 invert_colors : bool, default=False 

328 False to use black for high intensity rays, True to use white 

329 **kwargs 

330 Other keyword arguments applicable for `bellhop.plot.plot()` are also supported 

331 

332 Notes 

333 ----- 

334 If environment definition is provided, it is overlayed over this plot using default 

335 parameters for `bellhop.plot_env()`. Without an environment file, no axis labels etc 

336 are provided, you are in charge of that. 

337 

338 Examples 

339 -------- 

340 >>> import aubellhop as bh 

341 >>> env = bh.Environment() 

342 >>> rays = bh.compute_eigenrays(env) 

343 >>> bh.plot_rays(rays, width=1000) 

344 """ 

345 if env is not None: 

346 env.check() 

347 

348 rays = rays.sort_values('bottom_bounces', ascending=False) 

349 dim = rays["ray"].iloc[0][0].shape[0] 

350 

351 if ax is None: 

352 fig = _pyplt.figure() 

353 if dim == 2: 

354 ax = fig.add_subplot() 

355 elif dim == 3: 

356 ax = fig.add_subplot(projection='3d') 

357 assert(isinstance(ax, Axes)) 

358 

359 max_amp = np.max(np.abs(rays.bottom_bounces)) if len(rays.bottom_bounces) > 0 else 0 

360 if max_amp <= 0: 

361 max_amp = 1 

362 divisor = 1 

363 r = [] 

364 for _, row in rays.iterrows(): 

365 r += list(row.ray[:, 0]) 

366 if max(r) - min(r) > 10000: 

367 divisor = 1000 

368 for _, row in rays.iterrows(): 

369 rr = float( row.bottom_bounces / (max_amp + 1) ) # avoid rr = 1 == 100% white 

370 c = 1.0 - rr if invert_colors else rr 

371 cmap = _pyplt.get_cmap("gray") 

372 col_str = _mplc.to_hex(cmap(c)) 

373 if dim == 2: 

374 if "color" in kwargs.keys(): 

375 ax.plot(row.ray[:, 0] / divisor, row.ray[:, 1], **kwargs) 

376 else: 

377 ax.plot(row.ray[:, 0] / divisor, row.ray[:, 1], color=col_str, **kwargs) 

378 if dim == 3: 

379 if "color" in kwargs.keys(): 

380 ax.plot(row.ray[:, 0] / divisor, row.ray[:, 1], row.ray[:, 2], **kwargs) 

381 else: 

382 ax.plot(row.ray[:, 0] / divisor, row.ray[:, 1], row.ray[:, 2], color=col_str, **kwargs) 

383 if env is not None: 

384 if dim == 2: 

385 pyplot_env2d(env,ax=ax,receiver_plot=False) 

386 elif dim == 3: 

387 pyplot_env3d(env,ax=ax) 

388 

389 return ax 

390 

391def pyplot_transmission_loss( 

392 tloss: Any, 

393 env: Environment | None = None, 

394 ax: Any | None = None, 

395 **kwargs: Any 

396 ) -> Axes: 

397 """Plots transmission loss with matplotlib. 

398 

399 Parameters 

400 ---------- 

401 tloss : pandas.DataFrame 

402 Complex transmission loss 

403 env : Environment, optional 

404 Environment definition 

405 **kwargs 

406 Other keyword arguments applicable for `bellhop.plot.image()` are also supported 

407 

408 Notes 

409 ----- 

410 If environment definition is provided, it is overlayed over this plot using default 

411 parameters for `bellhop.plot_env()`. 

412 

413 Examples 

414 -------- 

415 >>> import aubellhop as bh 

416 >>> import numpy as np 

417 >>> env = bh.Environment( 

418 receiver_depth=np.arange(0, 25), 

419 receiver_range=np.arange(0, 1000), 

420 beam_angle_min=-45, 

421 beam_angle_max=45 

422 ) 

423 >>> tloss = bh.compute_transmission_loss(env) 

424 >>> bh.plot_transmission_loss(tloss, width=1000) 

425 """ 

426 if env is not None: 

427 env.check() 

428 

429 if ax is None: 

430 fig = _pyplt.figure() 

431 ax = fig.add_subplot() 

432 assert(isinstance(ax, Axes)) 

433 

434 xr = (min(tloss.columns), max(tloss.columns)) 

435 yr = (max(tloss.index), min(tloss.index)) 

436 xlabel = 'Range (m)' 

437 if xr[1] - xr[0] > 10000: 

438 xr = (min(tloss.columns) / 1000, max(tloss.columns) / 1000) 

439 xlabel = 'Range (km)' 

440 

441 trans_loss = 20 * np.log10(_fi.epsilon + np.abs(np.flipud(np.array(tloss)))) 

442 x_mesh, y_mesh = np.meshgrid(np.linspace(xr[0], xr[1], trans_loss.shape[1]), 

443 np.linspace(yr[0], yr[1], trans_loss.shape[0])) 

444 

445 vmin = kwargs.get("vmin", None) 

446 vmax = kwargs.get("vmax", None) 

447 trans_loss = np.clip(trans_loss, vmin, vmax) 

448 

449 _pyplt.contourf(x_mesh, y_mesh, trans_loss, cmap="jet", **kwargs) 

450 _pyplt.xlabel(xlabel) 

451 _pyplt.ylabel('Depth (m)') 

452 _pyplt.colorbar(label="Transmission loss (dB)") 

453 if env is not None: 

454 pyplot_env2d(env, ax=ax, receiver_plot=False, fill=True) 

455 

456 return ax 

457 

458 

459### Export module names for auto-importing in __init__.py 

460 

461__all__ = [ 

462 name for name in globals() if not name.startswith("_") # ignore private names 

463]