Coverage for kwave/kWaveSimulation.py: 18%

581 statements  

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

1import logging 

2from dataclasses import dataclass 

3 

4import numpy as np 

5from deprecated import deprecated 

6 

7from kwave.data import Vector 

8from kwave.kgrid import kWaveGrid 

9from kwave.kmedium import kWaveMedium 

10from kwave.ksensor import kSensor 

11from kwave.ksource import kSource 

12from kwave.ktransducer import NotATransducer 

13from kwave.kWaveSimulation_helper import ( 

14 create_absorption_variables, 

15 display_simulation_params, 

16 expand_grid_matrices, 

17 scale_source_terms_func, 

18 set_sound_speed_ref, 

19) 

20from kwave.options.simulation_options import SimulationOptions, SimulationType 

21from kwave.recorder import Recorder 

22from kwave.utils.checks import check_stability 

23from kwave.utils.colormap import get_color_map 

24from kwave.utils.conversion import cart2grid, cast_to_type 

25from kwave.utils.data import get_date_string, get_smallest_possible_type 

26from kwave.utils.dotdictionary import dotdict 

27from kwave.utils.filters import smooth 

28from kwave.utils.matlab import matlab_find, matlab_mask 

29from kwave.utils.matrix import num_dim2 

30 

31 

32@dataclass 

33class kWaveSimulation(object): 

34 def __init__( 

35 self, kgrid: kWaveGrid, source: kSource, sensor: NotATransducer, medium: kWaveMedium, simulation_options: SimulationOptions 

36 ): 

37 self.precision = None 

38 self.kgrid = kgrid 

39 self.medium = medium 

40 self.source = source 

41 self.sensor = sensor 

42 self.options = simulation_options 

43 

44 # ========================================================================= 

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

46 # ========================================================================= 

47 # flags which control the characteristics of the sensor 

48 #: Whether the sensor sensor mask is defined by cuboid corners 

49 #: Whether time reversal simulation is enabled 

50 

51 # check if the sensor mask is defined as a list of cuboid corners 

52 if self.sensor.mask is not None and self.sensor.mask.shape[0] == (2 * self.kgrid.dim): 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true

53 self.userarg_cuboid_corners = True 

54 else: 

55 self.userarg_cuboid_corners = False 

56 

57 # check if performing time reversal, and replace inputs to explicitly use a 

58 # source with a dirichlet boundary condition 

59 if self.sensor.time_reversal_boundary_data is not None: 59 ↛ 77line 59 didn't jump to line 77 because the condition on line 59 was always true

60 # define a new source structure 

61 self.source = kSource() 

62 self.source.p_mask = self.sensor.mask 

63 self.source.p = np.flip(self.sensor.time_reversal_boundary_data, 1) 

64 self.source.p_mode = "dirichlet" 

65 

66 # define a new sensor structure 

67 Nx, Ny, Nz = self.kgrid.Nx, self.kgrid.Ny, self.kgrid.Nz 

68 self.sensor = kSensor(mask=np.ones((Nx, Ny, max(1, Nz))), record=["p_final"]) 

69 

70 # set time reversal flag 

71 self.userarg_time_rev = True 

72 

73 self.record = Recorder() 

74 self.record.p = False 

75 else: 

76 # set time reversal flag 

77 self.userarg_time_rev = False 

78 

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

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

81 #: and thus must be re-ordered 

82 self.reorder_data = False 

83 

84 #: Whether the sensor.mask is binary 

85 self.binary_sensor_mask = True 

86 

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

88 self.transducer_sensor = False 

89 

90 self.record = Recorder() 

91 

92 # transducer source flags 

93 #: transducer is object of kWaveTransducer class 

94 self.transducer_source = False 

95 

96 #: Apply receive elevation focus on the transducer 

97 self.transducer_receive_elevation_focus = False 

98 

99 # general 

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

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

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

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

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

105 

106 # source types 

107 self.SOURCE_S_MODE_DEF = "additive" #: source mode for stress sources 

108 self.SOURCE_P_MODE_DEF = "additive" #: source mode for pressure sources 

109 self.SOURCE_U_MODE_DEF = "additive" #: source mode for velocity sources 

110 

111 # filenames 

112 self.STREAM_TO_DISK_FILENAME = "temp_sensor_data.bin" #: default disk stream filename 

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

114 

115 self.calling_func_name = None 

116 logging.log(logging.INFO, f" start time: {get_date_string()}") 

117 

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

119 self.transducer_input_signal = None 

120 

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

122 self.p_source_pos_index = None 

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

124 self.u_source_pos_index = None 

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

126 self.s_source_pos_index = None 

127 

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

129 self.delay_mask = None 

130 

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

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

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

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

135 

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

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

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

139 self.index_data_type = None 

140 

141 @property 

142 def equation_of_state(self): 

143 """ 

144 Returns: 

145 Set equation of state variable 

146 """ 

147 if self.medium.absorbing: 

148 if self.medium.stokes: 

149 return "stokes" 

150 else: 

151 return "absorbing" 

152 else: 

153 return "loseless" 

154 

155 @property 

156 def use_sensor(self): 

157 """ 

158 Returns: 

159 False if no output of any kind is required 

160 

161 """ 

162 return self.sensor is not None 

163 

164 @property 

165 def blank_sensor(self): 

166 """ 

167 Returns 

168 True if sensor.mask is not defined but _max_all or _final variables are still recorded 

169 

170 """ 

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

