Coverage for kwave/kWaveSimulation.py: 16%

578 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-24 11:55 -0700

1from warnings import warn 

2 

3from kwave.kWaveSimulation_helper import * 

4from kwave.kgrid import * 

5from kwave.kmedium import kWaveMedium 

6from kwave.ksensor import kSensor 

7from kwave.ksource import kSource 

8from kwave.ktransducer import NotATransducer 

9from kwave.options import SimulationOptions 

10from kwave.recorder import Recorder 

11from kwave.utils import * 

12from kwave.utils import dotdict 

13 

14 

15@dataclass 

16class kWaveSimulation(object): 

17 

18 def __init__(self, 

19 kgrid: kWaveGrid, 

20 medium: kWaveMedium, 

21 source, 

22 sensor, 

23 **kwargs 

24 ): 

25 self.kgrid = kgrid 

26 self.medium = medium 

27 self.source = source 

28 self.sensor = sensor 

29 self.kwargs = kwargs 

30 

31 # ========================================================================= 

32 # FLAGS WHICH DEPEND ON USER INPUTS (THESE SHOULD NOT BE MODIFIED) 

33 # ========================================================================= 

34 # flags which control the type of simulation 

35 #: Whether simulation type is axisymmetric 

36 self.userarg_axisymmetric = kwargs['axisymmetric'] 

37 

38 # flags which control the characteristics of the sensor 

39 #: Whether time reversal simulation is enabled 

40 self.userarg_time_rev = kwargs['time_rev'] 

41 

42 #: Whether sensor.mask should be re-ordered. 

43 #: True if sensor.mask is Cartesian with nearest neighbour interpolation which is calculated using a binary mask 

44 #: and thus must be re-ordered 

45 self.reorder_data = False 

46 

47 #: Whether the sensor.mask is binary 

48 self.binary_sensor_mask = True 

49 

50 #: If the sensor.mask is a list of cuboid corners 

51 self.userarg_cuboid_corners = kwargs['cuboid_corners'] 

52 

53 #: If tse sensor is an object of the kWaveTransducer class 

54 self.transducer_sensor = False 

55 

56 self.record = Recorder() 

57 

58 # transducer source flags 

59 #: transducer is object of kWaveTransducer class 

60 self.transducer_source = False 

61 

62 #: Apply receive elevation focus on the transducer 

63 self.transducer_receive_elevation_focus = False 

64 

65 # general 

66 self.COLOR_MAP = get_color_map() #: default color map 

67 self.ESTIMATE_SIM_TIME_STEPS = 50 #: time steps used to estimate simulation time 

68 self.HIGHEST_PRIME_FACTOR_WARNING = 7 #: largest prime factor before warning 

69 self.KSPACE_CFL = 0.3 #: default CFL value used if kgrid.t_array is set to 'auto' 

70 self.PSTD_CFL = 0.1 #: default CFL value used if kgrid.t_array is set to 'auto' 

71 

72 # source types 

73 self.SOURCE_S_MODE_DEF = 'additive' #: source mode for stress sources 

74 self.SOURCE_P_MODE_DEF = 'additive' #: source mode for pressure sources 

75 self.SOURCE_U_MODE_DEF = 'additive' #: source mode for velocity sources 

76 

77 # filenames 

78 self.STREAM_TO_DISK_FILENAME = 'temp_sensor_data.bin' #: default disk stream filename 

79 self.LOG_NAME = ['k-Wave-Log-', get_date_string()] #: default log filename 

80 

81 del kwargs['axisymmetric'] 

82 del kwargs['cuboid_corners'] 

83 del kwargs['time_rev'] 

84 self.calling_func_name = None 

85 print(f' start time: {get_date_string()}') 

86 

87 self.options = None 

88 

89 self.c_ref, self.c_ref_compression, self.c_ref_shear = [None] * 3 

90 self.transducer_input_signal = None 

91 

92 #: Indexing variable corresponding to the location of all the pressure source elements 

93 self.p_source_pos_index = None 

94 #: Indexing variable corresponding to the location of all the velocity source elements 

95 self.u_source_pos_index = None 

96 #: Indexing variable corresponding to the location of all the stress source elements 

97 self.s_source_pos_index = None 

98 

99 #: Delay mask that accounts for the beamforming delays and elevation focussing 

100 self.delay_mask = None 

101 

102 self.absorb_nabla1 = None #: absorbing fractional Laplacian operator 

103 self.absorb_tau = None #: absorbing fractional Laplacian coefficient 

104 self.absorb_nabla2 = None #: dispersive fractional Laplacian operator 

105 self.absorb_eta = None #: dispersive fractional Laplacian coefficient 

106 

107 self.dt = None #: Alias to kgrid.dt 

108 self.rho0 = None #: Alias to medium.density 

109 self.c0 = None #: Alias to medium.sound_speed 

110 self.index_data_type = None 

111 

112 @property 

113 def equation_of_state(self): 

114 """ 

115 Set equation of state variable 

116 Returns: 

117 """ 

118 if self.medium.absorbing: 

119 if self.medium.stokes: 

120 return 'stokes' 

121 else: 

122 return 'absorbing' 

123 else: 

124 return 'loseless' 

125 

126 @property 

127 def use_sensor(self): 

128 """ 

129 False if no output of any kind is required 

130 Returns: 

131 

132 """ 

133 return self.sensor is not None 

134 

135 @property 

136 def blank_sensor(self): 

137 """ 

138 true if sensor.mask is not defined but _max_all or _final variables are still recorded 

139 Returns: 

140 

141 """ 

142 fields = ['p', 'p_max', 'p_min', 'p_rms', 'u', 'u_non_staggered', 

143 'u_split_field', 'u_max', 'u_min', 'u_rms', 'I', 'I_avg'] 

144 if not any(self.record.is_set(fields)) and not self.time_rev: 

145 return False 

146 return False 

147 

148 @property 

149 def elastic_code(self): 

150 """ 

151 Whether the simulation is elastic or fluid 

152 Returns: 

153 True if elastic simulation 

154 """ 

155 return self.calling_func_name.startswith(('pstdElastic', 'kspaceElastic')) 

156 

157 @property 

158 def kspace_elastic_code(self): 

159 """ 

160 Whether the simulation is k-space elastic code or an ordinary elastic code 

161 Returns: 

162 True if elastic simulation with k-space correction 

163 """ 

164 return self.calling_func_name.startswith('kspaceElastic') 

165 

166 @property 

167 def axisymmetric(self): 

168 """ 

169 Whether the code is axisymmetric 

170 Returns: 

171 True if fluid axisymmetric simulation 

172 """ 

173 if self.calling_func_name.startswith('kspaceFirstOrderAS'): 

174 return True 

175 else: 

176 return self.userarg_axisymmetric 

177 

178 @property 

179 def kelvin_voigt_model(self): 

180 """ 

181 Whether the simulation is elastic with absorption 

182 """ 

183 return False 

184 

185 @property 

186 def nonuniform_grid(self): 

187 """ 

188 Whether the grid is nonuniform 

189 Returns: 

190 True if the computational grid is non-uniform 

191 """ 

192 return self.kgrid.nonuniform 

193 

194 @property 

195 def time_rev(self): 

196 """ 

197 Whether the simulation is time reversal 

198 Returns: 

199 True for time reversal simulaions using sensor.time_reversal_boundary_data 

200 """ 

201 if self.sensor is not None and not isinstance(self.sensor, NotATransducer): 

202 if not self.elastic_code and self.sensor.time_reversal_boundary_data is not None: 

203 return True 

204 else: 

205 return self.userarg_time_rev 

206 

207 @property 

208 def elastic_time_rev(self): 

209 """ 

210 Whether the simulation is time reversal and elastic code 

211 Returns: 

212 True if using time reversal with the elastic code 

213 """ 

214 return False 

215 

216 @property 

217 def compute_directivity(self): 

218 """ 

219 Whether the directivity should be computed 

220 Returns: 

221 True if directivity calculations in 2D are used by setting sensor.directivity_angle 

222 """ 

223 if self.sensor is not None and not isinstance(self.sensor, NotATransducer): 

