Coverage for kwave/options/simulation_options.py: 39%

154 statements  

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

1from __future__ import annotations 

2 

3import os 

4from dataclasses import dataclass, field 

5from enum import Enum 

6from tempfile import gettempdir 

7from typing import TYPE_CHECKING, List, Optional 

8 

9import numpy as np 

10 

11if TYPE_CHECKING: 11 ↛ 13line 11 didn't jump to line 13 because the condition on line 11 was never true

12 # Found here: https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ 

13 from kwave.kgrid import kWaveGrid 

14from kwave.utils.data import get_date_string 

15from kwave.utils.io import get_h5_literals 

16from kwave.utils.pml import get_optimal_pml_size 

17 

18 

19class SimulationType(Enum): 

20 """ 

21 Enum for the simulation type 

22 

23 In the original matlab code, simulation type was determined 

24 by looking at the calling function name and the user args. 

25 

26 Rules from the original matlab code: 

27 AXISYMMETRIC => if the calling function name started with 'kspaceFirstOrderAS' 

28 or if the userarg_axisymmetric is set to true 

29 ELASTIC => if the calling function name started with 'pstdElastic' or 'kspaceElastic' 

30 ELASTIC_WITH_KSPACE_CORRECTION => if the calling function name started with 'kspaceElastic' 

31 """ 

32 

33 FLUID = 1 

34 AXISYMMETRIC = 2 

35 ELASTIC = 3 

36 ELASTIC_WITH_KSPACE_CORRECTION = 4 

37 

38 def is_elastic_simulation(self): 

39 return self in [SimulationType.ELASTIC, SimulationType.ELASTIC_WITH_KSPACE_CORRECTION] 

40 

41 def is_axisymmetric(self): 

42 return self == SimulationType.AXISYMMETRIC 

43 

44 

45@dataclass 

46class SimulationOptions(object): 

47 """ 

48 Args: 

49 axisymmetric: Flag that indicates whether axisymmetric simulation is used 

50 cart_interp: Interpolation mode used to extract the pressure when a Cartesian sensor mask is given. 

51 If set to 'nearest' and more than one Cartesian point maps to the same grid point, 

52 duplicated data points are discarded and sensor_data will be returned 

53 with less points than that specified by sensor.mask (default = 'linear'). 

54 pml_inside: put the PML inside the grid defined by the user 

55 pml_alpha: Absorption within the perfectly matched layer in Nepers per grid point (default = 2). 

56 save_to_disk: save the input data to a HDF5 file 

57 save_to_disk_exit: exit the simulation after saving the HDF5 file 

58 scale_source_terms: apply the source scaling term to time varying sources 

59 smooth_c0: smooth the sound speed distribution 

60 smooth_rho0: smooth the density distribution 

61 smooth_p0: smooth the initial pressure distribution 

62 use_kspace: use the k-space correction 

63 use_sg: use a staggered grid 

64 use_fd: Use finite difference gradients instead of spectral (in 1D) 

65 pml_auto: automatically choose the PML size to give small prime factors 

66 create_log: create a log using diary 

67 use_finite_difference: use finite difference gradients instead of spectral (in 1D) 

68 stream_to_disk: String containing a filename (including pathname if required). 

69 If set, after the precomputation phase, the input variables used in the time loop are saved 

70 the specified location in HDF5 format. The simulation then exits. 

71 The saved variables can be used to run simulations using the C++ code. 

72 data_recast: recast the sensor data back to double precision 

73 cartesian_interp: interpolation mode for Cartesian sensor mask 

74 hdf_compression_level: zip compression level for HDF5 input files 

75 data_cast: data cast 

76 pml_search_range: search range used when automatically determining PML size 

77 radial_symmetry: radial symmetry used in axisymmetric code 

78 multi_axial_PML_ratio: MPML settings 

79 pml_x_alpha: PML Alpha for x-axis 

80 pml_y_alpha: PML Alpha for y-axis 

81 pml_z_alpha: PML Alpha for z-axis 

82 pml_x_size: PML Size for x-axis 

83 pml_y_size: PML Size for y-axis 

84 pml_z_size: PML Size for z-axis 

85 """ 

86 

87 simulation_type: SimulationType = SimulationType.FLUID 

88 cart_interp: str = "linear" 

89 pml_inside: bool = True 

90 pml_alpha: float = 2.0 

91 save_to_disk: bool = False 

92 save_to_disk_exit: bool = False 

93 scale_source_terms: bool = True 

94 smooth_c0: bool = False 

95 smooth_rho0: bool = False 

96 smooth_p0: bool = True 

97 use_kspace: bool = True 

