1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import numpy as np
18
19from acts.controllers.monsoon_lib.sampling.engine.assembly_line import BufferList
20from acts.controllers.monsoon_lib.sampling.engine.transformer import ParallelTransformer
21from acts.controllers.monsoon_lib.sampling.engine.transformer import SequentialTransformer
22
23
24class Tee(SequentialTransformer):
25    """Outputs main_current values to the specified file.
26
27    Attributes:
28        _filename: the name of the file to open.
29        _fd: the filestream written to.
30    """
31
32    def __init__(self, filename, measure_after_seconds=0):
33        """Creates an OutputStream.
34
35        Args:
36            filename: the path to the file to write the collected data to.
37            measure_after_seconds: the number of seconds to skip before
38                logging data as part of the measurement.
39        """
40        super().__init__()
41        self._filename = filename
42        self._fd = None
43        self.measure_after_seconds = measure_after_seconds
44        # The time of the first sample gathered.
45        self._start_time = None
46
47    def on_begin(self):
48        self._fd = open(self._filename, 'w+')
49
50    def on_end(self):
51        self._fd.close()
52
53    def _transform_buffer(self, buffer):
54        """Writes the reading values to a file.
55
56        Args:
57            buffer: A list of HvpmReadings.
58        """
59        for sample in buffer:
60            if self._start_time is None:
61                self._start_time = sample.sample_time
62            if (sample.sample_time - self._start_time <
63                    self.measure_after_seconds):
64                continue
65            self._fd.write('%0.9f %.12f\n' %
66                           (sample.sample_time, sample.main_current))
67        self._fd.flush()
68        return BufferList([buffer])
69
70
71class PerfgateTee(SequentialTransformer):
72    """Outputs records of nanoseconds,current,voltage to the specified file.
73
74    Similar to Tee, but this version includes voltage, which may help with
75    accuracy in the power calculations.
76
77    This output type can be enabled by passing this transformer to the
78    transformers kwarg in Monsoon.measure_power():
79
80    # Uses the default Tee
81    > monsoon.measure_power(..., output_path=filename])
82
83    # Uses PerfgateTee
84    > monsoon.measure_power(..., transformers=[PerfgateTee(filename)])
85
86    Attributes:
87        _filename: the name of the file to open.
88        _fd: the filestream written to.
89    """
90
91    def __init__(self, filename, measure_after_seconds=0):
92        """Creates an OutputStream.
93
94        Args:
95            filename: the path to the file to write the collected data to.
96            measure_after_seconds: the number of seconds to skip before logging
97              data as part of the measurement.
98        """
99        super().__init__()
100        self._filename = filename
101        self._fd = None
102        self.measure_after_seconds = measure_after_seconds
103        # The time of the first sample gathered.
104        self._start_time = None
105
106    def on_begin(self):
107        self._fd = open(self._filename, 'w+')
108
109    def on_end(self):
110        self._fd.close()
111
112    def _transform_buffer(self, buffer):
113        """Writes the reading values to a file.
114
115            Args:
116                buffer: A list of HvpmReadings.
117        """
118        for sample in buffer:
119            if self._start_time is None:
120                self._start_time = sample.sample_time
121            if (sample.sample_time - self._start_time <
122                    self.measure_after_seconds):
123                continue
124            self._fd.write(
125                '%i,%.6f,%.6f\n' %
126                (sample.sample_time * 1e9, sample.main_current,
127                 sample.main_voltage))
128        self._fd.flush()
129        return BufferList([buffer])
130
131
132class SampleAggregator(ParallelTransformer):
133    """Aggregates the main current value and the number of samples gathered."""
134
135    def __init__(self, start_after_seconds=0):
136        """Creates a new SampleAggregator.
137
138        Args:
139            start_after_seconds: The number of seconds to wait before gathering
140                data. Useful for allowing the device to settle after USB
141                disconnect.
142        """
143        super().__init__()
144        self._num_samples = 0
145        self._sum_currents = 0
146        self.start_after_seconds = start_after_seconds
147        # The time of the first sample gathered.
148        self._start_time = None
149
150    def _transform_buffer(self, buffer):
151        """Aggregates the sample data.
152
153        Args:
154            buffer: A buffer of H/LvpmReadings.
155        """
156        for sample in buffer:
157            if self._start_time is None:
158                self._start_time = sample.sample_time
159            if sample.sample_time - self._start_time < self.start_after_seconds:
160                continue
161            self._num_samples += 1
162            self._sum_currents += sample.main_current
163        return buffer
164
165    @property
166    def num_samples(self):
167        """The number of samples read from the device."""
168        return self._num_samples
169
170    @property
171    def sum_currents(self):
172        """The total sum of current values gathered so far."""
173        return self._sum_currents
174
175
176class DownSampler(SequentialTransformer):
177    """Takes in sample outputs and returns a downsampled version of that data.
178
179    Note for speed, the downsampling must occur at a perfect integer divisor of
180    the Monsoon's sample rate (5000 hz).
181    """
182    _MONSOON_SAMPLE_RATE = 5000
183
184    def __init__(self, downsample_factor):
185        """Creates a DownSampler Transformer.
186
187        Args:
188            downsample_factor: The number of samples averaged together for a
189                single output sample.
190        """
191        super().__init__()
192
193        self._mean_width = int(downsample_factor)
194        self._leftovers = []
195
196    def _transform_buffer(self, buffer):
197        """Returns the buffer downsampled by an integer factor.
198
199        The algorithm splits data points into three categories:
200
201            tail: The remaining samples where not enough were collected to
202                  reach the integer factor for downsampling. The tail is stored
203                  in self._leftovers between _transform_buffer calls.
204            tailless_buffer: The samples excluding the tail that can be
205                             downsampled directly.
206
207        Below is a diagram explaining the buffer math:
208
209        input:          input buffer n              input buffer n + 1
210                 ╔══════════════════════════╗  ╔══════════════════════════╗
211             ... ║ ╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗ ║  ║ ╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗ ║ ...
212                 ║ ╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝ ║  ║ ╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝ ║
213                 ╚══════════════════════════╝  ╚══════════════════════════╝
214                               ▼                             ▼
215        alg:     ╔═════════════════════╦════╗  ╔═════════════════════╦════╗
216                 ║ ╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗║╔╗╔╗║  ║ ╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗║╔╗╔╗║
217                 ║ ╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝║╚╝╚╝║  ║ ╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝║╚╝╚╝║
218             ... ║   tailless_buffer   ║tail║  ║   tailless_buffer   ║tail║ ...
219                 ╚═════════════════════╩════╝  ╚═════════════════════╩════╝
220               ──┬───┘ └─┬─┘ ...  └─┬─┘ └────┬─────┘ └─┬─┘ ...  └─┬─┘ └──┬───
221                 ╔╗      ╔╗ ╔╗  ╔╗ ╔╗        ╔╗        ╔╗ ╔╗  ╔╗ ╔╗      ╔╗
222                 ╚╝      ╚╝ ╚╝  ╚╝ ╚╝        ╚╝        ╚╝ ╚╝  ╚╝ ╚╝      ╚╝
223                 └─────────┬────────┘        └──────────┬─────────┘
224                           ▼                            ▼
225        output:   ╔════════════════╗           ╔════════════════╗
226                  ║ ╔╗ ╔╗ ╔╗ ╔╗ ╔╗ ║           ║ ╔╗ ╔╗ ╔╗ ╔╗ ╔╗ ║
227                  ║ ╚╝ ╚╝ ╚╝ ╚╝ ╚╝ ║           ║ ╚╝ ╚╝ ╚╝ ╚╝ ╚╝ ║
228                  ╚════════════════╝           ╚════════════════╝
229                   output buffer n             output buffer n + 1
230        """
231        tail_length = int(
232            (len(buffer) + len(self._leftovers)) % self._mean_width)
233
234        tailless_buffer = np.array(buffer[:len(buffer) - tail_length])
235
236        sample_count = len(tailless_buffer) + len(self._leftovers)
237
238        downsampled_values = np.mean(
239            np.resize(
240                np.append(self._leftovers, tailless_buffer),
241                (sample_count // self._mean_width, self._mean_width)),
242            axis=1)
243
244        self._leftovers = buffer[len(buffer) - tail_length:]
245
246        return downsampled_values
247