224 if self.kgrid.dim == 2: 

225 # check for sensor directivity input and set flag 

226 directivity = self.sensor.directivity 

227 if directivity is not None and directivity.angle is not None: 

228 return True 

229 return False 

230 

231 @property 

232 def cuboid_corners(self): 

233 """ 

234 Whether the sensor.mask is a list of cuboid corners 

235 """ 

236 if self.sensor is not None and not isinstance(self.sensor, NotATransducer): 

237 if not self.blank_sensor and self.sensor.mask.shape[0] == 2 * self.kgrid.dim: 

238 return True 

239 return self.userarg_cuboid_corners 

240 

241 ############## 

242 # flags which control the types of source used 

243 ############## 

244 @property 

245 def source_p0(self): # initial pressure 

246 """ 

247 Whether initial pressure source is present (default=False) 

248 """ 

249 flag = False # default 

250 if not isinstance(self.source, NotATransducer) and self.source.p0 is not None: 

251 # set flag 

252 flag = True 

253 return flag 

254 

255 @property 

256 def source_p0_elastic(self): # initial pressure in the elastic code 

257 """ 

258 Whether initial pressure source is present in the elastic code (default=False) 

259 """ 

260 # Not clear where this flag is set 

261 return False 

262 

263 @property 

264 def source_p(self): 

265 """ 

266 Whether time-varying pressure source is present (default=False) 

267 """ 

268 flag = False 

269 if not isinstance(self.source, NotATransducer) and self.source.p is not None: 

270 # set source flag to the length of the source, this allows source.p 

271 # to be shorter than kgrid.Nt 

272 flag = len(self.source.p[0]) 

273 return flag 

274 

275 @property 

276 def source_p_labelled(self): # time-varying pressure with labelled source mask 

277 """ 

278 Whether time-varying pressure source with labelled source mask is present (default=False) 

279 Returns: 

280 True/False if labelled/binary source mask, respectively. 

281 """ 

282 flag = False 

283 if not isinstance(self.source, NotATransducer) and self.source.p is not None: 

284 # check if the mask is binary or labelled 

285 p_unique = np.unique(self.source.p_mask) 

286 flag = not (p_unique.size <= 2 and p_unique.sum() == 1) 

287 return flag 

288 

289 @property 

290 def source_ux(self) -> bool: 

291 """ 

292 Whether time-varying particle velocity source is used in X-direction 

293 """ 

294 flag = False 

295 if not isinstance(self.source, NotATransducer) and self.source.ux is not None: 

296 # set source flgs to the length of the sources, this allows the 

297 # inputs to be defined independently and be of any length 

298 flag = len(self.source.ux[0]) 

299 return flag 

300 

301 @property 

302 def source_uy(self) -> bool: 

303 """ 

304 Whether time-varying particle velocity source is used in Y-direction 

305 """ 

306 flag = False 

307 if not isinstance(self.source, NotATransducer) and self.source.uy is not None: 

308 # set source flgs to the length of the sources, this allows the 

309 # inputs to be defined independently and be of any length 

310 flag = len(self.source.uy[0]) 

311 return flag 

312 

313 @property 

314 def source_uz(self) -> bool: 

315 """ 

316 Whether time-varying particle velocity source is used in Z-direction 

317 """ 

318 flag = False 

319 if not isinstance(self.source, NotATransducer) and self.source.uz is not None: 

320 # set source flgs to the length of the sources, this allows the 

321 # inputs to be defined independently and be of any length 

322 flag = len(self.source.uz[0]) 

323 return flag 

324 

325 @property 

326 def source_u_labelled(self): 

327 """ 

328 Whether time-varying velocity source with labelled source mask is present (default=False) 

329 """ 

330 flag = False 

331 if not isinstance(self.source, NotATransducer) and self.source.u_mask is not None: 

332 # check if the mask is binary or labelled 

333 u_unique = np.unique(self.source.u_mask) 

334 if u_unique.size <= 2 and u_unique.sum() == 1: 

335 # binary source mask 

336 flag = False 

337 else: 

338 # labelled source mask 

339 flag = True 

340 return flag 

341 

342 @property 

343 def source_sxx(self): 

344 """ 

345 Whether time-varying stress source in X->X direction is present (default=False) 

346 """ 

347 flag = False 

348 if not isinstance(self.source, NotATransducer) and self.source.sxx is not None: 

349 flag = len(self.source.sxx[0]) 

350 return flag 

351 

352 @property 

353 def source_syy(self): 

354 """ 

355 Whether time-varying stress source in Y->Y direction is present (default=False) 

356 """ 

357 flag = False 

358 if not isinstance(self.source, NotATransducer) and self.source.syy is not None: 

359 flag = len(self.source.syy[0]) 

360 return flag 

361 

362 @property 

363 def source_szz(self): 

364 """ 

365 Whether time-varying stress source in Z->Z direction is present (default=False) 

366 """ 

367 flag = False 

368 if not isinstance(self.source, NotATransducer) and self.source.szz is not None: 

369 flag = len(self.source.szz[0]) 

370 return flag 

371 

372 @property 

373 def source_sxy(self): 

374 """ 

375 Whether time-varying stress source in X->Y direction is present (default=False) 

376 """ 

377 flag = False 

378 if not isinstance(self.source, NotATransducer) and self.source.sxy is not None: 

379 flag = len(self.source.sxy[0]) 

380 return flag 

381 

382 @property 

383 def source_sxz(self): 

384 """ 

385 Whether time-varying stress source in X->Z direction is present (default=False) 

386 """ 

387 flag = False 

388 if not isinstance(self.source, NotATransducer) and self.source.sxz is not None: 

389 flag = len(self.source.sxz[0]) 

390 return flag 

391 

392 @property 

393 def source_syz(self): 

394 """ 

395 Whether time-varying stress source in Y->Z direction is present (default=False) 

396 """ 

397 flag = False 

398 if not isinstance(self.source, NotATransducer) and self.source.syz is not None: 

399 flag = len(self.source.syz[0]) 

400 return flag 

401 

402 @property 

403 def source_s_labelled(self): 

404 """ 

405 Whether time-varying stress source with labelled source mask is present (default=False) 

406 """ 

407 flag = False 

408 if not isinstance(self.source, NotATransducer) and self.source.s_mask is not None: 

409 # check if the mask is binary or labelled 

410 s_unique = np.unique(self.source.s_mask) 

411 if s_unique.size <= 2 and s_unique.sum() == 1: 

412 # binary source mask 

413 flag = False 

414 else: 

415 # labelled source mask 

416 flag = True 

417 return flag 

418 

419 @property 

420 def use_w_source_correction_p(self): 

421 """ 

422 Whether to use the w source correction instead of the k-space source correction for pressure sources 

423 """ 

424 flag = False 

425 if not isinstance(self.source, NotATransducer): 

426 if self.source.p is not None and self.source.p_frequency_ref is not None: 

427 flag = True 

428 return flag 

429 

430 @property 

431 def use_w_source_correction_u(self): 

432 """ 

433 Whether to use the w source correction instead of the k-space source correction for velocity sources 

434 """ 

435 flag = False 

436 if not isinstance(self.source, NotATransducer): 

437 if any([(getattr(self.source, k) is not None) for k in ['ux', 'uy', 'uz', 'u_mask']]): 

438 if self.source.u_frequency_ref is not None: 

439 flag = True 

440 return flag 

441 

442 def input_checking(self, calling_func_name) -> None: 

443 """ 

444 Check the input fields for correctness and validness 

445 

446 Args: 

447 calling_func_name: Name of the script that calls this function 

448 

449 Returns: 

450 None 

451 """ 

452 self.calling_func_name = calling_func_name 

453 

454 k_dim = self.kgrid.dim 

455 k_Nx, k_Ny, k_Nz, k_Nt = self.kgrid.Nx, \ 

456 self.kgrid.Ny, \ 

457 self.kgrid.Nz, \ 

458 self.kgrid.Nt 

459 

460 self.check_calling_func_name_and_dim(calling_func_name, k_dim) 

461 

462 # run subscript to check optional inputs 

463 self.options = SimulationOptions.init(self.kgrid, self.elastic_code, self.axisymmetric, **self.kwargs) 