172 if not (isinstance(self.sensor, NotATransducer) or any(self.record.is_set(fields)) or self.time_rev): 

173 return True 

174 return False 

175 

176 @property 

177 def kelvin_voigt_model(self): 

178 """ 

179 Returns: 

180 Whether the simulation is elastic with absorption 

181 

182 """ 

183 return False 

184 

185 @property 

186 def nonuniform_grid(self): 

187 """ 

188 Returns: 

189 True if the computational grid is non-uniform 

190 

191 """ 

192 return self.kgrid.nonuniform 

193 

194 @property 

195 @deprecated(version="0.4.1", reason="Use TimeReversal class instead") 

196 def time_rev(self) -> bool: 

197 if self.sensor and not isinstance(self.sensor, NotATransducer): 197 ↛ 199line 197 didn't jump to line 199 because the condition on line 197 was always true

198 return not self.options.simulation_type.is_elastic_simulation() and self.sensor.time_reversal_boundary_data is not None 

199 return self.userarg_time_rev 

200 

201 @property 

202 @deprecated(version="0.4.1", reason="Use TimeReversal class instead") 

203 def elastic_time_rev(self): 

204 """ 

205 Returns: 

206 True if using time reversal with the elastic code 

207 

208 """ 

209 return False 

210 

211 @property 

212 def compute_directivity(self): 

213 """ 

214 Returns: 

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

216 

217 """ 

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

219 if self.kgrid.dim == 2: 

220 # check for sensor directivity input and set flag 

221 directivity = self.sensor.directivity 

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

223 return True 

224 return False 

225 

226 @property 

227 def cuboid_corners(self): 

228 """ 

229 Returns: 

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

231 """ 

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

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

234 return True 

235 return self.userarg_cuboid_corners 

236 

237 ############## 

238 # flags which control the types of source used 

239 ############## 

240 @property 

241 def source_p0(self): # initial pressure 

242 """ 

243 Returns: 

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

245 

246 """ 

247 flag = False # default 

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

249 # set flag 

250 flag = True 

251 return flag 

252 

253 @property 

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

255 """ 

256 Returns: 

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

258 

259 """ 

260 # Not clear where this flag is set 

261 return False 

262 

263 @property 

264 def source_p(self): 

265 """ 

266 Returns: 

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

268 

269 """ 

270 flag = False 

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

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

273 # to be shorter than kgrid.Nt 

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

275 return flag 

276 

277 @property 

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

279 """ 

280 Returns: 

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

282 

283 """ 

284 flag = False 

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

286 # check if the mask is binary or labelled 

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

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

289 return flag 

290 

291 @property 

292 def source_ux(self) -> bool: 

293 """ 

294 Returns: 

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

296 

297 """ 

298 flag = False 

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

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

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

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

303 return flag 

304 

305 @property 

306 def source_uy(self) -> bool: 

307 """ 

308 Returns: 

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

310 

311 """ 

312 flag = False 

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

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

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

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

317 return flag 

318 

319 @property 

320 def source_uz(self) -> bool: 

321 """ 

322 Returns: 

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

324 

325 """ 

326 flag = False 

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

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

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

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

331 return flag 

332 

333 @property 

334 def source_u_labelled(self): 

335 """ 

336 Returns: 

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

338 

339 """ 

340 flag = False 

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

342 # check if the mask is binary or labelled 

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

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

345 # binary source mask 

346 flag = False 

347 else: 

348 # labelled source mask 

349 flag = True 

350 return flag 

351 

352 @property 

353 def source_sxx(self): 

354 """ 

355 Returns: 

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

357 """ 

358 flag = False 

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

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

361 return flag 

362 

363 @property 

364 def source_syy(self): 

365 """ 

366 Returns: 

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

368 

369 """ 

370 flag = False 

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

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

373 return flag 

374 

375 @property 

376 def source_szz(self): 

377 """ 

378 Returns: 

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

380 

381 """ 

382 flag = False 

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

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

385 return flag 

386 

387 @property 

388 def source_sxy(self): 

389 """ 

390 Returns: 

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

392 

393 """ 

394 flag = False 

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

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

397 return flag 

398 

399 @property 

400 def source_sxz(self): 

401 """ 

402 Returns: 

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

404 

405 """ 

406 flag = False 

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

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

409 return flag 

410 

411 @property 

412 def source_syz(self): 

413 """ 

414 Returns: 

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

416 

417 """ 

418 flag = False 

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

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

421 return flag 

422 

423 @property 

424 def source_s_labelled(self): 

425 """ 

426 Returns: 

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

428 

429 """ 

430 flag = False 

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

432 # check if the mask is binary or labelled 

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

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

435 # binary source mask 

436 flag = False 

437 else: 

438 # labelled source mask 

439 flag = True 

440 return flag 

441 

442 @property 

443 def use_w_source_correction_p(self): 

444 """ 

445 Returns: 

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

447 """ 

448 flag = False 

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

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

451 flag = True 

452 return flag 

453 

454 @property 

455 def use_w_source_correction_u(self): 

456 """ 

457 Returns: 

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

459 """ 

460 flag = False 

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

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

463 if self.source.u_frequency_ref is not None: 

464 flag = True 

465 return flag 

466 

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

468 """ 

469 Check the input fields for correctness and validness 

470 

471 Args: 

472 calling_func_name: Name of the script that calls this function 

473 

474 Returns: 

475 None 

476 """ 

477 self.calling_func_name = calling_func_name 

