# 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')