77from helpers import dataclasses_are_equal
88
99from openlifu .xdc import Element , Transducer , TransducerArray
10+ from openlifu .xdc .transducerarray import (
11+ get_angle_from_gap ,
12+ get_gap_from_angle ,
13+ get_roc_from_angle ,
14+ )
1015
1116
1217@pytest .fixture ()
@@ -119,13 +124,21 @@ def test_transducer_calc_output_interpolates_dictionary_sensitivity():
119124 sensitivity = {"freq_Hz" : [100e3 , 300e3 ], "values_Pa_per_V" : [1.0 , 3.0 ]},
120125 )
121126 transducer .elements [0 ].sensitivity = 1.0
122- input_signal = np .array ([1.0 , - 1.0 , 0.5 ], dtype = float )
127+ cycles = 3
128+ dt = 1e-7
123129
124- output_mid = transducer .calc_output (input_signal , cycles = 3 , frequency = 200e3 , dt = 1e-7 )
125- output_low = transducer .calc_output (input_signal , cycles = 3 , frequency = 100e3 , dt = 1e-7 )
130+ output_mid = transducer .calc_output (cycles = cycles , frequency = 200e3 , dt = dt )
131+ output_low = transducer .calc_output (cycles = cycles , frequency = 100e3 , dt = dt )
126132
127- np .testing .assert_allclose (output_mid [0 , :len (input_signal )], 2.0 * input_signal )
128- np .testing .assert_allclose (output_low [0 , :len (input_signal )], 1.0 * input_signal )
133+ n_samples_mid = int (np .round (cycles / (200e3 * dt )))
134+ n_samples_low = int (np .round (cycles / (100e3 * dt )))
135+ t_mid = np .arange (n_samples_mid ) * dt
136+ t_low = np .arange (n_samples_low ) * dt
137+ expected_mid = 2.0 * np .sin (2 * np .pi * 200e3 * t_mid )
138+ expected_low = 1.0 * np .sin (2 * np .pi * 100e3 * t_low )
139+
140+ np .testing .assert_allclose (output_mid [0 ], expected_mid )
141+ np .testing .assert_allclose (output_low [0 ], expected_low )
129142
130143
131144def test_legacy_sensitivity_mapping_is_normalized_to_schema ():
@@ -145,25 +158,25 @@ def test_element_calc_output_generates_signal_from_scalar_input():
145158 dt = 1e-7
146159 n_samples = int (np .round (cycles / (frequency * dt )))
147160
148- output = element .calc_output (3.0 , cycles = cycles , frequency = frequency , dt = dt )
161+ output = element .calc_output (cycles = cycles , frequency = frequency , dt = dt , amplitude = 3.0 )
149162 t = np .arange (n_samples ) * dt
150163 expected = 2.0 * 3.0 * np .sin (2 * np .pi * frequency * t )
151164
152165 np .testing .assert_allclose (output , expected )
153166
154167
155- def test_element_calc_output_enforces_cycles_duration_for_array_input ():
168+ def test_element_calc_output_enforces_cycles_duration_for_generated_signal ():
156169 element = Element (sensitivity = 1.0 )
157170 cycles = 1
158171 frequency = 200e3
159172 dt = 1e-6
160173 n_samples = int (np .round (cycles / (frequency * dt )))
161- input_signal = np . arange ( 20 , dtype = float )
162-
163- output = element . calc_output ( input_signal , cycles = cycles , frequency = frequency , dt = dt )
174+ output = element . calc_output ( cycles = cycles , frequency = frequency , dt = dt )
175+ t = np . arange ( n_samples ) * dt
176+ expected = np . sin ( 2 * np . pi * frequency * t )
164177
165178 assert len (output ) == n_samples
166- np .testing .assert_allclose (output , input_signal [: n_samples ] )
179+ np .testing .assert_allclose (output , expected )
167180
168181
169182def test_merge_pushes_transducer_sensitivity_into_elements ():
@@ -207,3 +220,143 @@ def test_merge_rejects_mismatched_sensitivity_keys():
207220
208221 with pytest .raises (ValueError , match = "different frequency keys" ):
209222 Transducer .merge ([transducer_a , transducer_b ], merge_mismatched_sensitivity = True )
223+
224+
225+ @pytest .mark .parametrize (
226+ ("width" , "dth" , "roc" ),
227+ [
228+ (8.0 , 0.08 , 25.0 ),
229+ (10.0 , 0.12 , 30.0 ),
230+ (12.0 , 0.18 , 45.0 ),
231+ ],
232+ )
233+ def test_concave_geometry_helpers_are_mutual_inverses (width : float , dth : float , roc : float ):
234+ gap = get_gap_from_angle (width , dth , roc )
235+ recovered_roc = get_roc_from_angle (width , gap , dth )
236+ recovered_dth = get_angle_from_gap (width , gap , roc )
237+ recovered_gap = get_gap_from_angle (width , recovered_dth , roc )
238+
239+ assert np .isclose (recovered_roc , roc )
240+ assert np .isclose (recovered_dth , dth )
241+ assert np .isclose (recovered_gap , gap )
242+
243+
244+ def test_get_concave_cylinder_computes_gap_from_dth_and_roc_layout_spacing ():
245+ base = Transducer .gen_matrix_array (nx = 1 , ny = 1 , units = "mm" )
246+ width = 8.0
247+ dth = 0.12
248+ roc = 25.0
249+ array = TransducerArray .get_concave_cylinder (
250+ base ,
251+ rows = 2 ,
252+ cols = 1 ,
253+ width = width ,
254+ dth = dth ,
255+ roc = roc ,
256+ units = "mm" ,
257+ )
258+ merged = array .to_transducer ()
259+ positions = merged .get_positions (units = "mm" )
260+
261+ expected_gap = get_gap_from_angle (width , dth , roc )
262+ y_spacing = np .abs (positions [1 , 1 ] - positions [0 , 1 ])
263+
264+ assert np .isclose (y_spacing , width + expected_gap )
265+
266+
267+ def test_get_concave_cylinder_handles_zero_dth_without_roc ():
268+ base = Transducer .gen_matrix_array (nx = 1 , ny = 1 , units = "mm" )
269+ width = 10.0
270+ gap = 2.0
271+ array = TransducerArray .get_concave_cylinder (
272+ base ,
273+ rows = 1 ,
274+ cols = 2 ,
275+ width = width ,
276+ gap = gap ,
277+ dth = 0.0 ,
278+ units = "mm" ,
279+ )
280+ merged = array .to_transducer ()
281+ positions = merged .get_positions (units = "mm" )
282+
283+ x_spacing = np .abs (positions [1 , 0 ] - positions [0 , 0 ])
284+ z_values = positions [:, 2 ]
285+
286+ assert np .isclose (x_spacing , width + gap )
287+ np .testing .assert_allclose (z_values , np .zeros_like (z_values ))
288+
289+
290+ def test_get_concave_cylinder_rejects_gap_dth_roc_together ():
291+ base = Transducer .gen_matrix_array (nx = 1 , ny = 1 , units = "mm" )
292+ with pytest .raises (ValueError , match = "cannot specify all of gap, dth, and roc" ):
293+ TransducerArray .get_concave_cylinder (
294+ base ,
295+ rows = 1 ,
296+ cols = 2 ,
297+ width = 10.0 ,
298+ gap = 1.0 ,
299+ dth = 0.2 ,
300+ roc = 20.0 ,
301+ units = "mm" ,
302+ )
303+
304+
305+ def test_transducer_calc_output_combines_frequency_dependent_sensitivities ():
306+ transducer = Transducer .gen_matrix_array (
307+ nx = 1 ,
308+ ny = 1 ,
309+ units = "mm" ,
310+ sensitivity = {"freq_Hz" : [100e3 , 300e3 ], "values_Pa_per_V" : [2.0 , 4.0 ]},
311+ )
312+ transducer .elements [0 ].sensitivity = {"freq_Hz" : [100e3 , 300e3 ], "values_Pa_per_V" : [5.0 , 9.0 ]}
313+
314+ frequency = 200e3
315+ dt = 1e-7
316+ cycles = 3
317+ n_samples = int (np .round (cycles / (frequency * dt )))
318+ t = np .arange (n_samples ) * dt
319+ expected_drive = np .sin (2 * np .pi * frequency * t )
320+
321+ output = transducer .calc_output (cycles = cycles , frequency = frequency , dt = dt )
322+
323+ np .testing .assert_allclose (output [0 ], 21.0 * expected_drive )
324+
325+
326+ def test_transducer_array_to_transducer_preserves_frequency_dependent_sensitivities ():
327+ transducer_a = Transducer .gen_matrix_array (
328+ nx = 1 ,
329+ ny = 1 ,
330+ units = "mm" ,
331+ sensitivity = {"freq_Hz" : [100e3 , 300e3 ], "values_Pa_per_V" : [2.0 , 4.0 ]},
332+ )
333+ transducer_b = Transducer .gen_matrix_array (
334+ nx = 1 ,
335+ ny = 1 ,
336+ units = "mm" ,
337+ sensitivity = {"freq_Hz" : [100e3 , 300e3 ], "values_Pa_per_V" : [1.0 , 3.0 ]},
338+ )
339+ transducer_a .elements [0 ].sensitivity = 5.0
340+ transducer_b .elements [0 ].sensitivity = 7.0
341+
342+ array = TransducerArray .get_concave_cylinder (
343+ [transducer_a , transducer_b ],
344+ rows = 1 ,
345+ cols = 2 ,
346+ width = 10.0 ,
347+ gap = 0.0 ,
348+ units = "mm" ,
349+ )
350+ merged = array .to_transducer ()
351+
352+ frequency = 200e3
353+ dt = 1e-7
354+ cycles = 2
355+ n_samples = int (np .round (cycles / (frequency * dt )))
356+ t = np .arange (n_samples ) * dt
357+ expected_drive = np .sin (2 * np .pi * frequency * t )
358+
359+ output = merged .calc_output (cycles = cycles , frequency = frequency , dt = dt )
360+
361+ np .testing .assert_allclose (output [0 ], 15.0 * expected_drive )
362+ np .testing .assert_allclose (output [1 ], 14.0 * expected_drive )
0 commit comments