feat(jumpavg): support small values via unit param
[csit.git] / resources / libraries / python / jumpavg / bit_counting_group.py
1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Module holding BitCountingGroup class."""
15
16 import collections
17 import dataclasses
18 import typing
19
20 from .avg_stdev_stats import AvgStdevStats
21 from .bit_counting_stats import BitCountingStats
22
23
24 @dataclasses.dataclass
25 class BitCountingGroup(collections.abc.Sequence):
26     """Group of runs which tracks bit count in an efficient manner.
27
28     This class contains methods that mutate the internal state,
29     use copy() method to save the previous state.
30
31     The Sequence-like access is related to the list of runs,
32     for example group[0] returns the first run in the list.
33     Writable list-like methods are not implemented.
34
35     As the group bit count depends on previous average
36     and overall maximal value, those values are assumed
37     to be known beforehand (and immutable).
38
39     As the caller is allowed to divide runs into groups in any way,
40     a method to add a single run in an efficient manner is provided.
41     """
42
43     run_list: typing.List[typing.Union[float, AvgStdevStats]]
44     """List of run to compose into this group.
45     The init call takes ownership of the list,
46     so the caller should clone it to avoid unexpected muations."""
47     max_value: float
48     """Maximal sample value to expect."""
49     unit: float = 1.0
50     """Typical resolution of the values"""
51     comment: str = "normal"
52     """Any string giving more info, e.g. "regression"."""
53     prev_avg: typing.Optional[float] = None
54     """Average of the previous group, if any."""
55     stats: AvgStdevStats = None
56     """Stats object used for computing bits.
57     Almost always recomputed, except when non-None in init."""
58     cached_bits: typing.Optional[float] = None
59     """Cached value of information content.
60     Noned on edit, recomputed if needed and None."""
61
62     def __post_init__(self):
63         """Recompute stats is None.
64
65         It is not verified whether the user provided values are valid,
66         e.g. whether the stats and bits values reflect the runs.
67         """
68         if self.stats is None:
69             self.stats = AvgStdevStats.for_runs(runs=self.run_list)
70
71     @property
72     def bits(self) -> float:
73         """Return overall bit content of the group list.
74
75         If not cached, compute from stats and cache.
76
77         :returns: The overall information content in bits.
78         :rtype: float
79         """
80         if self.cached_bits is None:
81             self.cached_bits = BitCountingStats.for_runs_and_params(
82                 runs=[self.stats],
83                 max_value=self.max_value,
84                 unit=self.unit,
85                 prev_avg=self.prev_avg,
86             ).bits
87         return self.cached_bits
88
89     def __getitem__(self, index: int) -> typing.Union[float, AvgStdevStats]:
90         """Return the run at the index.
91
92         :param index: Index of the run to return.
93         :type index: int
94         :returns: The run at the index.
95         :rtype: typing.Union[float, AvgStdevStats]
96         """
97         return self.run_list[index]
98
99     def __len__(self) -> int:
100         """Return the number of runs in the group.
101
102         :returns: The Length of run_list.
103         :rtype: int
104         """
105         return len(self.run_list)
106
107     def copy(self) -> "BitCountingGroup":
108         """Return a new instance with copied internal state.
109
110         Stats are preserved to avoid re-computation.
111         As both float and AvgStdevStats are effectively immutable,
112         only a shallow copy of the runs list is performed.
113
114         :returns: The copied instance.
115         :rtype: BitCountingGroup
116         """
117         stats = AvgStdevStats.for_runs([self.stats])
118         return self.__class__(
119             run_list=list(self.run_list),
120             stats=stats,
121             cached_bits=self.cached_bits,
122             max_value=self.max_value,
123             unit=self.unit,
124             prev_avg=self.prev_avg,
125             comment=self.comment,
126         )
127
128     def append(
129         self, run: typing.Union[float, AvgStdevStats]
130     ) -> "BitCountingGroup":
131         """Mutate to add the new run, return self.
132
133         Stats are updated, but old bits value is deleted from cache.
134
135         :param run: The run value to add to the group.
136         :type value: typing.Union[float, AvgStdevStats]
137         :returns: The updated self.
138         :rtype: BitCountingGroup
139         """
140         self.run_list.append(run)
141         self.stats = AvgStdevStats.for_runs([self.stats, run])
142         self.cached_bits = None
143         return self
144
145     def extend(
146         self, runs: typing.Iterable[typing.Union[float, AvgStdevStats]]
147     ) -> "BitCountingGroup":
148         """Mutate to add the new runs, return self.
149
150         This is saves small amount of computation
151         compared to adding runs one by one in a loop.
152
153         Stats are updated, but old bits value is deleted from cache.
154
155         :param runs: The runs to add to the group.
156         :type value: typing.Iterable[typing.Union[float, AvgStdevStats]]
157         :returns: The updated self.
158         :rtype: BitCountingGroup
159         """
160         self.run_list.extend(runs)
161         self.stats = AvgStdevStats.for_runs([self.stats] + runs)
162         self.cached_bits = None
163         return self