478 

479 k_dim = self.kgrid.dim 

480 

481 self.check_calling_func_name_and_dim(calling_func_name, k_dim) 

482 

483 # run subscript to check optional inputs 

484 self.options = SimulationOptions.option_factory(self.kgrid, self.options) 

485 opt = self.options 

486 

487 # TODO(Walter): clean this up with getters in simulation options pml size 

488 pml_x_size, pml_y_size, pml_z_size = opt.pml_x_size, opt.pml_y_size, opt.pml_z_size 

489 pml_size = Vector([pml_x_size, pml_y_size, pml_z_size]) 

490 

491 is_elastic_code = opt.simulation_type.is_elastic_simulation() 

492 self.print_start_status(is_elastic_code=is_elastic_code) 

493 self.set_index_data_type() 

494 

495 user_medium_density_input = self.check_medium(self.medium, self.kgrid.k, simulation_type=opt.simulation_type) 

496 

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

498 self.c_ref, self.c_ref_compression, self.c_ref_shear = set_sound_speed_ref(self.medium, opt.simulation_type) 

499 

500 self.check_source(k_dim, self.kgrid.Nt) 

501 self.check_sensor(k_dim) 

502 self.check_kgrid_time() 

503 self.precision = self.select_precision(opt) 

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

505 

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

507 display_simulation_params(self.kgrid, self.medium, is_elastic_code) 

508 

509 self.smooth_and_enlarge(self.source, k_dim, Vector(self.kgrid.N), opt) 

510 self.create_sensor_variables() 

511 self.create_absorption_vars() 

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

513 self.scale_source_terms(opt.scale_source_terms) 

514 self.create_pml_indices( 

515 kgrid_dim=self.kgrid.dim, 

516 kgrid_N=Vector(self.kgrid.N), 

517 pml_size=pml_size, 

518 pml_inside=opt.pml_inside, 

519 is_axisymmetric=opt.simulation_type.is_axisymmetric(), 

520 ) 

521 

522 @staticmethod 

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

524 """ 

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

526 

527 Args: 

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

529 kgrid_dim: Dimensionality of the kWaveGrid 

530 

531 Returns: 

532 None 

533 """ 

534 assert not calling_func_name.startswith(("pstdElastic", "kspaceElastic")), "Elastic simulation is not supported." 

535 

536 if calling_func_name == "kspaceFirstOrder1D": 

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

538 elif calling_func_name in ["kspaceFirstOrder2D", "pstdElastic2D", "kspaceElastic2D", "kspaceFirstOrderAS"]: 

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

540 elif calling_func_name in ["kspaceFirstOrder3D", "pstdElastic3D", "kspaceElastic3D"]: 

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

542 

543 @staticmethod 

544 def print_start_status(is_elastic_code: bool) -> None: 

545 """ 

546 Update command-line status with the start time 

547 

548 Args: 

549 is_elastic_code: is the simulation elastic 

550 

551 Returns: 

552 None 

553 """ 

554 if is_elastic_code: # pragma: no cover 

555 logging.log(logging.INFO, "Running k-Wave elastic simulation...") 

556 else: 

557 logging.log(logging.INFO, "Running k-Wave simulation...") 

558 logging.log(logging.INFO, f" start time: {get_date_string()}") 

559 

560 def set_index_data_type(self) -> None: 

561 """ 

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

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

564 

565 Returns: 

566 None 

567 """ 

568 total_grid_points = self.kgrid.total_grid_points 

569 self.index_data_type = get_smallest_possible_type(total_grid_points, "uint", default="double") 

570 

571 @staticmethod 

572 def check_medium(medium, kgrid_k, simulation_type: SimulationType) -> bool: 

573 """ 

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

575 

576 Args: 

577 medium: kWaveMedium instance 

578 kgrid_k: kWaveGrid.k matrix 

579 is_elastic: Whether the simulation is elastic 

580 is_axisymmetric: Whether the simulation is axisymmetric 

581 

582 Returns: 

583 Medium Density 

584 """ 

585 

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

587 if (not simulation_type.is_elastic_simulation()) and medium.density is None and medium.sound_speed.size == 1: 

588 user_medium_density_input = False 

589 medium.density = 1 

590 else: 

591 medium.ensure_defined("density") 

592 user_medium_density_input = True 

593 

594 # check medium absorption inputs for the fluid code 

595 is_absorbing = any(medium.is_defined("alpha_coeff", "alpha_power")) 

596 is_stokes = simulation_type.is_axisymmetric() or medium.alpha_mode == "stokes" 

597 medium.set_absorbing(is_absorbing, is_stokes) 

598 

599 if is_absorbing: 

600 medium.check_fields(kgrid_k.shape) 

601 return user_medium_density_input 

602 

603 def check_sensor(self, kgrid_dim) -> None: 

604 """ 

605 Check the Sensor properties for correctness and validity 

606 

607 Args: 

608 k_dim: kWaveGrid dimensionality 

609 

610 Returns: 

611 None 

612 """ 

613 # ========================================================================= 

614 # CHECK SENSOR STRUCTURE INPUTS 

615 # ========================================================================= 

616 # check sensor fields 

617 if self.sensor is not None: 

618 # check the sensor input is valid 

619 # TODO FARID move this check as a type checking 

620 assert isinstance( 

621 self.sensor, (kSensor, NotATransducer) 

622 ), "sensor must be defined as an object of the kSensor or kWaveTransducer class." 

623 

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

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

