Skip to content

Commit a60c8b2

Browse files
add crosstalk simulation
1 parent 8122163 commit a60c8b2

5 files changed

Lines changed: 86 additions & 17 deletions

File tree

examples/tutorials/03_Solution_Generation_and_Analysis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
)
163163
solution2.apodizations[0,:128] = 0
164164
params = protocol1.sim_setup.setup_sim_scene(protocol1.seg_method)
165-
simulation_result = solution2.simulate(params=params, use_gpu=True)
165+
simulation_result = solution2.simulate(params=params)
166166

167167
# %%
168168
simulation_result['p_min'].sel(focal_point_index=0).sel(y=0).plot.imshow()

src/openlifu/plan/protocol.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from openlifu.plan.solution_analysis import SolutionAnalysis, SolutionAnalysisOptions
2323
from openlifu.plan.target_constraints import TargetConstraints
2424
from openlifu.util.annotations import OpenLIFUFieldData
25-
from openlifu.util.checkgpu import gpu_available
2625
from openlifu.util.json import PYFUSEncoder
2726
from openlifu.virtual_fit import VirtualFitOptions
2827
from openlifu.xdc import Transducer
@@ -250,8 +249,8 @@ def calc_solution(
250249
sim_options: sim.SimSetup | None = None,
251250
analysis_options: SolutionAnalysisOptions | None = None,
252251
on_pulse_mismatch: OnPulseMismatchAction = OnPulseMismatchAction.ERROR,
253-
use_gpu: bool | None = None,
254-
voltage: float = 1.0
252+
voltage: float = 1.0,
253+
_force_cpu: bool = False
255254
) -> Tuple[Solution, xa.DataArray, SolutionAnalysis]:
256255
"""Calculate the solution and aggregated k-wave simulation outputs.
257256
@@ -291,9 +290,6 @@ def calc_solution(
291290
scaled_solution_analysis: SolutionAnalysis
292291
This is the resulting rescaled analysis, if scale is enabled.
293292
"""
294-
if use_gpu is None:
295-
use_gpu = gpu_available()
296-
297293
if sim_options is None:
298294
sim_options = self.sim_setup
299295
if analysis_options is None:
@@ -346,7 +342,7 @@ def calc_solution(
346342
simulation_result = solution.simulate(
347343
params=params,
348344
sim_options=sim_options,
349-
use_gpu=use_gpu)
345+
_force_cpu=_force_cpu)
350346
solution.simulation_result = simulation_result
351347

352348
# optionally scale the solution with simulation result

src/openlifu/plan/solution.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,21 @@ def num_foci(self) -> int:
139139
def simulate(self,
140140
params: xa.Dataset,
141141
sim_options: SimSetup | None = None,
142-
use_gpu: bool | None = None) -> xa.Dataset:
142+
_force_cpu: bool = False) -> xa.Dataset:
143+
"""Run a simulation for this solution and store the result in the `simulation_result` attribute.
143144
144-
if use_gpu is None:
145+
Args:
146+
params: The simulation parameters as an xarray Dataset.
147+
sim_options: Optional simulation setup options.
148+
_force_cpu: Whether to force the simulation to run on the CPU. If True, the GPU will not be used even if available.
149+
150+
Returns:
151+
The simulation result as an xarray Dataset.
152+
"""
153+
154+
if _force_cpu:
155+
use_gpu = False
156+
else:
145157
use_gpu = gpu_available()
146158

147159
if sim_options is None:

