99from openlifu .util .annotations import OpenLIFUFieldData
1010from 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
4083def matrix2xyz (matrix ):
4184 x = matrix [0 , 3 ]
0 commit comments