From baa398f23fd3d2fc4a4c3d9ac46f947fb0f19250 Mon Sep 17 00:00:00 2001 From: Sergio Aguirre Date: Sat, 31 Oct 2026 20:30:04 +0100 Subject: [PATCH] [FEATURE] add SignalObj.uncalibrate uncalibrate undoes calib_pressure and calib_voltage for one channel or for all of them: the samples are divided by the correction factor and the channel goes back to full scale (CF = 1, unit 'FS'). The check of the calibrator frequency took the argmax of the complex spectrum, which compares real parts and misses a sine, so it warned for a correct calibrator. In calib_volt the warning then failed, since it formatted an integer refFreq with a precision. The check now uses the magnitude of the spectrum. Co-Authored-By: Claude Fable 5.1 --- pytta/classes/_base.py | 8 +++--- pytta/classes/signal.py | 29 +++++++++++++++++++++ tests/test_uncalibrate.py | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 tests/test_uncalibrate.py diff --git a/pytta/classes/_base.py b/pytta/classes/_base.py index 13cb139..dbaf626 100644 --- a/pytta/classes/_base.py +++ b/pytta/classes/_base.py @@ -397,12 +397,12 @@ def __truediv__(self, other): def calib_volt(self, refSignalObj, refVrms, refFreq): Vrms = refSignalObj.rms()[0] freqFound = np.round(refSignalObj.freqVector[np.argmax( - refSignalObj.freqSignal)]) + np.abs(refSignalObj.freqSignal[:, 0]))]) if not np.isclose(freqFound, float(refFreq), rtol=1e-4): print('\x1b[0;30;43mATTENTION! Found calibration frequency (' + - '{:.2}'.format(freqFound) + + '{}'.format(freqFound) + ' [Hz]) differs from refFreq (' + - '{:.2}'.format(refFreq) + ' [Hz])\x1b[0m') + '{}'.format(refFreq) + ' [Hz])\x1b[0m') self.CF = refVrms/Vrms self.unit = 'V' return @@ -411,7 +411,7 @@ def calib_press(self, refSignalObj, refPrms, refFreq): # Prms = np.max(np.abs(refSignalObj.freqSignal[:, 0])) #/ (2**(1/2)) Prms = refSignalObj.rms()[0] freqFound = np.round(refSignalObj.freqVector[np.argmax( - refSignalObj.freqSignal)]) + np.abs(refSignalObj.freqSignal[:, 0]))]) if not np.isclose(freqFound, float(refFreq), rtol=1e-4): print('\x1b[0;30;43mATTENTION! Found calibration frequency (' + '{}'.format(freqFound) + diff --git a/pytta/classes/signal.py b/pytta/classes/signal.py index 79151c3..d59f223 100644 --- a/pytta/classes/signal.py +++ b/pytta/classes/signal.py @@ -881,6 +881,35 @@ def calib_pressure(self, chIndex, refSignalObj, raise IndexError('chIndex greater than channels number') return + def uncalibrate(self, chIndex=None): + """ + Undo calib_pressure or calib_voltage: divide the samples of the + channel by its correction factor and set it back to full scale + (CF = 1, unit 'FS'). Channels that are not calibrated are left as + they are. + + Parameters (default), (type): + ------------------------------ + * chIndex (None), (int): + channel index, starting in 0; None undoes every calibrated + channel. + """ + if chIndex is None: + indexes = range(self.numChannels) + elif chIndex in range(self.numChannels): + indexes = [chIndex] + else: + raise IndexError('chIndex greater than channels number') + for idx in indexes: + channel = self.channels[self.channels.mapping[idx]] + if channel.calibCheck: + self._timeSignal[:, idx] /= channel.CF + channel.CF = 1 + channel.calibCheck = False + channel.unit = 'FS' + self._fft() + return + def _to_dict(self): out = super()._to_dict() out['channels'] = self.channels._to_dict() diff --git a/tests/test_uncalibrate.py b/tests/test_uncalibrate.py new file mode 100644 index 0000000..6ba912d --- /dev/null +++ b/tests/test_uncalibrate.py @@ -0,0 +1,54 @@ +""" +Tests for SignalObj.uncalibrate, which undoes calib_pressure and +calib_voltage: the samples are divided by the correction factor and the +channel goes back to full scale. Each test pins one rule. +""" +import os +import unittest + +os.environ.setdefault('MPLBACKEND', 'Agg') + +import numpy as np + +import pytta + +FS = 48000 + + +def calibrator(rms): + tt = np.arange(FS) / FS + return pytta.SignalObj(rms * np.sqrt(2) * np.sin(2 * np.pi * 1000 * tt), + 'time', FS) + + +class TestUncalibrate(unittest.TestCase): + + def setUp(self): + rng = np.random.default_rng(0) + self.sig = pytta.SignalObj(rng.standard_normal((FS, 2)), 'time', FS) + self.raw = self.sig.timeSignal.copy() + self.sig.calib_pressure(0, calibrator(0.1), refPrms=1.0) + self.sig.calib_voltage(1, calibrator(0.2), refVrms=1.0) + + def test_undoes_the_calibration_of_every_channel(self): + self.sig.uncalibrate() + self.assertTrue(np.allclose(self.sig.timeSignal, self.raw, + rtol=1e-5, atol=1e-6)) + for ch in self.sig.channels.mapping: + channel = self.sig.channels[ch] + self.assertEqual((channel.CF, channel.calibCheck, channel.unit, + channel.dBRef), (1, False, 'FS', 1)) + + def test_undoes_one_channel(self): + calibrated = self.sig.timeSignal[:, 1].copy() + self.sig.uncalibrate(0) + self.assertTrue(np.allclose(self.sig.timeSignal[:, 0], self.raw[:, 0], + rtol=1e-5, atol=1e-6)) + self.assertTrue(np.array_equal(self.sig.timeSignal[:, 1], calibrated)) + self.assertEqual(self.sig.channels[2].unit, 'V') + with self.assertRaises(IndexError): + self.sig.uncalibrate(2) + + +if __name__ == '__main__': + unittest.main()