aboutsummaryrefslogtreecommitdiff
path: root/wa/instruments/energy_measurement.py
blob: f46f62a3e5ed9ac9a736127e8165123ed896ded8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
#    Copyright 2013-2017 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


# pylint: disable=W0613,E1101

from collections import defaultdict
import os
import shutil

from wa.utils.types import list_of_numbers

from devlib import DerivedEnergyMeasurements
from devlib.instrument import CONTINUOUS
from devlib.instrument.energy_probe import EnergyProbeInstrument
from devlib.instrument.arm_energy_probe import ArmEnergyProbeInstrument
from devlib.instrument.daq import DaqInstrument
from devlib.instrument.acmecape import AcmeCapeInstrument
from devlib.instrument.monsoon import MonsoonInstrument
from devlib.platform.arm import JunoEnergyInstrument
from devlib.utils.misc import which

from wa import Instrument, Parameter
from wa.framework import pluginloader
from wa.framework.plugin import Plugin
from wa.framework.exception import ConfigError, InstrumentError
from wa.utils.types import list_of_strings, list_of_ints, list_or_string, obj_dict, identifier


class EnergyInstrumentBackend(Plugin):

    name = None
    kind = 'energy_instrument_backend'
    parameters = []

    instrument = None

    def get_parameters(self):
        return {p.name : p for p in self.parameters}

    def validate_parameters(self, params):
        pass

    def get_instruments(self, target, metadir, **kwargs):
        """
        Get a dict mapping device keys to an Instruments

        Typically there is just a single device/instrument, in which case the
        device key is arbitrary.
        """
        return {None: self.instrument(target, **kwargs)}


class DAQBackend(EnergyInstrumentBackend):

    name = 'daq'
    description="""
    National Instruments Data Acquisition device

    For more information about the device, please see the NI website:

    http://www.ni.com/data-acquisition/

    This backend has been used with USB-62xx and USB-63xx devices, though other
    models (e.g. the PCIe variants will most likely also work).

    This backend relies on the daq-server running on a machinge connected to a
    DAQ device:

        https://github.com/ARM-software/daq-server

    The server is necessary because DAQ devices have drivers only for Windows
    and very specific (old) Linux kernels, so the machine interfacing with the
    DAQ is most likely going to be different from the machinge running WA.

    """

    parameters = [
        Parameter('resistor_values', kind=list_of_numbers,
                  global_alias='daq_resistor_values',
                  description="""
                  The values of resistors (in Ohms) across which the voltages
                  are measured on.
                  """),
        Parameter('labels', kind=list_of_strings,
                  global_alias='daq_labels',
                  description="""
                  'List of port labels. If specified, the length of the list
                  must match the length of ``resistor_values``.
                  """),
        Parameter('host', kind=str, default='localhost',
                  global_alias='daq_server_host',
                  description="""
                  The host address of the machine that runs the daq Server which
                  the instrument communicates with.
                  """),
        Parameter('port', kind=int, default=45677,
                  global_alias='daq_server_port',
                  description="""
                  The port number for daq Server in which daq instrument
                  communicates with.
                  """),
        Parameter('device_id', kind=str, default='Dev1',
                  global_alias='daq_device_id',
                  description="""
                  The ID under which the DAQ is registered with the driver.
                  """),
        Parameter('v_range', kind=str, default=2.5,
                  global_alias='daq_v_range',
                  description="""
                  Specifies the voltage range for the SOC voltage channel on the
                  DAQ (please refer to ``daq-server`` package documentation for
                  details).
                  """),
        Parameter('dv_range', kind=str, default=0.2,
                  global_alias='daq_dv_range',
                  description="""
                  Specifies the voltage range for the resistor voltage channel
                  on the DAQ (please refer to ``daq-server`` package
                  documentation for details).
                  """),
        Parameter('sample_rate_hz', kind=int, default=10000,
                  global_alias='daq_sampling_rate',
                  description="""
                  Specify the sample rate in Hz.
                  """),
        Parameter('channel_map', kind=list_of_ints,
                  default=(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23),
                  global_alias='daq_channel_map',
                  description="""
                  Represents mapping from  logical AI channel number to physical
                  connector on the DAQ (varies between DAQ models). The default
                  assumes DAQ 6363 and similar with AI channels on connectors
                  0-7 and 16-23.
                  """)
        ]

    instrument = DaqInstrument

    def validate_parameters(self, params):
        if not params.get('resistor_values'):
            raise ConfigError('Mandatory parameter "resistor_values" is not set.')
        if params.get('labels'):
            if len(params.get('labels')) != len(params.get('resistor_values')):
                msg = 'Number of DAQ port labels does not match the number of resistor values.'
                raise ConfigError(msg)


