Coverage for kwave/ktransducer.py: 17%

293 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-03-24 12:06 -0700

1import logging 

2 

3import numpy as np 

4 

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 

12 

13 

14# force value to be a positive integer 

15def make_pos_int(val): 

16 return np.abs(val).astype(int) 

17 

18 

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] 

39 

40 """ 

41 

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 

45 

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) 

50 

51 if position is None: 

52 position = [1, 1, 1] 

53 self.position = make_pos_int(position) 

54 

55 assert np.isinf(radius), "Only a value of transducer.radius = inf is currently supported" 

56 self.radius = radius 

57 

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") 

72 

73 @property 

74 def element_pitch(self): 

75 return (self.element_spacing + self.element_width) * self.grid_spacing[1] 

76 

77 @property 

78 def transducer_width(self): 

79 """ 

80 

81 Total width of the transducer in grid points 

82 

83 Returns: 

84 the overall length of the transducer 

85 

86 """ 

87 return self.number_elements * self.element_width + (self.number_elements - 1) * self.element_spacing 

88 

89 

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 

108 

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] 

120 

121 """ 

122 

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 

139 

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." 

143 

144 #################################### 

145 

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") 

153 

154 if active_elements is None: 

155 active_elements = np.ones((transducer.number_elements, 1)) 

156 self.active_elements = active_elements 

157 

158 self.elevation_focus_distance = elevation_focus_distance 

159 

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 

165 

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 

171 

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 

175 

176 self.focus_distance = focus_distance 

177 

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 

182 

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 

186 

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 

191 

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") 

196 

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) 

201 

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) 

207 

208 # create the corresponding indexing variable 1:M 

209 element_voxel_index = np.tile(np.arange(transducer.element_length) + 1, (transducer.element_width, 1)) 

210 

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] 

217 

218 element_pos_x = element_pos_x - 1 

219 element_pos_y = element_pos_y - 1 

220 element_pos_z = element_pos_z - 1 

221 

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 

229 

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 

237 

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" 

242 

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 

246 

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 

252 

253 def __contains__(self, item): 

254 return self.isfield(item) 

255 

256 @property 

257 def beamforming_delays(self): 

258 """ 

259 calculate the beamforming delays based on the focus and steering settings 

260 

261 """ 

262 # calculate the element pitch in [m] 

263 element_pitch = self.transducer.element_pitch 

264 

265 # create indexing variable 

266 element_index = np.arange(-(self.number_active_elements - 1) / 2, (self.number_active_elements + 1) / 2) 

267 

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] 

272 

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] 

287 

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 

291 

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) 

296 

297 Returns: 

298 the stored value of the offset used to force the values in delay_mask to be >= 0 

299 

300 """ 

301 

302 return self.stored_beamforming_delays_offset 

303 

304 @property 

305 def mask(self): 

306 """ 

307 Allow mask query to allow compatibility with regular sensor structure - return the active sensor mask 

308 

309 """ 

310 

311 return self.active_elements_mask 

312 

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 

319 

320 mask = np.copy(mask) 

321 

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 

326 

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 

331 

332 @property 

333 def indexed_elements_mask(self): # nr 

334 return self.indexed_mask 

335 

336 @property 

337 def steering_angle(self): # nr 

338 return self.stored_steering_angle 

339 

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) 

345 

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." 

348 

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.") 

352 

353 # update the stored value 

354 self.stored_steering_angle = steering_angle 

355 

356 @property 

357 def steering_angle_max(self): 

358 return self.stored_steering_angle_max 

359 

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) 

364 

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." 

367 

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." 

372 

373 # overwrite the stored value 

374 self.stored_steering_angle_max = steering_angle_max 

375 

376 # store a copy of the current value for the steering angle 

377 current_steering_angle = self.stored_steering_angle 

378 

379 # overwrite with the user defined maximum value 

380 self.stored_steering_angle = steering_angle_max 

381 

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 

386 

387 # get the element beamforming delays and reverse 

388 delay_times = -self.beamforming_delays 

389 

390 # get the maximum and minimum beamforming delays 

391 min_delay, max_delay = delay_times.min(), delay_times.max() 

392 

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) 

397 

398 # set the beamforming offset to the difference between the 

399 # maximum and minimum delay 

400 self.stored_appended_zeros = max_delay - min_delay 

401 

402 # set the minimum beamforming delay (converting to a positive number) 

403 self.stored_beamforming_delays_offset = -min_delay 

404 

405 # reset the previous value of the steering angle 

406 self.stored_steering_angle = current_steering_angle 

407 

408 @property 

409 def elevation_beamforming_mask(self): # nr 

410 # get elevation beamforming mask 

411 delay_mask = self.delay_mask(2) 

412 

413 # extract the active elements 

414 delay_mask = delay_mask[self.active_elements_mask != 0] 

415 

416 # force delays to start from zero 

417 delay_mask = delay_mask - delay_mask.min() 

418 

419 # create an empty output mask 

420 mask = np.zeros((delay_mask.size, delay_mask.max() + 1)) 

421 

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 

425 

426 # flip the mask so the shortest delays are at the right 

427 return np.fliplr(mask) 

428 

429 @property 

430 def input_signal(self): 

431 signal = self.stored_input_signal 

432 

433 # check the signal is not empty 

434 assert signal is not None, "Transducer input signal is not defined" 

435 

436 # automatically prepend and append zeros if the beamforming 

437 # delay offset is set 

438 

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))]) 

448 

449 else: 

450 # get the current delay beam forming 

451 delay_mask = self.delay_mask() 

452 

453 # find the maximum delay 

454 delay_max = delay_mask.max() 

455 

456 # count the number of leading zeros in the input signal 

457 leading_zeros = matlab_find(signal)[0, 0] - 1 

458 

459 # count the number of trailing zeros in the input signal 

460 trailing_zeros = matlab_find(np.flipud(signal))[0, 0] - 1 

461 

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") 

466 

467 # prepend extra leading zeros 

468 signal = np.vstack([np.zeros((delay_max - leading_zeros + 1, 1)), signal]) 

469 

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") 

474 

475 # append extra trailing zeros 

476 signal = np.vstack([signal, np.zeros((delay_max - trailing_zeros + 1, 1))]) 

477 

478 return signal 

479 

480 @property 

481 def number_active_elements(self): 

482 return int(self.active_elements.sum()) 

483 

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) 

489 

490 """ 

