Skip to content

Commit 8327bed

Browse files
adjust frequency-dependent sensitivity
1 parent f53546d commit 8327bed

4 files changed

Lines changed: 61 additions & 44 deletions

File tree

src/openlifu/xdc/element.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class Element:
113113
size: Annotated[np.ndarray, OpenLIFUFieldData("Size", "Size of the element in 2D")] = field(default_factory=lambda: np.array([1., 1.]))
114114
""" Size of the element in 2D as a numpy array [width, length]."""
115115

116-
sensitivity: Annotated[float | dict[float, float], OpenLIFUFieldData("Sensitivity", "Sensitivity of the element (Pa/V), scalar or {frequency_hz: sensitivity}")] = 1.0
116+
sensitivity: Annotated[float | dict, OpenLIFUFieldData("Sensitivity", "Sensitivity of the element (Pa/V), scalar or {'freq_Hz':[...], 'values_Pa_per_V':[...]}")] = 1.0
117117
"""Sensitivity of the element (Pa/V)"""
118118

119119
pin: Annotated[int, OpenLIFUFieldData("Pin", "Channel pin to which the element is connected")] = -1
@@ -134,16 +134,7 @@ def __post_init__(self):
134134
raise ValueError("Size must be a 2-element array.")
135135
if self.sensitivity is None:
136136
self.sensitivity = 1.0
137-
elif isinstance(self.sensitivity, dict):
138-
if len(self.sensitivity) == 0:
139-
raise ValueError("Sensitivity dictionary must not be empty.")
140-
mapping = {float(k): float(v) for k, v in self.sensitivity.items()}
141-
freqs = np.array(sorted(mapping.keys()), dtype=np.float64)
142-
if np.any(np.diff(freqs) <= 0):
143-
raise ValueError("Sensitivity dictionary frequencies must be strictly increasing.")
144-
self.sensitivity = {float(f): mapping[float(f)] for f in freqs}
145-
else:
146-
self.sensitivity = float(self.sensitivity)
137+
self.sensitivity = normalize_sensitivity(self.sensitivity)
147138

148139
@property
149140
def x(self):

src/openlifu/xdc/transducer.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from openlifu.xdc.element import (
1515
Element,
1616
generate_drive_signal,
17+
normalize_sensitivity,
1718
sensitivity_at_frequency,
1819
)
1920

@@ -22,21 +23,37 @@
2223

2324

2425
def _combine_sensitivities(
25-
base_sensitivity: float | dict[float, float],
26-
scale_sensitivity: float | dict[float, float],
27-
) -> float | dict[float, float]:
26+
base_sensitivity: float | dict,
27+
scale_sensitivity: float | dict,
28+
) -> float | dict[str, list[float]]:
29+
base_sensitivity = normalize_sensitivity(base_sensitivity)
30+
scale_sensitivity = normalize_sensitivity(scale_sensitivity)
31+
2832
if isinstance(base_sensitivity, dict) and isinstance(scale_sensitivity, dict):
29-
base_keys = tuple(base_sensitivity.keys())
30-
scale_keys = tuple(scale_sensitivity.keys())
31-
if base_keys != scale_keys:
33+
base_freqs = np.asarray(base_sensitivity["freq_Hz"], dtype=np.float64)
34+
scale_freqs = np.asarray(scale_sensitivity["freq_Hz"], dtype=np.float64)
35+
if not np.array_equal(base_freqs, scale_freqs):
3236
raise ValueError("Cannot combine sensitivity dictionaries with different frequency keys.")
33-
return {frequency: base_sensitivity[frequency] * scale_sensitivity[frequency] for frequency in base_keys}
37+
base_values = np.asarray(base_sensitivity["values_Pa_per_V"], dtype=np.float64)
38+
scale_values = np.asarray(scale_sensitivity["values_Pa_per_V"], dtype=np.float64)
39+
return {
40+
"freq_Hz": [float(f) for f in base_freqs],
41+
"values_Pa_per_V": [float(v) for v in (base_values * scale_values)],
42+
}
3443
if isinstance(base_sensitivity, dict):
3544
factor = float(scale_sensitivity)
36-
return {frequency: value * factor for frequency, value in base_sensitivity.items()}
45+
values = np.asarray(base_sensitivity["values_Pa_per_V"], dtype=np.float64)
46+
return {
47+
"freq_Hz": [float(f) for f in base_sensitivity["freq_Hz"]],
48+
"values_Pa_per_V": [float(v) for v in (values * factor)],
49+
}
3750
if isinstance(scale_sensitivity, dict):
3851
factor = float(base_sensitivity)
39-
return {frequency: factor * value for frequency, value in scale_sensitivity.items()}
52+
values = np.asarray(scale_sensitivity["values_Pa_per_V"], dtype=np.float64)
53+
return {
54+
"freq_Hz": [float(f) for f in scale_sensitivity["freq_Hz"]],
55+
"values_Pa_per_V": [float(v) for v in (factor * values)],
56+
}
4057
return float(base_sensitivity) * float(scale_sensitivity)
4158