464 opt = self.options 

465 

466 pml_x_size, pml_y_size, pml_z_size = opt.pml_x_size, opt.pml_y_size, opt.pml_z_size 

467 pml_size = Array([pml_x_size, pml_y_size, pml_z_size]) 

468 

469 self.print_start_status(self.elastic_code) 

470 self.set_index_data_type() 

471 

472 user_medium_density_input = self.check_medium(self.medium, self.kgrid.k, self.elastic_code, self.axisymmetric) 

473 

474 # select the reference sound speed used in the k-space operator 

475 self.c_ref, self.c_ref_compression, self.c_ref_shear \ 

476 = set_sound_speed_ref(self.medium, self.elastic_code, self.kspace_elastic_code) 

477 

478 self.check_source(k_dim) 

479 self.check_sensor(k_dim, k_Nt) 

480 self.check_kgrid_time() 

481 self.precision = self.select_precision(opt) 

482 self.check_input_combinations(opt, user_medium_density_input, k_dim, pml_size, self.kgrid.N) 

483 

484 # run subscript to display time step, max supported frequency etc. 

485 display_simulation_params(self.kgrid, self.medium, self.elastic_code) 

486 

487 self.smooth_and_enlarge(self.source, k_dim, Array(self.kgrid.N), opt) 

488 self.create_sensor_variables() 

489 self.create_absorption_vars() 

490 self.assign_pseudonyms(self.medium, self.kgrid) 

491 self.scale_source_terms(opt.scale_source_terms) 

492 self.create_pml_indices(self.kgrid.dim, Array(self.kgrid.N), pml_size, opt.pml_inside, self.axisymmetric) 

493 

494 @staticmethod 

495 def check_calling_func_name_and_dim(calling_func_name, kgrid_dim) -> None: 

496 """ 

497 Check correct function has been called for the dimensionality of kgrid 

498 

499 Args: 

500 calling_func_name: Name of the script that makes calls to kWaveSimulation 

501 kgrid_dim: Dimensionality of the kWaveGrid 

502 

503 Returns: 

504 None 

505 """ 

506 assert not calling_func_name.startswith(('pstdElastic', 'kspaceElastic')), \ 

507 "Elastic simulation is not supported." 

508 

509 if calling_func_name == 'kspaceFirstOrder1D': 

510 assert kgrid_dim == 1, f'kgrid has the wrong dimensionality for {calling_func_name}.' 

511 elif calling_func_name in ['kspaceFirstOrder2D', 'pstdElastic2D', 'kspaceElastic2D', 'kspaceFirstOrderAS']: 

512 assert kgrid_dim == 2, f'kgrid has the wrong dimensionality for {calling_func_name}.' 

513 elif calling_func_name in ['kspaceFirstOrder3D', 'pstdElastic3D', 'kspaceElastic3D']: 

514 assert kgrid_dim == 3, f'kgrid has the wrong dimensionality for {calling_func_name}.' 

515 

516 @staticmethod 

517 def print_start_status(is_elastic_code) -> None: 

518 """ 

519 Update command-line status with the start time 

520 

521 Args: 

522 is_elastic_code: is the simulation elastic 

523 

524 Returns: 

525 None 

526 """ 

527 if is_elastic_code: # pragma: no cover 

528 print('Running k-Wave elastic simulation...') 

529 else: 

530 print('Running k-Wave simulation...') 

531 print(f' start time: {get_date_string()}') 

532 

533 def set_index_data_type(self) -> None: 

534 """ 

535 Pre-calculate the data type needed to store the matrix indices given the 

536 total number of grid points: indexing variables will be created using this data type to save memory 

537 

538 Returns: 

539 None 

540 """ 

541 total_grid_points = self.kgrid.total_grid_points 

542 self.index_data_type = get_smallest_possible_type(total_grid_points, 'uint', default='double') 

543 

544 @staticmethod 

545 def check_medium(medium, kgrid_k, is_elastic, is_axisymmetric): 

546 """ 

547 Check the properties of the medium structure for correctness and validity 

548 

549 Args: 

550 medium: kWaveMedium instance 

551 kgrid_k: kWaveGrid.k matrix 

552 is_elastic: Whether the simulation is elastic 

553 is_axisymmetric: Whether the simulation is axisymmetric 

554 

555 Returns: 

556 Medium Density 

557 """ 

558 

559 # if using the fluid code, allow the density field to be blank if the medium is homogeneous 

560 if not is_elastic and medium.density is None and medium.sound_speed.size == 1: 

561 user_medium_density_input = False 

562 medium.density = 1 

563 else: 

564 medium.ensure_defined('density') 

565 user_medium_density_input = True 

566 

567 # check medium absorption inputs for the fluid code 

568 is_absorbing = any(medium.is_defined('alpha_coeff', 'alpha_power')) 

569 is_stokes = (is_axisymmetric or medium.alpha_mode == 'stokes') 

570 medium.set_absorbing(is_absorbing, is_stokes) 

571 

572 if is_absorbing: 

573 medium.check_fields(kgrid_k.shape) 

574 return user_medium_density_input 

575 

576 def check_source(self, kgrid_dim) -> None: 

577 """ 

578 Check the source properties for correctness and validity 

579 

580 Args: 

581 kgrid_dim: kWaveGrid dimension 

582 

583 Returns: 

584 None 

585 """ 

586 # ========================================================================= 

587 # CHECK SENSOR STRUCTURE INPUTS 

588 # ========================================================================= 

589 # check sensor fields 

590 if self.sensor is not None: 

591 

592 # check the sensor input is valid 

593 # TODO FARID move this check as a type checking 

594 assert isinstance(self.sensor, (kSensor, NotATransducer)), \ 

595 'sensor must be defined as an object of the kSensor or kWaveTransducer class.' 

596 

597 # check if sensor is a transducer, otherwise check input fields 

598 if not isinstance(self.sensor, NotATransducer): 

599 if kgrid_dim == 2: 

600 

601 # check field names, including the directivity inputs for the 

602 # regular 2D code, but not the axisymmetric code 

603 # TODO question to Walter: we do not need following checks anymore because we have kSensor that defines the structure, right? 

604 # if self.axisymmetric: 

605 # check_field_names(self.sensor, *['mask', 'time_reversal_boundary_data', 'frequency_response', 

606 # 'record_mode', 'record', 'record_start_index']) 

607 # else: 

608 # check_field_names(self.sensor, *['mask', 'directivity', 'time_reversal_boundary_data', 

609 # 'frequency_response', 'record_mode', 'record', 'record_start_index']) 

610 

611 # check for sensor directivity input and set flag 

612 directivity = self.sensor.directivity 

613 if directivity is not None and self.sensor.directivity.angle is not None: 

614 

615 # make sure the sensor mask is not blank 

616 assert self.sensor.mask is not None, f'The mask must be defined for the sensor' 

617 

618 # check sensor.directivity.pattern and sensor.mask have the same size 

619 assert directivity.angle.shape == self.sensor.mask.shape, \ 

620 'sensor.directivity.angle and sensor.mask must be the same size.' 

621 

622 # check if directivity size input exists, otherwise make it 

623 # a constant times kgrid.dx 

624 if directivity.size is None: 

625 directivity.set_default_size(self.kgrid) 

626 

627 # find the unique directivity angles 

628 # assign the wavenumber vectors 

629 directivity.set_unique_angles(self.sensor.mask) 

630 directivity.set_wavenumbers(self.kgrid) 

631 

632 else: 

633 # TODO question to Walter: we do not need following checks anymore because we have kSensor that defines the structure, right? 

634 # check field names without directivity inputs (these are not supported in 1 or 3D) 

635 # check_field_names(self.sensor, *['mask', 'time_reversal_boundary_data', 'frequency_response', 

636 # 'record_mode', 'record', 'record_start_index']) 

637 pass 

638 

639 # check for time reversal inputs and set flgs 

640 if not self.elastic_code and self.sensor.time_reversal_boundary_data is not None: 

641 self.record.p = False 

642 

643 # check for sensor.record and set usage flgs - if no flgs are 

