@@ -232,6 +232,88 @@ def test_fit_parameter_dataset_preserves_dc_axis_for_4d_sho_maps(self):
232232 self .assertEqual (result ["dimensions" ][3 ]["name" ], "fit_parameter" )
233233 self .assertEqual (result ["dimensions" ][3 ]["values" ], [0 , 1 , 2 , 3 , 4 ])
234234
235+ def test_align_loop_trace_honors_explicit_roll_steps (self ):
236+ dc_voltage = np .array ([1.0 , 2.0 , - 2.0 , - 1.0 , 0.0 ])
237+ loop_trace = np .array ([10.0 , 11.0 , 12.0 , 13.0 , 14.0 ])
238+
239+ aligned_dc , aligned_trace , applied_roll = mcp_mod ._align_loop_trace (
240+ dc_voltage ,
241+ loop_trace ,
242+ roll_steps = - 2 ,
243+ )
244+
245+ np .testing .assert_array_equal (aligned_dc , np .array ([- 2.0 , - 1.0 , 0.0 , 1.0 , 2.0 ]))
246+ np .testing .assert_array_equal (aligned_trace , np .array ([12.0 , 13.0 , 14.0 , 10.0 , 11.0 ]))
247+ self .assertEqual (applied_roll , - 2 )
248+
249+ def test_align_loop_trace_infers_minimum_dc_roll (self ):
250+ dc_voltage = np .array ([1.0 , 2.0 , - 2.0 , - 1.0 , 0.0 ])
251+ loop_trace = np .array ([10.0 , 11.0 , 12.0 , 13.0 , 14.0 ])
252+
253+ aligned_dc , aligned_trace , applied_roll = mcp_mod ._align_loop_trace (dc_voltage , loop_trace )
254+
255+ np .testing .assert_array_equal (aligned_dc , np .array ([- 2.0 , - 1.0 , 0.0 , 1.0 , 2.0 ]))
256+ np .testing .assert_array_equal (aligned_trace , np .array ([12.0 , 13.0 , 14.0 , 10.0 , 11.0 ]))
257+ self .assertEqual (applied_roll , - 2 )
258+
259+ def test_real_file_path_workflow_returns_sho_and_beps_datasets (self ):
260+ from pathlib import Path
261+ import shutil
262+ import os
263+ import tempfile
264+
265+ os .environ .setdefault ("NUMBA_DISABLE_JIT" , "1" )
266+ try :
267+ import SciFiReaders as sr
268+ except Exception as exc :
269+ self .skipTest (f"SciFiReaders is required for the workflow file-path regression test: { exc } " )
270+
271+ file_path = Path ("/Users/rvv/Downloads/PTO_5x5.h5" )
272+ if not file_path .exists ():
273+ self .skipTest (f"Real BEPS fixture not found: { file_path } " )
274+
275+ fd , tmp_name = tempfile .mkstemp (prefix = "pto_5x5_sidpy_" , suffix = ".h5" )
276+ os .close (fd )
277+ workflow_path = Path (tmp_name )
278+ shutil .copy2 (file_path , workflow_path )
279+
280+ reader = sr .NSIDReader (str (workflow_path ))
281+ data = reader .read ()["Channel_000" ]
282+ array = np .asarray (data )
283+
284+ result = mcp_mod .fit_beps_dataset_from_scifireaders_file (
285+ str (workflow_path ),
286+ channel_name = "Channel_000" ,
287+ dataset_name = "PTO_5x5.h5" ,
288+ sho_cycle_index = 1 ,
289+ use_kmeans = False ,
290+ n_clusters = 4 ,
291+ return_cov = False ,
292+ loss = "linear" ,
293+ sho_dataset_name = "pto_5x5_sho_workflow" ,
294+ beps_dataset_name = "pto_5x5_beps_workflow" ,
295+ )
296+
297+ self .assertIn ("sho_dataset_id" , result )
298+ self .assertIn ("beps_dataset_id" , result )
299+ self .assertEqual (result ["sho_dataset" ]["shape" ], [5 , 5 , array .shape [3 ], 4 ])
300+ self .assertEqual (result ["beps_dataset" ]["shape" ], [5 , 5 , 9 ])
301+ self .assertEqual (
302+ result ["loop_input" ]["source_slice" ],
303+ {
304+ "cycle_index" : 1 ,
305+ "signal" : "projected_piezoresponse" ,
306+ "projection_tool" : "BGlib.projectLoop" ,
307+ },
308+ )
309+ self .assertEqual (result ["sho_dataset" ]["metadata" ]["fit_kind" ], "sho" )
310+ self .assertEqual (result ["beps_dataset" ]["metadata" ]["fit_kind" ], "beps_loop" )
311+ self .assertGreater (result ["sho_dataset" ]["metadata" ]["fit_quality" ]["overall_r2" ], 0.8 )
312+ self .assertGreater (result ["sho_dataset" ]["metadata" ]["fit_quality" ]["amplitude_overall_r2" ], 0.9 )
313+ self .assertGreater (result ["beps_dataset" ]["metadata" ]["fit_quality" ]["overall_r2" ], 0.75 )
314+ self .assertIn ("increasing_branch_r2" , result ["beps_dataset" ]["metadata" ]["fit_quality" ])
315+ self .assertIn ("decreasing_branch_r2" , result ["beps_dataset" ]["metadata" ]["fit_quality" ])
316+
235317 def test_real_file_scifireaders_payload_can_drive_the_executable_workflow (self ):
236318 from pathlib import Path
237319 import shutil
@@ -348,10 +430,13 @@ def test_real_file_scifireaders_payload_can_drive_the_executable_workflow(self):
348430 loop_input = mcp_mod .derive_loop_input_from_sho_result (sho_dataset ["dataset_id" ])
349431 self .assertIn ("beps_data" , loop_input )
350432 self .assertIn ("dc_voltage" , loop_input )
433+ expected_roll_steps = - int (np .argmin (beps_axis ))
434+ expected_dc_voltage = np .roll (beps_axis , expected_roll_steps )
351435 self .assertEqual (
352436 loop_input ["source_slice" ],
353437 {
354438 "cycle_index" : sho_cycle_idx ,
439+ "loop_roll_steps" : expected_roll_steps ,
355440 "signal" : "projected_piezoresponse" ,
356441 "projection_tool" : "BGlib.projectLoop" ,
357442 },
@@ -367,7 +452,10 @@ def test_real_file_scifireaders_payload_can_drive_the_executable_workflow(self):
367452 for col in range (sho_params .shape [1 ])
368453 ]
369454 ).reshape (beps_data .shape )
455+ expected_projected = np .roll (expected_projected , expected_roll_steps , axis = - 1 )
456+ expected_projected = expected_projected * 1e3
370457 np .testing .assert_allclose (np .asarray (loop_input ["beps_data" ]), expected_projected )
458+ np .testing .assert_allclose (np .asarray (loop_input ["dc_voltage" ]), expected_dc_voltage )
371459
372460 beps_result = mcp_mod .fit_beps_loops (
373461 loop_input ["beps_data" ],
@@ -388,6 +476,25 @@ def test_real_file_scifireaders_payload_can_drive_the_executable_workflow(self):
388476 ]).reshape (np .asarray (loop_input ["beps_data" ]).shape )
389477 beps_r2 = r2_score (np .asarray (loop_input ["beps_data" ]).reshape (- 1 ), beps_pred .reshape (- 1 ))
390478 self .assertGreater (beps_r2 , 0.75 )
479+ branch_quality = mcp_mod ._branch_r2_scores (
480+ np .asarray (loop_input ["dc_voltage" ]),
481+ np .asarray (loop_input ["beps_data" ]),
482+ beps_pred ,
483+ )
484+ self .assertIn ("increasing_branch_r2" , branch_quality )
485+ self .assertIn ("decreasing_branch_r2" , branch_quality )
486+
487+ def test_fit_beps_loops_rejects_multi_cycle_dc_voltage (self ):
488+ dc_voltage = np .array ([
489+ - 1.0 , - 0.5 , 0.0 , 0.5 , 1.0 ,
490+ 0.5 , 0.0 , - 0.5 , - 1.0 ,
491+ - 0.5 , 0.0 , 0.5 , 1.0 ,
492+ 0.5 , 0.0 , - 0.5 , - 1.0 ,
493+ ])
494+ data = np .zeros ((1 , 1 , dc_voltage .size ))
495+
496+ with self .assertRaises (ValueError ):
497+ mcp_mod .fit_beps_loops (data , dc_voltage )
391498
392499 def test_real_file_fits_round_trip_to_sidpy_datasets (self ):
393500 from pathlib import Path
@@ -942,6 +1049,7 @@ def test_stdio_mcp_client_can_run_full_beps_dataset_workflow(self):
9421049 payload ["loop_input" ]["source_slice" ],
9431050 {
9441051 "cycle_index" : 1 ,
1052+ "loop_roll_steps" : expected_roll_steps ,
9451053 "signal" : "projected_piezoresponse" ,
9461054 "projection_tool" : "BGlib.projectLoop" ,
9471055 },
@@ -961,6 +1069,9 @@ def test_stdio_mcp_client_can_run_full_beps_dataset_workflow(self):
9611069 for col in range (sho_params .shape [1 ])
9621070 ]
9631071 ).reshape (beps_data .shape )
1072+ expected_roll_steps = - int (np .argmin (beps_axis ))
1073+ expected_projected = np .roll (expected_projected , expected_roll_steps , axis = - 1 )
1074+ expected_projected = expected_projected * 1e3
9641075 np .testing .assert_allclose (np .asarray (payload ["loop_input" ]["beps_data" ]), expected_projected )
9651076
9661077
0 commit comments