4259
@dataclass
@@ -78,7 +95,7 @@ class Transducer:
7895
The units of this transform are assumed to be the native units of the transducer, the `Transducer.units` field.
7996
"""
8097

81-
sensitivity: Annotated[float | dict[float, float], OpenLIFUFieldData("Sensitivity", "Sensitivity of the element (Pa/V), scalar or {frequency_hz: sensitivity}")] = 1.0
98+
sensitivity: Annotated[float | dict, OpenLIFUFieldData("Sensitivity", "Sensitivity of the transducer (Pa/V), scalar or {'freq_Hz':[...], 'values_Pa_per_V':[...]}")] = 1.0
8299
"""Sensitivity of the transducer (Pa/V), scalar or frequency-dependent dictionary."""
83100

84101
crosstalk_frac: Annotated[float, OpenLIFUFieldData("Crosstalk fraction", "Fraction of the signal that leaks into other elements due to crosstalk")] = 0.0
@@ -98,16 +115,7 @@ def __post_init__(self):
98115
element.rescale(self.units)
99116
if self.sensitivity is None:
100117
self.sensitivity = 1.0
101-
elif isinstance(self.sensitivity, dict):
102-
if len(self.sensitivity) == 0:
103-
raise ValueError("Sensitivity dictionary must not be empty.")
104-
mapping = {float(k): float(v) for k, v in self.sensitivity.items()}
105-
freqs = np.array(sorted(mapping.keys()), dtype=np.float64)
106-
if np.any(np.diff(freqs) <= 0):
107-
raise ValueError("Sensitivity dictionary frequencies must be strictly increasing.")
108-
self.sensitivity = {float(f): mapping[float(f)] for f in freqs}
109-
else:
110-
self.sensitivity = float(self.sensitivity)
118+
self.sensitivity = normalize_sensitivity(self.sensitivity)
111119

112120
def get_sensitivity(self, frequency: float) -> float:
113121
return sensitivity_at_frequency(self.sensitivity, frequency)
@@ -252,18 +260,23 @@ def merge(list_of_transducers:List[Transducer], offset_pins:bool=False, offset_i
252260
array_copies = [arr.copy() for arr in list_of_transducers]
253261
dict_key_sets = set()
254262
for array in array_copies:
263+
array.sensitivity = normalize_sensitivity(array.sensitivity)
255264
if isinstance(array.sensitivity, dict):
256-
dict_key_sets.add(tuple(array.sensitivity.keys()))
265+
dict_key_sets.add(tuple(array.sensitivity["freq_Hz"]))
257266
for el in array.elements:
267+
el.sensitivity = normalize_sensitivity(el.sensitivity)
258268
if isinstance(el.sensitivity, dict):
259-
dict_key_sets.add(tuple(el.sensitivity.keys()))
269+
dict_key_sets.add(tuple(el.sensitivity["freq_Hz"]))
260270
if len(dict_key_sets) > 1:
261271
raise ValueError("Cannot merge sensitivities with different frequency keys.")
262272

263273
sensitivity_signatures = []
264274
for array in array_copies:
265275
if isinstance(array.sensitivity, dict):
266-
sensitivity_signatures.append((tuple(array.sensitivity.keys()), tuple(array.sensitivity.values())))
276+
sensitivity_signatures.append((
277+
tuple(array.sensitivity["freq_Hz"]),
278+
tuple(array.sensitivity["values_Pa_per_V"]),
279+
))
267280
else:
268281
sensitivity_signatures.append(float(array.sensitivity))
269282

src/openlifu/xdc/util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,10 @@ def report_to_matrix_dict(report_df: pd.DataFrame, focal_gain_lut=FOCAL_GAIN_LUT
300300
freq_df["Frequency"] = freq_df['Item'].apply(lambda x: float(re.search(r"(?<=^PNP \()\d+(?= kHz\)$)", x).group(0)))
301301
freq_df['focal_gain'] = freq_df['Frequency'].apply(lambda f: focal_gain_lut.interp(f0=f*1e3, crosstalk=matrix_dict['crosstalk_frac']).item())
302302
freq_df['Sensitivity'] = freq_df['PNP'].astype(float)*1e6/freq_df['focal_gain']/voltage
303-
matrix_dict['sensitivity'] = {f*1e3:sens for f, sens, in zip(freq_df["Frequency"], freq_df['Sensitivity'])}
303+
matrix_dict['sensitivity'] = {
304+
'freq_Hz': [float(f * 1e3) for f in freq_df["Frequency"]],
305+
'values_Pa_per_V': [float(sens) for sens in freq_df['Sensitivity']],
306+
}
304307
matrix_dict['id'] = matrix_dict['id'].format(sn=sn.lower())
305308
matrix_dict['name'] = matrix_dict['name'].format(sn=sn)
306309
return matrix_dict

tests/test_transducer.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def test_transducer_calc_output_interpolates_dictionary_sensitivity():
116116
nx=1,
117117
ny=1,
118118
units="mm",
119-
sensitivity={100e3: 1.0, 300e3: 3.0},
119+
sensitivity={"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [1.0, 3.0]},
120120
)
121121
transducer.elements[0].sensitivity = 1.0
122122
input_signal = np.array([1.0, -1.0, 0.5], dtype=float)
@@ -128,6 +128,16 @@ def test_transducer_calc_output_interpolates_dictionary_sensitivity():
128128
np.testing.assert_allclose(output_low[0, :len(input_signal)], 1.0 * input_signal)
129129

130130

131+
def test_legacy_sensitivity_mapping_is_normalized_to_schema():
132+
transducer = Transducer(
133+
sensitivity={"100000.0": 1.0, "300000.0": 3.0},
134+
)
135+
assert transducer.sensitivity == {
136+
"freq_Hz": [100000.0, 300000.0],
137+
"values_Pa_per_V": [1.0, 3.0],
138+
}
139+
140+
131141
def test_element_calc_output_generates_signal_from_scalar_input():
132142
element = Element(sensitivity=2.0)
133143
cycles = 4
@@ -161,39 +171,39 @@ def test_merge_pushes_transducer_sensitivity_into_elements():
161171
nx=1,
162172
ny=1,
163173
units="mm",
164-
sensitivity={100e3: 2.0, 300e3: 4.0},
174+
sensitivity={"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [2.0, 4.0]},
165175
)
166176
transducer_b = Transducer.gen_matrix_array(
167177
nx=1,
168178
ny=1,
169179
units="mm",
170-
sensitivity={100e3: 3.0, 300e3: 6.0},
180+
sensitivity={"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [3.0, 6.0]},
171181
)
172182
transducer_a.elements[0].sensitivity = 5.0
173183
transducer_b.elements[0].sensitivity = 7.0
174184

175185
merged = Transducer.merge([transducer_a, transducer_b], merge_mismatched_sensitivity=True)
176186

177187
assert merged.sensitivity == 1.0
178-
assert merged.elements[0].sensitivity == {100e3: 10.0, 300e3: 20.0}
179-
assert merged.elements[1].sensitivity == {100e3: 21.0, 300e3: 42.0}
188+
assert merged.elements[0].sensitivity == {"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [10.0, 20.0]}
189+
assert merged.elements[1].sensitivity == {"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [21.0, 42.0]}
180190

181191

182192
def test_merge_rejects_mismatched_sensitivity_keys():
183193
transducer_a = Transducer.gen_matrix_array(
184194
nx=1,
185195
ny=1,
186196
units="mm",
187-
sensitivity={100e3: 2.0, 300e3: 4.0},
197+
sensitivity={"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [2.0, 4.0]},
188198
)
189199
transducer_b = Transducer.gen_matrix_array(
190200
nx=1,
191201
ny=1,
192202
units="mm",
193-
sensitivity={100e3: 3.0, 400e3: 6.0},
203+
sensitivity={"freq_Hz": [100e3, 400e3], "values_Pa_per_V": [3.0, 6.0]},
194204
)
195-
transducer_a.elements[0].sensitivity = {100e3: 5.0, 300e3: 7.0}
196-
transducer_b.elements[0].sensitivity = {100e3: 11.0, 400e3: 13.0}
205+
transducer_a.elements[0].sensitivity = {"freq_Hz": [100e3, 300e3], "values_Pa_per_V": [5.0, 7.0]}
206+
transducer_b.elements[0].sensitivity = {"freq_Hz": [100e3, 400e3], "values_Pa_per_V": [11.0, 13.0]}
197207

198208
with pytest.raises(ValueError, match="different frequency keys"):
199209
Transducer.merge([transducer_a, transducer_b], merge_mismatched_sensitivity=True)

0 commit comments

Comments
 (0)