Skip to content

Commit f53546d

Browse files
fix kwave bugs
2 parents dec962d + 97f90c9 commit f53546d

4 files changed

Lines changed: 74 additions & 29 deletions

File tree

src/openlifu/plan/solution.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,14 @@ def analyze(self,
256256
apodization = self.apodizations[focus_index]
257257
origin = self.transducer.get_effective_origin(apodizations=apodization, units=options.distance_units)
258258

259-
output_signal_Pa = self.transducer.calc_output(
260-
input_signal_V,
261-
cycles=self.pulse.duration * self.pulse.frequency,
259+
p0_Pa = np.max(self.transducer.calc_output(
260+
cycles=1,
262261
frequency=self.pulse.frequency,
263262
dt=dt,
264263
delays=self.delays[focus_index, :],
265264
apod=self.apodizations[focus_index, :],
266-
)
267-
p0_Pa = np.max(output_signal_Pa, axis=1)
265+
amplitude=self.pulse.amplitude * self.voltage,
266+
), axis=1)
268267

269268
mainlobe_mask = get_mask(
270269
pnp_MPa,

src/openlifu/sim/kwave_if.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def run_simulation(arr: xdc.Transducer,
139139
apod = apod if apod is not None else np.ones(arr.numelements())
140140
kgrid = get_kgrid(params.coords, dt=dt, t_end=t_end, cfl=cfl)
141141
t = np.arange(0, np.min([cycles / freq, (kgrid.Nt-np.ceil(max(delays)/kgrid.dt))*kgrid.dt]), kgrid.dt)
142-
input_signal = amplitude * np.sin(2 * np.pi * freq * t)
142+
if cycles/freq > t[-1]:
143+
cycles = int(t[-1]*freq)
143144
units = [params[dim].attrs['units'] for dim in params.dims]
144145
if not all(unit == units[0] for unit in units):
145146
raise ValueError("All dimensions must have the same units")
@@ -156,14 +157,15 @@ def run_simulation(arr: xdc.Transducer,
156157
if _source is not None:
157158
source = _source
158159
else:
159-
source_mat = arr.calc_output(input_signal, cycles=cycles, frequency=freq, dt=kgrid.dt, delays=delays, apod=apod)
160+
source_mat = arr.calc_output(amplitude=amplitude, cycles=cycles, frequency=freq, dt=kgrid.dt, delays=delays, apod=apod)
161+
source_mat = source_mat[:, :kgrid.Nt]
160162
if arr.crosstalk_frac != 0:
161163
# Simulate crosstalk by adding additional elements to the array for each element that
162164
# is within the crosstalk distance, with the signal scaled by the crosstalk fraction.
163165
# This is a simple model of crosstalk and may not capture all the complexities of real
164166
# crosstalk, but it can be useful for testing and simulation purposes.
165-
crosstalk_arr = arr.copy()
166-
crosstalk_mat = source_mat
167+
#crosstalk_arr = arr.copy()
168+
crosstalk_mat = source_mat.copy()
167169
positions = arr.get_positions(units="m")
168170
for src_idx in range(arr.numelements()):
169171
for dst_idx in range(arr.numelements()):
@@ -173,9 +175,10 @@ def run_simulation(arr: xdc.Transducer,
173175
dst_pos = np.array(positions[dst_idx])
174176
dist = np.linalg.norm(src_pos - dst_pos)
175177
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
178+
# crosstalk_arr.elements += [arr.elements[dst_idx].copy()]
179+
crosstalk_mat[dst_idx,:] += arr.crosstalk_frac*source_mat[src_idx,:]
180+
#crosstalk_mat = np.vstack((crosstalk_mat, arr.crosstalk_frac*source_mat[src_idx,:]))
181+
#arr = crosstalk_arr
179182
source_mat = crosstalk_mat
180183
karray = get_karray(arr,
181184
translation=array_offset,

src/openlifu/xdc/element.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,66 @@
99
from openlifu.util.annotations import OpenLIFUFieldData
1010
from openlifu.util.units import getunitconversion
1111

12+
SENS_FREQ_KEY = "freq_Hz"
13+
SENS_VALUE_KEY = "values_Pa_per_V"
1214

13-
def sensitivity_at_frequency(sensitivity: float | dict[float, float], frequency: float) -> float:
15+
16+
def normalize_sensitivity(sensitivity: float | dict) -> float | dict[str, list[float]]:
17+
"""Normalize sensitivity to a canonical representation.
18+
19+
Canonical frequency-dependent representation is:
20+
{"freq_Hz": [...], "values_Pa_per_V": [...]}
21+
22+
Backward-compatible legacy representation with frequency keys is accepted and
23+
converted to the canonical representation.
24+
"""
25+
if isinstance(sensitivity, dict):
26+
if SENS_FREQ_KEY in sensitivity or SENS_VALUE_KEY in sensitivity:
27+
if SENS_FREQ_KEY not in sensitivity or SENS_VALUE_KEY not in sensitivity:
28+
raise ValueError("Sensitivity dictionary must include both 'freq_Hz' and 'values_Pa_per_V'.")
29+
freqs = np.asarray(sensitivity[SENS_FREQ_KEY], dtype=np.float64).reshape(-1)
30+
values = np.asarray(sensitivity[SENS_VALUE_KEY], dtype=np.float64).reshape(-1)
31+
else:
32+
# Legacy format: {frequency_hz: sensitivity}
33+
if len(sensitivity) == 0:
34+
raise ValueError("Sensitivity dictionary must not be empty.")
35+
mapping = {float(k): float(v) for k, v in sensitivity.items()}
36+
freqs = np.array(list(mapping.keys()), dtype=np.float64)
37+
values = np.array(list(mapping.values()), dtype=np.float64)
38+
39+
if len(freqs) == 0:
40+
raise ValueError("Sensitivity frequency list must not be empty.")
41+
if len(freqs) != len(values):
42+
raise ValueError("Sensitivity frequency and value lists must have the same length.")
43+
44+
order = np.argsort(freqs)
45+
freqs = freqs[order]
46+
values = values[order]
47+
if np.any(np.diff(freqs) <= 0):
48+
raise ValueError("Sensitivity frequencies must be strictly increasing.")
49+
50+
return {
51+
SENS_FREQ_KEY: [float(f) for f in freqs],
52+
SENS_VALUE_KEY: [float(v) for v in values],
53+
}
54+
55+
return float(sensitivity)
56+
57+
58+
def sensitivity_at_frequency(sensitivity: float | dict, frequency: float) -> float:
59+
sensitivity = normalize_sensitivity(sensitivity)
1460
if isinstance(sensitivity, dict):
15-
freqs = np.array(list(sensitivity.keys()), dtype=np.float64)
16-
values = np.array(list(sensitivity.values()), dtype=np.float64)
17-
return float(np.interp(frequency, freqs, values, left=values[0], right=values[-1]))
61+
if frequency in sensitivity[SENS_FREQ_KEY]:
62+
idx = sensitivity[SENS_FREQ_KEY].index(frequency)
63+
return float(sensitivity[SENS_VALUE_KEY][idx])
64+
else:
65+
freqs = np.array(sensitivity[SENS_FREQ_KEY], dtype=np.float64)
66+
values = np.array(sensitivity[SENS_VALUE_KEY], dtype=np.float64)
67+
return float(np.interp(frequency, freqs, values, left=values[0], right=values[-1]))
1868
return float(sensitivity)
1969

2070

21-
def generate_drive_signal(input_signal, cycles: float, frequency: float, dt: float) -> np.ndarray:
71+
def generate_drive_signal(cycles: float, frequency: float, dt: float, amplitude: float = 1.0) -> np.ndarray:
2272
"""Generate a drive signal with duration constrained by cycles/frequency."""
2373
if dt <= 0:
2474
raise ValueError("dt must be positive.")
@@ -27,15 +77,8 @@ def generate_drive_signal(input_signal, cycles: float, frequency: float, dt: flo
2777
if cycles <= 0:
2878
raise ValueError("cycles must be positive.")
2979
n_samples = max(1, int(np.round(cycles / (frequency * dt))))
30-
if np.isscalar(input_signal):
31-
t = np.arange(n_samples, dtype=np.float64) * dt
32-
return float(input_signal) * np.sin(2 * np.pi * frequency * t)
33-
base = np.asarray(input_signal, dtype=np.float64).reshape(-1)
34-
drive_signal = np.zeros(n_samples, dtype=np.float64)
35-
n_copy = min(n_samples, len(base))
36-
drive_signal[:n_copy] = base[:n_copy]
37-
return drive_signal
38-
80+
t = np.arange(n_samples, dtype=np.float64) * dt
81+
return amplitude * np.sin(2 * np.pi * frequency * t)
3982

4083
def matrix2xyz(matrix):
4184
x = matrix[0, 3]

src/openlifu/xdc/transducer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,16 @@ def __post_init__(self):
112112
def get_sensitivity(self, frequency: float) -> float:
113113
return sensitivity_at_frequency(self.sensitivity, frequency)
114114

115-
def calc_output(self, input_signal, cycles: float, frequency: float, dt: float, delays: np.ndarray = None, apod: np.ndarray = None):
115+
def calc_output(self, cycles: float, frequency: float, dt: float, delays: np.ndarray = None, apod: np.ndarray = None, amplitude: float = 1.0) -> np.ndarray:
116116
if delays is None:
117117
delays = np.zeros(self.numelements())
118118
if apod is None:
119119
apod = np.ones(self.numelements())
120-
drive_signal = generate_drive_signal(input_signal, cycles=cycles, frequency=frequency, dt=dt)
120+
drive_signal = generate_drive_signal(cycles=cycles, frequency=frequency, dt=dt, amplitude=amplitude)
121121
base_output = drive_signal * self.get_sensitivity(frequency)
122122
outputs = [
123123
np.concatenate(
124-
[np.zeros(int(delay / dt)), a * element.get_sensitivity(frequency) * base_output],
124+
[np.zeros(int(delay / dt)), a * element.get_sensitivity(frequency) * base_output],
125125
axis=0,
126126
)
127127
for element, delay, a, in zip(self.elements, delays, apod)

0 commit comments

Comments
 (0)