1
0
mirror of https://github.com/ARM-software/workload-automation.git synced 2024-10-06 19:01:15 +01:00
workload-automation/wa/instrumentation/energy_measurement.py
Marc Bonnici e9a9f16032 EnergyMeasurement: Adds a wrapper for devlib instrumentation
The instrument allows for various devlib instruments (currently only
daq and energy_probe) to be used in WA to collect energy measurements.
2017-07-25 16:08:17 +01:00

250 lines
9.8 KiB
Python

# 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 __future__ import division
import os
from collections import defaultdict
from devlib.instrument import CONTINUOUS
from devlib.instrument.energy_probe import EnergyProbeInstrument
from devlib.instrument.daq import DaqInstrument
from wa import Instrument, Parameter
from wa.framework import pluginloader
from wa.framework.plugin import Plugin
from wa.framework.exception import ConfigError
from wa.utils.types import list_of_strings, list_of_ints, list_or_string
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
class DAQBackend(EnergyInstrumentBackend):
name = 'daq'
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('host', kind=str, default='localhost',
description="""
The host address of the machine that runs the daq Server which
the instrument communicates with.
"""),
Parameter('port', kind=int, default=45677,
description="""
The port number for daq Server in which daq instrument
communicates with.
"""),
Parameter('device_id', kind=str, default='Dev1',
description="""
The ID under which the DAQ is registered with the driver.
"""),
Parameter('v_range', kind=str, default=2.5,
description="""
Specifies the voltage range for the SOC voltage channel on the
DAQ (please refer to :ref:`daq_setup` for details).
"""),
Parameter('dv_range', kind=str, default=0.2,
description="""
Specifies the voltage range for the resistor voltage channel
on the DAQ (please refer to :ref:`daq_setup` for details).
"""),
Parameter('sample_rate_hz', kind=str, default=10000,
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),
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'
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 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'],
description="""
Specify the energy instrumentation to be enabled.
"""),
Parameter('instrument_parameters', kind=dict, default={},
description="""
Specify the parameters used to initialize the desired
instrumentation.
"""),
Parameter('sites', kind=list_or_string, default=[],
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, default=[],
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, default=[],
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.instrumentation = None
self.measurement_csv = None
self.loader = loader
self.backend = self.loader.get_plugin(self.instrument)
self.params = {}
if self.backend.instrument.mode != CONTINUOUS:
msg = '{} instrument does not support continuous measurement collection'
raise ConfigError(msg.format(self.instrument))
supported_params = self.backend.get_parameters()
for name, value in supported_params.iteritems():
if name in self.instrument_parameters:
self.params[name] = self.instrument_parameters[name]
elif value.default:
self.params[name] = value.default
self.backend.validate_parameters(self.params)
def initialize(self, context):
self.instrumentation = self.backend.instrument(self.target, **self.params)
for channel in self.channels:
if not self.instrumentation.get_channels(channel):
raise ConfigError('No channels found for "{}"'.format(channel))
def setup(self, context):
self.instrumentation.reset(sites=self.sites,
kinds=self.kinds,
channels=self.channels)
def start(self, context):
self.instrumentation.start()
def stop(self, context):
self.instrumentation.stop()
def update_result(self, context):
outfile = os.path.join(context.output_directory, 'energy_instrument_output.csv')
self.measurement_csv = self.instrumentation.get_data(outfile)
context.add_artifact('energy_instrument_output', outfile, 'data')
self.extract_metrics(context)
def extract_metrics(self, context):
measurements = self.measurement_csv.itermeasurements()
energy_results = defaultdict(dict)
power_results = defaultdict(int)
for count, row in enumerate(measurements):
for entry in row:
channel = entry.channel
if channel.kind == 'energy':
if count == 0:
energy_results[channel.site]['start'] = entry.value
else:
energy_results[channel.site]['end'] = entry.value
elif channel.kind == 'power':
power_results[channel.site] += entry.value
for site in energy_results:
total_energy = energy_results[site]['end'] - energy_results[site]['start']
context.add_metric('{}_energy'.format(site), total_energy, 'joules')
for site in power_results:
power = power_results[site] / count + 1 #pylint: disable=undefined-loop-variable
context.add_metric('{}_power'.format(site), power, 'watts')