Coverage for kwave/kWaveSimulation.py: 16%

1 from warnings import warn

2

3 from kwave.kWaveSimulation_helper import *

4 from kwave.kgrid import *

5 from kwave.kmedium import kWaveMedium

6 from kwave.ksensor import kSensor

7 from kwave.ksource import kSource

8 from kwave.ktransducer import NotATransducer

9 from kwave.options import SimulationOptions

10 from kwave.recorder import Recorder

11 from kwave.utils import *

12 from kwave.utils import dotdict

13

14

15@dataclass

16 class 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)