src/openlifu/sim/kwave_if.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,43 @@ def run_simulation(arr: xdc.Transducer,
9595
return_kwave_outputs: bool = False,
9696
return_kwave_inputs: bool = False,
9797
sensor_record: List[str] = ['p_max', 'p_min'],
98-
_source: kSource|None = None,
99-
_sensor: kSensor|None = None
98+
_source = None,
99+
_sensor = None
100100
):
101+
""" Run a k-wave simulation for the given transducer array and parameters.
102+
Args:
103+
arr: The transducer array to simulate.
104+
params: The simulation parameters as an xarray Dataset. Must include 'sound_speed', '
105+
density', and 'attenuation' variables with appropriate units.
106+
delays: Optional array of time delays for each element in the transducer array, in seconds. If None, no delays will be applied.
107+
apod: Optional array of apodization values for each
108+
element in the transducer array. If None, no apodization will be applied.
109+
freq: The frequency of the input signal in Hz. Default is 1 MHz.
110+
cycles: The number of cycles in the input signal. Default is 20.
111+
amplitude: The amplitude of the input signal. Default is 1.
112+
dt: The time step for the simulation in seconds. If 0, it will be automatically calculated based on the CFL condition.
113+
t_end: The total time for the simulation in seconds. If 0, it will be automatically calculated based on the input signal duration and the CFL condition.
114+
cfl: The Courant-Friedrichs-Lewy (CFL) number for the simulation. Default is 0.5.
115+
bli_tolerance: The tolerance for the boundary layer integral method used in k-wave. Default
116+
is 0.05.
117+
upsampling_rate: The upsampling rate for the boundary layer integral method used in k-wave
118+
Default is 5.
119+
gpu: Whether to use GPU for the simulation. Default is True. If False, the simulation will run on the CPU. Note that running on CPU may be very slow for large simulations.
120+
ref_values_only: Whether to use only the reference values for the medium properties (sound speed, density, attenuation) instead of the full spatial maps. Default is False. Setting this to True can significantly speed up the simulation, but will not capture any spatial variations in the medium properties.
121+
return_kwave_outputs: Whether to return the raw outputs from k-wave in addition to the processed xarray Dataset. Default is False.
122+
return_kwave_inputs: Whether to return the inputs to k-wave (kgrid, source, sensor, medium) in addition to the processed xarray Dataset. Default is False.
123+
sensor_record: List of strings specifying which k-wave sensor outputs to record. Can include '
124+
p_max', 'p_min', and 'p'. Default is ['p_max', 'p_min'].
125+
126+
Additional Args:
127+
_source: Optional kSource object to use for the simulation. If None, a source will be created based on the transducer array and input signal.
128+
_sensor: Optional kSensor object to use for the simulation. If None, a sensor
129+
130+
Returns:
131+
An xarray Dataset containing the simulation results, with variables corresponding to the requested sensor outputs and
132+
coordinates corresponding to the spatial dimensions of the simulation. If return_kwave_outputs is True, also returns a dictionary containing the raw outputs from k-wave. If return_kwave_inputs is True, also returns a dictionary containing the inputs to k-wave.
133+
134+
"""
101135
from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D
102136
from kwave.options.simulation_execution_options import SimulationExecutionOptions
103137
from kwave.options.simulation_options import SimulationOptions
@@ -111,10 +145,7 @@ def run_simulation(arr: xdc.Transducer,
111145
raise ValueError("All dimensions must have the same units")
112146
scl = getunitconversion(units[0], 'm')
113147
array_offset =[-float(coord.mean())*scl for coord in params.coords.values()]
114-
karray = get_karray(arr,
115-
translation=array_offset,
116-
bli_tolerance=bli_tolerance,
117-
upsampling_rate=upsampling_rate)
148+
118149
medium = get_medium(params, ref_values_only=ref_values_only)
119150
if _sensor is not None:
120151
sensor = _sensor
@@ -126,7 +157,31 @@ def run_simulation(arr: xdc.Transducer,
126157
source = _source
127158
else:
128159
source_mat = arr.calc_output(input_signal, kgrid.dt, delays, apod)
129-
source = get_source(kgrid, karray, source_mat)
160+
if arr.crosstalk_frac != 0:
161+
""" Simulate crosstalk by adding additional elements to the array for each element that
162+
is within the crosstalk distance, with the signal scaled by the crosstalk fraction.
163+
This is a simple model of crosstalk and may not capture all the complexities of real
164+
crosstalk, but it can be useful for testing and simulation purposes."""
165+
crosstalk_arr = arr.copy()
166+
crosstalk_mat = source_mat
167+
positions = arr.get_positions(units="m")
168+
for src_idx in range(arr.numelements()):
169+
for dst_idx in range(arr.numelements()):
170+
if src_idx == dst_idx:
171+
continue
172+
src_pos = np.array(positions[src_idx])
173+
dst_pos = np.array(positions[dst_idx])
174+
dist = np.linalg.norm(src_pos - dst_pos)
175+
if dist <= arr.crosstalk_dist:
176+
crosstalk_arr.elements += [arr.elements[dst_idx].copy()]
177+
crosstalk_mat = np.vstack((crosstalk_mat, arr.crosstalk_frac*source_mat[src_idx,:]))
178+
arr = crosstalk_arr
179+
source_mat = crosstalk_mat
180+
karray = get_karray(arr,
181+
translation=array_offset,
182+
bli_tolerance=bli_tolerance,
183+
upsampling_rate=upsampling_rate)
184+
source = get_source(kgrid, karray, source_mat)
130185
logging.info("Running simulation")
131186
simulation_options = SimulationOptions(
132187
pml_auto=True,

src/openlifu/xdc/transducer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ class Transducer:
5858
sensitivity: Annotated[float | None, OpenLIFUFieldData("Sensitivity", "Sensitivity of the element (Pa/V)")] = None
5959
"""Sensitivity of the element (Pa/V)"""
6060

61+
crosstalk_frac: Annotated[float, OpenLIFUFieldData("Crosstalk fraction", "Fraction of the signal that leaks into other elements due to crosstalk")] = 0.0
62+
"""Fraction of the signal that leaks into other elements due to crosstalk"""
63+
64+
crosstalk_dist: Annotated[float, OpenLIFUFieldData("Crosstalk distance", "Distance within which elements experience crosstalk")] = 0.0
65+
"""Distance within which elements experience crosstalk"""
66+
6167
impulse_response: Annotated[np.ndarray | None, OpenLIFUFieldData("Impulse response", "Impulse response of the element")] = None
6268
"""Impulse response of the element, can be a single value or an array of values. If an array, `impulse_dt` must be set to the time step of the impulse response. Is convolved with the input signal."""
6369

0 commit comments

Comments
 (0)