From b03fedcea5743ff3bce3bc0d39b5178c61c57585 Mon Sep 17 00:00:00 2001 From: nocturnalastro Date: Thu, 5 Oct 2023 17:53:50 +0100 Subject: [PATCH] Extend plotter --- src/vse_sync_pp/analyzers/analyzer.py | 48 +++++++----- src/vse_sync_pp/plot.py | 86 +++++++++++++++------ src/vse_sync_pp/requirements.py | 14 ++-- tests/vse_sync_pp/analyzers/test_gnss.py | 16 ---- tests/vse_sync_pp/analyzers/test_phc2sys.py | 16 ---- tests/vse_sync_pp/analyzers/test_ppsdpll.py | 18 ----- tests/vse_sync_pp/analyzers/test_ts2phc.py | 16 ---- tests/vse_sync_pp/test_requirements.py | 21 +++-- 8 files changed, 107 insertions(+), 128 deletions(-) diff --git a/src/vse_sync_pp/analyzers/analyzer.py b/src/vse_sync_pp/analyzers/analyzer.py index 041dcc4..32886e5 100644 --- a/src/vse_sync_pp/analyzers/analyzer.py +++ b/src/vse_sync_pp/analyzers/analyzer.py @@ -379,21 +379,29 @@ def _test_common(self, data): return (False, "short test duration") if len(data) - 1 < self._duration_min: return (False, "short test samples") - if self._rate is None: - self._rate = self.calculate_rate(data) - if self._lpf_signal is None: - self._lpf_signal = calculate_filter(data, self._transient, self._rate) return None def _explain_common(self, data): if len(data) == 0: return {} if self._rate is None: - self._rate = self.calculate_rate(data) + self._rate = self.calculate_rate(self._data) if self._lpf_signal is None: self._lpf_signal = calculate_filter(data, self._transient, self._rate) return None + def toplot(self): + self.close() + self._generate_taus() + yield from zip(self._taus, self._samples) + + def _generate_taus(self): + if self._rate is None: + self._rate = self.calculate_rate(self._data) + if self._lpf_signal is None: + self._lpf_signal = calculate_filter(self._data, self._transient, self._rate) + return None + class TimeDeviationAnalyzerBase(TimeIntervalErrorAnalyzerBase): """Analyze Time Deviation (TDEV). @@ -413,11 +421,15 @@ def __init__(self, config): # TDEV samples self._samples = None + def _generate_taus(self): + super()._generate_taus() + if self._samples is None: + self._taus, self._samples, errors, ns = allantools.tdev(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + def test(self, data): result = self._test_common(data) if result is None: - if self._samples is None: - self._taus, self._samples, errors, ns = allantools.tdev(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + self._generate_taus() if out_of_range(self._taus, self._samples, self._accuracy, self._limit): return (False, "unacceptable time deviation") return (True, None) @@ -426,14 +438,11 @@ def test(self, data): def explain(self, data): analysis = self._explain_common(data) if analysis is None: - if self._samples is None: - self._taus, self._samples, errors, ns = allantools.tdev(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + self._generate_taus() return { 'timestamp': self._timestamp_from_dec(data.iloc[0].timestamp), 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, 'tdev': self._statistics(self._samples, 'ns'), - 'tdev_taus': self._taus.tolist(), - 'tdev_samples': self._samples.tolist(), } return analysis @@ -446,8 +455,8 @@ class MaxTimeIntervalErrorAnalyzerBase(TimeIntervalErrorAnalyzerBase): """ def __init__(self, config): super().__init__(config) - # required system maximum time interval error output in us - self._accuracy = config.requirement('maximum-time-interval-error-in-locked-mode/us') + # required system maximum time interval error output in ns + self._accuracy = config.requirement('maximum-time-interval-error-in-locked-mode/ns') # limit of inaccuracy at observation point self._limit = config.parameter('maximum-time-interval-error-limit/%') # list of observation windows intervals to calculate MTIE @@ -456,11 +465,15 @@ def __init__(self, config): # MTIE samples self._samples = None + def _generate_taus(self): + super()._generate_taus() + if self._samples is None: + self._taus, self._samples, errors, ns = allantools.mtie(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + def test(self, data): result = self._test_common(data) if result is None: - if self._samples is None: - self._taus, self._samples, errors, ns = allantools.mtie(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + self._generate_taus() if out_of_range(self._taus, self._samples, self._accuracy, self._limit): return (False, "unacceptable mtie") return (True, None) @@ -469,13 +482,10 @@ def test(self, data): def explain(self, data): analysis = self._explain_common(data) if analysis is None: - if self._samples is None: - self._taus, self._samples, errors, ns = allantools.mtie(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + self._generate_taus() return { 'timestamp': self._timestamp_from_dec(data.iloc[0].timestamp), 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, 'mtie': self._statistics(self._samples, 'ns'), - 'mtie_taus': self._taus.tolist(), - 'mtie_samples': self._samples.tolist(), } return analysis diff --git a/src/vse_sync_pp/plot.py b/src/vse_sync_pp/plot.py index 28daaf9..ccd5560 100644 --- a/src/vse_sync_pp/plot.py +++ b/src/vse_sync_pp/plot.py @@ -6,49 +6,85 @@ import numpy as np import matplotlib.pyplot as plt +from collections import namedtuple from .common import open_input from .parsers import PARSERS +Axis = namedtuple("Axis", ["desc", "attr", "scale", "scale_kwargs"], defaults=[None, None, None, None]) +TIMESERIES = Axis("Time", "timestamp") + + class Plotter(): """Rudimentary plotter of data values against timestamp""" - def __init__(self, y_name, y_desc=None): - self._x_name = 'timestamp' - self._y_name = y_name - if y_desc is None: - self._y_desc = y_name - else: - self._y_desc = y_desc + def __init__(self, x, y): + self._x = x + self._y = y self._x_data = [] self._y_data = [] + @staticmethod + def _extract_attr(axis, data): + return getattr(data, axis.attr) + + def _set_yscale(self, ax): + if self._y.scale is not None: + ax.set_yscale(self._y.scale, **(self._y.scale_kwargs or {})) + elif any((abs(v) > 10 for v in self._y_data)): + ax.set_yscale("symlog", linthresh=10) + def append(self, data): """Append x and y data points extracted from `data`""" - x_val = getattr(data, self._x_name) - self._x_data.append(x_val) - y_val = getattr(data, self._y_name) - self._y_data.append(y_val) + self._x_data.append(self._extract_attr(self._x, data)) + self._y_data.append(self._extract_attr(self._y, data)) + + def _plot_scatter(self, ax): + ax.axhline(0, color='black') + self._set_yscale(ax) + if self._x.scale is not None: + ax.set_xscale(self._x.scale, **(self._x.scale_kwargs or {})) + ax.plot(self._x_data, self._y_data, '.') + ax.grid() + ax.set_title(f'{self._x.desc} vs {self._y.desc}') + + def _plot_hist(self, ax): + counts, bins = np.histogram( + np.array(self._y_data, dtype=float), + bins='fd' + ) + ax.hist(bins[:-1], bins, weights=counts) + self._set_yscale(ax) + if self._x.scale is not None: + ax.set_xscale(self._x.scale, **(self._x.scale_kwargs or {})) + ax.set_title(f'Histogram of {self._y.desc}') def plot(self, filename): """Plot data to `filename`""" fig, (ax1, ax2) = plt.subplots(2, constrained_layout=True) fig.set_size_inches(10, 8) - ax1.axhline(0, color='black') - if any((abs(v) > 10 for v in self._y_data)): - ax1.set_yscale('symlog', linthresh=10) - ax1.plot(self._x_data, self._y_data, '.') - ax1.grid() - ax1.set_title(f'{self._x_name} vs {self._y_desc}') - counts, bins = np.histogram( - np.array(self._y_data, dtype=float), - bins='scott', - ) - ax2.hist(bins[:-1], bins, weights=counts) - ax2.set_yscale('symlog', linthresh=10) - ax2.set_title(f'Histogram of {self._y_desc}') + self._plot_scatter(ax1) + self._plot_hist(ax2) + ax3 = ax2.twinx() + ax3.set_ylabel('CDF') + ax3.ecdf(np.array(self._y_data, dtype=float), color="black", linewidth=2) + plt.savefig(filename) + return fig, (ax1, ax2, ax3) + + def plot_scatter(self, filename): + fig, ax = plt.subplots(1, constrained_layout=True) + fig.set_size_inches(10, 4) + self._plot_scatter(ax) + plt.savefig(filename) + return fig, ax + + def plot_histogram(self, filename): + fig, ax = plt.subplots(1, constrained_layout=True) + fig.set_size_inches(10, 4) + self._plot_hist(ax) plt.savefig(filename) + return fig, ax def main(): @@ -75,7 +111,7 @@ def main(): ) args = aparser.parse_args() parser = PARSERS[args.parser]() - plotter = Plotter(parser.y_name) + plotter = Plotter(TIMESERIES, Axis(parser.y_name, parser.y_name)) with open_input(args.input) as fid: method = parser.canonical if args.canonical else parser.parse for parsed in method(fid): diff --git a/src/vse_sync_pp/requirements.py b/src/vse_sync_pp/requirements.py index 8f8d653..7a338a2 100644 --- a/src/vse_sync_pp/requirements.py +++ b/src/vse_sync_pp/requirements.py @@ -4,9 +4,9 @@ REQUIREMENTS = { 'G.8272/PRTC-A': { - 'maximum-time-interval-error-in-locked-mode/us': { - (None, 273): lambda t: 0.000275 * t + 0.025, - (274, None): lambda t: 0.10 + 'maximum-time-interval-error-in-locked-mode/ns': { + (None, 273): lambda t: 0.275 * t + 25, + (274, 10000): lambda t: 100 }, 'time-deviation-in-locked-mode/ns': { (None, 100): lambda t: 3, @@ -16,9 +16,9 @@ 'time-error-in-locked-mode/ns': 100, }, 'G.8272/PRTC-B': { - 'maximum-time-interval-error-in-locked-mode/us': { - (None, 54.5): lambda t: 0.000275 * t + 0.025, - (54.5, None): lambda t: 0.04 + 'maximum-time-interval-error-in-locked-mode/ns': { + (None, 54.5): lambda t: 0.275 * t + 25, + (54.5, 10000): lambda t: 40 }, 'time-deviation-in-locked-mode/ns': { (None, 100): lambda t: 1, @@ -32,7 +32,7 @@ 'time-deviation-in-locked-mode/ns': { (None, 100000): lambda t: 100 }, - 'maximum-time-interval-error-in-locked-mode/us': { + 'maximum-time-interval-error-in-locked-mode/ns': { (None, 100000): lambda t: 1 } }, diff --git a/tests/vse_sync_pp/analyzers/test_gnss.py b/tests/vse_sync_pp/analyzers/test_gnss.py index 0751162..5979463 100644 --- a/tests/vse_sync_pp/analyzers/test_gnss.py +++ b/tests/vse_sync_pp/analyzers/test_gnss.py @@ -318,8 +318,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -359,8 +357,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -400,8 +396,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -441,8 +435,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, ) @@ -534,8 +526,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -583,8 +573,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -632,8 +620,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -681,8 +667,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, ) diff --git a/tests/vse_sync_pp/analyzers/test_phc2sys.py b/tests/vse_sync_pp/analyzers/test_phc2sys.py index 56294ba..c0571f9 100644 --- a/tests/vse_sync_pp/analyzers/test_phc2sys.py +++ b/tests/vse_sync_pp/analyzers/test_phc2sys.py @@ -297,8 +297,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -338,8 +336,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -379,8 +375,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -420,8 +414,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, ) @@ -513,8 +505,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -562,8 +552,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -611,8 +599,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -660,8 +646,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, ) diff --git a/tests/vse_sync_pp/analyzers/test_ppsdpll.py b/tests/vse_sync_pp/analyzers/test_ppsdpll.py index 662a4de..59c3d80 100644 --- a/tests/vse_sync_pp/analyzers/test_ppsdpll.py +++ b/tests/vse_sync_pp/analyzers/test_ppsdpll.py @@ -321,8 +321,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0., 0., 0.], - 'mtie_taus': [1., 2., 3.], }, }, { @@ -372,8 +370,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0., 0., 0.], - 'mtie_taus': [1., 2., 3.], }, }, { @@ -423,8 +419,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0., 0., 0., 0.], - 'mtie_taus': [1., 2., 3., 4.], }, }, { @@ -463,8 +457,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -504,8 +496,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, ) @@ -596,8 +586,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [3.2684080872601985e-17], - 'tdev_taus': [1.], }, }, { @@ -647,8 +635,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [3.2684080872601985e-17], - 'tdev_taus': [1.], }, }, { @@ -696,8 +682,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [2.2204460492503132e-17], - 'tdev_taus': [1.], }, }, { @@ -747,8 +731,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_taus': [1.], - 'tdev_samples': [3.2684080872601985e-17], }, }, ) diff --git a/tests/vse_sync_pp/analyzers/test_ts2phc.py b/tests/vse_sync_pp/analyzers/test_ts2phc.py index 54770c5..4e241a8 100644 --- a/tests/vse_sync_pp/analyzers/test_ts2phc.py +++ b/tests/vse_sync_pp/analyzers/test_ts2phc.py @@ -297,8 +297,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -338,8 +336,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -379,8 +375,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, { @@ -420,8 +414,6 @@ class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'mtie_samples': [0.], - 'mtie_taus': [1.], }, }, ) @@ -513,8 +505,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -562,8 +552,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -611,8 +599,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, { @@ -660,8 +646,6 @@ class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): 'stddev': 0, 'variance': 0, }, - 'tdev_samples': [0.], - 'tdev_taus': [1.], }, }, ) diff --git a/tests/vse_sync_pp/test_requirements.py b/tests/vse_sync_pp/test_requirements.py index 83bb786..4071c54 100644 --- a/tests/vse_sync_pp/test_requirements.py +++ b/tests/vse_sync_pp/test_requirements.py @@ -16,11 +16,10 @@ def test_g8272_prtc_a(self): self.assertEqual(REQUIREMENTS['G.8272/PRTC-A']['time-error-in-locked-mode/ns'], 100) - (interval1, func1), (interval2, func2) = REQUIREMENTS['G.8272/PRTC-A']['maximum-time-interval-error-in-locked-mode/us'].items() # noqa + (interval1, func1), (interval2, func2) = REQUIREMENTS['G.8272/PRTC-A']['maximum-time-interval-error-in-locked-mode/ns'].items() # noqa - # floating point math representation - self.assertEqual(func1(100), 0.052500000000000005) - self.assertEqual(func2(300), 0.10) + self.assertEqual(func1(100), 52.5) + self.assertEqual(func2(300), 100) (interval1, func1), (interval2, func2), (interval3, func3) = REQUIREMENTS['G.8272/PRTC-A']['time-deviation-in-locked-mode/ns'].items() # noqa self.assertEqual(func1(100), 3) @@ -32,15 +31,15 @@ def test_g8272_prtc_b(self): self.assertEqual(REQUIREMENTS['G.8272/PRTC-B']['time-error-in-locked-mode/ns'], 40) - (a, f1), (c, f2) = REQUIREMENTS['G.8272/PRTC-B']['maximum-time-interval-error-in-locked-mode/us'].items() + (interval1, func1), (interval2, func2) = REQUIREMENTS['G.8272/PRTC-B']['maximum-time-interval-error-in-locked-mode/ns'].items() # noqa - self.assertEqual(f1(100), 0.052500000000000005) - self.assertEqual(f2(300), 0.04) + self.assertEqual(func1(100), 52.5) + self.assertEqual(func2(300), 40) - (a, f1), (c, f2), (e, f3) = REQUIREMENTS['G.8272/PRTC-B']['time-deviation-in-locked-mode/ns'].items() - self.assertEqual(f1(100), 1) - self.assertEqual(f2(150), 1.5) - self.assertEqual(f3(550), 5) + (interval1, func1), (interval2, func2), (interval3, func3) = REQUIREMENTS['G.8272/PRTC-B']['time-deviation-in-locked-mode/ns'].items() # noqa + self.assertEqual(func1(100), 1) + self.assertEqual(func2(150), 1.5) + self.assertEqual(func3(550), 5) def test_workload_RAN(self): """Test workload/RAN requirement values"""