644 # given, the time history of the acoustic pressure is recorded by 

645 # default 

646 if self.sensor.record is not None: 

647 

648 # check for time reversal data 

649 if self.time_rev: 

650 warn('WARNING: sensor.record is not used for time reversal reconstructions') 

651 

652 # check the input is a cell array 

653 assert isinstance(self.sensor.record, list), 'sensor.record must be given as a list, e.g. ["p", "u"]' 

654 

655 # check the sensor record flgs 

656 self.record.set_flags_from_list(self.sensor.record, self.elastic_code) 

657 

658 # enforce the sensor.mask field unless just recording the max_all 

659 # and _final variables 

660 fields = ['p', 'p_max', 'p_min', 'p_rms', 'u', 'u_non_staggered', 'u_split_field', 'u_max', 'u_min', 'u_rms', 'I', 'I_avg'] 

661 if any(self.record.is_set(fields)): 

662 assert self.sensor.mask is not None 

663 

664 # check if sensor mask is a binary grid, a set of cuboid corners, 

665 # or a set of Cartesian interpolation points 

666 if not self.blank_sensor: 

667 if (kgrid_dim == 3 and num_dim2(self.sensor.mask) == 3) or (kgrid_dim != 3 and (self.sensor.mask.shape == self.kgrid.k.shape)): 

668 

669 # check the grid is binary 

670 assert self.sensor.mask.sum() == (self.sensor.mask.size - (self.sensor.mask == 0).sum()), \ 

671 'sensor.mask must be a binary grid (numeric values must be 0 or 1).' 

672 

673 # check the grid is not empty 

674 assert self.sensor.mask.sum() != 0, 'sensor.mask must be a binary grid with at least one element set to 1.' 

675 

676 elif self.sensor.mask.shape[0] == 2 * kgrid_dim: 

677 

678 # make sure the points are integers 

679 assert np.all(self.sensor.mask % 1 == 0), 'sensor.mask cuboid corner indices must be integers.' 

680 

681 # store a copy of the cuboid corners 

682 self.record.cuboid_corners_list = self.sensor.mask 

683 

684 # check the list makes sense 

685 if np.any(self.sensor.mask[self.kgrid.dim:, :] - self.sensor.mask[:self.kgrid.dim, :] < 0): 

686 if kgrid_dim == 1: 

687 raise ValueError('sensor.mask cuboid corners must be defined as [x1, x2; ...].'' where x2 => x1, etc.') 

688 elif kgrid_dim == 2: 

689 raise ValueError('sensor.mask cuboid corners must be defined as [x1, y1, x2, y2; ...].'' where x2 => x1, etc.') 

690 elif kgrid_dim == 3: 

691 raise ValueError('sensor.mask cuboid corners must be defined as [x1, y1, z1, x2, y2, z2; ...].'' where x2 => x1, etc.') 

692 

693 # check the list are within bounds 

694 if np.any(self.sensor.mask < 1): 

695 raise ValueError('sensor.mask cuboid corners must be within the grid.') 

696 else: 

697 if kgrid_dim == 1: 

698 if np.any(self.sensor.mask > self.kgrid.Nx): 

699 raise ValueError('sensor.mask cuboid corners must be within the grid.') 

700 elif kgrid_dim == 2: 

701 if np.any(self.sensor.mask[[0, 2], :] > self.kgrid.Nx) or np.any(self.sensor.mask[[1, 3], :] > self.kgrid.Ny): 

702 raise ValueError('sensor.mask cuboid corners must be within the grid.') 

703 elif kgrid_dim == 3: 

704 if np.any(self.sensor.mask[[0, 3], :] > self.kgrid.Nx) or \ 

705 np.any(self.sensor.mask[[1, 4], :] > self.kgrid.Ny) or \ 

706 np.any(self.sensor.mask[[2, 5], :] > self.kgrid.Nz): 

707 raise ValueError('sensor.mask cuboid corners must be within the grid.') 

708 

709 # create a binary mask for display from the list of corners 

710 # TODO FARID mask should be init in sensor not here 

711 self.sensor.mask = np.zeros_like(self.kgrid.k, dtype=bool) 

712 for cuboid_index in range(self.record.cuboid_corners_list.shape[1]): 

713 if self.kgrid.dim == 1: 

714 self.sensor.mask[ 

715 self.record.cuboid_corners_list[0, cuboid_index]:self.record.cuboid_corners_list[1, cuboid_index] 

716 ] = 1 

717 if self.kgrid.dim == 2: 

718 self.sensor.mask[ 

719 self.record.cuboid_corners_list[0, cuboid_index]:self.record.cuboid_corners_list[2, cuboid_index], 

720 self.record.cuboid_corners_list[1, cuboid_index]:self.record.cuboid_corners_list[3, cuboid_index] 

721 ] = 1 

722 if self.kgrid.dim == 3: 

723 self.sensor.mask[ 

724 self.record.cuboid_corners_list[0, cuboid_index]:self.record.cuboid_corners_list[3, cuboid_index], 

725 self.record.cuboid_corners_list[1, cuboid_index]:self.record.cuboid_corners_list[4, cuboid_index], 

726 self.record.cuboid_corners_list[2, cuboid_index]:self.record.cuboid_corners_list[5, cuboid_index] 

727 ] = 1 

728 else: 

729 # check the Cartesian sensor mask is the correct size 

730 # (1 x N, 2 x N, 3 x N) 

731 assert self.sensor.mask.shape[0] == kgrid_dim and num_dim2(self.sensor.mask) <= 2, \ 

732 f'Cartesian sensor.mask for a {kgrid_dim}D simulation must be given as a {kgrid_dim} by N array.' 

733 

734 # set Cartesian mask flag (this is modified in 

735 # createStorageVariables if the interpolation setting is 

736 # set to nearest) 

737 self.binary_sensor_mask = False 

738 

739 # extract Cartesian data from sensor mask 

740 if kgrid_dim == 1: 

741 # align sensor data as a column vector to be the 

742 # same as kgrid.x_vec so that calls to interp1 

743 # return data in the correct dimension 

744 self.sensor_x = np.reshape((self.sensor.mask, (-1, 1))) 

745 

746 # add sensor_x to the record structure for use with 

747 # the _extractSensorData subfunction 

748 self.record.sensor_x = self.sensor_x 

749 "record.sensor_x = sensor_x;" 

750 

751 elif kgrid_dim == 2: 

752 self.sensor_x = self.sensor.mask[0, :] 

753 self.sensor_y = self.sensor.mask[1, :] 

754 elif kgrid_dim == 3: 

755 self.sensor_x = self.sensor.mask[0, :] 

756 self.sensor_y = self.sensor.mask[1, :] 

757 self.sensor_z = self.sensor.mask[2, :] 

758 

759 # compute an equivalent sensor mask using nearest neighbour 

760 # interpolation, if flgs.time_rev = false and 

761 # cartesian_interp = 'linear' then this is only used for 

762 # display, if flgs.time_rev = true or cartesian_interp = 

763 # 'nearest' this grid is used as the sensor.mask 

764 self.sensor.mask, self.order_index, self.reorder_index = cart2grid(self.kgrid, self.sensor.mask, self.axisymmetric) 

765 

766 # if in time reversal mode, reorder the p0 input data in 

767 # the order of the binary sensor_mask 

768 if self.time_rev: 

769 raise NotImplementedError 

770 """ 

771 # append the reordering data 

772 new_col_pos = length(sensor.time_reversal_boundary_data(1, :)) + 1; 

773 sensor.time_reversal_boundary_data(:, new_col_pos) = order_index; 

774  

775 # reorder p0 based on the order_index 

776 sensor.time_reversal_boundary_data = sortrows(sensor.time_reversal_boundary_data, new_col_pos); 

777  

778 # remove the reordering data 

779 sensor.time_reversal_boundary_data = sensor.time_reversal_boundary_data(:, 1:new_col_pos - 1); 

780 """ 

781 else: 

782 # set transducer sensor flag 

783 self.transducer_sensor = True 

784 self.record.p = False 

785 

786 # check to see if there is an elevation focus 

787 if not np.isinf(self.sensor.elevation_focus_distance): 