class EnergyProbeBackend(EnergyInstrumentBackend):

    name = 'energy_probe'
    description="""
    Arm Energy Probe caiman version

    This backend relies on caiman utility:

        https://github.com/ARM-software/caiman

    For more information about Arm Energy Probe please see

        https://developer.arm.com/products/software-development-tools/ds-5-development-studio/streamline/arm-energy-probe

    """

    parameters = [
        Parameter('resistor_values', kind=list_of_ints,
                  description="""
                  The values of resistors (in Ohms) across which the voltages
                  are measured on.
                  """),
        Parameter('labels', kind=list_of_strings,
                  description="""
                  'List of port labels. If specified, the length of the list
                  must match the length of ``resistor_values``.
                  """),
        Parameter('device_entry', kind=str, default='/dev/ttyACM0',
                  description="""
                  Path to /dev entry for the energy probe (it should be /dev/ttyACMx)
                  """),
    ]

    instrument = EnergyProbeInstrument

    def validate_parameters(self, params):
        if not params.get('resistor_values'):
            raise ConfigError('Mandatory parameter "resistor_values" is not set.')
        if params.get('labels'):
            if len(params.get('labels')) != len(params.get('resistor_values')):
                msg = 'Number of Energy Probe port labels does not match the number of resistor values.'
                raise ConfigError(msg)


class ArmEnergyProbeBackend(EnergyInstrumentBackend):

    name = 'arm_energy_probe'
    description="""
    Arm Energy Probe arm-probe version

    An alternative Arm Energy Probe backend that relies on arm-probe utility:

        https://git.linaro.org/tools/arm-probe.git

    For more information about Arm Energy Probe please see

        https://developer.arm.com/products/software-development-tools/ds-5-development-studio/streamline/arm-energy-probe


    """

    parameters = [
        Parameter('config_file', kind=str,
                  description="""
                  Path to config file of the AEP
                  """),
    ]

    instrument = ArmEnergyProbeInstrument

    def get_instruments(self, target, metadir, **kwargs):
        """
        Get a dict mapping device keys to an Instruments

        Typically there is just a single device/instrument, in which case the
        device key is arbitrary.
        """

        shutil.copy(self.config_file, metadir)

        return {None: self.instrument(target, **kwargs)}

    def validate_parameters(self, params):
        if not params.get('config_file'):
            raise ConfigError('Mandatory parameter "config_file" is not set.')
        self.config_file = params.get('config_file')
        if not os.path.exists(self.config_file):
            raise ConfigError('"config_file" does not exist.')


class AcmeCapeBackend(EnergyInstrumentBackend):

    name = 'acme_cape'
    description="""
    BayLibre ACME cape

    This backend relies on iio-capture utility:

        https://github.com/BayLibre/iio-capture

    For more information about ACME cape please see:

        https://baylibre.com/acme/

    """

    parameters = [
        Parameter('iio-capture', default=which('iio-capture'),
                  description="""
                  Path to the iio-capture binary will be taken from the
                  environment, if not specfied.
                  """),
        Parameter('host', default='baylibre-acme.local',
                  description="""
                  Host name (or IP address) of the ACME cape board.
                  """),
        Parameter('iio-devices', default='iio:device0',
                  kind=list_or_string,
                  description="""
                  """),
        Parameter('buffer-size', kind=int, default=256,
                  description="""
                  Size of the capture buffer (in KB).
                  """),
    ]

    def get_instruments(self, target, metadir,
                        iio_capture, host, iio_devices, buffer_size):

        #
        # Devlib's ACME instrument uses iio-capture under the hood, which can
        # only capture data from one IIO device at a time. Devlib's instrument
        # API expects to produce a single CSV file for the Instrument, with a
        # single axis of sample timestamps. These two things cannot be correctly
        # reconciled without changing the devlib Instrument API - get_data would
        # need to be able to return two distinct sets of data.
        #
        # Instead, where required WA will instantiate the ACME instrument
        # multiple times (once for each IIO device), producing two separate CSV
        # files. Aggregated energy info _can_ be meaningfully combined from
        # multiple IIO devices, so we will later sum the derived stats across
        # each of the channels reported by the instruments.
        #

        ret = {}
        for iio_device in iio_devices:
            ret[iio_device] = AcmeCapeInstrument(
                target, iio_capture=iio_capture, host=host,
                iio_device=iio_device, buffer_size=buffer_size)
        return ret

class MonsoonBackend(EnergyInstrumentBackend):

    name = 'monsoon'
    description="""
    Monsoon Solutions power monitor

    To use this instrument, you need to install the monsoon.py script available
    from the Android Open Source Project. As of May 2017 this is under the CTS
    repository:

        https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py

    Collects power measurements only, from a selection of two channels, the USB
    passthrough channel and the main output channel.

    """

    parameters = [
        Parameter('monsoon_bin', default=which('monsoon.py'),
                  description="""
                  Path to monsoon.py executable. If not provided,
                  ``PATH`` is searched.
                  """),
        Parameter('tty_device', default='/dev/ttyACM0',
                  description="""
                  TTY device to use to communicate with the Power
                  Monitor. If not provided, /dev/ttyACM0 is used.
                  """)
    ]

    instrument = MonsoonInstrument


class JunoEnergyBackend(EnergyInstrumentBackend):

    name = 'juno_readenergy'
    description="""
    Arm Juno development board on-board energy meters

    For more information about Arm Juno board see:

        https://developer.arm.com/products/system-design/development-boards/juno-development-board

    """

    instrument = JunoEnergyInstrument