626 if kgrid_dim == 2: 

627 # check for sensor directivity input and set flag 

628 directivity = self.sensor.directivity 

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

630 # make sure the sensor mask is not blank 

631 assert self.sensor.mask is not None, "The mask must be defined for the sensor" 

632 

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

634 assert ( 

635 directivity.angle.shape == self.sensor.mask.shape 

636 ), "sensor.directivity.angle and sensor.mask must be the same size." 

637 

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

639 # a constant times kgrid.dx 

640 if directivity.size is None: 

641 directivity.set_default_size(self.kgrid) 

642 

643 # find the unique directivity angles 

644 # assign the wavenumber vectors 

645 directivity.set_unique_angles(self.sensor.mask) 

646 directivity.set_wavenumbers(self.kgrid) 

647 

648 # check for time reversal inputs and set flags 

649 if not self.options.simulation_type.is_elastic_simulation() and self.sensor.time_reversal_boundary_data is not None: 

650 self.record.p = False 

651 

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

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

654 # default 

655 if self.sensor.record is not None: 

656 # check for time reversal data 

657 if self.time_rev: 

658 logging.log(logging.WARN, "sensor.record is not used for time reversal reconstructions") 

659 

660 # check the input is a cell array 

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

662 

663 # check the sensor record flgs 

664 self.record.set_flags_from_list(self.sensor.record, self.options.simulation_type.is_elastic_simulation()) 

665 

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

667 # and _final variables 

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

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

670 assert self.sensor.mask is not None 

671 

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

673 # or a set of Cartesian interpolation points 

674 if not self.blank_sensor: 

675 if (kgrid_dim == 3 and num_dim2(self.sensor.mask) == 3) or ( 

676 kgrid_dim != 3 and (self.sensor.mask.shape == self.kgrid.k.shape) 

677 ): 

678 # check the grid is binary 

679 assert self.sensor.mask.sum() == ( 

680 self.sensor.mask.size - (self.sensor.mask == 0).sum() 

681 ), "sensor.mask must be a binary grid (numeric values must be 0 or 1)." 

682 

683 # check the grid is not empty 

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

685 

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

687 # make sure the points are integers 

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

689 

690 # store a copy of the cuboid corners 

691 self.record.cuboid_corners_list = self.sensor.mask 

692 

693 # check the list makes sense 

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

695 if kgrid_dim == 1: 

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

697 elif kgrid_dim == 2: 

698 raise ValueError( 

699 "sensor.mask cuboid corners must be defined " "as [x1, y1, x2, y2; ...]." " where x2 => x1, etc." 

700 ) 

701 elif kgrid_dim == 3: 

702 raise ValueError( 

703 "sensor.mask cuboid corners must be defined" 

704 " as [x1, y1, z1, x2, y2, z2; ...]." 

705 " where x2 => x1, etc." 

706 ) 

707 

708 # check the list are within bounds 

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

710 raise ValueError("sensor.mask cuboid corners must be within the grid.") 

711 else: 

712 if kgrid_dim == 1: 

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

714 raise ValueError("sensor.mask cuboid corners must be within the grid.") 

715 elif kgrid_dim == 2: 

716 if np.any(self.sensor.mask[[0, 2], :] > self.kgrid.Nx) or np.any( 

717 self.sensor.mask[[1, 3], :] > self.kgrid.Ny 

718 ): 

719 raise ValueError("sensor.mask cuboid corners must be within the grid.") 

720 elif kgrid_dim == 3: 

721 if ( 

722 np.any(self.sensor.mask[[0, 3], :] > self.kgrid.Nx) 

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

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

725 ): 

726 raise ValueError("sensor.mask cuboid corners must be within the grid.") 

727 

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

729 # TODO FARID mask should be option_factory in sensor not here 

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

731 cuboid_corners_list = self.record.cuboid_corners_list 

732 for cuboid_index in range(cuboid_corners_list.shape[1]): 

733 if self.kgrid.dim == 1: 

734 self.sensor.mask[cuboid_corners_list[0, cuboid_index] : cuboid_corners_list[1, cuboid_index]] = 1 

735 if self.kgrid.dim == 2: 

736 self.sensor.mask[ 

737 cuboid_corners_list[0, cuboid_index] : cuboid_corners_list[2, cuboid_index], 

738 cuboid_corners_list[1, cuboid_index] : cuboid_corners_list[3, cuboid_index], 

739 ] = 1 

740 if self.kgrid.dim == 3: 

741 self.sensor.mask[ 

742 cuboid_corners_list[0, cuboid_index] : cuboid_corners_list[3, cuboid_index], 

743 cuboid_corners_list[1, cuboid_index] : cuboid_corners_list[4, cuboid_index], 

744 cuboid_corners_list[2, cuboid_index] : cuboid_corners_list[5, cuboid_index], 

745 ] = 1 

746 else: 

747 # check the Cartesian sensor mask is the correct size 

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

749 assert ( 

750 self.sensor.mask.shape[0] == kgrid_dim and num_dim2(self.sensor.mask) <= 2 

751 ), f"Cartesian sensor.mask for a {kgrid_dim}D simulation must be given as a {kgrid_dim} by N array." 

752 

753 # set Cartesian mask flag (this is modified in 

754 # createStorageVariables if the interpolation setting is 

755 # set to nearest) 

756 self.binary_sensor_mask = False 

757 

758 # extract Cartesian data from sensor mask 

759 if kgrid_dim == 1: 

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

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