98 use_sg: bool = True 

99 use_fd: Optional[int] = None 

100 pml_auto: bool = False 

101 create_log: bool = False 

102 use_finite_difference: bool = False 

103 stream_to_disk: bool = False 

104 data_recast: Optional[bool] = False 

105 cartesian_interp: str = "linear" 

106 hdf_compression_level: Optional[int] = None 

107 data_cast: str = "off" 

108 pml_search_range: List[int] = field(default_factory=lambda: [10, 40]) 

109 radial_symmetry: str = "WSWA-FFT" 

110 multi_axial_PML_ratio: float = 0.1 

111 data_path: Optional[str] = field(default_factory=lambda: gettempdir()) 

112 input_filename: Optional[str] = field(default_factory=lambda: f"{get_date_string()}_kwave_input.h5") 

113 output_filename: Optional[str] = field(default_factory=lambda: f"{get_date_string()}_kwave_output.h5") 

114 pml_x_alpha: Optional[float] = None 

115 pml_y_alpha: Optional[float] = None 

116 pml_z_alpha: Optional[float] = None 

117 pml_size: Optional[List[int]] = None 

118 pml_x_size: Optional[int] = None 

119 pml_y_size: Optional[int] = None 

120 pml_z_size: Optional[int] = None 

121 

122 def __post_init__(self): 

123 assert self.cartesian_interp in [ 

124 "linear", 

125 "nearest", 

126 ], "Optional input ''cartesian_interp'' must be set to ''linear'' or ''nearest''." 

127 

128 assert isinstance(self.data_cast, str), "Optional input ''data_cast'' must be a string." 

129 

130 assert self.data_cast in ["off", "double", "single"], "Invalid input for ''data_cast''." 

131 

132 if self.data_cast == "double": 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true

133 self.data_cast = "off" 

134 

135 # load the HDF5 literals (for the default compression level) 

136 h5_literals = get_h5_literals() 

137 self.hdf_compression_level = h5_literals.HDF_COMPRESSION_LEVEL 

138 # check value is an integer between 0 and 9 

139 assert ( 

140 isinstance(self.hdf_compression_level, int) and 0 <= self.hdf_compression_level <= 9 

141 ), "Optional input ''hdf_compression_level'' must be an integer between 0 and 9." 

142 

143 assert ( 

144 np.isscalar(self.multi_axial_PML_ratio) and self.multi_axial_PML_ratio >= 0 

145 ), "Optional input ''multi_axial_PML_ratio'' must be a single positive value." 

146 

147 assert np.isscalar(self.stream_to_disk) or isinstance( 

148 self.stream_to_disk, bool 

149 ), "Optional input ''stream_to_disk'' must be a single scalar or Boolean value." 

150 

151 boolean_inputs = { 

152 "use_sg": self.use_sg, 

153 "data_recast": self.data_recast, 

154 "save_to_disk_exit": self.save_to_disk_exit, 

155 "use_kspace": self.use_kspace, 

156 "save_to_disk": self.save_to_disk, 

157 "pml_inside": self.pml_inside, 

158 "create_log": self.create_log, 

159 "scale_source_terms": self.scale_source_terms, 

160 } 

161 

162 for key, val in boolean_inputs.items(): 

163 assert isinstance(val, bool), f"Optional input ''{key}'' must be Boolean." 

164 

165 assert self.radial_symmetry in [ 

166 "WSWA", 

167 "WSWS", 

168 "WSWA-FFT", 

169 "WSWS-FFT", 

170 ], "Optional input ''RadialSymmetry'' must be set to ''WSWA'', ''WSWS'', ''WSWA-FFT'', ''WSWS-FFT''." 

171 

172 # automatically assign the PML size to give small prime factors 

173 if self.pml_auto and self.pml_inside: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 raise NotImplementedError("''pml_size'' set to ''auto'' is only supported with ''pml_inside'' set to false.") 

175 

176 if self.pml_size is not None: 176 ↛ 178line 176 didn't jump to line 178 because the condition on line 176 was never true

177 # TODO(walter): remove auto option in exchange for pml_auto=True 

178 if isinstance(self.pml_size, int): 

179 self.pml_size = np.array([self.pml_size]) 

180 if not isinstance(self.pml_size, (list, np.ndarray)): 

181 raise ValueError("Optional input ''PMLSize'' must be a integer array of 1, 2 or 3 dimensions.") 

182 

183 # Check if each member variable is None, and set it to self.pml_alpha if it is 

184 self.pml_x_alpha = self.pml_alpha if self.pml_x_alpha is None else self.pml_x_alpha 

