create base class MeasurementData for ADC_Result and Accumulation

This commit is contained in:
markusro committed 2026-10-20 23:21:43 +02:00
1 parent 5a18e48c2d
commit bd060dc4d0
6 files changed
+184 -47

No files matched your search

+30
View File
@@ -759,6 +759,36 @@ Example:
- **get_ymax()**
Returns maximum of y
- **get_ymean(start=None, stop=None)**
Returns the mean value of every channel. If *start* and *stop* are given,
they select a time interval in seconds. The calculation uses raw values
for **timesignal** and the public mean values for **accu**.
Example:
.. code-block:: python
means = timesignal.get_ymean(start=1e-6, stop=4e-6)
means = accu.get_ymean()
- **get_phase(pos=None)**
Returns the phase in degrees from the first two channels at the first
sample or at time *pos* in seconds.
Example:
.. code-block:: python
phase = timesignal.get_phase(pos=1e-6)
- **get_ymin_channels()/get_ymax_channels()**
Return NumPy arrays containing the minimum or maximum for each channel.
**get_ymin()** and **get_ymax()** return the corresponding global extrema.
- **get_yrms()/get_ystd()**
Return NumPy arrays containing the RMS or population standard deviation
for each channel.
.. bibliography::
+2 -1
View File
@@ -6,6 +6,7 @@ from .Resultable import Resultable
from .Drawable import Drawable
from .Signalpath import Signalpath
from .DamarisFFT import DamarisFFT
from .MeasurementData import MeasurementData
import threading
import numpy
import sys
@@ -27,7 +28,7 @@ _Description = dict[str, str]
# #
#############################################################################
class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
class ADC_Result(MeasurementData, Resultable, Drawable, DamarisFFT, Signalpath):
default_bit_depth = 14
"""
Represents the result of an ADC, encapsulating data and metadata
+2 -46
View File
@@ -16,6 +16,7 @@ from .Drawable import Drawable
from .DamarisFFT import DamarisFFT
from .Signalpath import Signalpath
from .ADC_Result import ADC_Result
from .MeasurementData import MeasurementData
import sys
import threading
@@ -33,7 +34,7 @@ _IndexList = list[tuple[int, int]]
_Description = dict[str, str]
class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
class Accumulation(MeasurementData, Errorable, Drawable, DamarisFFT, Signalpath):
"""
Represents an accumulation of sampled data with various attributes and methods
for data manipulation, analysis, and exporting.
@@ -220,44 +221,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.lock.release()
return tmp_y
def get_ymean(self, start: float | None = None, stop: float | None = None) -> numpy.ndarray:
"""
return mean data from start to stop (in time units). Defaults to mean of whole channel data.
Example:
accu = Accumulation(...)
means = accu.get_ymean(start=1e-6, stop=4e-6)
:param start: start of the data
:param stop: stop of the data
:return: means for each channel
"""
if start == None and stop == None:
means = [ch.mean() for ch in self.y]
else:
istart = int(self.get_sampling_rate()*start)
istop = int(self.get_sampling_rate()*stop)
means = [ch[istart:istop].mean() for ch in self.y]
return numpy.array(means)
def get_phase(self, pos: float | None = None) -> float:
"""
Return phase angle in degrees at a given time position.
The phase is calculated from the first two channels using arctan2(y1, y0)
and converted to degrees. If pos is None, the phase at the first sample is returned.
Example:
accu = Accumulation(...)
phase = accu.get_phase(pos=1e-6)
:param pos: time position in seconds
:return: phase angle in degrees
"""
if pos is None:
ipos = 0
else:
ipos = int(self.get_sampling_rate()*pos)
return numpy.arctan2(self.y[1][ipos], self.y[0][ipos])*180/numpy.pi
def get_ymin(self) -> float|int:
@@ -671,13 +634,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# External interfaces ------------------------------------------------------------------
# Overloaded operators -----------------------------------------------------------------
def __len__(self) -> int:
"""
return number of samples per channel, 0 if empty
"""
if len(self.y)>0:
return len(self.y[0])
return 0
def __repr__(self) -> str:
"""Redefining repr(Accumulation)"""
+127
View File
@@ -0,0 +1,127 @@
# -*- coding: iso-8859-1 -*-
from __future__ import annotations
import numpy
class MeasurementData:
"""Common channel-series access and numerical analysis for measurements.
Concrete result classes provide storage and ``get_ydata``; all calculations
in this class therefore use the public channel values rather than their
internal representation.
"""
def contains_data(self) -> bool:
"""Return whether the measurement contains samples."""
raise NotImplementedError
def get_ydata(self, channel: int = 0) -> numpy.ndarray:
"""Return public values for one channel."""
return self.y[channel]
def get_number_of_channels(self) -> int:
"""Return the number of data channels."""
return len(self.y)
def get_sampling_rate(self) -> float:
"""Return the sampling frequency in hertz."""
return self.sampling_rate
def __len__(self) -> int:
"""Return the number of samples per channel.
Example:
samples = len(timesignal)
"""
if not self.get_number_of_channels():
return 0
return len(self.get_ydata(0))
def get_ymean(self, start: float | None = None,
stop: float | None = None) -> numpy.ndarray:
"""Return the mean of every channel over a time interval.
Example:
means = timesignal.get_ymean(start=1e-6, stop=4e-6)
"""
if not self.contains_data():
return numpy.array([])
if (start is None) != (stop is None):
raise ValueError("start and stop must be specified together")
if start is None:
values = [self.get_ydata(channel).mean()
for channel in range(self.get_number_of_channels())]
else:
istart = int(self.get_sampling_rate() * start)
istop = int(self.get_sampling_rate() * stop)
values = [self.get_ydata(channel)[istart:istop].mean()
for channel in range(self.get_number_of_channels())]
return numpy.asarray(values)
def get_phase(self, pos: float | None = None) -> float:
"""Return the phase in degrees from the first two channels.
Example:
phase = timesignal.get_phase(pos=1e-6)
"""
if self.get_number_of_channels() < 2:
raise ValueError("phase requires at least two channels")
ipos = 0 if pos is None else int(self.get_sampling_rate() * pos)
return float(numpy.degrees(numpy.arctan2(
self.get_ydata(1)[ipos], self.get_ydata(0)[ipos])))
def get_ymin_channels(self) -> numpy.ndarray:
"""Return the minimum value of each channel.
Example:
minima = accu.get_ymin_channels()
"""
if not self.contains_data():
return numpy.array([])
return numpy.asarray([self.get_ydata(i).min()
for i in range(self.get_number_of_channels())])
def get_ymax_channels(self) -> numpy.ndarray:
"""Return the maximum value of each channel.
Example:
maxima = accu.get_ymax_channels()
"""
if not self.contains_data():
return numpy.array([])
return numpy.asarray([self.get_ydata(i).max()
for i in range(self.get_number_of_channels())])
def get_ymin(self) -> float | int:
"""Return the global minimum across all channels."""
values = self.get_ymin_channels()
return 0 if len(values) == 0 else values.min()
def get_ymax(self) -> float | int:
"""Return the global maximum across all channels."""
values = self.get_ymax_channels()
return 0 if len(values) == 0 else values.max()
def get_yrms(self) -> numpy.ndarray:
"""Return the root-mean-square value of each channel.
Example:
rms = timesignal.get_yrms()
"""
if not self.contains_data():
return numpy.array([])
return numpy.asarray([numpy.sqrt(numpy.mean(self.get_ydata(i) ** 2))
for i in range(self.get_number_of_channels())])
def get_ystd(self) -> numpy.ndarray:
"""Return the population standard deviation of each channel.
Example:
deviation = accu.get_ystd()
"""
if not self.contains_data():
return numpy.array([])
return numpy.asarray([numpy.std(self.get_ydata(i))
for i in range(self.get_number_of_channels())])
+15
View File
@@ -30,6 +30,21 @@ class TestADCResult(unittest.TestCase):
self.assertEqual(len(adc.x), 0)
self.assertEqual(len(adc.y), 0)
def test_shared_measurement_statistics(self):
adc = self.create_adc_result()
np.testing.assert_allclose(adc.get_ymean(), [20, 25])
np.testing.assert_allclose(adc.get_ymin_channels(), [10, 15])
np.testing.assert_allclose(adc.get_ymax_channels(), [30, 35])
np.testing.assert_allclose(adc.get_yrms(),
[np.sqrt(np.mean(np.array([10, 20, 30]) ** 2)),
np.sqrt(np.mean(np.array([15, 25, 35]) ** 2))])
np.testing.assert_allclose(adc.get_ystd(), [np.std([10, 20, 30]),
np.std([15, 25, 35])])
def test_shared_phase_uses_public_channel_values(self):
adc = self.create_adc_result()
self.assertAlmostEqual(adc.get_phase(), np.degrees(np.arctan2(15, 10)))
def test_constructor_with_arguments(self):
"""Test initializer with all arguments."""
x = np.array([0.0, 1.0, 2.0])
+8
View File
@@ -33,6 +33,14 @@ class TestAccumulation(unittest.TestCase):
self.assertEqual(len(accu.x), 0)
self.assertEqual(len(accu.y), 0)
def test_shared_measurement_statistics_use_accumulation_means(self):
accu = Accumulation()
accu += self.create_adc_result()
np.testing.assert_allclose(accu.get_ymean(), [20, 25])
np.testing.assert_allclose(accu.get_ymin_channels(), [10, 15])
np.testing.assert_allclose(accu.get_ymax_channels(), [30, 35])
self.assertAlmostEqual(accu.get_phase(), np.degrees(np.arctan2(15, 10)), places=6)
def test_constructor_with_arguments(self):
"""Test initializer with all arguments."""
x = np.array([0.0, 1.0, 2.0])