788 # set flag 

789 self.transducer_receive_elevation_focus = True 

790 

791 # get the elevation mask that is used to extract the correct values 

792 # from the sensor data buffer for averaging 

793 self.transducer_receive_mask = self.sensor.elevation_beamforming_mask 

794 

795 # check for directivity inputs with time reversal 

796 if kgrid_dim == 2 and self.use_sensor and self.compute_directivity and self.time_rev: 

797 warn('WARNING: sensor directivity fields are not used for time reversal.') 

798 

799 def check_sensor(self, k_dim, k_Nt) -> None: 

800 """ 

801 Check the Sensor properties for correctness and validity 

802 

803 Args: 

804 k_dim: kWaveGrid dimensionality 

805 k_Nt: Number of time steps in kWaveGrid 

806 

807 Returns: 

808 None 

809 """ 

810 # ========================================================================= 

811 # CHECK SOURCE STRUCTURE INPUTS 

812 # ========================================================================= 

813 

814 # check source inputs 

815 if not isinstance(self.source, (kSource, NotATransducer)): 

816 # allow an invalid or empty source input if computing time reversal, 

817 # otherwise return error 

818 assert self.time_rev, 'source must be defined as an object of the kSource or kWaveTransducer classes.' 

819 

820 elif not isinstance(self.source, NotATransducer): 

821 

822 # -------------------------- 

823 # SOURCE IS NOT A TRANSDUCER 

824 # -------------------------- 

825 

826 ''' 

827 check allowable source types 

828  

829 Depending on the kgrid dimensionality and the simulation type,  

830 following fields are allowed & might be use: 

831  

832 kgrid.dim == 1: 

833 non-elastic code: 

834 ['p0', 'p', 'p_mask', 'p_mode', 'p_frequency_ref', 'ux', 'u_mask', 'u_mode', 'u_frequency_ref'] 

835 kgrid.dim == 2: 

836 non-elastic code: 

837 ['p0', 'p', 'p_mask', 'p_mode', 'p_frequency_ref', 'ux', 'uy', 'u_mask', 'u_mode', 'u_frequency_ref'] 

838 elastic code: 

839 ['p0', 'sxx', 'syy', 'sxy', 's_mask', 's_mode', 'ux', 'uy', 'u_mask', 'u_mode'] 

840 kgrid.dim == 3: 

841 non-elastic code: 

842 ['p0', 'p', 'p_mask', 'p_mode', 'p_frequency_ref', 'ux', 'uy', 'uz', 'u_mask', 'u_mode', 'u_frequency_ref'] 

843 elastic code: 

844 ['p0', 'sxx', 'syy', 'szz', 'sxy', 'sxz', 'syz', 's_mask', 's_mode', 'ux', 'uy', 'uz', 'u_mask', 'u_mode'] 

845 ''' 

846 

847 self.source.validate(self.kgrid) 

848 

849 # check for a time varying pressure source input 

850 if self.source.p is not None: 

851 

852 # check the source mode input is valid 

853 if self.source.p_mode is None: 

854 self.source.p_mode = self.SOURCE_P_MODE_DEF 

855 

856 if self.source_p > k_Nt: 

857 warn(' WARNING: source.p has more time points than kgrid.Nt, remaining time points will not be used.') 

858 

859 # create an indexing variable corresponding to the location of all the source elements 

860 self.p_source_pos_index = matlab_find(self.source.p_mask) 

861 

862 # check if the mask is binary or labelled 

863 p_unique = np.unique(self.source.p_mask) 

864 

865 # create a second indexing variable 

866 if p_unique.size <= 2 and p_unique.sum() == 1: 

867 # set signal index to all elements 

868 self.p_source_sig_index = ':' 

869 else: 

870 # set signal index to the labels (this allows one input signal 

871 # to be used for each source label) 

872 self.p_source_sig_index = self.source.p_mask(self.source.p_mask != 0) 

873 

874 # convert the data type depending on the number of indices 

875 self.p_source_pos_index = cast_to_type(self.p_source_pos_index, self.index_data_type) 

876 if self.source_p_labelled: 

877 self.p_source_sig_index = cast_to_type(self.p_source_sig_index, self.index_data_type) 

878 

879 # check for time varying velocity source input and set source flag 

880 if any([(getattr(self.source, k) is not None) for k in ['ux', 'uy', 'uz', 'u_mask']]): 

881 

882 # check the source mode input is valid 

883 if self.source.u_mode is None: 

884 self.source.u_mode = self.SOURCE_U_MODE_DEF 

885 

886 # create an indexing variable corresponding to the location of all 

887 # the source elements 

888 self.u_source_pos_index = matlab_find(self.source.u_mask) 

889 

890 # check if the mask is binary or labelled 

891 u_unique = np.unique(self.source.u_mask) 

892 

893 # create a second indexing variable 

894 if u_unique.size <= 2 and u_unique.sum() == 1: 

895 

896 # set signal index to all elements 

897 self.u_source_sig_index = ':' 

898 else: 

899 # set signal index to the labels (this allows one input signal 

900 # to be used for each source label) 

901 self.u_source_sig_index = self.source.u_mask[self.source.u_mask != 0] 

902 

903 # convert the data type depending on the number of indices 

904 self.u_source_pos_index = cast_to_type(self.u_source_pos_index, self.index_data_type) 

905 if self.source_u_labelled: 

906 self.u_source_sig_index = cast_to_type(self.u_source_sig_index, self.index_data_type) 

907 

908 # check for time varying stress source input and set source flag 

909 if any([(getattr(self.source, k) is not None) for k in ['sxx', 'syy', 'szz', 'sxy', 'sxz', 'syz', 's_mask']]): 

910 # create an indexing variable corresponding to the location of all 

911 # the source elements 

912 raise NotImplementedError 

913 's_source_pos_index = find(source.s_mask != 0);' 

914 

915 # check if the mask is binary or labelled 

916 's_unique = unique(source.s_mask);' 

917 

918 # create a second indexing variable 

919 if eng.eval('numel(s_unique) <= 2 && sum(s_unique) == 1'): 

920 # set signal index to all elements 

921 eng.workspace['s_source_sig_index'] = ':' 

922 

923 else: 

924 # set signal index to the labels (this allows one input signal 

925 # to be used for each source label) 

926 s_source_sig_index = source.s_mask(source.s_mask != 0) 

927 

928 f's_source_pos_index = {self.index_data_type}(s_source_pos_index);' 

929 if self.source_s_labelled: 

930 f's_source_sig_index = {self.index_data_type}(s_source_sig_index);' 

931 

932 else: 

933 # ---------------------- 

934 # SOURCE IS A TRANSDUCER 

935 # ---------------------- 

936 

937 # if the sensor is a transducer, check that the simulation is in 3D 

938 assert k_dim == 3, 'Transducer inputs are only compatible with 3D simulations.' 

939 

940 # get the input signal - this is appended with zeros if required to 

941 # account for the beamforming delays (this will throw an error if the 

942 # input signal is not defined) 

943 self.transducer_input_signal = self.source.input_signal 

944 

945 # get the delay mask that accounts for the beamforming delays and 

946 # elevation focussing; this is used so that a single time series can be 

947 # applied to the complete transducer mask with different delays 

948 delay_mask = self.source.delay_mask() 

949 

950 # set source flag - this should be the length of signal minus the 

951 # maximum delay 

952 self.transducer_source = self.transducer_input_signal.size - delay_mask.max() 

953 

954 # get the active elements mask 

955 active_elements_mask = self.source.active_elements_mask 

956 

957 # get the apodization mask if not set to 'Rectangular' and convert to a 

958 # linear array 

959 if self.source.transmit_apodization == 'Rectangular': 

960 self.transducer_transmit_apodization = 1 

961 else: 

962 self.transducer_transmit_apodization = self.source.transmit_apodization_mask 

963 self.transducer_transmit_apodization = self.transducer_transmit_apodization[active_elements_mask != 0] 

964 

965 # create indexing variable corresponding to the active elements 

966 # and convert the data type depending on the number of indices 

967 self.u_source_pos_index = matlab_find(active_elements_mask).astype(self.index_data_type) 

