Skip to content

Commit 97f90c9

Browse files
fix some frequency-depended issues
1 parent 9f17b94 commit 97f90c9

4 files changed

Lines changed: 100 additions & 30 deletions

File tree

src/openlifu/plan/solution.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +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(input_signal_V, dt, delays=self.delays[focus_index, :], apod=self.apodizations[focus_index, :])
260-
p0_Pa = np.max(output_signal_Pa, axis=1)
259+
p0_Pa = np.max(self.transducer.calc_output(
260+
cycles=1,
261+
frequency=self.pulse.frequency,
262+
dt=dt,
263+
delays=self.delays[focus_index, :],
264+
apod=self.apodizations[focus_index, :],
265+
amplitude=self.pulse.amplitude * self.voltage,
266+
), axis=1)
261267

262268
mainlobe_mask = get_mask(
263269
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, kgrid.dt, delays, 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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,77 @@
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"
14+
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)
60+
if isinstance(sensitivity, dict):
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]))
68+
return float(sensitivity)
69+
70+
71+
def generate_drive_signal(cycles: float, frequency: float, dt: float, amplitude: float = 1.0) -> np.ndarray:
72+
"""Generate a drive signal with duration constrained by cycles/frequency."""
73+
if dt <= 0:
74+
raise ValueError("dt must be positive.")
75+
if frequency <= 0:
76+
raise ValueError("frequency must be positive.")
77+
if cycles <= 0:
78+
raise ValueError("cycles must be positive.")
79+
n_samples = max(1, int(np.round(cycles / (frequency * dt))))
80+
t = np.arange(n_samples, dtype=np.float64) * dt
81+
return amplitude * np.sin(2 * np.pi * frequency * t)
82+
1283

1384
def matrix2xyz(matrix):
1485
x = matrix[0, 3]

src/openlifu/xdc/transducer.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from openlifu.util.annotations import OpenLIFUFieldData
1313
from openlifu.util.units import getunitconversion
14-
from openlifu.xdc.element import Element
14+
from openlifu.xdc.element import Element, generate_drive_signal
1515

1616
DIMS = ['x', 'y', 'z']
1717
LDIMS = Literal['x','y','z']
@@ -87,30 +87,20 @@ def __post_init__(self):
8787
raise ValueError("Impulse response timestep must be set if impulse response is set.")
8888

8989

90-
def interp_impulse_response(self, dt=None):
91-
if dt is None:
92-
dt = self.impulse_dt
93-
n0 = len(self.impulse_response)
94-
t0 = self.impulse_dt * np.arange(n0)
95-
t1 = np.arange(0, t0[-1] + dt, dt)
96-
impulse_response = np.interp(t1, t0, self.impulse_response)
97-
impulse_t = np.arange(len(impulse_response)) * dt
98-
impulse_t = impulse_t - np.mean(impulse_t)
99-
return impulse_response, impulse_t
100-
101-
def calc_output(self, input_signal, dt, delays: np.ndarray = None, apod: np.ndarray = None):
90+
def calc_output(self, cycles: float, frequency: float, dt: float, delays: np.ndarray = None, apod: np.ndarray = None, amplitude: float = 1.0) -> np.ndarray:
10291
if delays is None:
10392
delays = np.zeros(self.numelements())
10493
if apod is None:
10594
apod = np.ones(self.numelements())
106-
if self.impulse_response is None:
107-
filtered_input_signal = input_signal * 1
108-
else:
109-
impulse = self.interp_impulse_response(dt)
110-
filtered_input_signal = np.convolve(input_signal, impulse, mode='full')
111-
if self.sensitivity is not None:
112-
filtered_input_signal = filtered_input_signal * self.sensitivity
113-
outputs = [np.concatenate([np.zeros(int(delay/dt)), a*element.calc_output(filtered_input_signal, dt)],axis=0) for element, delay, a, in zip(self.elements, delays, apod)]
95+
drive_signal = generate_drive_signal(cycles=cycles, frequency=frequency, dt=dt, amplitude=amplitude)
96+
base_output = drive_signal * self.get_sensitivity(frequency)
97+
outputs = [
98+
np.concatenate(
99+
[np.zeros(int(delay / dt)), a * element.get_sensitivity(frequency) * base_output],
100+
axis=0,
101+
)
102+
for element, delay, a, in zip(self.elements, delays, apod)
103+
]
114104
max_len = max([len(o) for o in outputs])
115105
output_signal = np.zeros([self.numelements(), max_len])
116106
for i, o in enumerate(outputs):

0 commit comments

Comments
 (0)