Coverage for kwave/options/simulation_execution_options.py: 37%

147 statements  

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

1import os 

2import warnings 

3from pathlib import Path 

4from typing import Optional, Union 

5 

6from kwave import BINARY_DIR, PLATFORM 

7from kwave.ksensor import kSensor 

8 

9 

10class SimulationExecutionOptions: 

11 """ 

12 A class to manage and configure the execution options for k-Wave simulations. 

13 """ 

14 

15 def __init__( 

16 self, 

17 is_gpu_simulation: bool = False, 

18 binary_path: Optional[str] = None, 

19 binary_dir: Optional[str] = None, 

20 binary_name: Optional[str] = None, 

21 kwave_function_name: Optional[str] = "kspaceFirstOrder3D", 

22 delete_data: bool = True, 

23 device_num: Optional[int] = None, 

24 num_threads: Optional[int] = None, 

25 thread_binding: Optional[bool] = None, 

26 system_call: Optional[str] = None, 

27 verbose_level: int = 0, 

28 auto_chunking: Optional[bool] = True, 

29 show_sim_log: bool = True, 

30 ): 

31 self.is_gpu_simulation = is_gpu_simulation 

32 self._binary_path = binary_path 

33 self._binary_name = binary_name 

34 self._binary_dir = binary_dir 

35 self.kwave_function_name = kwave_function_name 

36 self.delete_data = delete_data 

37 self.device_num = device_num 

38 self.num_threads = num_threads 

39 self.thread_binding = thread_binding 

40 self.system_call = system_call 

41 self.verbose_level = verbose_level 

42 self.auto_chunking = auto_chunking 

43 self.show_sim_log = show_sim_log 

44 

45 @property 

46 def num_threads(self) -> Union[int, str]: 

47 return self._num_threads 

48 

49 @num_threads.setter 

50 def num_threads(self, value: Union[int, str]): 

51 cpu_count = os.cpu_count() 

52 if cpu_count is None: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true

53 raise RuntimeError("Unable to determine the number of CPUs on this system. Please specify the number of threads explicitly.") 

54 

55 if value == "all": 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

56 warnings.warn( 

57 "The 'all' option is deprecated. The value of None sets the maximal number of threads (excluding Windows).", 

58 DeprecationWarning, 

59 ) 

60 value = cpu_count 

61 

62 if value is None: 62 ↛ 65line 62 didn't jump to line 65 because the condition on line 62 was always true

63 value = cpu_count 

64 

65 if not isinstance(value, int): 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true

66 raise ValueError("Got {value}. Number of threads must be 'all' or a positive integer") 

67 

68 if value <= 0 or value > cpu_count: 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true

69 raise ValueError(f"Number of threads {value} must be a positive integer and less than total threads on the system {cpu_count}.") 

70 

71 self._num_threads = value 

72 

73 @property 

74 def verbose_level(self) -> int: 

75 return self._verbose_level 

76 

77 @verbose_level.setter 

78 def verbose_level(self, value: int): 

79 if not (isinstance(value, int) and 0 <= value <= 2): 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true

80 raise ValueError("Verbose level must be between 0 and 2") 

81 self._verbose_level = value 

82 

83 @property 

84 def is_gpu_simulation(self) -> Optional[bool]: 

85 return self._is_gpu_simulation 

86 

87 @is_gpu_simulation.setter 

88 def is_gpu_simulation(self, value: Optional[bool]): 

89 "Set the flag to enable default GPU simulation. This option will supersede custom binary paths." 

90 self._is_gpu_simulation = value 

91 # Automatically update the binary name based on the GPU simulation flag 

92 if value is not None: 92 ↛ exitline 92 didn't return from function 'is_gpu_simulation' because the condition on line 92 was always true

93 self._binary_name = None 

94 

95 @property 

96 def binary_name(self) -> str: 

97 valid_binary_names = ["kspaceFirstOrder-OMP", "kspaceFirstOrder-CUDA"] 

98 if self._binary_name is None: 

99 # set default binary name based on GPU simulation value 

100 if self.is_gpu_simulation is None: 

101 raise ValueError("`is_gpu_simulation` must be set to either True or False before determining the binary name.") 

102 

103 if self.is_gpu_simulation: 

104 self._binary_name = "kspaceFirstOrder-CUDA" 

105 else: 

106 self._binary_name = "kspaceFirstOrder-OMP" 

107 

108 if PLATFORM == "windows": 

109 self._binary_name += ".exe" 

110 valid_binary_names = [name + ".exe" for name in valid_binary_names] 

111 

112 elif self._binary_name not in valid_binary_names: 

113 warnings.warn("Custom binary name set. Ignoring `is_gpu_simulation` state.") 

114 return self._binary_name 

115 