491 

492 return self.stored_appended_zeros 

493 

494 @property 

495 def grid_size(self): 

496 """ 

497 Returns: 

498 grid size 

499 

500 """ 

501 return self.transducer.stored_grid_size 

502 

503 @property 

504 def active_elements_mask(self): 

505 """ 

506 Returns: 

507 A binary mask showing the locations of the active elements 

508 

509 """ 

510 indexed_mask = np.copy(self.indexed_mask) 

511 active_elements = self.active_elements.squeeze() 

512 number_elements = int(self.transducer.number_elements) 

513 

514 # copy the indexed elements mask 

515 mask = indexed_mask 

516 

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] 

520 

521 # convert remaining mask to binary 

522 mask[mask != 0] = 1 

523 

524 return mask 

525 

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) 

531 

532 """ 

533 

534 mask = np.copy(self.indexed_mask) 

535 mask[mask != 0] = 1 

536 return mask 

537 

538 def expand_grid(self, expand_size): 

539 self.indexed_mask = expand_matrix(self.indexed_mask, expand_size, 0) 

540 

541 def retract_grid(self, retract_size): 

542 indexed_mask = self.indexed_mask 

543 retract_size = np.array(retract_size[0]).astype(np.int_) 

544 

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 ] 

548 

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) 

558 

559 """ 

560 

561 # get transmit apodization 

562 apodization = self.get_transmit_apodization() 

563 

564 # create an empty mask; 

565 mask = np.zeros(self.transducer.stored_grid_size) 

566 

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 

572 

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 

578 

579 """ 

580 

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" 

588 

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 

600 

601 def delay_mask(self, mode=None): 

602 """ 

603 mode == 1: both delays 

604 mode == 2: elevation only 

605 mode == 3: azimuth only 

606 

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) 

611 

612 if indexed_active_elements_mask_copy is None: 

613 return mask 

614 

615 active_elements_index = matlab_find(indexed_active_elements_mask_copy) 

616 

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 

621 

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() 

627 

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 

632 

633 # get current mask 

634 element_voxel_mask = self.indexed_element_voxel_mask 

635 

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 

640 

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) 

649 

650 @property 

651 def elevation_beamforming_delays(self): 

652 """ 

653 Calculate the elevation beamforming delays based on the focus setting 

654 

655 """ 

656 if not np.isinf(self.elevation_focus_distance): 

657 # create indexing variable 

658 

659 element_index = np.arange(-(self.transducer.element_length - 1) / 2, (self.transducer.element_length + 1) / 2) 

660 

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 

666 

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) 

669 

670 else: 

671 # create an empty array 

672 delay_times = np.zeros((1, self.transducer.element_length)) 

673 return delay_times 

674 

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) 

691 

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() 

698 

699 # Get the current beamforming weights and reverse 

700 delays = -self.beamforming_delays 

701 

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 ) 

718 

719 # Form the line summing across the elements 

720 line = np.sum(sensor_data, axis=0) 

721 return line 

722 

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 ) 

730 

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] 

734 

735 # create empty output 

736 sensor_data_sum = np.zeros((int(self.number_active_elements), sensor_data.shape[1])) 

737 

738 # check if an elevation focus is set 

739 if np.isinf(self.elevation_focus_distance): 

740 raise NotImplementedError 

741 

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 

746 

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) 

755 

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] :] 

761 

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