Coverage for python/bellhop/main.py: 95%
133 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 12:04 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 12:04 +0000
1##############################################################################
2#
3# Copyright (c) 2025-, Will Robertson
4# Copyright (c) 2018-2025, Mandar Chitre
5#
6# This file was originally part of arlpy, released under Simplified BSD License.
7# It has been relicensed in this repository to be compatible with the Bellhop licence (GPL).
8#
9##############################################################################
11"""Underwater acoustic propagation modeling toolbox.
13This toolbox uses the Bellhop acoustic propagation model. For this model
14to work, the complete bellhop.py package must be built and installed
15and `bellhop.exe` should be in your PATH.
16"""
18from typing import Any, List, Optional, Union, Tuple
20import numpy as _np
21import pandas as _pd
23from bellhop.constants import _Strings, Defaults
25# this format to explicitly mark the functions as public:
26from bellhop.readers import read_env as read_env
27from bellhop.readers import read_ssp as read_ssp
28from bellhop.readers import read_ati as read_ati
29from bellhop.readers import read_bty as read_bty
30from bellhop.readers import read_sbp as read_sbp
31from bellhop.readers import read_trc as read_trc
32from bellhop.readers import read_brc as read_brc
34from bellhop.readers import read_shd as read_shd
35from bellhop.readers import read_rays as read_rays
36from bellhop.readers import read_arrivals as read_arrivals
38from bellhop.environment import Environment
39from bellhop.bellhop import Bellhop
41_models: List[Bellhop] = []
43def new_model(name: str, **kwargs: Any) -> Bellhop:
44 """Instantiate a new Bellhop model and add it to the list of models.
46 Creates a Bellhop instance with the specified parameters and
47 adds it to the internal registry of models for later access.
49 Parameters
50 ----------
51 name : str
52 Descriptive name for this model instance, must be unique
54 **kwargs
55 Keyword arguments passed directly to the Bellhop constructor.
56 Common parameters include:
57 - exe : str
58 Filename of the Bellhop executable
60 Returns
61 -------
62 Bellhop
63 The newly created Bellhop model instance.
65 Examples
66 --------
67 >>> bh.models() # there is always a default model
68 ['bellhop']
69 >>> bh.new_model(name="bellhop-at", exe="bellhop_at.exe")
70 >>> bh.models()
71 ['bellhop', 'bellhop-at']
72 """
73 for m in _models:
74 if name == m.name:
75 raise ValueError(f"Bellhop model with this name ('{name}') already exists.")
76 model = Bellhop(name=name, **kwargs)
77 _models.append(model)
78 return model
80new_model(name=Defaults.model_name)
82def models(env: Optional[Environment] = None, task: Optional[str] = None) -> List[str]:
83 """List available models.
85 Parameters
86 ----------
87 env : dict, optional
88 Environment to model
89 task : str, optional
90 Task type: arrivals/eigenrays/rays/coherent/incoherent/semicoherent
92 Returns
93 -------
94 list of str
95 List of models that can be used
97 Examples
98 --------
99 >>> import bellhop as bh
100 >>> bh.models()
101 ['bellhop']
102 >>> env = bh.create_env()
103 >>> bh.models(env, task="coherent")
104 ['bellhop']
105 """
106 if env is not None:
107 env = check_env(env)
108 if (env is None and task is not None) or (env is not None and task is None):
109 raise ValueError('env and task should be both specified together')
110 rv: List[str] = []
111 for m in _models:
112 if m.supports(env, task):
113 rv.append(m.name)
114 return rv
116def create_env2d(**kv: Any) -> Environment:
117 """Backwards compatibility for create_env"""
118 return create_env(**kv)
120def create_env(**kv: Any) -> Environment:
121 """Create a new underwater environment.
123 Parameters
124 ----------
125 **kv : dict
126 Keyword arguments for environment configuration.
128 Returns
129 -------
130 env : dict
131 A new underwater environment dictionary.
133 Raises
134 ------
135 ValueError
136 If any parameter value is invalid according to BELLHOP constraints.
138 Example
139 -------
141 To see all the parameters available and their default values:
143 >>> import bellhop as bh
144 >>> env = bh.create_env()
145 >>> print(env)
147 The environment parameters may be changed by passing keyword arguments
148 or modified later using dictionary notation:
150 >>> import bellhop as bh
151 >>> env = bh.create_env(depth=40, soundspeed=1540)
152 >>> print(env)
153 >>> env['depth'] = 25
154 >>> env['bottom_soundspeed'] = 1800
155 >>> print(env)
157 The default environment has a constant sound speed.
158 A depth dependent sound speed profile be provided as a Nx2 array of (depth, sound speed):
160 >>> import bellhop as bh
161 >>> env = bh.create_env(depth=20,
162 >>>. soundspeed=[[0,1540], [5,1535], [10,1535], [20,1530]])
164 A range-and-depth dependent sound speed profile can be provided as a Pandas frame:
166 >>> import bellhop as bh
167 >>> import pandas as pd
168 >>> ssp2 = pd.DataFrame({
169 0: [1540, 1530, 1532, 1533], # profile at 0 m range
170 100: [1540, 1535, 1530, 1533], # profile at 100 m range
171 200: [1530, 1520, 1522, 1525] }, # profile at 200 m range
172 index=[0, 10, 20, 30]) # depths of the profile entries in m
173 >>> env = bh.create_env(depth=20, soundspeed=ssp2)
175 The default environment has a constant water depth. A range dependent bathymetry
176 can be provided as a Nx2 array of (range, water depth):
178 >>> import bellhop as bh
179 >>> env = bh.create_env(depth=[[0,20], [300,10], [500,18], [1000,15]])
180 """
181 env = Environment()
183 # Apply user-provided values to environment
184 for k, v in kv.items():
185 if k not in env.keys():
186 raise KeyError('Unknown key: '+k)
188 # Convert everything to ndarray except DataFrames and scalars
189 if isinstance(v, _pd.DataFrame):
190 env[k] = v
191 elif _np.isscalar(v):
192 env[k] = v
193 else:
194 env[k] = _np.asarray(v, dtype=_np.float64)
196 return env
200def check_env(env: Environment) -> Environment:
201 """Check the validity of a underwater environment definition.
203 This function is automatically executed before any of the compute_ functions,
204 but must be called manually after setting environment parameters if you need to
205 query against defaults that may be affected.
207 Parameters
208 ----------
209 env : dict
210 Environment definition
212 Returns
213 -------
214 dict
215 Updated environment definition
217 Raises
218 ------
219 ValueError
220 If the environment is invalid
222 Examples
223 --------
224 >>> import bellhop as bh
225 >>> env = bh.create_env()
226 >>> env = check_env(env)
227 """
229 env._finalise()
230 return env.check()
233def check_env2d(env: Environment) -> Environment:
234 """Backwards compatibility for check_env"""
235 return check_env(env=env)
237def compute(
238 env: Union[Environment,List[Environment]],
239 model: Optional[Any] = None,
240 task: Optional[Any] = None,
241 debug: bool = False,
242 fname_base: Optional[str] = None
243 ) -> Union[ Any,
244 Environment,
245 Tuple[List[Environment], _pd.DataFrame]
246 ]:
247 """Compute Bellhop task(s) for given model(s) and environment(s).
249 Parameters
250 ----------
251 env : dict or list of dict
252 Environment definition (which includes the task specification)
253 model : str, optional
254 Propagation model to use (None to auto-select)
255 task : str or list of str, optional
256 Optional task or list of tasks ("arrivals", etc.)
257 debug : bool, default=False
258 Generate debug information for propagation model
259 fname_base : str, optional
260 Base file name for Bellhop working files, default (None), creates a temporary file
262 Returns
263 -------
264 dict
265 Single run result (and associated metadata) if only one computation is performed.
266 tuple of (list of dict, pandas.DataFrame)
267 List of results and an index DataFrame if multiple computations are performed.
269 Notes
270 -----
271 If any of env, model, and/or task are lists then multiple runs are performed
272 with a list of dictionary outputs returned. The ordering is based on loop iteration
273 but might not be deterministic; use the index DataFrame to extract and filter the
274 output logically.
276 Examples
277 --------
278 Single task based on reading a complete `.env` file:
279 >>> import bellhop as bh
280 >>> env = bh.read_env("...")
281 >>> output = bh.compute(env)
282 >>> assert output['task'] == "arrivals"
283 >>> bh.plot_arrivals(output['results'])
285 Multiple tasks:
286 >>> import bellhop as bh
287 >>> env = bh.create_env()
288 >>> output, ind_df = bh.compute(env,task=["arrivals", "eigenrays"])
289 >>> bh.plot_arrivals(output[0]['results'])
290 """
291 envs = env if isinstance(env, list) else [env]
292 models = model if isinstance(model, list) else [model]
293 tasks = task if isinstance(task, list) else [task]
294 results: List[Any] = []
295 for this_env in envs:
296 debug and print(f"Using environment: {this_env['name']}")
297 for this_model in models:
298 debug and print(f"Using model: {'[None] (default)' if this_model is None else this_model.get('name')}")
299 for this_task in tasks:
300 debug and print(f"Using task: {this_task}")
301 env_chk = check_env(this_env)
302 this_task = this_task or env_chk.get('task')
303 if this_task is None:
304 raise ValueError("Task must be specified in env or as parameter")
305 model_fn = _select_model(env_chk, this_task, this_model, debug)
306 results.append({
307 "name": env_chk["name"],
308 "model": this_model,
309 "task": this_task,
310 "results": model_fn.run(env_chk, this_task, debug, fname_base),
311 })
312 assert len(results) > 0, "No results generated"
313 index_df = _pd.DataFrame([
314 {
315 "i": i,
316 "name": r["name"],
317 "model": getattr(r["model"], "name", str(r["model"])) if r["model"] is not None else None,
318 "task": r["task"],
319 }
320 for i, r in enumerate(results)
321 ])
322 index_df.set_index("i", inplace=True)
323 if len(results) > 1:
324 return results, index_df
325 else:
326 return results[0]
328def _select_model(env: Environment,
329 task: str,
330 model: Optional[str] = None,
331 debug: bool = False
332 ) -> Any:
333 """Finds a model to use, or if a model is requested validate it.
335 Parameters
336 ----------
337 env : dict
338 The environment dictionary
339 task : str
340 The task to be computed
341 model : str, optional
342 Specified model to use
343 debug : bool, default=False
344 Whether to print diagnostics
346 Returns
347 -------
348 Bellhop
349 The model function to evaluate its `.run()` method
351 Notes
352 -----
353 The intention of this function is to allow multiple models to be "loaded" and the
354 first appropriate model found is used for the computation.
356 This is likely to be more useful once we extend the code to handle things like 3D
357 bellhop models, GPU bellhop models, and so on.
358 """
359 if model is not None:
360 for m in _models:
361 if m.name == model:
362 debug and print(f'Model selected: {m.name}')
363 return m
364 raise ValueError(f"Unknown model: '{model}'")
366 debug and print("Searching for propagation model:")
367 for mm in _models:
368 if mm.supports(env, task):
369 debug and print(f'Model found: {mm.name}')
370 return mm
371 raise ValueError('No suitable propagation model available')
373def compute_arrivals(env: Environment, model: Optional[Any] = None, debug: bool = False, fname_base: Optional[str] = None) -> Any:
374 """Compute arrivals between each transmitter and receiver.
376 Parameters
377 ----------
378 env : dict
379 Environment definition
380 model : str, optional
381 Propagation model to use (None to auto-select)
382 debug : bool, default=False
383 Generate debug information for propagation model
384 fname_base : str, optional
385 Base file name for Bellhop working files, default (None), creates a temporary file
387 Returns
388 -------
389 pandas.DataFrame
390 Arrival times and coefficients for all transmitter-receiver combinations
392 Examples
393 --------
394 >>> import bellhop as bh
395 >>> env = bh.create_env()
396 >>> arrivals = bh.compute_arrivals(env)
397 >>> bh.plot_arrivals(arrivals)
398 """
399 output = compute(env, model, _Strings.arrivals, debug, fname_base)
400 assert isinstance(output, dict), "Single env should return single result"
401 return output['results']
403def compute_eigenrays(env: Environment, source_depth_ndx: int = 0, receiver_depth_ndx: int = 0, receiver_range_ndx: int = 0, model: Optional[Any] = None, debug: bool = False, fname_base: Optional[str] = None) -> Any:
404 """Compute eigenrays between a given transmitter and receiver.
406 Parameters
407 ----------
408 env : dict
409 Environment definition
410 source_depth_ndx : int, default=0
411 Transmitter depth index
412 receiver_depth_ndx : int, default=0
413 Receiver depth index
414 receiver_range_ndx : int, default=0
415 Receiver range index
416 model : str, optional
417 Propagation model to use (None to auto-select)
418 debug : bool, default=False
419 Generate debug information for propagation model
420 fname_base : str, optional
421 Base file name for Bellhop working files, default (None), creates a temporary file
423 Returns
424 -------
425 pandas.DataFrame
426 Eigenrays paths
428 Examples
429 --------
430 >>> import bellhop as bh
431 >>> env = bh.create_env()
432 >>> rays = bh.compute_eigenrays(env)
433 >>> bh.plot_rays(rays, width=1000)
434 """
435 env = check_env(env)
436 env = env.copy()
437 if _np.size(env['source_depth']) > 1:
438 env['source_depth'] = env['source_depth'][source_depth_ndx]
439 if _np.size(env['receiver_depth']) > 1:
440 env['receiver_depth'] = env['receiver_depth'][receiver_depth_ndx]
441 if _np.size(env['receiver_range']) > 1:
442 env['receiver_range'] = env['receiver_range'][receiver_range_ndx]
443 output = compute(env, model, _Strings.eigenrays, debug, fname_base)
444 assert isinstance(output, dict), "Single env should return single result"
445 return output['results']
447def compute_rays(env: Environment, source_depth_ndx: int = 0, model: Optional[Any] = None, debug: bool = False, fname_base: Optional[str] = None) -> Any:
448 """Compute rays from a given transmitter.
450 Parameters
451 ----------
452 env : dict
453 Environment definition
454 source_depth_ndx : int, default=0
455 Transmitter depth index
456 model : str, optional
457 Propagation model to use (None to auto-select)
458 debug : bool, default=False
459 Generate debug information for propagation model
460 fname_base : str, optional
461 Base file name for Bellhop working files, default (None), creates a temporary file
463 Returns
464 -------
465 pandas.DataFrame
466 Ray paths
468 Examples
469 --------
470 >>> import bellhop as bh
471 >>> env = bh.create_env()
472 >>> rays = bh.compute_rays(env)
473 >>> bh.plot_rays(rays, width=1000)
474 """
475 env = check_env(env)
476 if _np.size(env['source_depth']) > 1:
477 env = env.copy()
478 env['source_depth'] = env['source_depth'][source_depth_ndx]
479 output = compute(env, model, _Strings.rays, debug, fname_base)
480 assert isinstance(output, dict), "Single env should return single result"
481 return output['results']
483def compute_transmission_loss(env: Environment, source_depth_ndx: int = 0, mode: Optional[str] = None, model: Optional[Any] = None, debug: bool = False, fname_base: Optional[str] = None) -> Any:
484 """Compute transmission loss from a given transmitter to all receviers.
486 Parameters
487 ----------
488 env : dict
489 Environment definition
490 source_depth_ndx : int, default=0
491 Transmitter depth index
492 mode : str, optional
493 Coherent, incoherent or semicoherent
494 model : str, optional
495 Propagation model to use (None to auto-select)
496 debug : bool, default=False
497 Generate debug information for propagation model
498 fname_base : str, optional
499 Base file name for Bellhop working files, default (None), creates a temporary file
501 Returns
502 -------
503 numpy.ndarray
504 Complex transmission loss at each receiver depth and range
506 Examples
507 --------
508 >>> import bellhop as bh
509 >>> env = bh.create_env()
510 >>> tloss = bh.compute_transmission_loss(env, mode=bh.incoherent)
511 >>> bh.plot_transmission_loss(tloss, width=1000)
512 """
513 env = env.copy()
514 task = mode or env.get("interference_mode") or Defaults.interference_mode
515 env['interference_mode'] = task
516 debug and print(f" {task=}")
517 env = check_env(env)
518 if _np.size(env['source_depth']) > 1:
519 env['source_depth'] = env['source_depth'][source_depth_ndx]
520 output = compute(env, model, task, debug, fname_base)
521 assert isinstance(output, dict), "Single env should return single result"
522 return output['results']
524def arrivals_to_impulse_response(arrivals: Any, fs: float, abs_time: bool = False) -> Any:
525 """Convert arrival times and coefficients to an impulse response.
527 Parameters
528 ----------
529 arrivals : pandas.DataFrame
530 Arrivals times (s) and coefficients
531 fs : float
532 Sampling rate (Hz)
533 abs_time : bool, default=False
534 Absolute time (True) or relative time (False)
536 Returns
537 -------
538 numpy.ndarray
539 Impulse response
541 Notes
542 -----
543 If `abs_time` is set to True, the impulse response is placed such that
544 the zero time corresponds to the time of transmission of signal.
546 Examples
547 --------
548 >>> import bellhop as bh
549 >>> env = bh.create_env()
550 >>> arrivals = bh.compute_arrivals(env)
551 >>> ir = bh.arrivals_to_impulse_response(arrivals, fs=192000)
552 """
553 t0 = 0 if abs_time else min(arrivals.time_of_arrival)
554 irlen = int(_np.ceil((max(arrivals.time_of_arrival)-t0)*fs))+1
555 ir = _np.zeros(irlen, dtype=_np.complex128)
556 for _, row in arrivals.iterrows():
557 ndx = int(_np.round((row.time_of_arrival.real-t0)*fs))
558 ir[ndx] = row.arrival_amplitude
559 return ir
561### Export module names for auto-importing in __init__.py
563__all__ = [
564 name for name in globals() if not name.startswith("_") # ignore private names
565]