185 self.pml_y_alpha = self.pml_alpha if self.pml_y_alpha is None else self.pml_y_alpha 

186 self.pml_z_alpha = self.pml_alpha if self.pml_z_alpha is None else self.pml_z_alpha 

187 

188 # add pathname to input and output filenames 

189 self.input_filename = os.path.join(self.data_path, self.input_filename) 

190 self.output_filename = os.path.join(self.data_path, self.output_filename) 

191 

192 assert self.use_fd is None or ( 

193 np.issubdtype(self.use_fd, np.number) and self.use_fd in [2, 4] 

194 ), "Optional input ''UseFD'' can only be set to 2, 4." 

195 

196 @staticmethod 

197 def option_factory(kgrid: "kWaveGrid", options: SimulationOptions): 

198 """ 

199 Initialize the Simulation Options 

200 

201 Args: 

202 kgrid: kWaveGrid instance 

203 elastic_code: Flag that indicates whether elastic simulation is used 

204 **kwargs: Dictionary that holds following optional simulation properties: 

205 

206 * cart_interp: Interpolation mode used to extract the pressure when a Cartesian sensor mask is given. 

207 If set to 'nearest', duplicated data points are discarded and sensor_data 

208 will be returned with fewer points than specified by sensor.mask (default = 'linear'). 

209 * create_log: Boolean controlling whether the command line output is saved using the diary function 

210 with a date and time stamped filename (default = false). 

211 * data_cast: String input of the data type that variables are cast to before computation. 

212 For example, setting to 'single' will speed up the computation time 

213 (due to the improved efficiency of fftn and ifftn for this data type) at the expense 

214 of a loss in precision. This variable is also useful for utilising GPU parallelisation 

215 through libraries such as the Parallel Computing Toolbox 

216 by setting 'data_cast' to 'gpuArray-single' (default = 'off'). 

217 * data_recast: Boolean controlling whether the output data is cast back to double precision. 

218 If set to false, sensor_data will be returned in 

219 the data format set using the 'data_cast' option. 

220 * hdf_compression_level: Compression level used for writing the input HDF5 file when using 

221 'save_to_disk' or kspaceFirstOrder3DC. Can be set to an integer 

222 between 0 (no compression, the default) and 9 (maximum compression). 

223 The compression is lossless. Increasing the compression level will reduce 

224 the file size if there are portions of the medium that are homogeneous, 

225 but will also increase the time to create the HDF5 file. 

226 * multi_axial_pml_ratio: MPML settings 

227 * pml_alpha: Absorption within the perfectly matched layer in Nepers per grid point (default = 2). 

228 * pml_inside: Boolean controlling whether the perfectly matched layer is inside or outside the grid. 

229 If set to false, the input grids are enlarged by pml_size 

230 before running the simulation (default = true). 

231 * pml_range: Search range used when automatically determining PML size. Tuple of two elements 

232 * pml_size: Size of the perfectly matched layer in grid points. By default, the PML is added evenly to 

233 all sides of the grid, however, both pml_size and pml_alpha can be given as three element 

234 arrays to specify the x, y, and z properties, respectively. 

235 To remove the PML, set the appropriate pml_alpha to zero rather than forcing 

236 the PML to be of zero size (default = 10). 

237 * radial_symmetry: Radial symmetry used in axisymmetric code 

238 * stream_to_disk: Boolean controlling whether sensor_data is periodically saved to disk to avoid storing 

239 the complete matrix in memory. StreamToDisk may also be given as an integer which 

240 specifies the number of time steps that are taken before the data 

241 is saved to disk (default = 200). 

242 * save_to_disk: String containing a filename (including pathname if required). 

243 If set, after the precomputation phase, the input variables used in the time loop are 

244 saved the specified location in HDF5 format. The simulation then exits. 

245 The saved variables can be used to run simulations using the C++ code. 

246 * save_to_disk_exit: Exit the simulation after saving the HDF5 file 

247 * scale_source_terms: Apply the source scaling term to time varying sources 

248 * use_fd: Use finite difference gradients instead of spectral (in 1D) 

249 * use_k_space: use the k-space correction 

250 * use_sg: Use a staggered grid 

251 

252 

253 Returns: 

254 SimulationOptions instance 

255 """ 

256 

257 STREAM_TO_DISK_STEPS_DEF = 200 # number of steps before streaming to disk 

258 

259 if options.pml_size is not None and not isinstance(options.pml_size, bool): 

260 if len(options.pml_size) > kgrid.dim: 

261 if kgrid.dim > 1: 

262 raise ValueError(f"Optional input ''pml_size'' must be a 1 or {kgrid.dim} element numerical array.") 

