From dd6f5c6109269a199057d2d6ac88dab45b9a7aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Nu=C3=B1ez?= Date: Fri, 15 Sep 2023 19:55:43 +0200 Subject: [PATCH] TDEV and MTIE Analyzer --- src/vse_sync_pp/analyzers/__init__.py | 8 + src/vse_sync_pp/analyzers/analyzer.py | 210 ++++++++ src/vse_sync_pp/analyzers/gnss.py | 18 + src/vse_sync_pp/analyzers/phc2sys.py | 16 + src/vse_sync_pp/analyzers/ppsdpll.py | 28 ++ src/vse_sync_pp/analyzers/ts2phc.py | 17 +- src/vse_sync_pp/requirements.py | 26 +- tests/vse_sync_pp/analyzers/test_gnss.py | 457 +++++++++++++++++- tests/vse_sync_pp/analyzers/test_phc2sys.py | 438 ++++++++++++++++- tests/vse_sync_pp/analyzers/test_ppsdpll.py | 504 +++++++++++++++++++- tests/vse_sync_pp/analyzers/test_ts2phc.py | 436 ++++++++++++++++- tests/vse_sync_pp/test_requirements.py | 41 +- 12 files changed, 2181 insertions(+), 18 deletions(-) diff --git a/src/vse_sync_pp/analyzers/__init__.py b/src/vse_sync_pp/analyzers/__init__.py index 58d2de7..cf5851e 100644 --- a/src/vse_sync_pp/analyzers/__init__.py +++ b/src/vse_sync_pp/analyzers/__init__.py @@ -19,5 +19,13 @@ ts2phc.TimeErrorAnalyzer, phc2sys.TimeErrorAnalyzer, pmc.ClockStateAnalyzer, + gnss.TimeDeviationAnalyzer, + ppsdpll.TimeDeviationAnalyzer, + ts2phc.TimeDeviationAnalyzer, + phc2sys.TimeDeviationAnalyzer, + gnss.MaxTimeIntervalErrorAnalyzer, + ppsdpll.MaxTimeIntervalErrorAnalyzer, + ts2phc.MaxTimeIntervalErrorAnalyzer, + phc2sys.MaxTimeIntervalErrorAnalyzer, ) } diff --git a/src/vse_sync_pp/analyzers/analyzer.py b/src/vse_sync_pp/analyzers/analyzer.py index 2337099..3758dd9 100644 --- a/src/vse_sync_pp/analyzers/analyzer.py +++ b/src/vse_sync_pp/analyzers/analyzer.py @@ -6,6 +6,11 @@ from pandas import DataFrame from datetime import (datetime, timezone) +import allantools +import numpy as np + +from scipy import signal as scipy_signal + from ..requirements import REQUIREMENTS @@ -265,3 +270,208 @@ def explain(self, data): 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, 'terror': self._statistics(data.terror, 'ns'), } + + +def calculate_limit(accuracy, limit_percentage, tau): + """Calculate upper limit based on tau + + `accuracy` is the list of functions to calculate upper limits + `limit_percentage` is the unaccuracy percentage + `tau` is the observation window interval + + Return the upper limit value based on `tau` + """ + for (low, high), f in accuracy.items(): + if ((low is None or tau > low) and (tau <= high)): + return f(tau) * (limit_percentage / 100) + + +def out_of_range(taus, samples, accuracy, limit): + """Check if the input samples are out of range. + + `taus` list of observation windows intervals + `samples` are input samples + `accuracy` contains the list of upper bound limit functions + `limit` is the percentage to apply the upper limit + + Return `True` if any value in `samples` is out of range + """ + for tau, sample in zip(taus, samples): + mask = calculate_limit(accuracy, limit, tau) + if mask <= sample: + return True + return False + + +def calculate_filter(input_signal, transient, sample_rate): + """Calculate digital low-pass filter from `input_signal` + + scipy_signal.butter input arguments: + order of the filter: 1 + critical Frequency in half cycles per sample: + (for digital filters is normalized from 0 to 1 where 1 is Nyquist frequency) + cutoff Frequency: 0.1Hz + sample_rate in samples per second + btype is band type or type of filter + analog=False since it is always a digital filter + scipy_signal.butter return arguments: + `numerator` coefficient vector and `denominator coefficient vector of the butterworth digital filter + """ + numerator, denominator = scipy_signal.butter(1, 0.1 / (sample_rate / 2), btype="low", analog=False, output="ba") + lpf_signal = scipy_signal.filtfilt(numerator, denominator, input_signal.terror) + lpf_signal = lpf_signal[transient:len(lpf_signal)] + return lpf_signal + + +class TimeIntervalErrorAnalyzerBase(Analyzer): + """Analyze Time Interval Error (also referred to as Wander). + + Derived classes calculate specific Time Interval Error metric focused on measuring + the change of Time Error. + """ + locked = frozenset() + + def __init__(self, config): + super().__init__(config) + # samples in the initial transient period are ignored + self._transient = config.parameter('transient-period/s') + # minimum test duration for a valid test + self._duration_min = config.parameter('min-test-duration/s') + # limit initial tau observation windows from 1 to 10k taus + taus_below_10k = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, + 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, + 4000, 5000, 6000, 7000, 8000, 9000, 10000]) + # observation window upper limit to 100k samples in 5k increments + taus_above_10k = np.arange(15000, 100000, 5000) + # `taus_list` contains range limit of obervation window intervals for which to compute the statistic + self._taus_list = np.concatenate((taus_below_10k, taus_above_10k)) + self._rate = None + self._lpf_signal = None + + def prepare(self, rows): + idx = 0 + try: + tstart = rows[0].timestamp + self._transient + except IndexError: + pass + else: + while idx < len(rows): + if tstart <= rows[idx].timestamp: + break + idx += 1 + return super().prepare(rows[idx:]) + + @staticmethod + def calculate_rate(data): + # calculate sample rate using 100 samples + cumdelta = 0 + + for i in range(1, 100 if len(data) > 100 else len(data)): + cumdelta = cumdelta + data.iloc[i].timestamp - data.iloc[i - 1].timestamp + return round((1 / (cumdelta / 100))) + + def _test_common(self, data): + if len(data) == 0: + return ("error", "no data") + if frozenset(data.state.unique()).difference(self.locked): + return (False, "loss of lock") + if data.iloc[-1].timestamp - data.iloc[0].timestamp < self._duration_min: + 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) + if self._lpf_signal is None: + self._lpf_signal = calculate_filter(data, self._transient, self._rate) + return None + + +class TimeDeviationAnalyzerBase(TimeIntervalErrorAnalyzerBase): + """Analyze Time Deviation (TDEV). + + Derived classes must override class attribute `locked`, specifying a + frozenset of values representing locked states. + """ + def __init__(self, config): + super().__init__(config) + # required system time deviation output + self._accuracy = config.requirement('time-deviation-in-locked-mode/ns') + # limit of inaccuracy at observation point + self._limit = config.parameter('time-deviation-limit/%') + # list of observation windows intervals to calculate TDEV + # `_taus` is a subset of `taus_list` + self._taus = None + # TDEV samples + self._samples = None + + 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 + if out_of_range(self._taus, self._samples, self._accuracy, self._limit): + return (False, "unacceptable time deviation") + return (True, None) + return result + + 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 + 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'), + } + return analysis + + +class MaxTimeIntervalErrorAnalyzerBase(TimeIntervalErrorAnalyzerBase): + """Analyze Maximum Time Interval Error (MTIE). + + Derived classes must override class attribute `locked`, specifying a + frozenset of values representing locked states. + """ + 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') + # limit of inaccuracy at observation point + self._limit = config.parameter('maximum-time-interval-error-limit/%') + # list of observation windows intervals to calculate MTIE + # `_taus` will be a subset of `taus_list` + self._taus = None + # MTIE samples + self._samples = None + + 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 + if out_of_range(self._taus, self._samples, self._accuracy, self._limit): + return (False, "unacceptable mtie") + return (True, None) + return result + + 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 + 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'), + } + return analysis diff --git a/src/vse_sync_pp/analyzers/gnss.py b/src/vse_sync_pp/analyzers/gnss.py index eb0119c..7bc597d 100644 --- a/src/vse_sync_pp/analyzers/gnss.py +++ b/src/vse_sync_pp/analyzers/gnss.py @@ -3,6 +3,8 @@ """Analyze GNSS log messages""" from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase class TimeErrorAnalyzer(TimeErrorAnalyzerBase): @@ -17,3 +19,19 @@ class TimeErrorAnalyzer(TimeErrorAnalyzerBase): # 4 = GPS + dead reckoning combined # 5 = time only fix locked = frozenset({3, 4, 5}) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze time deviation""" + id_ = 'gnss/time-deviation' + parser = 'gnss/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({3, 4, 5}) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze time deviation""" + id_ = 'gnss/mtie' + parser = 'gnss/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({3, 4, 5}) diff --git a/src/vse_sync_pp/analyzers/phc2sys.py b/src/vse_sync_pp/analyzers/phc2sys.py index 11b9864..515a198 100644 --- a/src/vse_sync_pp/analyzers/phc2sys.py +++ b/src/vse_sync_pp/analyzers/phc2sys.py @@ -3,6 +3,8 @@ """Analyze phc2sys log messages""" from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase class TimeErrorAnalyzer(TimeErrorAnalyzerBase): @@ -10,3 +12,17 @@ class TimeErrorAnalyzer(TimeErrorAnalyzerBase): id_ = 'phc2sys/time-error' parser = id_ locked = frozenset({'s2'}) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze time deviation""" + id_ = 'phc2sys/time-deviation' + parser = 'phc2sys/time-error' + locked = frozenset({'s2'}) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze max time interval error""" + id_ = 'phc2sys/mtie' + parser = 'phc2sys/time-error' + locked = frozenset({'s2'}) diff --git a/src/vse_sync_pp/analyzers/ppsdpll.py b/src/vse_sync_pp/analyzers/ppsdpll.py index a0ac399..2453884 100644 --- a/src/vse_sync_pp/analyzers/ppsdpll.py +++ b/src/vse_sync_pp/analyzers/ppsdpll.py @@ -3,6 +3,8 @@ """Analyze ppsdpll log messages""" from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase class TimeErrorAnalyzer(TimeErrorAnalyzerBase): @@ -24,3 +26,29 @@ def prepare(self, rows): return super().prepare([ r._replace(terror=float(r.terror)) for r in rows ]) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze DPLL time deviation""" + id_ = 'ppsdpll/time-deviation' + parser = 'dpll/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({2, 3}) + + def prepare(self, rows): + return super().prepare([ + r._replace(terror=float(r.terror)) for r in rows + ]) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze DPLL max time interval error""" + id_ = 'ppsdpll/mtie' + parser = 'dpll/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({2, 3}) + + def prepare(self, rows): + return super().prepare([ + r._replace(terror=float(r.terror)) for r in rows + ]) diff --git a/src/vse_sync_pp/analyzers/ts2phc.py b/src/vse_sync_pp/analyzers/ts2phc.py index edab0a8..72f5696 100644 --- a/src/vse_sync_pp/analyzers/ts2phc.py +++ b/src/vse_sync_pp/analyzers/ts2phc.py @@ -1,8 +1,9 @@ ### SPDX-License-Identifier: GPL-2.0-or-later """Analyze ts2phc log messages""" - from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase class TimeErrorAnalyzer(TimeErrorAnalyzerBase): @@ -10,3 +11,17 @@ class TimeErrorAnalyzer(TimeErrorAnalyzerBase): id_ = 'ts2phc/time-error' parser = id_ locked = frozenset({'s2'}) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze time deviation""" + id_ = 'ts2phc/time-deviation' + parser = 'ts2phc/time-error' + locked = frozenset({'s2'}) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze max time interval error""" + id_ = 'ts2phc/mtie' + parser = 'ts2phc/time-error' + locked = frozenset({'s2'}) diff --git a/src/vse_sync_pp/requirements.py b/src/vse_sync_pp/requirements.py index ad40299..8f8d653 100644 --- a/src/vse_sync_pp/requirements.py +++ b/src/vse_sync_pp/requirements.py @@ -1,15 +1,39 @@ ### SPDX-License-Identifier: GPL-2.0-or-later -"""Requirements specified in standards""" +"""Requirements specified in ITU-T G.8272/Y.1367""" 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 + }, + 'time-deviation-in-locked-mode/ns': { + (None, 100): lambda t: 3, + (101, 1000): lambda t: 0.03 * t, + (1001, 10000): lambda t: 30 + }, '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 + }, + 'time-deviation-in-locked-mode/ns': { + (None, 100): lambda t: 1, + (101, 500): lambda t: 0.01 * t, + (501, 100000): lambda t: 5 + }, 'time-error-in-locked-mode/ns': 40, }, 'workload/RAN': { 'time-error-in-locked-mode/ns': 100, + 'time-deviation-in-locked-mode/ns': { + (None, 100000): lambda t: 100 + }, + 'maximum-time-interval-error-in-locked-mode/us': { + (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 5fbecc0..5979463 100644 --- a/tests/vse_sync_pp/analyzers/test_gnss.py +++ b/tests/vse_sync_pp/analyzers/test_gnss.py @@ -8,7 +8,9 @@ import math from vse_sync_pp.analyzers.gnss import ( - TimeErrorAnalyzer + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer ) from .test_analyzer import AnalyzerTestBuilder @@ -215,3 +217,456 @@ class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): }, }, ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.gnss.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'gnss/mtie' + parser = 'gnss/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + # state s1 causes failure + TERR(Decimal(1), 0, 1), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 15, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + # oops, missing sample + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 1), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + # minimum to compute valid MTIE + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.gnss.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'gnss/time-deviation' + parser = 'gnss/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + # state s1 causes failure + TERR(Decimal(1), 0, 1), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + # oops, missing sample + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + TERR(Decimal(21), 0, 5), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/tests/vse_sync_pp/analyzers/test_phc2sys.py b/tests/vse_sync_pp/analyzers/test_phc2sys.py index 5e0792e..c0571f9 100644 --- a/tests/vse_sync_pp/analyzers/test_phc2sys.py +++ b/tests/vse_sync_pp/analyzers/test_phc2sys.py @@ -7,7 +7,11 @@ from decimal import Decimal import math -from vse_sync_pp.analyzers.phc2sys import TimeErrorAnalyzer +from vse_sync_pp.analyzers.phc2sys import ( + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer +) from .test_analyzer import AnalyzerTestBuilder @@ -213,3 +217,435 @@ class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): }, }, ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'phc2sys/mtie' + parser = 'phc2sys/time-error' + expect = ( + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + # state s1 causes failure + TERR(Decimal(1), 0, 's1', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 15, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + # oops, missing sample + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + # minimum to compute valid MTIE + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'phc2sys/time-deviation' + parser = 'phc2sys/time-error' + expect = ( + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + # state s1 causes failure + TERR(Decimal(1), 0, 's1', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + # oops, missing sample + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + TERR(Decimal(21), 0, 's2', 620), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/tests/vse_sync_pp/analyzers/test_ppsdpll.py b/tests/vse_sync_pp/analyzers/test_ppsdpll.py index a94346c..9a5f600 100644 --- a/tests/vse_sync_pp/analyzers/test_ppsdpll.py +++ b/tests/vse_sync_pp/analyzers/test_ppsdpll.py @@ -7,7 +7,9 @@ from decimal import Decimal from vse_sync_pp.analyzers.ppsdpll import ( - TimeErrorAnalyzer + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer ) from .test_analyzer import AnalyzerTestBuilder @@ -232,3 +234,503 @@ class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): }, }, ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ppsdpll.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'ppsdpll/mtie' + parser = 'dpll/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 3, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + # oops going into freerun + DPLLS(Decimal('1876879.28'), 3, 1, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 10, + }, + 'rows': ( + # state 2 does not cause failure + DPLLS(Decimal('1876878.28'), 3, 2, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(0)), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 23, + }, + 'rows': ( + DPLLS(Decimal('1876877.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(0)), + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal('1876878.28'), + 'duration': Decimal(22), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + # oops, lost samples + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + DPLLS(Decimal('1695047659.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047660.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047661.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047662.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047663.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047664.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047665.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047666.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047667.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047668.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047669.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047670.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047671.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047672.996251'), 3, 3, Decimal(1)), + ), + 'result': True, + 'reason': None, + 'timestamp': '2023-09-18T14:34:20+00:00', + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ppsdpll.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'ppsdpll/time-deviation' + parser = 'dpll/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 3, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + # state s1 causes failure + DPLLS(Decimal('1876879.28'), 3, 1, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + # oops, missing sample + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(0)), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/tests/vse_sync_pp/analyzers/test_ts2phc.py b/tests/vse_sync_pp/analyzers/test_ts2phc.py index 16c497c..4e241a8 100644 --- a/tests/vse_sync_pp/analyzers/test_ts2phc.py +++ b/tests/vse_sync_pp/analyzers/test_ts2phc.py @@ -8,7 +8,9 @@ import math from vse_sync_pp.analyzers.ts2phc import ( - TimeErrorAnalyzer + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer ) from .test_analyzer import AnalyzerTestBuilder @@ -215,3 +217,435 @@ class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): }, }, ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'ts2phc/mtie' + parser = 'ts2phc/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + # state s1 causes failure + TERR(Decimal(1), 0, 's1'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 15, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + # oops, missing sample + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + # minimum to compute valid MTIE + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'ts2phc/time-deviation' + parser = 'ts2phc/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + # state s1 causes failure + TERR(Decimal(1), 0, 's1'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + # oops, missing sample + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + TERR(Decimal(21), 0, 's2'), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/tests/vse_sync_pp/test_requirements.py b/tests/vse_sync_pp/test_requirements.py index 0153d35..83bb786 100644 --- a/tests/vse_sync_pp/test_requirements.py +++ b/tests/vse_sync_pp/test_requirements.py @@ -13,18 +13,35 @@ class TestRequirements(TestCase): """Test cases for vse_sync_pp.requirements.REQUIREMENTS""" def test_g8272_prtc_a(self): """Test G.8272/PRTC-A requirement values""" - self.assertEqual( - REQUIREMENTS['G.8272/PRTC-A'], - { - 'time-error-in-locked-mode/ns': 100, - }, - ) + + 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 + + # floating point math representation + self.assertEqual(func1(100), 0.052500000000000005) + self.assertEqual(func2(300), 0.10) + + (interval1, func1), (interval2, func2), (interval3, func3) = REQUIREMENTS['G.8272/PRTC-A']['time-deviation-in-locked-mode/ns'].items() # noqa + self.assertEqual(func1(100), 3) + self.assertEqual(func2(150), 4.5) + self.assertEqual(func3(550), 30) def test_g8272_prtc_b(self): """Test G.8272/PRTC-B requirement values""" - self.assertEqual( - REQUIREMENTS['G.8272/PRTC-B'], - { - 'time-error-in-locked-mode/ns': 40, - }, - ) + + 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() + + self.assertEqual(f1(100), 0.052500000000000005) + self.assertEqual(f2(300), 0.04) + + (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) + + def test_workload_RAN(self): + """Test workload/RAN requirement values""" + self.assertEqual(REQUIREMENTS['workload/RAN']['time-error-in-locked-mode/ns'], 100)