968 

969 # convert the delay mask to an indexing variable (this doesn't need to 

970 # be modified if the grid is expanded) which tells each point in the 

971 # source mask which point in the input_signal should be used 

972 delay_mask = matlab_mask(delay_mask, active_elements_mask != 0) # compatibility 

973 

974 # convert the data type depending on the maximum value of the delay 

975 # mask and the length of the source 

976 smallest_type = get_smallest_possible_type(delay_mask.max(), 'uint') 

977 if smallest_type is not None: 

978 delay_mask = delay_mask.astype(smallest_type) 

979 

980 # move forward by 1 as a delay of 0 corresponds to the first point in the input signal 

981 self.delay_mask = delay_mask + 1 

982 

983 # clean up unused variables 

984 del active_elements_mask 

985 

986 def check_kgrid_time(self) -> None: 

987 """ 

988 Check time-related kWaveGrid inputs 

989 

990 Returns: 

991 None 

992 """ 

993 

994 # check kgrid for t_array existance, and create if not defined 

995 if isinstance(self.kgrid.t_array, str) and self.kgrid.t_array == 'auto': 

996 

997 # check for time reversal mode 

998 if self.time_rev: 

999 raise ValueError('kgrid.t_array (Nt and dt) must be defined explicitly in time reversal mode.') 

1000 

1001 # check for time varying sources 

1002 if (not self.source_p0_elastic) and ( 

1003 self.source_p or 

1004 self.source_ux or self.source_uy or self.source_uz or 

1005 self.source_sxx or self.source_syy or self.source_szz or 

1006 self.source_sxy or self.source_sxz or self.source_syz): 

1007 raise ValueError('kgrid.t_array (Nt and dt) must be defined explicitly when using a time-varying source.') 

1008 

1009 # create the time array using the compressional sound speed 

1010 self.kgrid.makeTime(self.medium.sound_speed, self.KSPACE_CFL) 

1011 

1012 # check kgrid.t_array for stability given medium properties 

1013 if not self.elastic_code: 

1014 

1015 # calculate the largest timestep for which the model is stable 

1016 

1017 dt_stability_limit = check_stability(self.kgrid, self.medium) 

1018 

1019 # give a warning if the timestep is larger than stability limit allows 

1020 if self.kgrid.dt > dt_stability_limit: 

1021 warn(' WARNING: time step may be too large for a stable simulation.') 

1022 

1023 @staticmethod 

1024 def select_precision(opt: SimulationOptions): 

1025 """ 

1026 Select the minimal precision for storing the data 

1027 

1028 Args: 

1029 opt: SimulationOptions instance 

1030 

1031 Returns: 

1032 Minimal precision for variable allocation 

1033 

1034 """ 

1035 # set storage variable type based on data_cast - this enables the 

1036 # output variables to be directly created in the data_cast format, 

1037 # rather than creating them in double precision and then casting them 

1038 if opt.data_cast == 'off': 

1039 precision = 'double' 

1040 elif opt.data_cast == 'single': 

1041 precision = 'single' 

1042 elif opt.data_cast == 'gsingle': 

1043 precision = 'single' 

1044 elif opt.data_cast == 'gdouble': 

1045 precision = 'double' 

1046 elif opt.data_cast == 'gpuArray': 

1047 raise NotImplementedError("gpuArray is not supported in Python-version") 

1048 elif opt.data_cast == 'kWaveGPUsingle': 

1049 precision = 'single' 

1050 elif opt.data_cast == 'kWaveGPUdouble': 

1051 precision = 'double' 

1052 else: 

1053 raise ValueError("'Unknown ''DataCast'' option'") 

1054 return precision 

1055 

1056 def check_input_combinations(self, opt: SimulationOptions, user_medium_density_input: bool, k_dim, pml_size, kgrid_N) -> None: 

1057 """ 

1058 Check the input combinations for correctness and validity 

1059 

1060 Args: 

1061 opt: SimulationOptions instance 

1062 user_medium_density_input: Medium Density 

1063 k_dim: kWaveGrid dimensionality 

1064 pml_size: Size of the PML 

1065 kgrid_N: kWaveGrid size in each direction 

1066 

1067 Returns: 

1068 None 

1069 """ 

1070 # ========================================================================= 

1071 # CHECK FOR VALID INPUT COMBINATIONS 

1072 # ========================================================================= 

1073 

1074 # enforce density input if velocity sources or output are being used 

1075 if not user_medium_density_input and (self.source_ux or self.source_uy or self.source_uz or self.record.u or self.record.u_max or self.record.u_rms): 

1076 raise ValueError('medium.density must be explicitly defined if velocity inputs or outputs are used, even in homogeneous media.') 

1077 

1078 # enforce density input if nonlinear equations are being used 

1079 if not user_medium_density_input and self.medium.is_nonlinear(): 

1080 raise ValueError('medium.density must be explicitly defined if medium.BonA is specified.') 

1081 

1082 # check sensor compatability options for flgs.compute_directivity 

1083 if self.use_sensor and k_dim == 2 and self.compute_directivity and not self.binary_sensor_mask and opt.cartesian_interp == 'linear': 

1084 raise ValueError('sensor directivity fields are only compatible with binary sensor masks or ''CartInterp'' set to ''nearest''.') 

1085 

1086 # check for split velocity output 

1087 if self.record.u_split_field and not self.binary_sensor_mask: 

1088 raise ValueError('The option sensor.record = {''u_split_field''} is only compatible with a binary sensor mask.') 

1089 

1090 # check input options for data streaming ***** 

1091 if opt.stream_to_disk: 

1092 if not self.use_sensor or self.time_rev: 

1093 raise ValueError('The optional input ''StreamToDisk'' is currently only compatible with forward simulations using a non-zero sensor mask.'); 

1094 elif self.sensor.record is not None and self.sensor.record.ismember(self.record.flags[1:]).any(): 

1095 raise ValueError('The optional input ''StreamToDisk'' is currently only compatible with sensor.record = {''p''} (the default).') 

1096 

1097 # make sure the PML size is smaller than the grid if PMLInside is true 

1098 if opt.pml_inside and ( 

1099 (k_dim == 1 and ((pml_size.x * 2 > self.kgrid.Nx))) or 

1100 (k_dim == 2 and ~self.axisymmetric and ((pml_size.x * 2 > kgrid_N[0]) or (pml_size.y * 2 > kgrid_N[1]))) or 

1101 (k_dim == 2 and self.axisymmetric and ((pml_size.x * 2 > kgrid_N[0]) or (pml_size.y > kgrid_N[1]))) or 

1102 (k_dim == 3 and ((pml_size.x*2 > kgrid_N[0]) or (pml_size.x*2 > kgrid_N[1]) or (pml_size.z * 2 > kgrid_N[2]) ))): 

1103 raise ValueError('The size of the PML must be smaller than the size of the grid.') 

1104 

1105 # make sure the PML is inside if using a non-uniform grid 

1106 if self.nonuniform_grid and not opt.pml_inside: 

1107 raise ValueError("''PMLInside'' must be true for simulations using non-uniform grids.") 

1108 

1109 # check for compatible input options if saving to disk 

1110 # modified by Farid | disabled temporarily! 

1111 # if k_dim == 3 and isinstance(self.options.save_to_disk, str) and (not self.use_sensor or not self.binary_sensor_mask or self.time_rev): 

1112 # raise ValueError('The optional input ''SaveToDisk'' is currently only compatible with forward simulations using a non-zero binary sensor mask.') 

1113 

1114 # check the record start time is within range 

1115 record_start_index = self.sensor.record_start_index 

1116 if self.use_sensor and ((record_start_index > self.kgrid.Nt) or (record_start_index < 1)): 

1117 raise ValueError('sensor.record_start_index must be between 1 and the number of time steps.') 

1118 

1119 # ensure 'WSWA' symmetry if using axisymmetric code with 'SaveToDisk' 

1120 if self.axisymmetric and self.options.radial_symmetry != 'WSWA' and isinstance(self.options.save_to_disk, str): 

1121 

1122 # display a warning only if using WSWS symmetry (not WSWA-FFT) 

