Coverage for kwave/ktransducer.py: 17%
293 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-24 12:06 -0700
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-24 12:06 -0700
1import logging
3import numpy as np
5from kwave.kgrid import kWaveGrid
6from kwave.ksensor import kSensor
7from kwave.utils.checks import is_number
8from kwave.utils.data import get_smallest_possible_type
9from kwave.utils.matlab import matlab_find, matlab_mask, unflatten_matlab_mask
10from kwave.utils.matrix import expand_matrix
11from kwave.utils.signals import get_win
14# force value to be a positive integer
15def make_pos_int(val):
16 return np.abs(val).astype(int)
19class kWaveTransducerSimple(object):
20 def __init__(
21 self,
22 kgrid: kWaveGrid,
23 number_elements=128,
24 element_width=1,
25 element_length=20,
26 element_spacing=0,
27 position=None,
28 radius=float("inf"),
29 ):
30 """
31 Args:
32 kgrid: kWaveGrid object
33 number_elements: the total number of transducer elements
34 element_width: the width of each element in grid points
35 element_length: the length of each element in grid points
36 element_spacing: the spacing (kerf width) between the transducer elements in grid points
37 position: the position of the corner of the transducer in the grid
38 radius: the radius of curvature of the transducer [m]
40 """
42 # allocate the grid size and spacing
43 self.stored_grid_size = [kgrid.Nx, kgrid.Ny, kgrid.Nz] # size of the grid in which the transducer is defined
44 self.grid_spacing = [kgrid.dx, kgrid.dy, kgrid.dz] # corresponding grid spacing
46 self.number_elements = make_pos_int(number_elements)
47 self.element_width = make_pos_int(element_width)
48 self.element_length = make_pos_int(element_length)
49 self.element_spacing = make_pos_int(element_spacing)
51 if position is None:
52 position = [1, 1, 1]
53 self.position = make_pos_int(position)
55 assert np.isinf(radius), "Only a value of transducer.radius = inf is currently supported"
56 self.radius = radius
58 # check the transducer fits into the grid
59 if np.sum(self.position == 0):
60 raise ValueError("The defined transducer must be positioned within the grid")
61 elif (
62 self.position[1] + self.number_elements * self.element_width + (self.number_elements - 1) * self.element_spacing
63 ) > self.stored_grid_size[1]:
64 raise ValueError("The defined transducer is too large or positioned outside the grid in the y-direction")
65 elif (self.position[2] + self.element_length) > self.stored_grid_size[2]:
66 logging.log(logging.INFO, self.position[2])
67 logging.log(logging.INFO, self.element_length)
68 logging.log(logging.INFO, self.stored_grid_size[2])
69 raise ValueError("The defined transducer is too large or positioned outside the grid in the z-direction")
70 elif self.position[0] > self.stored_grid_size[0]:
71 raise ValueError("The defined transducer is positioned outside the grid in the x-direction")
73 @property
74 def element_pitch(self):
75 return (self.element_spacing + self.element_width) * self.grid_spacing[1]
77 @property
78 def transducer_width(self):
79 """
81 Total width of the transducer in grid points
83 Returns:
84 the overall length of the transducer
86 """
87 return self.number_elements * self.element_width + (self.number_elements - 1) * self.element_spacing
90class NotATransducer(kSensor):
91 def __init__(
92 self,
93 transducer: kWaveTransducerSimple,
94 kgrid: kWaveGrid,
95 active_elements=None,
96 focus_distance=float("inf"),
97 elevation_focus_distance=float("inf"),
98 receive_apodization="Rectangular",
99 transmit_apodization="Rectangular",
100 sound_speed=1540,
101 input_signal=None,
102 steering_angle_max=None,
103 steering_angle=None,
104 ):
105 """
106 'time_reversal_boundary_data' and 'record' fields should not be defined
107 for the objects of this class
109 Args:
110 kgrid: kWaveGrid object
111 active_elements: the transducer elements that are currently active elements
112 elevation_focus_distance: the focus depth in the elevation direction [m]
113 receive_apodization: transmit apodization
114 transmit_apodization: receive apodization
115 sound_speed: sound speed used to calculate beamforming delays [m/s]
116 focus_distance: focus distance used to calculate beamforming delays [m]
117 input_signal:
118 steering_angle_max: max steering angle [deg]
119 steering_angle: steering angle [deg]
121 """
123 super().__init__()
124 assert isinstance(transducer, kWaveTransducerSimple)
125 self.transducer = transducer
126 # time index to start recording if transducer is used as a sensor
127 self.record_start_index = 1
128 # stored value of appended_zeros (accessed using get and set methods).
129 # This is used to set the number of zeros that are appended and prepended to the input signal.
130 self.stored_appended_zeros = "auto"
131 # stored value of the minimum beamforming delay.
132 # This is used to offset the delay mask so that all the delays are >= 0
133 self.stored_beamforming_delays_offset = "auto"
134 # stored value of the steering_angle_max (accessed using get and set methods).
135 # This can be set by the user and is used to derive the two parameters above.
136 self.stored_steering_angle_max = "auto"
137 # stored value of the steering_angle (accessed using get and set methods)
138 self.stored_steering_angle = 0
140 ####################################
141 # if the sensor is a transducer, check that the simulation is in 3D
142 assert kgrid.dim == 3, "Transducer inputs are only compatible with 3D simulations."
144 ####################################
146 # allocate the temporal spacing
147 if is_number(kgrid.dt):
148 self.dt = kgrid.dt
149 elif kgrid.t_array is not None:
150 self.dt = kgrid.t_array[1] - kgrid.t_array[0]
151 else:
152 raise ValueError("kgrid.dt or kgrid.t_array must be explicitly defined")
154 if active_elements is None:
155 active_elements = np.ones((transducer.number_elements, 1))
156 self.active_elements = active_elements
158 self.elevation_focus_distance = elevation_focus_distance
160 # check the length of the input
161 assert (
162 not is_number(receive_apodization) or len(receive_apodization) == self.number_active_elements
163 ), "The length of the receive apodization input must match the number of active elements"
164 self.receive_apodization = receive_apodization
166 # check the length of the input
167 assert (
168 not is_number(transmit_apodization) or len(transmit_apodization) == self.number_active_elements
169 ), "The length of the transmit apodization input must match the number of active elements"
170 self.transmit_apodization = transmit_apodization
172 # check to see the sound_speed is positive
173 assert sound_speed > 0, "transducer.sound_speed must be greater than 0"
174 self.sound_speed = sound_speed
176 self.focus_distance = focus_distance
178 if input_signal is not None:
179 input_signal = np.squeeze(input_signal)
180 assert input_signal.ndim == 1, "transducer.input_signal must be a one-dimensional array"
181 self.stored_input_signal = np.atleast_2d(input_signal).T # force the input signal to be a column vector
183 if steering_angle_max is not None:
184 # set the maximum steering angle using the set method (this avoids having to duplicate error checking)
185 self.steering_angle_max = steering_angle_max
187 if steering_angle is not None:
188 # set the maximum steering angle using the set method (this
189 # avoids having to duplicate error checking)
190 self.steering_angle = steering_angle
192 # assign the data type for the transducer matrix based on the
193 # number of different elements (uint8 supports 255 numbers so
194 # most of the time this data type will be used)
195 mask_type = get_smallest_possible_type(transducer.number_elements, "uint")
197 # create an empty transducer mask (the grid points within
198 # element 'n' are all given the value 'n')
199 assert transducer.stored_grid_size is not None
200 self.indexed_mask = np.zeros(transducer.stored_grid_size, dtype=mask_type)
202 # create a second empty mask used for the elevation beamforming
203 # delays (the grid points across each element are numbered 1 to
204 # M, where M is the number of grid points in the elevation
205 # direction)
206 self.indexed_element_voxel_mask = np.zeros(transducer.stored_grid_size, dtype=mask_type)
208 # create the corresponding indexing variable 1:M
209 element_voxel_index = np.tile(np.arange(transducer.element_length) + 1, (transducer.element_width, 1))
211 # for each transducer element, calculate the grid point indices
212 for element_index in range(0, transducer.number_elements):
213 # assign the current transducer position
214 element_pos_x = transducer.position[0]
215 element_pos_y = transducer.position[1] + (transducer.element_width + transducer.element_spacing) * element_index
216 element_pos_z = transducer.position[2]
218 element_pos_x = element_pos_x - 1
219 element_pos_y = element_pos_y - 1
220 element_pos_z = element_pos_z - 1
222 # assign the grid points within the current element the
223 # index of the element
224 self.indexed_mask[
225 element_pos_x,
226 element_pos_y : element_pos_y + transducer.element_width,
227 element_pos_z : element_pos_z + transducer.element_length,
228 ] = element_index + 1
230 # assign the individual grid points an index corresponding
231 # to their order across the element
232 self.indexed_element_voxel_mask[
233 element_pos_x,
234 element_pos_y : element_pos_y + transducer.element_width,
235 element_pos_z : element_pos_z + transducer.element_length,
236 ] = element_voxel_index
238 # double check the transducer fits within the desired grid size
239 assert (
240 np.array(self.indexed_mask.shape) == transducer.stored_grid_size
241 ).all(), "Desired transducer is larger than the input grid_size"
243 self.sxx = self.syy = self.szz = self.sxy = self.sxz = self.syz = None
244 self.u_mode = self.p_mode = None
245 self.ux = self.uy = self.uz = None
247 @staticmethod
248 def isfield(_):
249 # return field_name in dir(self)
250 # return eng.isfield(self.transducer, field_name)
251 return False # this method call was returning false always because Matlab 'isfield' calls are false for classes
253 def __contains__(self, item):
254 return self.isfield(item)
256 @property
257 def beamforming_delays(self):
258 """
259 calculate the beamforming delays based on the focus and steering settings
261 """
262 # calculate the element pitch in [m]
263 element_pitch = self.transducer.element_pitch
265 # create indexing variable
266 element_index = np.arange(-(self.number_active_elements - 1) / 2, (self.number_active_elements + 1) / 2)
268 # check for focus depth
269 if np.isinf(self.focus_distance):
270 # calculate time delays for a steered beam
271 delay_times = element_pitch * element_index * np.sin(self.steering_angle * np.pi / 180) / self.sound_speed # [s]
273 else:
274 # calculate time delays for a steered and focussed beam
275 delay_times = (
276 self.focus_distance
277 / self.sound_speed
278 * (
279 1
280 - np.sqrt(
281 1
282 + (element_index * element_pitch / self.focus_distance) ** 2
283 - 2 * (element_index * element_pitch / self.focus_distance) * np.sin(self.steering_angle * np.pi / 180)
284 )
285 )
286 ) # [s]
288 # convert the delays to be in units of time points
289 delay_times = (delay_times / self.dt).round().astype(int)
290 return delay_times
292 @property
293 def beamforming_delays_offset(self):
294 """
295 Offset used to make all the delays in the delay_mask positive (either set to 'auto' or based on the setting for steering_angle_max)
297 Returns:
298 the stored value of the offset used to force the values in delay_mask to be >= 0
300 """
302 return self.stored_beamforming_delays_offset
304 @property
305 def mask(self):
306 """
307 Allow mask query to allow compatibility with regular sensor structure - return the active sensor mask
309 """
311 return self.active_elements_mask
313 @property
314 def indexed_active_elements_mask(self):
315 # copy the indexed elements mask
316 mask = self.indexed_mask
317 if mask is None:
318 return None
320 mask = np.copy(mask)
322 # remove inactive elements from the mask
323 for element_index in range(self.transducer.number_elements):
324 if not self.active_elements[element_index]:
325 mask[mask == (element_index + 1)] = 0 # +1 compatibility
327 # force the lowest element index to be 1
328 lowest_active_element_index = matlab_find(self.active_elements)[0][0]
329 mask[mask != 0] = mask[mask != 0] - lowest_active_element_index + 1
330 return mask
332 @property
333 def indexed_elements_mask(self): # nr
334 return self.indexed_mask
336 @property
337 def steering_angle(self): # nr
338 return self.stored_steering_angle
340 # set the stored value of the steering angle
341 @steering_angle.setter
342 def steering_angle(self, steering_angle):
343 # force to be scalar
344 steering_angle = float(steering_angle)
346 # check if the steering angle is between -90 and 90
347 assert -90 < steering_angle < 90, "Input for steering_angle must be betweeb -90 and 90 degrees."
349 # check if the steering angle is less than the maximum steering angle
350 if self.stored_steering_angle_max != "auto" and (abs(steering_angle) > self.stored_steering_angle_max):
351 raise ValueError("Input for steering_angle cannot be greater than steering_angle_max.")
353 # update the stored value
354 self.stored_steering_angle = steering_angle
356 @property
357 def steering_angle_max(self):
358 return self.stored_steering_angle_max
360 @steering_angle_max.setter
361 def steering_angle_max(self, steering_angle_max):
362 # force input to be scalar and positive
363 steering_angle_max = float(steering_angle_max)
365 # check the steering angle is within range
366 assert -90 < steering_angle_max < 90, "Input for steering_angle_max must be between -90 and 90."
368 # check the maximum steering angle is greater than the current steering angle
369 assert (
370 self.stored_steering_angle_max == "auto" or abs(self.stored_steering_angle) <= steering_angle_max
371 ), "Input for steering_angle_max cannot be less than the current steering_angle."
373 # overwrite the stored value
374 self.stored_steering_angle_max = steering_angle_max
376 # store a copy of the current value for the steering angle
377 current_steering_angle = self.stored_steering_angle
379 # overwrite with the user defined maximum value
380 self.stored_steering_angle = steering_angle_max
382 # set the beamforming delay offset to zero (this means the delays will contain negative
383 # values which we can use to derive the new values for the offset)
384 self.stored_appended_zeros = 0
385 self.stored_beamforming_delays_offset = 0
387 # get the element beamforming delays and reverse
388 delay_times = -self.beamforming_delays
390 # get the maximum and minimum beamforming delays
391 min_delay, max_delay = delay_times.min(), delay_times.max()
393 # add the maximum and minimum elevation delay if the elevation focus is not set to infinite
394 if not np.isinf(self.elevation_focus_distance):
395 max_delay = max_delay + max(self.elevation_beamforming_delays)
396 min_delay = min_delay + min(self.elevation_beamforming_delays)
398 # set the beamforming offset to the difference between the
399 # maximum and minimum delay
400 self.stored_appended_zeros = max_delay - min_delay
402 # set the minimum beamforming delay (converting to a positive number)
403 self.stored_beamforming_delays_offset = -min_delay
405 # reset the previous value of the steering angle
406 self.stored_steering_angle = current_steering_angle
408 @property
409 def elevation_beamforming_mask(self): # nr
410 # get elevation beamforming mask
411 delay_mask = self.delay_mask(2)
413 # extract the active elements
414 delay_mask = delay_mask[self.active_elements_mask != 0]
416 # force delays to start from zero
417 delay_mask = delay_mask - delay_mask.min()
419 # create an empty output mask
420 mask = np.zeros((delay_mask.size, delay_mask.max() + 1))
422 # populate the mask by setting 1's at the index given by the delay time
423 for index in range(delay_mask.size):
424 mask[index, delay_mask[index]] = 1
426 # flip the mask so the shortest delays are at the right
427 return np.fliplr(mask)
429 @property
430 def input_signal(self):
431 signal = self.stored_input_signal
433 # check the signal is not empty
434 assert signal is not None, "Transducer input signal is not defined"
436 # automatically prepend and append zeros if the beamforming
437 # delay offset is set
439 # check if the beamforming delay offset is set. If so, use this
440 # number to prepend and append this number of zeros to the
441 # input signal. Otherwise, calculate how many zeros are needed
442 # and prepend and append these.
443 stored_appended_zeros = self.stored_appended_zeros
444 if stored_appended_zeros != "auto":
445 # use the current value of the beamforming offset to add
446 # zeros to the input signal
447 signal = np.vstack([np.zeros((stored_appended_zeros, 1)), signal, np.zeros((stored_appended_zeros, 1))])
449 else:
450 # get the current delay beam forming
451 delay_mask = self.delay_mask()
453 # find the maximum delay
454 delay_max = delay_mask.max()
456 # count the number of leading zeros in the input signal
457 leading_zeros = matlab_find(signal)[0, 0] - 1
459 # count the number of trailing zeros in the input signal
460 trailing_zeros = matlab_find(np.flipud(signal))[0, 0] - 1
462 # check the number of leading zeros is sufficient given the
463 # maximum delay
464 if leading_zeros < delay_max + 1:
465 logging.log(logging.INFO, f" prepending transducer.input_signal with {delay_max - leading_zeros + 1} leading zeros")
467 # prepend extra leading zeros
468 signal = np.vstack([np.zeros((delay_max - leading_zeros + 1, 1)), signal])
470 # check the number of leading zeros is sufficient given the
471 # maximum delay
472 if trailing_zeros < delay_max + 1:
473 logging.log(logging.INFO, f" appending transducer.input_signal with {delay_max - trailing_zeros + 1} trailing zeros")
475 # append extra trailing zeros
476 signal = np.vstack([signal, np.zeros((delay_max - trailing_zeros + 1, 1))])
478 return signal
480 @property
481 def number_active_elements(self):
482 return int(self.active_elements.sum())
484 @property
485 def appended_zeros(self):
486 """
487 Number of zeros appended to input signal to allow a single time series to be used
488 within kspaceFirstOrder3D (either set to 'auto' or based on the setting for steering_angle_max)
490 """
492 return self.stored_appended_zeros
494 @property
495 def grid_size(self):
496 """
497 Returns:
498 grid size
500 """
501 return self.transducer.stored_grid_size
503 @property
504 def active_elements_mask(self):
505 """
506 Returns:
507 A binary mask showing the locations of the active elements
509 """
510 indexed_mask = np.copy(self.indexed_mask)
511 active_elements = self.active_elements.squeeze()
512 number_elements = int(self.transducer.number_elements)
514 # copy the indexed elements mask
515 mask = indexed_mask
517 # remove inactive elements from the mask
518 for element_index in range(1, number_elements + 1):
519 mask[mask == element_index] = active_elements[element_index - 1]
521 # convert remaining mask to binary
522 mask[mask != 0] = 1
524 return mask
526 @property
527 def all_elements_mask(self):
528 """
529 Returns:
530 A binary mask showing the locations of all the elements (both active and inactive)
532 """
534 mask = np.copy(self.indexed_mask)
535 mask[mask != 0] = 1
536 return mask
538 def expand_grid(self, expand_size):
539 self.indexed_mask = expand_matrix(self.indexed_mask, expand_size, 0)
541 def retract_grid(self, retract_size):
542 indexed_mask = self.indexed_mask
543 retract_size = np.array(retract_size[0]).astype(np.int_)
545 self.indexed_mask = indexed_mask[
546 retract_size[0] : -retract_size[0], retract_size[1] : -retract_size[1], retract_size[2] : -retract_size[2]
547 ]
549 @property
550 def transmit_apodization_mask(self):
551 """
552 convert the transmit wave apodization into the form of a element mask,
553 where the apodization values are placed at the grid points
554 belonging to the active transducer elements. These values are
555 then extracted in the correct order within
556 kspaceFirstOrder_inputChecking using apodization =
557 transmit_apodization_mask(active_elements_mask ~= 0)
559 """
561 # get transmit apodization
562 apodization = self.get_transmit_apodization()
564 # create an empty mask;
565 mask = np.zeros(self.transducer.stored_grid_size)
567 # assign the apodization values to every grid point in the transducer
568 mask_index = self.indexed_active_elements_mask
569 mask_index = mask_index[mask_index != 0]
570 mask[self.active_elements_mask == 1] = apodization[mask_index - 1, 0] # -1 for conversion
571 return mask
573 def get_transmit_apodization(self):
574 """
575 Returns:
576 return the transmit apodization, converting strings of window
577 type to actual numbers using getWin
579 """
581 # check if a user defined apodization is given and whether this
582 # is still the correct size (in case the number of active
583 # elements has changed)
584 if is_number(self.transmit_apodization):
585 assert (
586 self.transmit_apodization.size == self.number_active_elements
587 ), "The length of the transmit apodization input must match the number of active elements"
589 # assign apodization
590 apodization = self.transmit_apodization
591 else:
592 # if the number of active elements is greater than 1,
593 # create apodization using getWin, otherwise, assign 1
594 if self.number_active_elements > 1:
595 apodization, _ = get_win(int(self.number_active_elements), type_=self.transmit_apodization)
596 else:
597 apodization = 1
598 apodization = np.array(apodization)
599 return apodization
601 def delay_mask(self, mode=None):
602 """
603 mode == 1: both delays
604 mode == 2: elevation only
605 mode == 3: azimuth only
607 """
608 # assign the delays to a new mask using the indexed_element_mask
609 indexed_active_elements_mask_copy = self.indexed_active_elements_mask
610 mask = np.zeros(self.transducer.stored_grid_size, dtype=np.float32)
612 if indexed_active_elements_mask_copy is None:
613 return mask
615 active_elements_index = matlab_find(indexed_active_elements_mask_copy)
617 # calculate azimuth focus delay times provided they are not all zero
618 if (not np.isinf(self.focus_distance) or (self.steering_angle != 0)) and (mode is None or mode != 2):
619 # get the element beamforming delays and reverse
620 delay_times = -self.beamforming_delays
622 # add delay times
623 # mask[active_elements_index] = delay_times[indexed_active_elements_mask_copy[active_elements_index]]
624 mask[unflatten_matlab_mask(mask, active_elements_index, diff=-1)] = matlab_mask(
625 delay_times, matlab_mask(indexed_active_elements_mask_copy, active_elements_index, diff=-1), diff=-1
626 ).squeeze()
628 # calculate elevation focus time delays provided each element is longer than one grid point
629 if not np.isinf(self.elevation_focus_distance) and (self.transducer.element_length > 1) and (mode is None or mode != 3):
630 # get elevation beamforming delays
631 elevation_delay_times = self.elevation_beamforming_delays
633 # get current mask
634 element_voxel_mask = self.indexed_element_voxel_mask
636 # add delay times
637 mask[unflatten_matlab_mask(mask, active_elements_index - 1)] += matlab_mask(
638 elevation_delay_times, matlab_mask(element_voxel_mask, active_elements_index - 1) - 1
639 )[:, 0] # -1s compatibility
641 # shift delay times (these should all be >= 0, where a value of 0 means no delay)
642 if self.stored_appended_zeros == "auto":
643 mask[unflatten_matlab_mask(mask, active_elements_index - 1)] -= mask[
644 unflatten_matlab_mask(mask, active_elements_index - 1)
645 ].min() # -1s compatibility
646 else:
647 mask[unflatten_matlab_mask(mask, active_elements_index - 1)] += self.stored_beamforming_delays_offset # -1s compatibility
648 return mask.astype(np.uint16)
650 @property
651 def elevation_beamforming_delays(self):
652 """
653 Calculate the elevation beamforming delays based on the focus setting
655 """
656 if not np.isinf(self.elevation_focus_distance):
657 # create indexing variable
659 element_index = np.arange(-(self.transducer.element_length - 1) / 2, (self.transducer.element_length + 1) / 2)
661 # calculate time delays for a focussed beam
662 delay_times = self.elevation_focus_distance - np.sqrt(
663 (element_index * self.transducer.grid_spacing[2]) ** 2 + self.elevation_focus_distance**2
664 )
665 delay_times /= self.sound_speed
667 # convert the delays to be in units of time points and then reverse
668 delay_times = -np.round(delay_times / self.dt).astype(np.int32)
670 else:
671 # create an empty array
672 delay_times = np.zeros((1, self.transducer.element_length))
673 return delay_times
675 def get_receive_apodization(self):
676 """
677 Get the current receive apodization setting.
678 """
679 # Example implementation, adjust based on actual logic
680 if is_number(self.receive_apodization):
681 assert (
682 self.receive_apodization.size == self.number_active_elements
683 ), "The length of the receive apodization input must match the number of active elements"
684 return self.receive_apodization
685 else:
686 if self.number_active_elements > 1:
687 apodization, _ = get_win(int(self.number_active_elements), type_=self.receive_apodization)
688 else:
689 apodization = 1
690 return np.array(apodization)
692 def scan_line(self, sensor_data):
693 """
694 Apply beamforming and apodization to the sensor data.
695 """
696 # Get the current apodization setting
697 apodization = self.get_receive_apodization()
699 # Get the current beamforming weights and reverse
700 delays = -self.beamforming_delays
702 # Offset the received sensor_data by the beamforming delays and apply receive apodization
703 for element_index in range(self.number_active_elements):
704 if delays[element_index] > 0:
705 # Shift element data forwards
706 sensor_data[element_index, :] = (
707 np.pad(sensor_data[element_index, delays[element_index] :], (0, delays[element_index]), "constant")
708 * apodization[element_index]
709 )
710 elif delays[element_index] < 0:
711 # Shift element data backwards
712 sensor_data[element_index, :] = (
713 np.pad(
714 sensor_data[element_index, : sensor_data.shape[1] + delays[element_index]], (-delays[element_index], 0), "constant"
715 )
716 * apodization[element_index]
717 )
719 # Form the line summing across the elements
720 line = np.sum(sensor_data, axis=0)
721 return line
723 def combine_sensor_data(self, sensor_data):
724 # check the data is the correct size
725 if sensor_data.shape[0] != (self.number_active_elements * self.transducer.element_width * self.transducer.element_length):
726 raise ValueError(
727 "The number of time series in the input sensor_data must "
728 "match the number of grid points in the active tranducer elements."
729 )
731 # get index of which element each time series belongs to
732 # Tricky things going on here
733 ind = self.indexed_active_elements_mask[0].T[self.indexed_active_elements_mask[0].T > 0]
735 # create empty output
736 sensor_data_sum = np.zeros((int(self.number_active_elements), sensor_data.shape[1]))
738 # check if an elevation focus is set
739 if np.isinf(self.elevation_focus_distance):
740 raise NotImplementedError
742 # # loop over time series and sum
743 # for ii = 1:length(ind)
744 # sensor_data_sum(ind(ii), :) = sensor_data_sum(ind(ii), :) + sensor_data(ii, :);
745 # end
747 else:
748 # get the elevation delay for each grid point of each
749 # active transducer element (this is given in units of grid
750 # points)
751 dm = self.delay_mask(2)
752 # dm = dm[self.active_elements_mask != 0]
753 dm = dm[0].T[self.active_elements_mask[0].T != 0]
754 dm = dm.astype(np.int32)
756 # loop over time series, shift and sum
757 for ii in range(len(ind)):
758 # FARID: something nasty can be here
759 end = -dm[ii] if dm[ii] != 0 else sensor_data_sum.shape[-1]
760 sensor_data_sum[ind[ii] - 1, 0:end] = sensor_data_sum[ind[ii] - 1, 0:end] + sensor_data[ii, dm[ii] :]
762 # divide by number of time series in each element
763 sensor_data_sum = sensor_data_sum * (1 / (self.transducer.element_width * self.transducer.element_length))
764 return sensor_data_sum