Coverage for kwave/kWaveSimulation.py: 16%
578 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 11:55 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 11:55 -0700
1from warnings import warn
3from kwave.kWaveSimulation_helper import *
4from kwave.kgrid import *
5from kwave.kmedium import kWaveMedium
6from kwave.ksensor import kSensor
7from kwave.ksource import kSource
8from kwave.ktransducer import NotATransducer
9from kwave.options import SimulationOptions
10from kwave.recorder import Recorder
11from kwave.utils import *
12from kwave.utils import dotdict
15@dataclass
16class kWaveSimulation(object):
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
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']
38 # flags which control the characteristics of the sensor
39 #: Whether time reversal simulation is enabled
40 self.userarg_time_rev = kwargs['time_rev']
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
47 #: Whether the sensor.mask is binary
48 self.binary_sensor_mask = True
50 #: If the sensor.mask is a list of cuboid corners
51 self.userarg_cuboid_corners = kwargs['cuboid_corners']
53 #: If tse sensor is an object of the kWaveTransducer class
54 self.transducer_sensor = False
56 self.record = Recorder()
58 # transducer source flags
59 #: transducer is object of kWaveTransducer class
60 self.transducer_source = False
62 #: Apply receive elevation focus on the transducer
63 self.transducer_receive_elevation_focus = False
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'
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
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
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()}')
87 self.options = None
89 self.c_ref, self.c_ref_compression, self.c_ref_shear = [None] * 3
90 self.transducer_input_signal = None
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
99 #: Delay mask that accounts for the beamforming delays and elevation focussing
100 self.delay_mask = None
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
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
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'
126 @property
127 def use_sensor(self):
128 """
129 False if no output of any kind is required
130 Returns:
132 """
133 return self.sensor is not None
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:
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
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'))
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')
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
178 @property
179 def kelvin_voigt_model(self):
180 """
181 Whether the simulation is elastic with absorption
182 """
183 return False
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
442 def input_checking(self, calling_func_name) -> None:
443 """
444 Check the input fields for correctness and validness
446 Args:
447 calling_func_name: Name of the script that calls this function
449 Returns:
450 None
451 """
452 self.calling_func_name = calling_func_name
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
460 self.check_calling_func_name_and_dim(calling_func_name, k_dim)
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
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])
469 self.print_start_status(self.elastic_code)
470 self.set_index_data_type()
472 user_medium_density_input = self.check_medium(self.medium, self.kgrid.k, self.elastic_code, self.axisymmetric)
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)
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)
484 # run subscript to display time step, max supported frequency etc.
485 display_simulation_params(self.kgrid, self.medium, self.elastic_code)
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)
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
499 Args:
500 calling_func_name: Name of the script that makes calls to kWaveSimulation
501 kgrid_dim: Dimensionality of the kWaveGrid
503 Returns:
504 None
505 """
506 assert not calling_func_name.startswith(('pstdElastic', 'kspaceElastic')), \
507 "Elastic simulation is not supported."
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}.'
516 @staticmethod
517 def print_start_status(is_elastic_code) -> None:
518 """
519 Update command-line status with the start time
521 Args:
522 is_elastic_code: is the simulation elastic
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()}')
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
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')
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
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
555 Returns:
556 Medium Density
557 """
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
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)
572 if is_absorbing:
573 medium.check_fields(kgrid_k.shape)
574 return user_medium_density_input
576 def check_source(self, kgrid_dim) -> None:
577 """
578 Check the source properties for correctness and validity
580 Args:
581 kgrid_dim: kWaveGrid dimension
583 Returns:
584 None
585 """
586 # =========================================================================
587 # CHECK SENSOR STRUCTURE INPUTS
588 # =========================================================================
589 # check sensor fields
590 if self.sensor is not None:
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.'
597 # check if sensor is a transducer, otherwise check input fields
598 if not isinstance(self.sensor, NotATransducer):
599 if kgrid_dim == 2:
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'])
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:
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'
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.'
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)
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)
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
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
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:
648 # check for time reversal data
649 if self.time_rev:
650 warn('WARNING: sensor.record is not used for time reversal reconstructions')
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"]'
655 # check the sensor record flgs
656 self.record.set_flags_from_list(self.sensor.record, self.elastic_code)
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
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)):
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).'
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.'
676 elif self.sensor.mask.shape[0] == 2 * kgrid_dim:
678 # make sure the points are integers
679 assert np.all(self.sensor.mask % 1 == 0), 'sensor.mask cuboid corner indices must be integers.'
681 # store a copy of the cuboid corners
682 self.record.cuboid_corners_list = self.sensor.mask
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.')
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.')
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.'
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
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)))
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;"
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, :]
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)
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;
775 # reorder p0 based on the order_index
776 sensor.time_reversal_boundary_data = sortrows(sensor.time_reversal_boundary_data, new_col_pos);
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
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
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
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.')
799 def check_sensor(self, k_dim, k_Nt) -> None:
800 """
801 Check the Sensor properties for correctness and validity
803 Args:
804 k_dim: kWaveGrid dimensionality
805 k_Nt: Number of time steps in kWaveGrid
807 Returns:
808 None
809 """
810 # =========================================================================
811 # CHECK SOURCE STRUCTURE INPUTS
812 # =========================================================================
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.'
820 elif not isinstance(self.source, NotATransducer):
822 # --------------------------
823 # SOURCE IS NOT A TRANSDUCER
824 # --------------------------
826 '''
827 check allowable source types
829 Depending on the kgrid dimensionality and the simulation type,
830 following fields are allowed & might be use:
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 '''
847 self.source.validate(self.kgrid)
849 # check for a time varying pressure source input
850 if self.source.p is not None:
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
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.')
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)
862 # check if the mask is binary or labelled
863 p_unique = np.unique(self.source.p_mask)
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)
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)
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']]):
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
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)
890 # check if the mask is binary or labelled
891 u_unique = np.unique(self.source.u_mask)
893 # create a second indexing variable
894 if u_unique.size <= 2 and u_unique.sum() == 1:
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]
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)
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);'
915 # check if the mask is binary or labelled
916 's_unique = unique(source.s_mask);'
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'] = ':'
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)
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);'
932 else:
933 # ----------------------
934 # SOURCE IS A TRANSDUCER
935 # ----------------------
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.'
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
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()
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()
954 # get the active elements mask
955 active_elements_mask = self.source.active_elements_mask
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]
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)
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
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)
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
983 # clean up unused variables
984 del active_elements_mask
986 def check_kgrid_time(self) -> None:
987 """
988 Check time-related kWaveGrid inputs
990 Returns:
991 None
992 """
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':
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.')
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.')
1009 # create the time array using the compressional sound speed
1010 self.kgrid.makeTime(self.medium.sound_speed, self.KSPACE_CFL)
1012 # check kgrid.t_array for stability given medium properties
1013 if not self.elastic_code:
1015 # calculate the largest timestep for which the model is stable
1017 dt_stability_limit = check_stability(self.kgrid, self.medium)
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.')
1023 @staticmethod
1024 def select_precision(opt: SimulationOptions):
1025 """
1026 Select the minimal precision for storing the data
1028 Args:
1029 opt: SimulationOptions instance
1031 Returns:
1032 Minimal precision for variable allocation
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
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
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
1067 Returns:
1068 None
1069 """
1070 # =========================================================================
1071 # CHECK FOR VALID INPUT COMBINATIONS
1072 # =========================================================================
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.')
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.')
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''.')
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.')
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).')
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.')
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.")
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.')
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.')
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):
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''.')
1126 # update setting
1127 self.options.radial_symmetry = 'WSWA'
1129 # ensure p0 smoothing is switched off if p0 is empty
1130 if not self.source_p0:
1131 self.options.smooth_p0 = False
1133 # start log if required
1134 if opt.create_log:
1135 raise NotImplementedError(f"diary({self.LOG_NAME}.txt');")
1137 # update command line status
1138 if self.time_rev:
1139 print(' time reversal mode')
1141 # cleanup unused variables
1142 for k in list(self.__dict__.keys()):
1143 if k.endswith('_DEF'):
1144 delattr(self, k)
1146 def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) -> None:
1147 """
1148 Smooth and enlarge grids
1150 Args:
1151 source: kWaveSource instance
1152 k_dim: kWaveGrid dimensionality
1153 kgrid_N: kWaveGrid size in each direction
1154 opt: SimulationOptions
1156 Returns:
1157 None
1158 """
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:
1170 # update command line status
1171 print(' smoothing p0 distribution...')
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])
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:])
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])
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])
1195 # smooth p0
1196 p0_exp = smooth(p0_exp, True)
1198 # trim back to original size
1199 source.p0 = p0_exp[:, 0:self.kgrid.Ny]
1201 # clean up unused variables
1202 del kgrid_exp
1203 del p0_exp
1204 else:
1205 source.p0 = smooth(source.p0, True)
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,
1223 'source_p0': self.source_p0,
1224 'source_p': self.source_p,
1226 'source_ux': self.source_ux,
1227 'source_uy': self.source_uy,
1228 'source_uz': self.source_uz,
1230 'transducer_source': self.transducer_source,
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
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()
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
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);')
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);')
1265 def create_sensor_variables(self) -> None:
1266 """
1267 Create the sensor related variables
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:
1277 # create empty list of sensor indices
1278 self.sensor_mask_index = []
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]):
1284 # create empty binary mask
1285 temp_mask = np.zeros_like(self.kgrid.k, dtype=bool)
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
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)
1307 # cleanup unused variables
1308 del temp_mask
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]
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)
1320 else:
1321 # set the sensor mask index variable to be empty
1322 self.sensor_mask_index = []
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
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)
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)
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)
1365 Args:
1366 medium: kWaveMedium instance
1367 kgrid: kWaveGrid instance
1369 Returns:
1370 None
1371 """
1372 self.dt = float(kgrid.dt)
1373 self.rho0 = medium.density
1374 self.c0 = medium.sound_speed
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
1380 Args:
1381 is_scale_source_terms: Should the source terms be scaled
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
1393 try:
1394 s_source_pos_index = self.s_source_pos_index
1395 except AttributeError:
1396 s_source_pos_index = None
1398 try:
1399 u_source_pos_index = self.u_source_pos_index
1400 except AttributeError:
1401 u_source_pos_index = None
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,
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 )
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
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
1438 Returns:
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
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)