762 # return data in the correct dimension 

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

764 

765 # add sensor_x to the record structure for use with 

766 # the _extractSensorData subfunction 

767 self.record.sensor_x = self.sensor_x 

768 "record.sensor_x = sensor_x;" 

769 

770 elif kgrid_dim == 2: 

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

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

773 elif kgrid_dim == 3: 

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

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

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

777 

778 # compute an equivalent sensor mask using nearest neighbour 

779 # interpolation, if flgs.time_rev = false and 

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

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

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

783 self.sensor.mask, self.order_index, self.reorder_index = cart2grid( 

784 self.kgrid, self.sensor.mask, self.options.simulation_type.is_axisymmetric() 

785 ) 

786 

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

788 # the order of the binary sensor_mask 

789 if self.time_rev: 

790 raise NotImplementedError 

791 """ 

792 # append the reordering data 

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

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

795  

796 # reorder p0 based on the order_index 

797 sensor.time_reversal_boundary_data = sort_rows(sensor.time_reversal_boundary_data, new_col_pos); 

798  

799 # remove the reordering data 

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

801 """ 

802 else: 

803 # set transducer sensor flag 

804 self.transducer_sensor = True 

805 self.record.p = False 

806 

807 # check to see if there is an elevation focus 

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

809 # set flag 

810 self.transducer_receive_elevation_focus = True 

811 

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

813 # from the sensor data buffer for averaging 

814 self.transducer_receive_mask = self.sensor.elevation_beamforming_mask 

815 

816 # check for directivity inputs with time reversal 

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

818 logging.log(logging.WARN, "sensor directivity fields are not used for time reversal.") 

819 

820 def check_source(self, k_dim, k_Nt) -> None: 

821 """ 

822 Check the source properties for correctness and validity 

823 

824 Args: 

825 kgrid_dim: kWaveGrid dimension 

826 k_Nt: Number of time steps in kWaveGrid 

827 

828 Returns: 

829 None 

830 """ 

831 # ========================================================================= 

832 # CHECK SOURCE STRUCTURE INPUTS 

833 # ========================================================================= 

834 

835 # check source inputs 

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

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

838 # otherwise return error 

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

840 

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

842 # -------------------------- 

843 # SOURCE IS NOT A TRANSDUCER 

844 # -------------------------- 

845 

846 """ 

847 check allowable source types 

848  

849 Depending on the kgrid dimensionality and the simulation type,  

850 following fields are allowed & might be use: 

851  

852 kgrid.dim == 1: 

853 non-elastic code: 

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

855 kgrid.dim == 2: 

856 non-elastic code: 

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

858 elastic code: 

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

860 kgrid.dim == 3: 

861 non-elastic code: 

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

863 elastic code: 

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

865 """ 

866 

867 self.source.validate(self.kgrid) 

868 

869 # check for a time varying pressure source input 

870 if self.source.p is not None: 

871 # check the source mode input is valid 

872 if self.source.p_mode is None: 

873 self.source.p_mode = self.SOURCE_P_MODE_DEF 

874 

875 if self.source_p > k_Nt: 

876 logging.log(logging.WARN, " source.p has more time points than kgrid.Nt, remaining time points will not be used.") 

877 

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

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

880 

881 # check if the mask is binary or labelled 

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

883 

884 # create a second indexing variable 

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

886 # set signal index to all elements 

887 self.p_source_sig_index = ":" 

888 else: 

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

890 # to be used for each source label) 

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

892 

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

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

895 if self.source_p_labelled: 

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

897 

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

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

900 # check the source mode input is valid 

901 if self.source.u_mode is None: 

902 self.source.u_mode = self.SOURCE_U_MODE_DEF 

903 

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

905 # the source elements 

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

907 

908 # check if the mask is binary or labelled 

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

910 

911 # create a second indexing variable 

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

913 # set signal index to all elements 

914 self.u_source_sig_index = ":" 

915 else: 

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

917 # to be used for each source label) 

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

919 

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

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

922 if self.source_u_labelled: 

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

924 

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

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

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

928 # the source elements 

929 raise NotImplementedError 

930 "s_source_pos_index = find(source.s_mask != 0);" 

931 

932 # check if the mask is binary or labelled 

933 "s_unique = unique(source.s_mask);" 

934 

935 # create a second indexing variable 

936 if eng.eval("numel(s_unique) <= 2 && sum(s_unique) == 1"): # noqa: F821 

937 # set signal index to all elements 

938 eng.workspace["s_source_sig_index"] = ":" # noqa: F821 

939 

940 else: 

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

942 # to be used for each source label) 

943 s_source_sig_index = source.s_mask(source.s_mask != 0) # noqa 

944 

945 f"s_source_pos_index = {self.index_data_type}(s_source_pos_index);" 

946 if self.source_s_labelled: 

947 f"s_source_sig_index = {self.index_data_type}(s_source_sig_index);" 

948 

949 else: 

950 # ---------------------- 

951 # SOURCE IS A TRANSDUCER 

952 # ---------------------- 

953 

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

955 assert k_dim == 3, "Transducer inputs are only compatible with 3D simulations." 

956 

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

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

959 # input signal is not defined) 

960 self.transducer_input_signal = self.source.input_signal 

961 

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

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

964 # applied to the complete transducer mask with different delays 

965 delay_mask = self.source.delay_mask() 

966 

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

968 # maximum delay 

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

970 

971 # get the active elements mask 