1123 if self.options.radial_symmetry.startswith('WSWS'): 

1124 print(' WARNING: Optional input ''RadialSymmetry'' changed to ''WSWA'' for compatability with ''SaveToDisk''.') 

1125 

1126 # update setting 

1127 self.options.radial_symmetry = 'WSWA' 

1128 

1129 # ensure p0 smoothing is switched off if p0 is empty 

1130 if not self.source_p0: 

1131 self.options.smooth_p0 = False 

1132 

1133 # start log if required 

1134 if opt.create_log: 

1135 raise NotImplementedError(f"diary({self.LOG_NAME}.txt');") 

1136 

1137 # update command line status 

1138 if self.time_rev: 

1139 print(' time reversal mode') 

1140 

1141 # cleanup unused variables 

1142 for k in list(self.__dict__.keys()): 

1143 if k.endswith('_DEF'): 

1144 delattr(self, k) 

1145 

1146 def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) -> None: 

1147 """ 

1148 Smooth and enlarge grids 

1149 

1150 Args: 

1151 source: kWaveSource instance 

1152 k_dim: kWaveGrid dimensionality 

1153 kgrid_N: kWaveGrid size in each direction 

1154 opt: SimulationOptions 

1155 

1156 Returns: 

1157 None 

1158 """ 

1159 

1160 # smooth the initial pressure distribution p0 if required, and then restore 

1161 # the maximum magnitude 

1162 # NOTE 1: if p0 has any values at the edge of the domain, the smoothing 

1163 # may cause part of p0 to wrap to the other side of the domain 

1164 # NOTE 2: p0 is smoothed before the grid is expanded to ensure that p0 is 

1165 # exactly zero within the PML 

1166 # NOTE 3: for the axisymmetric code, p0 is smoothed assuming WS origin 

1167 # symmetry 

1168 if self.source_p0 and self.options.smooth_p0: 

1169 

1170 # update command line status 

1171 print(' smoothing p0 distribution...') 

1172 

1173 if self.axisymmetric: 

1174 if self.options.radial_symmetry in ['WSWA-FFT', 'WSWA']: 

1175 # create a new kWave grid object with expanded radial grid 

1176 kgrid_exp = kWaveGrid([kgrid_N.x, kgrid_N.y * 4], [self.kgrid.dx, self.kgrid.dy]) 

1177 

1178 # mirror p0 in radial dimension using WSWA symmetry 

1179 self.source.p0 = self.source.p0.astype(float) 

1180 p0_exp = np.zeros((kgrid_exp.Nx, kgrid_exp.Ny)) 

1181 p0_exp[:, kgrid_N.y*0 + 0:kgrid_N.y*1] = self.source.p0 

1182 p0_exp[:, kgrid_N.y*1 + 1:kgrid_N.y*2] = -np.fliplr(self.source.p0[:, 1:]) 

1183 p0_exp[:, kgrid_N.y*2 + 0:kgrid_N.y*3] = -self.source.p0 

1184 p0_exp[:, kgrid_N.y*3 + 1:kgrid_N.y*4] = np.fliplr(self.source.p0[:, 1:]) 

1185 

1186 elif self.options.radial_symmetry in ['WSWS-FFT', 'WSWS']: 

1187 # create a new kWave grid object with expanded radial grid 

1188 kgrid_exp = kWaveGrid([kgrid_N.x, kgrid_N.y * 2 - 2], [self.kgrid.dx, self.kgrid.dy]) 

1189 

1190 # mirror p0 in radial dimension using WSWS symmetry 

1191 p0_exp = np.zeros((kgrid_exp.Nx, kgrid_exp.Ny)) 

1192 p0_exp[:, 1:kgrid_N.y] = source.p0 

1193 p0_exp[:, kgrid_N.y + 0:kgrid_N.y*2 - 2] = np.fliplr(source.p0[:, 1:-1]) 

1194 

1195 # smooth p0 

1196 p0_exp = smooth(p0_exp, True) 

1197 

1198 # trim back to original size 

1199 source.p0 = p0_exp[:, 0:self.kgrid.Ny] 

1200 

1201 # clean up unused variables 

1202 del kgrid_exp 

1203 del p0_exp 

1204 else: 

1205 source.p0 = smooth(source.p0, True) 

1206 

1207 # expand the computational grid if the PML is set to be outside the input 

1208 # grid defined by the user 

1209 if not opt.pml_inside: 

1210 expand_results = expand_grid_matrices( 

1211 self.kgrid, self.medium, self.source, self.sensor, self.options, 

1212 dotdict({ 

1213 'p_source_pos_index': self.p_source_pos_index, 

1214 'u_source_pos_index': self.u_source_pos_index, 

1215 's_source_pos_index': self.s_source_pos_index, 

1216 }), 

1217 dotdict({ 

1218 'axisymmetric': self.axisymmetric, 

1219 'use_sensor': self.use_sensor, 

1220 'blank_sensor': self.blank_sensor, 

1221 'cuboid_corners': self.cuboid_corners, 

1222 

1223 'source_p0': self.source_p0, 

1224 'source_p': self.source_p, 

1225 

1226 'source_ux': self.source_ux, 

1227 'source_uy': self.source_uy, 

1228 'source_uz': self.source_uz, 

1229 

1230 'transducer_source': self.transducer_source, 

1231 

1232 'source_sxx': self.source_sxx, 

1233 'source_syy': self.source_syy, 

1234 'source_szz': self.source_szz, 

1235 'source_sxy': self.source_sxy, 

1236 'source_sxz': self.source_sxz, 

1237 'source_syz': self.source_syz 

1238 }) 

1239 ) 

1240 self.kgrid, self.index_data_type, self.p_source_pos_index, self.u_source_pos_index, self.s_source_pos_index = expand_results 

1241 

1242 # get maximum prime factors 

1243 if self.axisymmetric: 

1244 prime_facs = self.kgrid.highest_prime_factors(self.options.radial_symmetry[:4]) 

1245 else: 

1246 prime_facs = self.kgrid.highest_prime_factors() 

1247 

1248 # give warning for bad dimension sizes 

1249 if prime_facs.max() > self.HIGHEST_PRIME_FACTOR_WARNING: 

1250 prime_facs = prime_facs[prime_facs != 0] 

1251 warn(f'WARNING: Highest prime factors in each dimension are {prime_facs}') 

1252 warn('Use dimension sizes with lower prime factors to improve speed') 

1253 del prime_facs 

1254 

1255 # smooth the sound speed distribution if required 

1256 if opt.smooth_c0 and num_dim2(self.medium.sound_speed) == k_dim and self.medium.sound_speed.size > 1: 

1257 print(' smoothing sound speed distribution...') 

1258 ev('medium.sound_speed = smooth(medium.sound_speed);') 

1259 

1260 # smooth the ambient density distribution if required 

1261 if opt.smooth_rho0 and num_dim2(self.medium.density) == k_dim and self.medium.density.size > 1: 

1262 print('smoothing density distribution...') 

1263 ev('medium.density = smooth(medium.density);') 

1264 

1265 def create_sensor_variables(self) -> None: 

1266 """ 

1267 Create the sensor related variables 

1268 

1269 Returns: 

1270 None 

1271 """ 

1272 # define the output variables and mask indices if using the sensor 

1273 if self.use_sensor: 

1274 if not self.blank_sensor or isinstance(self.options.save_to_disk, str): 

1275 if self.cuboid_corners: 

1276 

1277 # create empty list of sensor indices 

1278 self.sensor_mask_index = [] 

1279 

1280 # loop through the list of cuboid corners, and extract the 

1281 # sensor mask indices for each cube 

1282 for cuboid_index in range(self.record.cuboid_corners_list.shape[1]): 

1283 

1284 # create empty binary mask 

1285 temp_mask = np.zeros_like(self.kgrid.k, dtype=bool) 

1286 

1287 if self.kgrid.dim == 1: 

1288 self.sensor.mask[ 

1289 self.record.cuboid_corners_list[0, cuboid_index]:self.record.cuboid_corners_list[1, cuboid_index] 

1290 ] = 1 

1291 if self.kgrid.dim == 2: 

