some fixes
This commit is contained in:
@@ -135,6 +135,41 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
np.testing.assert_allclose(result.real, expected, atol=1e-3)
|
||||
np.testing.assert_allclose(result.imag, 0.0, atol=1e-3)
|
||||
|
||||
def test_recovers_sweep_offset_by_a_full_branch(self) -> None:
|
||||
"""A stray +/-2*pi branch jump on the unwrap anchor must be snapped back.
|
||||
|
||||
The same physical sweep, offset by one full turn (as happens when trigger
|
||||
jitter carries sample 0 across the +/-pi cut), must map to the SAME frequency
|
||||
axis as the unshifted sweep instead of sliding ~one branch off the band.
|
||||
"""
|
||||
processor = self._processor()
|
||||
phase = np.linspace(0.0, 100.0, 401) # freq [2.0, 4.0] GHz, spans the band
|
||||
baseline = processor.reference_frequency_axis(_reference(phase))
|
||||
for turns in (+1, -1, +2):
|
||||
shifted = processor.reference_frequency_axis(_reference(phase + turns * 2.0 * np.pi))
|
||||
np.testing.assert_allclose(shifted, baseline, atol=1e-3)
|
||||
|
||||
def test_branch_recovery_keeps_a_glitched_sweep_usable(self) -> None:
|
||||
"""A branch-jumped sweep is pulled back onto the band, not rejected."""
|
||||
processor = self._processor()
|
||||
phase = np.linspace(0.0, 100.0, 401) + 2.0 * np.pi # one full turn off
|
||||
ref = _reference(phase)
|
||||
result = processor.process(np.abs(ref).astype(np.complex128), ref)
|
||||
self.assertIsNotNone(result)
|
||||
np.testing.assert_allclose(np.abs(result), 1.0, atol=1e-3)
|
||||
|
||||
def test_leaves_genuine_sub_branch_float_untouched(self) -> None:
|
||||
"""A real <pi sweep-to-sweep float must NOT be mistaken for a branch jump."""
|
||||
processor = self._processor()
|
||||
phase = np.linspace(0.0, 100.0, 401)
|
||||
baseline = processor.reference_frequency_axis(_reference(phase))
|
||||
for float_rad in (0.5, -0.5, 2.0, -2.0):
|
||||
floated = processor.reference_frequency_axis(_reference(phase + float_rad))
|
||||
# The float shifts the axis by float_rad * hz_per_rad and is preserved,
|
||||
# i.e. it is not snapped away as if it were a 2*pi branch error.
|
||||
expected = baseline + float_rad * processor.params.hz_per_rad
|
||||
np.testing.assert_allclose(floated, expected, atol=1e-3)
|
||||
|
||||
def test_handles_descending_phase_direction(self) -> None:
|
||||
# phase 0 -> 2 GHz, phase -100 -> 4 GHz (negative slope). Phase ramp
|
||||
# 0 -> -100 therefore sweeps frequency UP across the band.
|
||||
|
||||
@@ -225,7 +225,15 @@ class CycleAlignmentToleranceCalibrationTest(unittest.TestCase):
|
||||
|
||||
|
||||
class LastCycleSParameterTest(unittest.TestCase):
|
||||
"""Reference/measurement division + the corrupt-reference and NaN guards."""
|
||||
"""Reference/measurement division, gap zero-filling, and the over-loss guard.
|
||||
|
||||
A handful of dropped USB frames (points that never arrived, left as ``NaN``)
|
||||
are emitted as zero so the cycle is kept instead of stalling on recovery, while
|
||||
delivered points keep their true normalized value. Losing more than
|
||||
``_MAX_ZERO_FILLED_POINTS_FRACTION`` of a trace is treated as a genuine fault
|
||||
and rejected as transient. The tiny-trace cases below cross that fraction with a
|
||||
single missing point, so they still raise.
|
||||
"""
|
||||
|
||||
def test_divides_measurement_by_reference(self) -> None:
|
||||
reference = {1: np.array([[2 + 0j, 0 + 2j, 4 + 0j]], dtype=complex)}
|
||||
@@ -233,6 +241,49 @@ class LastCycleSParameterTest(unittest.TestCase):
|
||||
result = calculate_last_cycle_s_parameters(measurement, reference)
|
||||
np.testing.assert_allclose(result["s31"], np.array([2 + 0j, 2 + 0j, 0.5 + 0j]))
|
||||
|
||||
def test_single_dropped_measurement_is_zero_filled(self) -> None:
|
||||
# One missing point in a realistically sized trace (< the loss fraction) is
|
||||
# emitted as zero; every delivered point keeps its real value, no extras.
|
||||
point_count = 200
|
||||
reference = {1: np.full((1, point_count), 2 + 0j, dtype=complex)}
|
||||
measurement = {"S31": np.full((1, point_count), 4 + 0j, dtype=complex)}
|
||||
measurement["S31"][0, 100] = np.nan + 1j * np.nan
|
||||
result = calculate_last_cycle_s_parameters(measurement, reference)
|
||||
self.assertEqual(result["s31"][100], 0)
|
||||
np.testing.assert_allclose(np.delete(result["s31"], 100), 2 + 0j)
|
||||
|
||||
def test_single_dropped_reference_is_zero_filled(self) -> None:
|
||||
# A missing master reference cannot normalize that point for any trace, so
|
||||
# the point is zeroed; the rest of the trace is untouched.
|
||||
point_count = 200
|
||||
reference = {1: np.full((1, point_count), 2 + 0j, dtype=complex)}
|
||||
reference[1][0, 50] = np.nan + 1j * np.nan
|
||||
measurement = {"S31": np.full((1, point_count), 4 + 0j, dtype=complex)}
|
||||
result = calculate_last_cycle_s_parameters(measurement, reference)
|
||||
self.assertEqual(result["s31"][50], 0)
|
||||
np.testing.assert_allclose(np.delete(result["s31"], 50), 2 + 0j)
|
||||
|
||||
def test_single_near_zero_reference_is_zero_filled(self) -> None:
|
||||
# A ~zero incident reference would explode the division into noise; one such
|
||||
# point is zeroed rather than aborting the whole cycle, and stays finite.
|
||||
point_count = 200
|
||||
reference = {1: np.full((1, point_count), 2 + 0j, dtype=complex)}
|
||||
reference[1][0, 75] = 0 + 0j
|
||||
measurement = {"S31": np.full((1, point_count), 4 + 0j, dtype=complex)}
|
||||
result = calculate_last_cycle_s_parameters(measurement, reference)
|
||||
self.assertEqual(result["s31"][75], 0)
|
||||
self.assertTrue(np.isfinite(result["s31"]).all())
|
||||
|
||||
def test_excessive_loss_is_transient(self) -> None:
|
||||
# Losing more than the tolerated fraction is a real fault, not a stray
|
||||
# dropped frame: reject as transient so the caller recovers properly.
|
||||
point_count = 200
|
||||
reference = {1: np.full((1, point_count), 2 + 0j, dtype=complex)}
|
||||
measurement = {"S31": np.full((1, point_count), 4 + 0j, dtype=complex)}
|
||||
measurement["S31"][0, :40] = np.nan + 1j * np.nan # 20% > 5% tolerance
|
||||
with self.assertRaises(TransientCollectionError):
|
||||
calculate_last_cycle_s_parameters(measurement, reference)
|
||||
|
||||
def test_nan_reference_is_transient(self) -> None:
|
||||
reference = {1: np.array([[2 + 0j, np.nan + 1j * np.nan, 4 + 0j]], dtype=complex)}
|
||||
measurement = {"S31": np.array([[4 + 0j, 4 + 0j, 2 + 0j]], dtype=complex)}
|
||||
|
||||
Reference in New Issue
Block a user