972 active_elements_mask = self.source.active_elements_mask 

973 

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

975 # linear array 

976 if self.source.transmit_apodization == "Rectangular": 

977 self.transducer_transmit_apodization = 1 

978 else: 

979 self.transducer_transmit_apodization = self.source.transmit_apodization_mask 

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

981 

982 # create indexing variable corresponding to the active elements 

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

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

985 

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

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

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

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

990 

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

992 # mask and the length of the source 

993 smallest_type = get_smallest_possible_type(delay_mask.max(), "uint") 

994 if smallest_type is not None: 

995 delay_mask = delay_mask.astype(smallest_type) 

996 

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

998 self.delay_mask = delay_mask + 1 

999 

1000 # clean up unused variables 

1001 del active_elements_mask 

1002 

1003 def check_kgrid_time(self) -> None: 

1004 """ 

1005 Check time-related kWaveGrid inputs 

1006 

1007 Returns: 

1008 None 

1009 """ 

1010 

1011 # check kgrid for t_array existence, and create if not defined 

1012 if isinstance(self.kgrid.t_array, str) and self.kgrid.t_array == "auto": 

1013 # check for time reversal mode 

1014 if self.time_rev: 

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

1016 

1017 # check for time varying sources 

1018 if (not self.source_p0_elastic) and ( 

1019 self.source_p 

1020 or self.source_ux 

1021 or self.source_uy 

1022 or self.source_uz 

1023 or self.source_sxx 

1024 or self.source_syy 

1025 or self.source_szz 

1026 or self.source_sxy 

1027 or self.source_sxz 

1028 or self.source_syz 

1029 ): 

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

1031 

1032 # create the time array using the compressional sound speed 

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

1034 

1035 # check kgrid.t_array for stability given medium properties 

1036 if not self.options.simulation_type.is_elastic_simulation(): 

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

1038 

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

1040 

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

1042 if self.kgrid.dt > dt_stability_limit: 

1043 logging.log(logging.WARN, " time step may be too large for a stable simulation.") 

1044 

1045 @staticmethod 

1046 def select_precision(opt: SimulationOptions): 

1047 """ 

1048 Select the minimal precision for storing the data 

1049 

1050 Args: 

1051 opt: SimulationOptions instance 

1052 

1053 Returns: 

1054 Minimal precision for variable allocation 

1055 

1056 """ 

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

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

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

1060 if opt.data_cast == "off": 

1061 precision = "double" 

1062 elif opt.data_cast == "single": 

1063 precision = "single" 

1064 elif opt.data_cast == "gsingle": 

1065 precision = "single" 

1066 elif opt.data_cast == "gdouble": 

1067 precision = "double" 

1068 elif opt.data_cast == "gpuArray": 

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

1070 elif opt.data_cast == "kWaveGPUsingle": 

1071 precision = "single" 

1072 elif opt.data_cast == "kWaveGPUdouble": 

1073 precision = "double" 

1074 else: 

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

1076 return precision 

1077 

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

1079 """ 

1080 Check the input combinations for correctness and validity 

1081 

1082 Args: 

1083 opt: SimulationOptions instance 

1084 user_medium_density_input: Medium Density 

1085 k_dim: kWaveGrid dimensionality 

1086 pml_size: Size of the PML 

1087 kgrid_N: kWaveGrid size in each direction 

1088 

1089 Returns: 

1090 None 

1091 """ 

1092 # ========================================================================= 

1093 # CHECK FOR VALID INPUT COMBINATIONS 

1094 # ========================================================================= 

1095 

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

1097 if not user_medium_density_input and ( 

1098 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 

1099 ): 

1100 raise ValueError( 

1101 "medium.density must be explicitly defined " "if velocity inputs or outputs are used, even in homogeneous media." 

1102 ) 

1103 

1104 # TODO(walter): move to check medium 

1105 # enforce density input if nonlinear equations are being used 

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

1107 raise ValueError("medium.density must be explicitly defined if medium.BonA is specified.") 

1108 

1109 # check sensor compatibility options for flgs.compute_directivity 

1110 if self.use_sensor and k_dim == 2 and self.compute_directivity and not self.binary_sensor_mask and opt.cartesian_interp == "linear": 

1111 raise ValueError( 

1112 "sensor directivity fields are only compatible " "with binary sensor masks or " "CartInterp" " set to " "nearest" "." 

1113 ) 

1114 

1115 # check for split velocity output 

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

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

1118 

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

1120 if opt.stream_to_disk: 

1121 if not self.use_sensor or self.time_rev: 

1122 raise ValueError( 

1123 "The optional input " 

1124 "StreamToDisk" 

1125 " is currently only compatible " 

1126 "with forward simulations using a non-zero sensor mask." 

1127 ) 

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

1129 raise ValueError( 

1130 "The optional input " "StreamToDisk" " is currently only compatible " "with sensor.record = {" "p" "} (the default)." 

1131 ) 

1132 

1133 is_axisymmetric = self.options.simulation_type.is_axisymmetric() 

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

1135 if opt.pml_inside and ( 

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

1137 or (k_dim == 2 and not is_axisymmetric and ((pml_size.x * 2 > kgrid_N[0]) or (pml_size.y * 2 > kgrid_N[1]))) 

1138 or (k_dim == 2 and is_axisymmetric and ((pml_size.x * 2 > kgrid_N[0]) or (pml_size.y > kgrid_N[1]))) 

1139 or (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]))) 

1140 ): 