1292 self.sensor.mask[ 

1293 self.record.cuboid_corners_list[0, cuboid_index]:self.record.cuboid_corners_list[2, cuboid_index], 

1294 self.record.cuboid_corners_list[1, cuboid_index]:self.record.cuboid_corners_list[3, cuboid_index] 

1295 ] = 1 

1296 if self.kgrid.dim == 3: 

1297 self.sensor.mask[ 

1298 self.record.cuboid_corners_list[0, cuboid_index]:self.record.cuboid_corners_list[3, cuboid_index], 

1299 self.record.cuboid_corners_list[1, cuboid_index]:self.record.cuboid_corners_list[4, cuboid_index], 

1300 self.record.cuboid_corners_list[2, cuboid_index]:self.record.cuboid_corners_list[5, cuboid_index] 

1301 ] = 1 

1302 

1303 # extract mask indices 

1304 self.sensor_mask_index.append(matlab_find(temp_mask)) 

1305 self.sensor_mask_index = np.array(self.sensor_mask_index) 

1306 

1307 # cleanup unused variables 

1308 del temp_mask 

1309 

1310 else: 

1311 # create mask indices (this works for both normal sensor and 

1312 # transducer inputs) 

1313 self.sensor_mask_index = np.where(self.sensor.mask.flatten(order='F') != 0)[0] + 1 # +1 due to matlab indexing 

1314 self.sensor_mask_index = np.expand_dims(self.sensor_mask_index, -1) # compatibility, n => [n, 1] 

1315 

1316 # convert the data type depending on the number of indices (this saves 

1317 # memory) 

1318 self.sensor_mask_index = cast_to_type(self.sensor_mask_index, self.index_data_type) 

1319 

1320 else: 

1321 # set the sensor mask index variable to be empty 

1322 self.sensor_mask_index = [] 

1323 

1324 # run subscript to create storage variables if not saving to disk 

1325 if self.use_sensor and not isinstance(self.options.save_to_disk, str): 

1326 result = create_storage_variables( 

1327 self.kgrid, self.sensor, self.options, 

1328 dotdict({ 

1329 'binary_sensor_mask': self.binary_sensor_mask, 

1330 'time_rev': self.time_rev, 

1331 'blank_sensor': self.blank_sensor, 

1332 'record_u_split_field': self.record_u_split_field, 

1333 'source_u_labelled': self.source_u_labelled, 

1334 'axisymmetric': self.axisymmetric, 

1335 'reorder_data': self.reorder_data, 

1336 'transducer_receive_elevation_focus': self.transducer_receive_elevation_focus, 

1337 }), 

1338 dotdict({ 

1339 'sensor_x': self.sensor_x, 

1340 'sensor_mask_index': self.sensor_mask_index, 

1341 'record': self.record, 

1342 'sensor_data_buffer_size': self.sensor_data_buffer_size, 

1343 }) 

1344 ) 

1345 self.binary_sensor_mask = result.binary_sensor_mask 

1346 self.reorder_data = result.reorder_data 

1347 self.transducer_receive_elevation_focus = result.transducer_receive_elevation_focus 

1348 

1349 def create_absorption_vars(self) -> None: 

1350 """ 

1351 Create absorption variables for the fluid code based on 

1352 the expanded and smoothed values of the medium parameters (if not saving to disk) 

1353 

1354 Returns: 

1355 None 

1356 """ 

1357 if not self.elastic_code and not isinstance(self.options.save_to_disk, str): 

1358 self.absorb_nabla1, self.absorb_nabla2, self.absorb_tau, self.absorb_eta = create_absorption_variables(self.kgrid, self.medium, self.equation_of_state) 

1359 

1360 def assign_pseudonyms(self, medium: kWaveMedium, kgrid: kWaveGrid) -> None: 

1361 """ 

1362 Shorten commonly used field names (these act only as pointers provided that the values aren't modified) 

1363 (done after enlarging and smoothing the grids) 

1364 

1365 Args: 

1366 medium: kWaveMedium instance 

1367 kgrid: kWaveGrid instance 

1368 

1369 Returns: 

1370 None 

1371 """ 

1372 self.dt = float(kgrid.dt) 

1373 self.rho0 = medium.density 

1374 self.c0 = medium.sound_speed 

1375 

1376 def scale_source_terms(self, is_scale_source_terms) -> None: 

1377 """ 

1378 Scale the source terms based on the expanded and smoothed values of the medium parameters 

1379 

1380 Args: 

1381 is_scale_source_terms: Should the source terms be scaled 

1382 

1383 Returns: 

1384 None 

1385 """ 

1386 if not is_scale_source_terms: 

1387 return 

1388 try: 

1389 p_source_pos_index = self.p_source_pos_index 

1390 except AttributeError: 

1391 p_source_pos_index = None 

1392 

1393 try: 

1394 s_source_pos_index = self.s_source_pos_index 

1395 except AttributeError: 

1396 s_source_pos_index = None 

1397 

1398 try: 

1399 u_source_pos_index = self.u_source_pos_index 

1400 except AttributeError: 

1401 u_source_pos_index = None 

1402 

1403 self.transducer_input_signal = scale_source_terms_func( 

1404 self.c0, self.dt, self.kgrid, self.source, 

1405 p_source_pos_index, s_source_pos_index, u_source_pos_index, self.transducer_input_signal, 

1406 dotdict({ 

1407 'nonuniform_grid': self.nonuniform_grid, 

1408 'source_ux': self.source_ux, 

1409 'source_uy': self.source_uy, 

1410 'source_uz': self.source_uz, 

1411 'transducer_source': self.transducer_source, 

1412 'source_p': self.source_p, 

1413 'source_p0': self.source_p0, 

1414 'use_w_source_correction_p': self.use_w_source_correction_p, 

1415 'use_w_source_correction_u': self.use_w_source_correction_u, 

1416 

1417 'source_sxx': self.source_sxx, 

1418 'source_syy': self.source_syy, 

1419 'source_szz': self.source_szz, 

1420 'source_sxy': self.source_sxy, 

1421 'source_sxz': self.source_sxz, 

1422 'source_syz': self.source_syz, 

1423 }) 

1424 ) 

1425 

1426 def create_pml_indices(self, kgrid_dim, kgrid_N: Array, pml_size, pml_inside, is_axisymmetric): 

1427 """ 

1428 Define index variables to remove the PML from the display if the optional 

1429 input 'PlotPML' is set to false 

1430 

1431 Args: 

1432 kgrid_dim: kWaveGrid dimensinality 

1433 kgrid_N: kWaveGrid size in each direction 

1434 pml_size: Size of the PML 

1435 pml_inside: Whether the PML is inside the grid defined by the user 

1436 is_axisymmetric: Whether the simulation is axisymmetric 

1437 

1438 Returns: 

1439 

1440 """ 

1441 # comment by Farid: PlotPML is always False in Python version, 

1442 # therefore if statement removed 

1443 if kgrid_dim == 1: 

1444 self.x1 = pml_size.x + 1.0 

1445 self.x2 = kgrid_N.x - pml_size.x 

1446 elif kgrid_dim == 2: 

1447 self.x1 = pml_size.x + 1.0 

1448 self.x2 = kgrid_N.x - pml_size.x 

1449 if self.axisymmetric: 

1450 self.y1 = 1.0 

1451 else: 

1452 self.y1 = pml_size.y + 1.0 

1453 self.y2 = kgrid_N.y - pml_size.y 

1454 elif kgrid_dim == 3: 

1455 self.x1 = pml_size.x + 1.0 

1456 self.x2 = kgrid_N.x - pml_size.x 

1457 self.y1 = pml_size.y + 1.0 

1458 self.y2 = kgrid_N.y - pml_size.y 

1459 self.z1 = pml_size.z + 1.0 

1460 self.z2 = kgrid_N.z - pml_size.z 

1461 

1462 # define index variables to allow original grid size to be maintained for 

1463 # the _final and _all output variables if 'PMLInside' is set to false 

1464 # if self.record is None: 

1465 # self.record = Recorder() 

1466 self.record.set_index_variables(self.kgrid, pml_size, pml_inside, is_axisymmetric)