Source code for uwacan.propagation

  1"""Classes for modeling and compensating propagation of sound.
  2
  3Main propagation models
  4-----------------------
  5.. autosummary::
  6    :toctree: generated
  7
  8    MlogR
  9    SmoothLloydMirror
 10    SeabedCriticalAngle
 11
 12Utilites
 13--------
 14.. autosummary::
 15    :toctree: generated
 16
 17    cutoff_frequency
 18    perkins_cutoff
 19    read_valeport_data
 20    seabed_properties
 21
 22Implementation interfaces
 23-------------------------
 24.. autosummary::
 25    :toctree: generated
 26
 27    PropagationModel
 28    NonlocalPropagationModel
 29
 30"""
 31
 32import numpy as np
 33import abc
 34from . import _core, spectral
 35import xarray as xr
 36
 37
[docs] 38class PropagationModel(abc.ABC): 39 """Base class for all propagation models with compensation.""" 40
[docs] 41 @abc.abstractmethod 42 def compensate_propagation(self, received_power, receiver, source): 43 """Compensate the propagation of measured power. 44 45 This takes a sound power measured at a receiver and compensates 46 the propagation from a source. 47 The ``received_power``, ``receiver``, and ``source`` can all have 48 a time axis, but then they should be aligned. 49 50 Parameters 51 ---------- 52 received_power : `~uwacan.FrequencyData` 53 The measured power. 54 receiver : `~uwacan.positional.Coordinates` 55 The placement of the receiver. Has latitude, longitude, and optionally depth. 56 source : `~uwacan.positional.Coordinates` 57 The placement of the source. Has latitude, longitude, and optionally depth. 58 59 Returns 60 ------- 61 source_power : `~uwacan.FrequencyData` 62 The source power. 63 64 Notes 65 ----- 66 Subclasses should implement this function to apply the compensation. 67 """
68 69
[docs] 70class NonlocalPropagationModel(PropagationModel): 71 """Class for propagation models and only depend on relative coordinates.""" 72
[docs] 73 @abc.abstractmethod 74 def power_propagation(self, distance, frequency, receiver_depth, source_depth): 75 """Compute the propagation factor. 76 77 The propagation factor is the ratio of received power to sent power. 78 Typically this is a value smaller than one, and is decreasing with 79 increasing distances. 80 81 Parameters 82 ---------- 83 distance : `xarray.DataArray` 84 The horizontal distance between source and receiver. 85 frequency : `xarray.DataArray` 86 The frequency to evaluate at. 87 receiver_depth : `xarray.DataArray` 88 The depth of the receiver. 89 source_depth : `xarray.DataArray` 90 The depth of the source. 91 92 Returns 93 ------- 94 F : `xarray.DataArray` 95 The evaluated propagation factor. 96 97 Notes 98 ----- 99 Subclasses must implement this method to compute the propagation factor. 100 The method has to accept all the arguments, but can ignore some of them 101 if they are not relevant. 102 """ 103 return 1
104
[docs] 105 @staticmethod 106 def slant_range(horizontal_distance, receiver_depth, source_depth=None): 107 """Compute the slant range from source to receiver. 108 109 Parameters 110 ---------- 111 horizontal_distance : array_like 112 The horizontal distance between source and receiver. 113 receiver_depth : array_like 114 The depth below surface of the receiver. 115 source_depth : array_like 116 The depth below surface of the source. 117 If the source depth is None, it defaults to 0. 118 119 Returns 120 ------- 121 slant_range : array_like or None 122 The computed slant range if the receiver depth is not None, 123 otherwise None. 124 """ 125 if receiver_depth is None: 126 return None 127 # Optionally used to calculate the distance between source and receiver, instead of source surface to receiver. 128 source_depth = source_depth or 0 129 return (horizontal_distance**2 + (receiver_depth - source_depth) ** 2) ** 0.5
130
[docs] 131 def compensate_propagation(self, received_power, receiver, source): # noqa: D102, takes the docstring from the superclass 132 distance = receiver.distance_to(source) 133 try: 134 receiver_depth = receiver.depth 135 except AttributeError: 136 try: 137 receiver_depth = receiver["depth"] 138 except KeyError: 139 receiver_depth = None 140 141 try: 142 source_depth = source.depth 143 except AttributeError: 144 try: 145 source_depth = source["depth"] 146 except KeyError: 147 source_depth = None 148 149 try: 150 frequency = received_power.frequency 151 except AttributeError: 152 frequency = None 153 154 power_loss = self.power_propagation( 155 distance=distance, 156 frequency=frequency, 157 source_depth=source_depth, 158 receiver_depth=receiver_depth, 159 ) 160 return received_power / power_loss
161 162
[docs] 163class MlogR(NonlocalPropagationModel): 164 """Geometrical spreading loss model. 165 166 This implements a simple ``m log(r) + offset`` model 167 168 Parameters 169 ---------- 170 m : numeric, default 20 171 The spreading factor. 172 ``m=20`` gives spherical spreading, ``m=10`` gives cylindrical spreading. 173 offset : numeric, default 0 174 The offset to the propagation model. 175 176 Notes 177 ----- 178 This models calculates the fraction of power lost due 179 to geometrical spreading of the energy, i.e:: 180 181 F = distance**(-m / 10) * 10 ** (-offset / 10) 182 183 The distance is the slant range if a receiver depth is 184 given, and the horizontal range otherwise. 185 """ 186 187 def __init__(self, m=20, offset=0, **kwargs): 188 super().__init__(**kwargs) 189 self.m = m 190 self.offset = offset 191
[docs] 192 def power_propagation(self, distance, receiver_depth=None, **kwargs): # noqa: D417, kwargs not documented 193 """Calculate simple geometrical spreading. 194 195 Parameters 196 ---------- 197 distance : array_like 198 The horizontal distance between source and receiver. 199 receiver_depth : array_like, optional 200 An optional receiver depth. If given, the spreading is 201 evaluated on the slant range instead of the horizontal distance. 202 203 Returns 204 ------- 205 F : `xarray.DataArray` 206 The evaluated propagation factor. 207 """ 208 if receiver_depth is not None: 209 distance = self.slant_range(distance, receiver_depth) 210 return distance ** (-self.m / 10) * 10 ** (-self.offset / 10)
211 212
[docs] 213class SmoothLloydMirror(MlogR): 214 """Geometrical spreading and average Lloyd mirror reflection loss model. 215 216 This model compensates geometrical spreading as well as source interaction with the water surface. 217 218 Parameters 219 ---------- 220 m : int, default 20 221 The spreading factor. 222 ``m=20`` gives spherical spreading, ``m=10`` gives cylindrical spreading. 223 speed_of_sound : numeric, default 1500 224 The speed of sound in the water, used to calculate wave numbers. 225 226 Notes 227 ----- 228 For high frequencies the surface interaction is a plain factor ``hf = 2``, 229 since we are interested in the average value. I.e., the surface boosts the 230 radiation output. 231 For low frequencies, the interaction factor is:: 232 233 lf = (2kd sin(θ))**2 234 235 where θ is the grazing angle, measured to the receiver from the surface above the source. 236 The low and high frequencies are mixed as:: 237 238 F_s = 1 / (1 / lf + 1 / hf). 239 240 This surface factor F_s is then multiplied with the geometrical spreading:: 241 242 F_g = distance**(-m / 10) * 10 ** (-offset / 10) 243 244 with the distance evaluated using the slant range. 245 """ 246 247 def __init__(self, m=20, speed_of_sound=1500, **kwargs): 248 super().__init__(m=m, **kwargs) 249 self.speed_of_sound = speed_of_sound 250
[docs] 251 def power_propagation(self, distance, frequency, receiver_depth, source_depth, **kwargs): # noqa: D417, ignored kwargs doc 252 """Calculate surface interactions and geometrical spreading. 253 254 Parameters 255 ---------- 256 distance : array_like 257 The horizontal distance between source and receiver. 258 frequency : array_like 259 The frequencies at which to evaluate. 260 receiver_depth : array_like 261 The receiver depth. Used to compute grazing angles and slant ranges. 262 source_depth : array_like 263 The source depth. Used to compute the ``kd`` term. 264 265 Returns 266 ------- 267 F : `xarray.DataArray` 268 The evaluated propagation factor. 269 """ 270 geometric_spreading = super().power_propagation( 271 distance=distance, frequency=frequency, receiver_depth=receiver_depth, source_depth=source_depth, **kwargs 272 ) 273 274 kd = 2 * np.pi * frequency * source_depth / self.speed_of_sound 275 slant_range = self.slant_range(distance, receiver_depth) 276 mirror_lf = 4 * kd**2 * (receiver_depth / slant_range) ** 2 277 mirror_hf = 2 278 mirror_reduction = 1 / (1 / mirror_lf + 1 / mirror_hf) 279 280 return geometric_spreading * mirror_reduction
281 282
[docs] 283class SeabedCriticalAngle(SmoothLloydMirror): 284 """The seabed critical angle propagation model. 285 286 This model accounts for geometrical spreading, surface interactions, and simple bottom interactions. 287 288 Parameters 289 ---------- 290 water_depth : numeric 291 The water depth to use for the cylindrical spreading. 292 n : numeric, default 10 293 The geometrical spreading factor to use for the cylindrical spreading. 294 m : numeric, default 20 295 The geometrical spreading factor to use for the spherical spreading. 296 speed_of_sound : numeric, default 1500 297 The speed of sound in the water. Used to calculate wave numbers and the critical angle. 298 substrate_compressional_speed, numeric, default 1500 299 The speed of sound in the water. Used to calculate the critical angle. 300 301 Notes 302 ----- 303 The model is split in two parts, one spherical and one cylindrical. The spherical part is 304 identical to the `SmoothLloydMirror` model, and gives the propagation factor:: 305 306 F_sphere = SmoothLloydMirror(...).power_propagation(...) 307 308 The general idea for the cylindrical part is that power radiated towards the bottom will either 309 stay in the water column, and thus arrive at the receiver at some point, 310 or get transmitted into the substrate. 311 The grazing angle below which power will be contained is the critical angle ψ. 312 For high frequencies, the bottom retains an average factor:: 313 314 hf = 2ψ 315 316 of the energy. 317 For low frequencies, we have a retention of:: 318 319 lf = 2 (kd)**2 (ψ - sin(ψ) cos(ψ)) 320 321 The low-high frequency mixing and the distance propagation is then done as:: 322 323 F_cylindrical = 1 / (rH) / (1 / lf + 1 / hf) 324 325 where we use the same low-high frequency mixing as for smooth Lloyd mirror, 326 but 1 / (rH) to accommodate the cylindrical domain. 327 The final propagation factor is the sum of the spherical and cylindrical energy:: 328 329 F = F_spherical + F_cylindrical 330 331 """ 332 333 def __init__(self, water_depth, n=10, substrate_compressional_speed=1500, **kwargs): 334 super().__init__(**kwargs) 335 self.n = n 336 self.substrate_compressional_speed = substrate_compressional_speed 337 self.water_depth = water_depth 338
[docs] 339 def power_propagation(self, distance, frequency, receiver_depth, source_depth, **kwargs): # noqa: D417, no docs for kwargs 340 """Calculate geometrical spreading and interactions with the surface and the bottom. 341 342 Parameters 343 ---------- 344 distance : array_like 345 The horizontal distance between source and receiver. 346 frequency : array_like 347 The frequencies at which to evaluate. 348 receiver_depth : array_like 349 The receiver depth. Used to compute grazing angles and slant ranges. 350 source_depth : array_like 351 The source depth. Used to compute the ``kd`` term. 352 353 Returns 354 ------- 355 F : `xarray.DataArray` 356 The evaluated propagation factor. 357 """ 358 surface_effect = super().power_propagation( 359 distance=distance, frequency=frequency, receiver_depth=receiver_depth, source_depth=source_depth, **kwargs 360 ) 361 362 slant_range = self.slant_range(distance, receiver_depth) 363 critical_angle = np.arccos(self.speed_of_sound / self.substrate_compressional_speed) 364 kd = 2 * np.pi * frequency * source_depth / self.speed_of_sound 365 lf_approx = 2 * kd**2 * (critical_angle - np.sin(critical_angle) * np.cos(critical_angle)) 366 hf_approx = 2 * critical_angle 367 cylindrical_spreading = 1 / (self.water_depth * slant_range ** (self.n / 10)) 368 bottom_effect = 1 / (1 / lf_approx + 1 / hf_approx) 369 370 return surface_effect + bottom_effect * cylindrical_spreading
371 372
[docs] 373def cutoff_frequency(water_depth, substrate_compressional_speed=np.inf, speed_of_sound=1500): 374 """Calculate cutoff frequency for shallow waters. 375 376 For shallow waters, the long distance cutoff describes 377 a frequency below which there will be no waveguide. 378 379 Parameters 380 ---------- 381 water_depth : array_like 382 The water depth, in meters. 383 substrate_compressional_speed : array_like 384 The speed of sound in the seabed, in m/s. 385 speed_of_sound : array_like, default=1500 386 The speed of sound in the water, in m/s. 387 388 Notes 389 ----- 390 This is evaluated as [1]_:: 391 392 speed_ratio = speed_of_sound / substrate_compressional_speed 393 speed_of_sound / (water_depth * 4 * (1 - speed_ratio**2)**0.5) 394 395 This means that the cutoff frequency goes up for softer substrates, 396 and up for shallower depths. 397 398 References 399 ---------- 400 .. [1] F. B. Jensen, Wi. A. Kuperman, M. B. Porter, and H. Schmidt, 401 "Computational Ocean Acoustics", 2nd ed. Springer New York, 2011. 402 Eq. (1.38). 403 """ 404 speed_ratio = speed_of_sound / substrate_compressional_speed 405 return speed_of_sound / (water_depth * 4 * (1 - speed_ratio**2) ** 0.5)
406 407
[docs] 408def perkins_cutoff(water_depth, substrate_compressional_speed=np.inf, speed_of_sound=1500, mode_order=1): 409 """Calculate modal cutoff for shallow waters. 410 411 For shallow waters, the long distance cutoff describes 412 a frequency below which a specific mode cannot propagate. 413 414 Parameters 415 ---------- 416 water_depth : array_like 417 The water depth, in meters. 418 substrate_compressional_speed : array_like 419 The speed of sound in the seabed, in m/s. 420 speed_of_sound : array_like, default=1500 421 The speed of sound in the water, in m/s. 422 mode_order : array_like, default=1 423 Which mode order to evaluate. 424 A mode order of 1 yields the same as `cutoff_frequency`. 425 426 Notes 427 ----- 428 This is evaluated as [1]_:: 429 430 speed_ratio = speed_of_sound / substrate_compressional_speed 431 (mode_order - 0.5) * speed_of_sound / (2 * water_depth * (1 - speed_ratio**2)**0.5) 432 433 This means that the cutoff frequency goes up for softer substrates, 434 and up for shallower depths, and is higher for higher modes. 435 436 References 437 ---------- 438 .. [1] F. B. Jensen, Wi. A. Kuperman, M. B. Porter, and H. Schmidt, 439 "Computational Ocean Acoustics", 2nd ed. Springer New York, 2011. 440 Eq. (2.191). 441 """ 442 speed_ratio = speed_of_sound / substrate_compressional_speed 443 return (mode_order - 0.5) * speed_of_sound / (2 * water_depth * (1 - speed_ratio**2) ** 0.5)
444 445 446seabed_properties = { 447 "very coarse sand": { 448 "grain size": -0.5, 449 "speed of sound": 1500 * 1.307, 450 }, 451 "coarse sand": { 452 "grain size": 0.5, 453 "speed of sound": 1500 * 1.250, 454 }, 455 "medium sand": { 456 "grain size": 1.5, 457 "speed of sound": 1500 * 1.198, 458 }, 459 "fine sand": { 460 "grain size": 2.5, 461 "speed of sound": 1500 * 1.152, 462 }, 463 "very fine sand": { 464 "grain size": 3.5, 465 "speed of sound": 1500 * 1.112, 466 }, 467 "coarse silt": { 468 "grain size": 4.5, 469 "speed of sound": 1500 * 1.077, 470 }, 471 "medium silt": { 472 "grain size": 5.5, 473 "speed of sound": 1500 * 1.048, 474 }, 475 "fine silt": { 476 "grain size": 6.5, 477 "speed of sound": 1500 * 1.024, 478 }, 479 "very fine silt": { 480 "grain size": 7.5, 481 "speed of sound": 1500 * 1.005, 482 }, 483} 484"""Dict with seabed properties. 485 486Properties included are grain size and speed of sound (compressional). 487Included substrates are 488 489- very coarse sand 490- coarse sand 491- medium sand 492- fine sand 493- very fine sand 494- coarse silt 495- medium silt 496- fine silt 497- very fine silt 498 499These substrates are the keys to the dict. 500 501Based on Ainslie, M.A. Principles of Sonar Performance Modeling, Springer-Verlag Berlin Heidelberg, 2010. 502""" 503 504
[docs] 505def read_valeport_data(filepath): 506 """Read Valeport swift data. 507 508 This reads data stored in the ``.vp2`` format. 509 """ 510 from io import StringIO 511 import pandas 512 import re 513 import xarray as xr 514 515 with open(filepath, "r") as file: 516 contents = file.read() 517 518 lat = float(re.search(r"Latitude=([\d.]*)", contents).groups()[0]) 519 lon = float(re.search(r"Longitude=([\d.]*)", contents).groups()[0]) 520 521 data_idx = contents.find("[DATA]") 522 data = contents[data_idx:].splitlines() 523 data_stream = StringIO("\n".join([data[1].strip()] + data[3:])) 524 df = pandas.read_csv(data_stream, delimiter="\t", parse_dates=["Date/Time"]) 525 ds = xr.Dataset.from_dataframe(df).set_coords("Depth").swap_dims(index="Depth").drop_vars("index") 526 return ds.assign(latitude=lat, longitude=lon)
527 528 529class Sweep: 530 @staticmethod 531 def modified_sweep_duration(duration, lower, upper): 532 k = round(lower * duration / np.log(upper / lower)) 533 return k * np.log(upper / lower) / lower 534 535 def __init__(self, duration, lower, upper, fade_in=None, fade_out=None, modify_duration=True): 536 if modify_duration: 537 duration = self.modified_sweep_duration(duration=duration, lower=lower, upper=upper) 538 self.duration = duration 539 self.lower = lower 540 self.upper = upper 541 self.fade_in = fade_in 542 self.fade_out = fade_out 543 544 @property 545 def phase_rate(self): 546 return self.duration / np.log(self.upper / self.lower) 547 548 def make_signal(self, samplerate, prepad=0, postpad=0): 549 num_samples = int(self.duration * samplerate) 550 t = np.arange(num_samples) / samplerate 551 sweep = np.sin(2 * np.pi * self.lower * self.phase_rate * np.exp(t / self.phase_rate)) 552 553 if self.fade_in: 554 fade_samples = int(self.fade_in * samplerate) 555 fade = np.sin(np.linspace(0, np.pi / 2, fade_samples))**2 556 sweep[:fade_samples] *= fade 557 if self.fade_out: 558 fade_samples = int(self.fade_out * samplerate) 559 fade = np.sin(np.linspace(np.pi / 2, 0, fade_samples))**2 560 sweep[-fade_samples:] *= fade 561 562 prepad = np.zeros(round(prepad * samplerate)) 563 postpad = np.zeros(round(postpad * samplerate)) 564 sweep = np.concatenate([prepad, sweep, postpad]) 565 return sweep 566 567 def deconvolve(self, response, nfft=None, samplerate=None): 568 if isinstance(response, _core.TimeData): 569 return _core.FrequencyData( 570 self.deconvolve(response.data, nfft=nfft), 571 ) 572 if isinstance(response, xr.DataArray): 573 nfft = nfft or response.time.size 574 samplerate = samplerate or response.time.rate 575 freq_data = xr.apply_ufunc( 576 self.deconvolve, 577 response, 578 input_core_dims=[["time"]], 579 output_core_dims=[["frequency"]], 580 kwargs={"nfft": nfft, "samplerate": samplerate}, 581 ) 582 freq_data.coords["frequency"] = np.fft.rfftfreq(nfft, 1 / samplerate) 583 center_offset = np.timedelta64(round(response.time.size / 2 / samplerate * 1e9), "ns") 584 freq_data.coords["time"] = response.time[0] + center_offset 585 freq_data.coords["start_time"] = response.time[0] 586 return freq_data 587 588 nfft = nfft or response.shape[-1] 589 if samplerate is None: 590 raise ValueError("Must supply samplerate to perform deconvolution") 591 592 # Sweep starts at the beginning of recording, nfft > sweep.size so it will be padded at the end. 593 # This means the conjugate (time-reversal) has the inverse sweep at the end. 594 # Now, the cyclic nature of the fft will effectively wrap the response to the beginning of the sweep, 595 # to that the pulse gets compressed to the start of the received signal. 596 sweep = self.make_signal(samplerate=samplerate, prepad=0, postpad=0) 597 sweep_spectrum = np.fft.rfft(sweep, n=nfft) / samplerate 598 response_spectrum = np.fft.rfft(response, n=nfft) / samplerate 599 600 # The sweep magnitude is (phase_rate / f)**0.5 / (2 * samplerate) 601 # It's in both the sent and received, so we divide by the square. 602 f = np.fft.rfftfreq(nfft, 1 / samplerate) 603 with np.errstate(divide="ignore"): 604 normalization = (self.phase_rate / f)**0.5 / 2 605 606 frequency_response = response_spectrum * sweep_spectrum.conjugate() / normalization**2 607 return frequency_response 608 609 def trim_frequency_response(self, frequency_response, pretrim, posttrim, sweep_start=None, fade_in=0, fade_out=0): 610 if isinstance(frequency_response, _core.FrequencyData): 611 return _core.FrequencyData( 612 self.trim_frequency_response( 613 frequency_response.data, 614 pretrim=pretrim, posttrim=posttrim, 615 sweep_start=sweep_start, 616 fade_in=fade_in, fade_out=fade_out, 617 ), 618 ) 619 620 h = spectral.ifft(frequency_response) 621 h_trim = self.trim_impulse_response( 622 h, 623 pretrim=pretrim, posttrim=posttrim, 624 sweep_start=sweep_start, 625 fade_in=fade_in, fade_out=fade_out, 626 ) 627 return spectral.fft(h_trim) 628 629 def trim_impulse_response(self, impulse_response, pretrim, posttrim, sweep_start=None, fade_in=0, fade_out=0): 630 if isinstance(impulse_response, _core.TimeData): 631 return _core.TimeData( 632 self.trim_impulse_response( 633 impulse_response.data, 634 pretrim=pretrim, posttrim=posttrim, 635 sweep_start=sweep_start, 636 fade_in=fade_in, fade_out=fade_out, 637 ), 638 ) 639 if isinstance(impulse_response, xr.DataArray): 640 if sweep_start is None: 641 sweep_start = int(np.abs(impulse_response).argmax()) 642 else: 643 sweep_start = _core.time_to_np(sweep_start) 644 sweep_start = int(abs(impulse_response.time - sweep_start).argmin()) 645 pretrim = int(pretrim * impulse_response.time.rate) 646 posttrim = int(posttrim * impulse_response.time.rate) 647 fade_in = int(fade_in * impulse_response.time.rate) 648 fade_out = int(fade_out * impulse_response.time.rate) 649 650 if sweep_start is None: 651 sweep_start = np.argmax(np.abs(impulse_response)) 652 653 start_idx = max(sweep_start - pretrim, 0) 654 stop_idx = sweep_start + posttrim 655 656 if isinstance(impulse_response, xr.DataArray): 657 trimmed = impulse_response.isel(time=slice(start_idx, stop_idx)) 658 else: 659 trimmed = impulse_response[..., start_idx:stop_idx] 660 661 if fade_in or fade_out: 662 if isinstance(impulse_response, xr.DataArray): 663 window = np.ones(trimmed.time.size) 664 else: 665 window = np.ones(trimmed.shape[-1]) 666 if fade_in: 667 window[:fade_in] = np.sin(np.linspace(0, np.pi / 2, fade_in))**2 668 if fade_out: 669 window[-fade_out:] = np.sin(np.linspace(np.pi / 2, 0, fade_out))**2 670 trimmed = trimmed * window 671 return trimmed 672 673 def frequency_over_time(self, t0, f0, t=None): 674 t0 = _core.time_to_np(t0) 675 def f(t): 676 dt = (t - t0) / np.timedelta64(1, "s") 677 f = f0 * np.exp(dt / self.phase_rate) 678 f = f[(self.lower<=f) & (f<=self.upper)] 679 return f 680 if t is not None: 681 return f(t) 682 return f 683 684 def average_in_bands(self, frequency_response, bands_per_decade): 685 if isinstance(frequency_response, _core.FrequencyData): 686 return _core.FrequencyData(self.average_in_bands(frequency_response.data, bands_per_decade)) 687 # Bands that allow just a little outside the swept range, due to numerical issues with exact band edges. 688 centers, (lowers, uppers) = spectral.nth_decade_band_edges( 689 bands_per_decade=bands_per_decade, 690 min_frequency=self.lower * 0.99, 691 max_frequency=self.upper / 0.99, 692 limit_to="strict" 693 ) 694 if not isinstance(frequency_response, xr.DataArray): 695 raise TypeError(f"Cannot compute the band average of frequency response of type `{frequency_response.__class__.__name__}`") 696 banded = spectral.linear_to_banded( 697 np.abs(frequency_response.data)**2, 698 lowers, uppers, 699 spectral_resolution=frequency_response.frequency[1].item(), 700 axis=frequency_response.get_axis_num("frequency"), 701 ) 702 return xr.DataArray( 703 banded ** 0.5, 704 coords={ 705 "frequency": centers, 706 "bandwidth": ("frequency", uppers - lowers) 707 }, 708 dims=frequency_response.dims, 709 )