class EnergyMeasurement(Instrument):

    name = 'energy_measurement'

    description = """
    This instrument is designed to be used as an interface to the various
    energy measurement instruments located in devlib.
    """

    parameters = [
        Parameter('instrument', kind=str, mandatory=True,
                  allowed_values=['daq', 'energy_probe', 'acme_cape', 'monsoon', 'juno_readenergy', 'arm_energy_probe'],
                  description="""
                  Specify the energy instruments to be enabled.
                  """),
        Parameter('instrument_parameters', kind=dict, default={},
                   description="""
                   Specify the parameters used to initialize the desired
                   instruments. To see parameters available for a particular
                   instrument, run

                        wa show <instrument name>

                   See help for ``instrument`` parameter to see available
                   options for <instrument name>.

                   """),
        Parameter('sites', kind=list_or_string,
                  description="""
                  Specify which sites measurements should be collected
                  from, if not specified the measurements will be
                  collected for all available sites.
                  """),
        Parameter('kinds', kind=list_or_string,
                  description="""
                  Specify the kinds of measurements should be collected,
                  if not specified measurements will be
                  collected for all available kinds.
                  """),
        Parameter('channels', kind=list_or_string,
                  description="""
                  Specify the channels to be collected,
                  if not specified the measurements will be
                  collected for all available channels.
                  """),
    ]

    def __init__(self, target, loader=pluginloader, **kwargs):
        super(EnergyMeasurement, self).__init__(target, **kwargs)
        self.instruments = None
        self.measurement_csvs = {}
        self.loader = loader
        self.backend = self.loader.get_plugin(self.instrument)
        self.params = obj_dict()

        instrument_parameters = {identifier(k): v
                                 for k, v in self.instrument_parameters.items()}
        supported_params = self.backend.get_parameters()
        for name, param in supported_params.items():
            value = instrument_parameters.pop(name, None)
            param.set_value(self.params, value)
        if instrument_parameters:
            msg = 'Unexpected parameters for backend "{}": {}'
            raise ConfigError(msg.format(self.instrument, instrument_parameters))
        self.backend.validate_parameters(self.params)

    def initialize(self, context):
        self.instruments = self.backend.get_instruments(self.target, context.run_output.metadir, **self.params)

        for instrument in self.instruments.values():
            if not (instrument.mode & CONTINUOUS):
                msg = '{} instrument does not support continuous measurement collection'
                raise ConfigError(msg.format(self.instrument))
            instrument.setup()

        for channel in self.channels or []:
            # Check that the expeccted channels exist.
            # If there are multiple Instruments, they were all constructed with
            # the same channels param, so check them all.
            for instrument in self.instruments.values():
                if not instrument.get_channels(channel):
                    raise ConfigError('No channels found for "{}"'.format(channel))

    def setup(self, context):
        for instrument in self.instruments.values():
            instrument.reset(sites=self.sites,
                             kinds=self.kinds,
                             channels=self.channels)

    def start(self, context):
        for instrument in self.instruments.values():
            instrument.start()

    def stop(self, context):
        for instrument in self.instruments.values():
            instrument.stop()

    def update_output(self, context):
        for device, instrument in self.instruments.items():
            # Append the device key to the filename and artifact name, unless
            # it's None (as it will be for backends with only 1
            # devce/instrument)
            if len(self.instruments) > 1:
                name = 'energy_instrument_output_{}'.format(device)
            else:
                name = 'energy_instrument_output'

            outfile = os.path.join(context.output_directory, '{}.csv'.format(name))
            measurements = instrument.get_data(outfile)
            if not measurements:
                raise InstrumentError("Failed to collect energy data from {}"
                                      .format(self.backend.name))

            self.measurement_csvs[device] = measurements
            context.add_artifact(name, measurements.path, 'data',
                                 classifiers={'device': device})
        self.extract_metrics(context)

    def extract_metrics(self, context):
        metrics_by_name = defaultdict(list)

        for device in self.instruments:
            csv = self.measurement_csvs[device]
            derived_measurements = DerivedEnergyMeasurements.process(csv)
            for meas in derived_measurements:
                # Append the device key to the metric name, unless it's None (as
                # it will be for backends with only 1 devce/instrument)
                if len(self.instruments) > 1:
                    metric_name = '{}_{}'.format(meas.name, device)
                else:
                    metric_name = meas.name
                context.add_metric(metric_name, meas.value, meas.units,
                                   classifiers={'device': device})

                metrics_by_name[meas.name].append(meas)

        # Where we have multiple instruments, add up all the metrics with the
        # same name. For instance with ACME we may have multiple IIO devices
        # each reporting 'device_energy' and 'device_power', so sum them up to
        # produce aggregated energy and power metrics.
        # (Note that metrics_by_name uses the metric name originally reported by
        #  the devlib instrument, before we potentially appended a device key to
        #  it)
        if len(self.instruments) > 1:
            for name, metrics in metrics_by_name.items():
                units = metrics[0].units
                value = sum(m.value for m in metrics)
                context.add_metric(name, value, units)