1141 raise ValueError("The size of the PML must be smaller than the size of the grid.") 

1142 

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

1144 if self.nonuniform_grid and not opt.pml_inside: 

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

1146 

1147 # check for compatible input options if saving to disk 

1148 # modified by Farid | disabled temporarily! 

1149 # if k_dim == 3 and isinstance(self.options.save_to_disk, str) and 

1150 # (not self.use_sensor or not self.binary_sensor_mask or self.time_rev): 

1151 # raise ValueError('The optional input ''SaveToDisk'' is currently only compatible 

1152 # with forward simulations using a non-zero binary sensor mask.') 

1153 

1154 # check the record start time is within range 

1155 record_start_index = self.sensor.record_start_index 

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

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

1158 

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

1160 if is_axisymmetric and self.options.radial_symmetry != "WSWA" and isinstance(self.options.save_to_disk, str): 

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

1162 if self.options.radial_symmetry.startswith("WSWS"): 

1163 logging.log( 

1164 logging.WARN, " Optional input " "RadialSymmetry" " changed to " "WSWA" " for compatibility with " "SaveToDisk" "." 

1165 ) 

1166 

1167 # update setting 

1168 self.options.radial_symmetry = "WSWA" 

1169 

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

1171 if not self.source_p0: 

1172 self.options.smooth_p0 = False 

1173 

1174 # start log if required 

1175 if opt.create_log: 

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

1177 

1178 # update command line status 

1179 if self.time_rev: 

1180 logging.log(logging.INFO, " time reversal mode") 

1181 

1182 # cleanup unused variables 

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

1184 if k.endswith("_DEF"): 

1185 delattr(self, k) 

1186 

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

1188 """ 

1189 Smooth and enlarge grids 

1190 

1191 Args: 

1192 source: kWaveSource instance 

1193 k_dim: kWaveGrid dimensionality 

1194 kgrid_N: kWaveGrid size in each direction 

1195 opt: SimulationOptions 

1196 

1197 Returns: 

1198 None 

1199 """ 

1200 

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

1202 # the maximum magnitude 

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

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

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

1206 # exactly zero within the PML 

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

1208 # symmetry 

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

1210 # update command line status 

1211 logging.log(logging.INFO, " smoothing p0 distribution...") 

1212 

1213 if self.options.simulation_type.is_axisymmetric(): 

1214 if self.options.radial_symmetry in ["WSWA-FFT", "WSWA"]: 

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

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

1217 

1218 # mirror p0 in radial dimension using WSWA symmetry 

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

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

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

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

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

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

1225 

1226 elif self.options.radial_symmetry in ["WSWS-FFT", "WSWS"]: 

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

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

1229 

1230 # mirror p0 in radial dimension using WSWS symmetry 

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

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

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

1234 

1235 # smooth p0 

1236 p0_exp = smooth(p0_exp, True) 

1237 

1238 # trim back to original size 

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

1240 

1241 # clean up unused variables 

1242 del kgrid_exp 

1243 del p0_exp 

1244 else: 

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

1246 

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

1248 # grid defined by the user 

1249 if opt.pml_inside is False: 

1250 expand_results = expand_grid_matrices( 

1251 self.kgrid, 

1252 self.medium, 

1253 self.source, 

1254 self.sensor, 

1255 self.options, 

1256 dotdict( 

1257 { 

1258 "p_source_pos_index": self.p_source_pos_index, 

1259 "u_source_pos_index": self.u_source_pos_index, 

1260 "s_source_pos_index": self.s_source_pos_index, 

1261 } 

1262 ), 

1263 dotdict( 

1264 { 

1265 "axisymmetric": self.options.simulation_type.is_axisymmetric(), 

1266 "use_sensor": self.use_sensor, 

1267 "blank_sensor": self.blank_sensor, 

1268 "cuboid_corners": self.cuboid_corners, 

1269 "source_p0": self.source_p0, 

1270 "source_p": self.source_p, 

1271 "source_ux": self.source_ux, 

1272 "source_uy": self.source_uy, 

1273 "source_uz": self.source_uz, 

1274 "transducer_source": self.transducer_source, 

1275 "source_sxx": self.source_sxx, 

1276 "source_syy": self.source_syy, 

1277 "source_szz": self.source_szz, 

1278 "source_sxy": self.source_sxy, 

1279 "source_sxz": self.source_sxz, 

1280 "source_syz": self.source_syz, 

1281 } 

1282 ), 

1283 ) 

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

1285 

1286 # get maximum prime factors 

1287 if self.options.simulation_type.is_axisymmetric(): 

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

1289 else: 

1290 prime_facs = self.kgrid.highest_prime_factors() 

1291 

1292 # give warning for bad dimension sizes 

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

1294 prime_facs = prime_facs[prime_facs != 0] 

1295 logging.log(logging.WARN, f"Highest prime factors in each dimension are {prime_facs}") 

1296 logging.log(logging.WARN, "Use dimension sizes with lower prime factors to improve speed") 

1297 del prime_facs 

1298 

1299 # smooth the sound speed distribution if required 

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

1301 logging.log(logging.INFO, " smoothing sound speed distribution...") 

1302 self.medium.sound_speed = smooth(self.medium.sound_speed) 

1303 

1304 # smooth the ambient density distribution if required 

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

1306 logging.log(logging.INFO, "smoothing density distribution...") 

1307 self.medium.density = smooth(self.medium.density) 

1308 

1309 def create_sensor_variables(self) -> None: 