263 else: 

264 raise ValueError("Optional input ''pml_size'' must be a single numerical value.") 

265 

266 if kgrid.dim == 1: 

267 options.pml_x_size = options.pml_size if options.pml_size else 20 

268 options.plot_scale = [-1.1, 1.1] 

269 elif kgrid.dim == 2: 

270 if options.pml_size is not None: 

271 if len(options.pml_size) == kgrid.dim: 

272 options.pml_x_size, options.pml_y_size = np.asarray(options.pml_size, dtype=int).ravel() 

273 else: 

274 options.pml_x_size, options.pml_y_size = (options.pml_size[0], options.pml_size[0]) 

275 else: 

276 options.pml_x_size, options.pml_y_size = (20, 20) 

277 options.plot_scale = [-1, 1] 

278 elif kgrid.dim == 3: 

279 if (options.pml_size is not None) and (len(options.pml_size) == kgrid.dim): 

280 options.pml_x_size, options.pml_y_size, options.pml_z_size = np.asarray(options.pml_size).ravel() 

281 else: 

282 if options.pml_size is None: 

283 options.pml_x_size = 10 

284 options.pml_y_size = 10 

285 options.pml_z_size = 10 

286 else: 

287 options.pml_x_size = options.pml_size[0] 

288 options.pml_y_size = options.pml_x_size 

289 options.pml_z_size = options.pml_x_size 

290 options.plot_scale = [-1, 1] 

291 

292 # replace defaults with user defined values if provided and check inputs 

293 if (val := options.pml_alpha) is not None and not isinstance(options.pml_alpha, str): 

294 # check input is correct size 

295 val = np.atleast_1d(val) 

296 if val.size > kgrid.dim: 

297 if kgrid.dim > 1: 

298 raise ValueError(f"Optional input ''pml_alpha'' must be a 1 or {kgrid.dim} element numerical array.") 

299 else: 

300 raise ValueError("Optional input ''pml_alpha'' must be a single numerical value.") 

301 

302 # assign input based on number of dimensions 

303 if kgrid.dim == 1: 

304 options.pml_x_alpha = val 

305 elif kgrid.dim == 2: 

306 options.pml_x_alpha = val[0] 

307 options.pml_y_alpha = val[-1] 

308 elif kgrid.dim == 3: 

309 options.pml_x_alpha = val[0] 

310 options.pml_y_alpha = val[len(val) // 2] 

311 options.pml_z_alpha = val[-1] 

312 

313 if options.save_to_disk_exit: 

314 assert kgrid.dim != 1, "Optional input ''save_to_disk'' is not compatible with 1D simulations." 

315 

316 if options.stream_to_disk: 

317 assert ( 

318 not options.simulation_type.is_elastic_simulation() and kgrid.dim == 3 

319 ), "Optional input ''stream_to_disk'' is currently only compatible with 3D fluid simulations." 

320 # if given as a Boolean, replace with the default number of time steps 

321 if isinstance(options.stream_to_disk, bool) and options.stream_to_disk: 

322 options.stream_to_disk = STREAM_TO_DISK_STEPS_DEF 

323 

324 if options.save_to_disk or options.save_to_disk_exit: 

325 assert kgrid.dim != 1, "Optional input ''save_to_disk'' is not compatible with 1D simulations." 

326 

327 if options.use_fd: 

328 # input only supported in 1D fluid code 

329 assert kgrid.dim == 1 and not options.simulation_type.is_elastic_simulation(), "Optional input ''use_fd'' only supported in 1D." 

330 # get optimal pml size 

331 if options.simulation_type.is_axisymmetric() or options.pml_auto: 

332 if options.simulation_type.is_axisymmetric(): 

333 pml_size_temp = get_optimal_pml_size(kgrid, options.pml_search_range, options.radial_symmetry[:4]) 

334 else: 

335 pml_size_temp = get_optimal_pml_size(kgrid, options.pml_search_range) 

336 

337 # assign to individual variables 

338 if kgrid.dim == 1: 

339 options.pml_x_size = int(pml_size_temp[0]) 

340 elif kgrid.dim == 2: 

341 options.pml_x_size = int(pml_size_temp[0]) 

342 options.pml_y_size = int(pml_size_temp[1]) 

343 elif kgrid.dim == 3: 

344 options.pml_x_size = int(pml_size_temp[0]) 

345 options.pml_y_size = int(pml_size_temp[1]) 

346 options.pml_z_size = int(pml_size_temp[2]) 

347 

348 # cleanup unused variables 

349 del pml_size_temp 

350 return options