fix Signalpath.phase(): do not modify original data in place
Build Debian Packages / build (bookworm, debian12) (push) Successful in 10m7s
Build Debian Packages / build (trixie, debian13) (push) Successful in 10m19s

phase() overwrote the underlying channel arrays (y[i] *= -1) and returned
self, so rotated copies aliased the original object and negative phase
angles garbled shared data. It now returns a copy with new arrays and
uses numpy.deg2rad for arbitrary angles.
Add unittests for phase() including negative angles and non-mutation.
This commit is contained in:
markusro committed 2026-10-16 11:21:10 +02:00
1 parent e963c2ef3c
commit 8d7d241ec7
2 files changed
+114 -13

No files matched your search

+17 -13
View File
@@ -1,3 +1,5 @@
import copy
import numpy as N
class Signalpath:
@@ -5,33 +7,35 @@ class Signalpath:
"""
rotate signal by **degrees** for phase cycling, etc.
The signalpath object itself is not modified, a copy containing the
rotated data is returned.
:param degrees: rotate signal by this value
:return:
:return: copy of the signalpath object with rotated data
"""
if self.get_number_of_channels() != 2:
raise Exception("rotation defined only for 2 channels")
rotated = copy.copy(self)
# simple case 0, 90, 180, 270 degree
reduced_angle = divmod(degrees, 90)
if abs(reduced_angle[1]) < 1e-6:
reduced_angle = reduced_angle[0] % 4
if reduced_angle == 0:
return self
rotated.y = list(self.y)
elif reduced_angle == 1:
self.y[1] *= -1
self.y = [self.y[1], self.y[0]]
rotated.y = [-self.y[1], self.y[0]]
elif reduced_angle == 2:
self.y[0] *= -1
self.y[1] *= -1
rotated.y = [-self.y[0], -self.y[1]]
elif reduced_angle == 3:
self.y[0] *= -1
self.y = [self.y[1], self.y[0]]
rotated.y = [self.y[1], -self.y[0]]
else:
sin_angle = N.sin(degrees / 180.0 * N.pi)
cos_angle = N.cos(degrees / 180.0 * N.pi)
self.y = [cos_angle * self.y[0] - sin_angle * self.y[1],
sin_angle * self.y[0] + cos_angle * self.y[1]]
rad_angle = N.deg2rad(degrees)
sin_angle = N.sin(rad_angle)
cos_angle = N.cos(rad_angle)
rotated.y = [cos_angle * self.y[0] - sin_angle * self.y[1],
sin_angle * self.y[0] + cos_angle * self.y[1]]
return self
return rotated
+97
View File
@@ -0,0 +1,97 @@
import unittest
from datetime import datetime
import numpy as np
from damaris.data.ADC_Result import ADC_Result
def rotate_reference(y, degrees):
"reference rotation of the two channels by degrees"
rad = np.deg2rad(degrees)
cos, sin = np.cos(rad), np.sin(rad)
return [cos * y[0] - sin * y[1], sin * y[0] + cos * y[1]]
class TestSignalpath(unittest.TestCase):
@staticmethod
def create_adc_result():
x = np.arange(4, dtype="float32")
y = [np.array([10, 20, 30, 40], dtype="int16"),
np.array([15, 25, 35, 45], dtype="int16")]
index = [(0, 3)]
job_date = datetime(2023, 1, 1)
desc = {"key": "value"}
sampl_freq = 1000.0
job_id = 11
adc = ADC_Result(x, y, index, sampl_freq, desc, job_id, job_date)
adc.set_nChannels(2)
return adc
def assertRotated(self, adc, degrees):
rotated = adc.phase(degrees)
expected = rotate_reference(adc.y, degrees)
np.testing.assert_allclose(rotated.y[0], expected[0])
np.testing.assert_allclose(rotated.y[1], expected[1])
return rotated
def test_phase_positive_quarter_turns(self):
"""Test phase() with 0, 90, 180 and 270 degrees."""
adc = self.create_adc_result()
for degrees in (0, 90, 180, 270):
self.assertRotated(adc, degrees)
def test_phase_negative_quarter_turns(self):
"""Test phase() with negative multiples of 90 degrees."""
adc = self.create_adc_result()
for degrees in (-90, -180, -270, -360):
self.assertRotated(adc, degrees)
def test_phase_negative_arbitrary_angles(self):
"""Test phase() with arbitrary (not multiple of 90) negative angles."""
adc = self.create_adc_result()
for degrees in (-45, -120, -315):
self.assertRotated(adc, degrees)
def test_phase_equals_equivalent_positive_angle(self):
"""Test that negative angles give the same result as the equivalent positive angle."""
adc = self.create_adc_result()
for degrees in (-90, -45, -120):
np.testing.assert_allclose(adc.phase(degrees).y,
adc.phase(degrees + 360).y)
def test_phase_does_not_modify_original(self):
"""Test that phase() leaves the original data (and its arrays) untouched."""
adc = self.create_adc_result()
original = [adc.y[0].copy(), adc.y[1].copy()]
for degrees in (0, 90, 180, 270, -90, -45, 45):
rotated = adc.phase(degrees)
self.assertIsNot(rotated, adc)
self.assertIsNot(rotated.y, adc.y)
for ch in (0, 1):
self.assertIsNot(rotated.y[ch], original[ch])
np.testing.assert_array_equal(adc.y[ch], original[ch])
def test_phase_result_independent(self):
"""Test that modifying the phase-rotated copy does not affect the original."""
adc = self.create_adc_result()
rotated = adc.phase(-90)
rotated.y[0][:] = 9999
np.testing.assert_array_equal(adc.y[0], [10, 20, 30, 40])
def test_phase_chaining(self):
"""Test that chained phase() calls add up correctly."""
adc = self.create_adc_result()
np.testing.assert_allclose(adc.phase(-90).phase(-90).y,
adc.phase(-180).y)
np.testing.assert_allclose(adc.phase(-90).phase(90).y,
adc.phase(0).y)
def test_phase_wrong_channel_number(self):
"""Test the Exception raised for signalpaths with other than 2 channels."""
adc = self.create_adc_result()
adc.y.pop()
self.assertRaises(Exception, adc.phase, -90)
if __name__ == "__main__":
unittest.main()