3737from vbeam .scan import LinearScan
3838from vbeam .wavefront import PlaneWavefront , ReflectedWavefront
3939
40- from mach import experimental
40+ from mach import experimental , kernel
4141from mach ._vis import save_debug_figures
42- from mach .io .uff import create_beamforming_setup
42+ from mach .io .uff import create_beamforming_setup , create_single_transmit_beamforming_setup
4343
4444# ============================================================================
4545# Fixtures
@@ -178,14 +178,44 @@ def vbeam_setup_uff(
178178
179179
180180@pytest .fixture (scope = "module" )
181- def picmus_phantom_resolution_beamform_kwargs (
181+ def mach_beamform_kwargs (
182182 picmus_phantom_resolution_channel_data : ChannelData , picmus_phantom_resolution_scan : Scan
183183) -> dict :
184184 """mach kwargs for UFF data."""
185185 return create_beamforming_setup (
186186 channel_data = picmus_phantom_resolution_channel_data ,
187187 scan = picmus_phantom_resolution_scan ,
188- xp = cp if HAS_CUPY else None ,
188+ xp = cp if HAS_CUPY else np ,
189+ )
190+
191+
192+ # ============================================================================
193+ # Single Transmit Test Fixtures
194+ # ============================================================================
195+
196+
197+ @pytest .fixture (scope = "module" , params = [0 , 1 , 10 , 37 , 74 ])
198+ def transmit_idx (request ):
199+ """Parametrized transmit index for single-transmit testing."""
200+ return request .param
201+
202+
203+ @pytest .fixture (scope = "module" )
204+ def vbeam_setup_uff_single_transmit (vbeam_setup_uff : SignalForPointSetup , transmit_idx : int ) -> SignalForPointSetup :
205+ """Create a single-transmit vbeam setup from the full UFF setup."""
206+ return vbeam_setup_uff .slice ["transmits" , transmit_idx ]
207+
208+
209+ @pytest .fixture (scope = "module" )
210+ def mach_single_transmit_kwargs (
211+ picmus_phantom_resolution_channel_data : ChannelData , picmus_phantom_resolution_scan : Scan , transmit_idx : int
212+ ) -> dict :
213+ """mach kwargs for UFF data."""
214+ return create_single_transmit_beamforming_setup (
215+ channel_data = picmus_phantom_resolution_channel_data ,
216+ scan = picmus_phantom_resolution_scan ,
217+ wave_index = transmit_idx ,
218+ xp = cp if HAS_CUPY else np ,
189219 )
190220
191221
@@ -261,13 +291,66 @@ def vbeam_beamform():
261291
262292@pytest .mark .skipif (not HAS_CUPY , reason = "CuPy not available" )
263293@pytest .mark .filterwarnings ("ignore:array is not contiguous, rearranging will add latency:UserWarning" )
264- def test_mach_matches_vbeam (
265- picmus_phantom_resolution_beamform_kwargs , vbeam_setup_uff : SignalForPointSetup , output_dir
294+ def test_mach_matches_vbeam_single_transmit (
295+ mach_single_transmit_kwargs : dict ,
296+ vbeam_setup_uff_single_transmit : SignalForPointSetup ,
297+ transmit_idx : int ,
298+ output_dir ,
266299):
300+ """Test mach vs vbeam on a single plane wave transmit to isolate core beamforming differences."""
301+ grid_shape = vbeam_setup_uff_single_transmit .scan .shape
302+
303+ # Run mach single-transmit beamforming using kernel.beamform directly
304+ gpu_result = kernel .beamform (** mach_single_transmit_kwargs , tukey_alpha = 0.0 )
305+ result = cp .asnumpy (gpu_result )
306+ # Reshape to (x, z)
307+ result = result .reshape (grid_shape )
308+
309+ # Verify basic properties
310+ assert np .isfinite (result ).all ()
311+
312+ # Run vbeam single-transmit beamforming
313+ beamformer = get_das_beamformer (
314+ vbeam_setup_uff_single_transmit ,
315+ compensate_for_apodization_overlap = False ,
316+ log_compress = False ,
317+ scan_convert = False ,
318+ )
319+ vbeam_result_jax = beamformer (** vbeam_setup_uff_single_transmit .data ).block_until_ready ()
320+ vbeam_result = np .asarray (vbeam_result_jax )
321+
322+ # Save debug output if requested
323+ if output_dir is not None :
324+ output_dir = output_dir / "single_transmit_comparison" / f"transmit_{ transmit_idx } "
325+ save_debug_figures (
326+ our_result = np .abs (result ),
327+ reference_result = np .abs (vbeam_result ),
328+ grid_shape = grid_shape ,
329+ x_axis = vbeam_setup_uff_single_transmit .scan .x ,
330+ z_axis = vbeam_setup_uff_single_transmit .scan .z ,
331+ output_dir = output_dir ,
332+ test_name = f"single_transmit_{ transmit_idx } " ,
333+ our_label = "mach" ,
334+ reference_label = "vbeam" ,
335+ )
336+
337+ np .testing .assert_allclose (
338+ actual = result ,
339+ desired = vbeam_result ,
340+ atol = 0.01 ,
341+ rtol = 1 / 100 ,
342+ err_msg = f"mach single transmit { transmit_idx } results do not match vbeam within expected tolerances" ,
343+ )
344+
345+
346+ @pytest .mark .skipif (not HAS_CUPY , reason = "CuPy not available" )
347+ @pytest .mark .filterwarnings ("ignore:array is not contiguous, rearranging will add latency:UserWarning" )
348+ def test_mach_matches_vbeam (mach_beamform_kwargs , vbeam_setup_uff : SignalForPointSetup , output_dir ):
267349 """Validate mach against vbeam output on a PICMUS UFF data file."""
268350 grid_shape = vbeam_setup_uff .scan .shape
269351
270- gpu_result = experimental .beamform (** picmus_phantom_resolution_beamform_kwargs )
352+ # Match our custom vbeam apodization settings
353+ gpu_result = experimental .beamform (** mach_beamform_kwargs , tukey_alpha = 0.0 )
271354 result = cp .asnumpy (gpu_result )
272355 # Reshape to (x, z)
273356 result = result .reshape (grid_shape )
@@ -286,7 +369,7 @@ def test_mach_matches_vbeam(
286369 vbeam_result_jax = beamformer (** vbeam_setup_uff .data ).block_until_ready ()
287370 vbeam_result = np .asarray (vbeam_result_jax )
288371
289- # Compare magnitudes because we handle the phase slightly differently from vbeam
372+ # Also show magnitude comparison for reference
290373 vbeam_magnitude = np .abs (vbeam_result )
291374 cuda_magnitude = np .abs (result )
292375
@@ -306,14 +389,12 @@ def test_mach_matches_vbeam(
306389 )
307390 print ("Saved debug figures to" , output_dir )
308391
309- # Validate mach against vbeam
310- # TODO: may want to further fine-tune these tolerances
311392 np .testing .assert_allclose (
312- actual = cuda_magnitude ,
313- desired = vbeam_magnitude ,
314- atol = 10 ,
315- rtol = 0.3 ,
316- err_msg = "mach results do not match vbeam within expected tolerances" ,
393+ actual = result ,
394+ desired = vbeam_result ,
395+ atol = 0.01 ,
396+ rtol = 1 / 100 ,
397+ err_msg = "mach complex results do not match vbeam within expected tolerances (with scaling correction) " ,
317398 )
318399
319400
0 commit comments