116 @binary_name.setter 

117 def binary_name(self, value: str): 

118 self._binary_name = value 

119 

120 @property 

121 def binary_path(self) -> Path: 

122 if self._binary_path is not None: 

123 return self._binary_path 

124 

125 binary_dir = BINARY_DIR if self._binary_dir is None else self._binary_dir 

126 

127 if binary_dir is None: 

128 raise ValueError("Binary directory is not specified.") 

129 

130 path = Path(binary_dir) / self.binary_name 

131 if PLATFORM == "windows" and not path.name.endswith(".exe"): 

132 path = path.with_suffix(".exe") 

133 return path 

134 

135 @binary_path.setter 

136 def binary_path(self, value: str): 

137 # check if the binary path is a valid path 

138 if not os.path.exists(value): 

139 raise FileNotFoundError( 

140 f"Binary path {value} does not exist. If you are trying to set `binary_dir`, use the `binary_dir` attribute instead." 

141 ) 

142 self._binary_path = value 

143 

144 @property 

145 def binary_dir(self) -> str: 

146 return BINARY_DIR if self._binary_dir is None else self._binary_dir 

147 

148 @binary_dir.setter 

149 def binary_dir(self, value: str): 

150 # check if binary_dir is a directory 

151 if not os.path.isdir(value): 

152 raise NotADirectoryError( 

153 f"{value} is not a directory. If you are trying to set the `binary_path`, use the `binary_path` attribute instead." 

154 ) 

155 self._binary_dir = Path(value) 

156 

157 @property 

158 def device_num(self) -> Optional[int]: 

159 return self._device_num 

160 

161 @device_num.setter 

162 def device_num(self, value: Optional[int]): 

163 if value is not None and value < 0: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true

164 raise ValueError("Device number must be non-negative") 

165 self._device_num = value 

166 

167 def as_list(self, sensor: kSensor) -> list[str]: 

168 options_list = [] 

169 

170 if self.device_num is not None: 

171 options_list.append("-g") 

172 options_list.append(str(self.device_num)) 

173 

174 if self._num_threads is not None and PLATFORM != "windows": 

175 options_list.append("-t") 

176 options_list.append(str(self._num_threads)) 

177 

178 if self.verbose_level > 0: 

179 options_list.append("--verbose") 

180 options_list.append(str(self.verbose_level)) 

181 

182 record_options_map = { 

183 "p": "p_raw", 

184 "p_max": "p_max", 

185 "p_min": "p_min", 

186 "p_rms": "p_rms", 

187 "p_max_all": "p_max_all", 

188 "p_min_all": "p_min_all", 

189 "p_final": "p_final", 

190 "u": "u_raw", 

191 "u_max": "u_max", 

192 "u_min": "u_min", 

193 "u_rms": "u_rms", 

194 "u_max_all": "u_max_all", 

195 "u_min_all": "u_min_all", 

196 "u_final": "u_final", 

197 } 

198 

199 if sensor.record is not None: 

200 matching_keys = sorted(set(sensor.record).intersection(record_options_map.keys())) 

201 options_list.extend([f"--{record_options_map[key]}" for key in matching_keys]) 

202 

203 if "u_non_staggered" in sensor.record or "I_avg" in sensor.record or "I" in sensor.record: 

204 options_list.append("--u_non_staggered_raw") 

205 

206 if ("I_avg" in sensor.record or "I" in sensor.record) and ("p" not in sensor.record): 

207 options_list.append("--p_raw") 

208 else: 

209 options_list.append("--p_raw") 

210 

211 if sensor.record_start_index is not None: 

212 options_list.append("-s") 

213 options_list.append(f"{sensor.record_start_index}") 

214 

215 return options_list 

216 

217 def get_options_string(self, sensor: kSensor) -> str: 

218 # raise a deprecation warning 

219 warnings.warn("This method is deprecated. Use `as_list` method instead.", DeprecationWarning) 

220 options_list = self.as_list(sensor) 

221 

222 return " ".join(options_list) 

223 

224 @property 

225 def env_vars(self) -> dict: 

226 env = os.environ 

227 

228 if PLATFORM != "darwin": 

229 env.update({"OMP_PLACES": "cores"}) 

230 

231 if self.thread_binding is not None: 

232 if PLATFORM == "darwin": 

233 raise ValueError("Thread binding is not supported in MacOS.") 

234 # read the parameters and update the system options 

235 if self.thread_binding: 

236 env.update({"OMP_PROC_BIND": "SPREAD"}) 

237 else: 

238 env.update({"OMP_PROC_BIND": "CLOSE"}) 

239 else: 

240 if PLATFORM != "darwin": 

241 env.update({"OMP_PROC_BIND": "SPREAD"}) 

242 

243 return env