Coverage for kwave/executor.py: 16%
71 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-24 12:06 -0700
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-24 12:06 -0700
1import os
2import stat
3import subprocess
4import sys
6import h5py
8import kwave
9from kwave.options.simulation_execution_options import SimulationExecutionOptions
10from kwave.utils.dotdictionary import dotdict
13class Executor:
14 def __init__(self, execution_options: SimulationExecutionOptions, simulation_options):
15 self.execution_options = execution_options
16 self.simulation_options = simulation_options
18 if os.environ.get("KWAVE_FORCE_CPU") == "1":
19 self.execution_options.is_gpu_simulation = False
20 self.execution_options.binary_name = "kspaceFirstOrder-OMP"
21 self.execution_options.binary_path = kwave.BINARY_PATH / self.execution_options.binary_name
22 self._make_binary_executable()
24 def _make_binary_executable(self):
25 binary_path = self.execution_options.binary_path
26 if not binary_path.exists():
27 if kwave.PLATFORM == "darwin" and self.execution_options.is_gpu_simulation:
28 raise ValueError(
29 "GPU simulations are currently not supported on MacOS. "
30 "Try running the simulation on CPU by setting is_gpu_simulation=False."
31 )
32 raise FileNotFoundError(f"Binary not found at {binary_path}")
33 binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC)
35 def run_simulation(self, input_filename: str, output_filename: str, options: list[str]) -> dotdict:
36 command = [str(self.execution_options.binary_path), "-i", input_filename, "-o", output_filename] + options
38 try:
39 with subprocess.Popen(
40 command, env=self.execution_options.env_vars, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
41 ) as proc:
42 stdout, stderr = "", ""
43 if self.execution_options.show_sim_log:
44 # Stream stdout in real-time
45 for line in proc.stdout:
46 print(line, end="")
48 stdout, stderr = proc.communicate()
50 proc.wait() # wait for process to finish before checking return code
51 if proc.returncode != 0:
52 raise subprocess.CalledProcessError(proc.returncode, command, stdout, stderr)
54 except subprocess.CalledProcessError as e:
55 # This ensures stdout is printed regardless of show_sim_logs value if an error occurs
56 print(e.stdout)
57 print(e.stderr, file=sys.stderr)
58 raise
60 sensor_data = self.parse_executable_output(output_filename)
61 if not self.simulation_options.pml_inside:
62 self._crop_pml(sensor_data)
64 return sensor_data
66 def _crop_pml(self, sensor_data: dotdict):
67 Nx = sensor_data["Nx"].item()
68 Ny = sensor_data["Ny"].item()
69 Nz = sensor_data["Nz"].item()
70 pml_x_size = sensor_data["pml_x_size"].item()
71 pml_y_size = sensor_data["pml_y_size"].item()
72 pml_z_size = 0 if Nz <= 1 else sensor_data["pml_z_size"].item()
73 axisymmetric = sensor_data["axisymmetric_flag"].item()
75 # if the PML is outside, set the index variables to remove the pml
76 # from the _all and _final variables
77 x1 = pml_x_size
78 x2 = Nx - pml_x_size
79 y1 = 0 if axisymmetric else pml_y_size
80 y2 = Ny - pml_y_size
81 z1 = pml_z_size
82 z2 = Nz - pml_z_size
84 possible_fields = [
85 "p_final",
86 "p_max_all",
87 "p_min_all",
88 "ux_max_all",
89 "uy_max_all",
90 "uz_max_all",
91 "ux_min_all",
92 "uy_min_all",
93 "uz_min_all",
94 "ux_final",
95 "uy_final",
96 "uz_final",
97 ]
98 for field in possible_fields:
99 if field in sensor_data:
100 if sensor_data[field].ndim == 2:
101 sensor_data[field] = sensor_data[field][y1:y2, x1:x2]
102 else:
103 sensor_data[field] = sensor_data[field][z1:z2, y1:y2, x1:x2]
105 @staticmethod
106 def parse_executable_output(output_filename: str) -> dotdict:
107 # Load the C++ data back from disk using h5py
108 with h5py.File(output_filename, "r") as output_file:
109 sensor_data = {}
110 for key in output_file.keys():
111 sensor_data[key] = output_file[f"/{key}"][:].squeeze()
113 # if self.simulation_options.cuboid_corners:
114 # sensor_data = [output_file[f'/p/{index}'][()] for index in range(1, len(key['mask']) + 1)]
115 #
116 # # Combine the sensor data if using a kWaveTransducer as a sensor
117 # if isinstance(sensor, kWaveTransducer):
118 # sensor_data['p'] = sensor.combine_sensor_data(sensor_data['p'])
120 # # Compute the intensity outputs
121 # if any(key.startswith(('I_avg', 'I')) for key in sensor.get('record', [])):
122 # flags = {
123 # 'record_I_avg': 'I_avg' in sensor['record'],
124 # 'record_I': 'I' in sensor['record'],
125 # 'record_p': 'p' in sensor['record'],
126 # 'record_u_non_staggered': 'u_non_staggered' in sensor['record']
127 # }
128 # kspaceFirstOrder_saveIntensity()
129 #
130 # # Filter the recorded time domain pressure signals using a Gaussian filter if defined
131 # if not time_rev and 'frequency_response' in sensor:
132 # frequency_response = sensor['frequency_response']
133 # sensor_data['p'] = gaussianFilter(sensor_data['p'], 1 / kgrid.dt, frequency_response[0], frequency_response[1])
134 #
135 # # Assign sensor_data.p to sensor_data if sensor.record is not given
136 # if 'record' not in sensor and not cuboid_corners:
137 # sensor_data = sensor_data['p']
138 #
139 # # Delete the input and output files
140 # if delete_data:
141 # os.remove(input_filename)
142 # os.remove(output_filename)
143 return sensor_data