1310 """ 

1311 Create the sensor related variables 

1312 

1313 Returns: 

1314 None 

1315 """ 

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

1317 if self.use_sensor: 

1318 if not self.blank_sensor or self.options.save_to_disk: 

1319 if self.cuboid_corners: 

1320 # create empty list of sensor indices 

1321 self.sensor_mask_index = [] 

1322 

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

1324 # sensor mask indices for each cube 

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

1326 # create empty binary mask 

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

1328 

1329 if self.kgrid.dim == 1: 

1330 self.sensor.mask[ 

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

1332 ] = 1 

1333 if self.kgrid.dim == 2: 

1334 self.sensor.mask[ 

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

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

1337 ] = 1 

1338 if self.kgrid.dim == 3: 

1339 self.sensor.mask[ 

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

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

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

1343 ] = 1 

1344 

1345 # extract mask indices 

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

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

1348 

1349 # cleanup unused variables 

1350 del temp_mask 

1351 

1352 else: 

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

1354 # transducer inputs) 

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

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

1357 

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

1359 # memory) 

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

1361 

1362 else: 

1363 # set the sensor mask index variable to be empty 

1364 self.sensor_mask_index = [] 

1365 

1366 def create_absorption_vars(self) -> None: 

1367 """ 

1368 Create absorption variables for the fluid code based on 

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

1370 

1371 Returns: 

1372 None 

1373 """ 

1374 if not self.options.simulation_type.is_elastic_simulation() and not self.options.save_to_disk: 

1375 self.absorb_nabla1, self.absorb_nabla2, self.absorb_tau, self.absorb_eta = create_absorption_variables( 

1376 self.kgrid, self.medium, self.equation_of_state 

1377 ) 

1378 

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

1380 """ 

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

1382 (done after enlarging and smoothing the grids) 

1383 

1384 Args: 

1385 medium: kWaveMedium instance 

1386 kgrid: kWaveGrid instance 

1387 

1388 Returns: 

1389 None 

1390 """ 

1391 self.dt = float(kgrid.dt) 

1392 self.rho0 = medium.density 

1393 self.c0 = medium.sound_speed 

1394 

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

1396 """ 

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

1398 

1399 Args: 

1400 is_scale_source_terms: Should the source terms be scaled 

1401 

1402 Returns: 

1403 None 

1404 """ 

1405 if not is_scale_source_terms: 

1406 return 

1407 try: 

1408 p_source_pos_index = self.p_source_pos_index 

1409 except AttributeError: 

1410 p_source_pos_index = None 

1411 

1412 try: 

1413 s_source_pos_index = self.s_source_pos_index 

1414 except AttributeError: 

1415 s_source_pos_index = None 

1416 

1417 try: 

1418 u_source_pos_index = self.u_source_pos_index 

1419 except AttributeError: 

1420 u_source_pos_index = None 

1421 

1422 self.transducer_input_signal = scale_source_terms_func( 

1423 self.c0, 

1424 self.dt, 

1425 self.kgrid, 

1426 self.source, 

1427 p_source_pos_index, 

1428 s_source_pos_index, 

1429 u_source_pos_index, 

1430 self.transducer_input_signal, 

1431 dotdict( 

1432 { 

1433 "nonuniform_grid": self.nonuniform_grid, 

1434 "source_ux": self.source_ux, 

1435 "source_uy": self.source_uy, 

1436 "source_uz": self.source_uz, 

1437 "transducer_source": self.transducer_source, 

1438 "source_p": self.source_p, 

1439 "source_p0": self.source_p0, 

1440 "use_w_source_correction_p": self.use_w_source_correction_p, 

1441 "use_w_source_correction_u": self.use_w_source_correction_u, 

1442 "source_sxx": self.source_sxx, 

1443 "source_syy": self.source_syy, 

1444 "source_szz": self.source_szz, 

1445 "source_sxy": self.source_sxy, 

1446 "source_sxz": self.source_sxz, 

1447 "source_syz": self.source_syz, 

1448 } 

1449 ), 

1450 ) 

1451 

1452 def create_pml_indices(self, kgrid_dim, kgrid_N: Vector, pml_size, pml_inside, is_axisymmetric): 

1453 """ 

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

1455 input 'PlotPML' is set to false 

1456 

1457 Args: 

1458 kgrid_dim: kWaveGrid dimensionality 

1459 kgrid_N: kWaveGrid size in each direction 

1460 pml_size: Size of the PML 

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

1462 is_axisymmetric: Whether the simulation is axisymmetric 

1463 

1464 """ 

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

1466 # therefore if statement removed 

1467 if kgrid_dim == 1: 

1468 self.x1 = pml_size.x + 1.0 

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

1470 elif kgrid_dim == 2: 

1471 self.x1 = pml_size.x + 1.0 

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

1473 if is_axisymmetric: 

1474 self.y1 = 1.0 

1475 else: 

1476 self.y1 = pml_size.y + 1.0 

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

1478 elif kgrid_dim == 3: 

1479 self.x1 = pml_size.x + 1.0 

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

1481 self.y1 = pml_size.y + 1.0 

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

1483 self.z1 = pml_size.z + 1.0 

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

1485 

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

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

1488 # if self.record is None: 

1489 # self.record = Recorder() 

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

1491 

1492 @deprecated(version="0.4.1", reason="Use TimeReversal class instead") 

1493 def check_time_reversal(self) -> bool: 

1494 return self.time_rev