Merge pull request #178 from setrofim/master
Various fixes.
diff --git a/devlib/__init__.py b/devlib/__init__.py
index b1b4fa3..42509f9 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -19,8 +19,8 @@
from devlib.instrument.netstats import NetstatsInstrument
from devlib.instrument.gem5power import Gem5PowerInstrument
-from devlib.derived import DerivedMeasurements
-from devlib.derived.derived_measurements import DerivedEnergyMeasurements
+from devlib.derived import DerivedMeasurements, DerivedMetric
+from devlib.derived.energy import DerivedEnergyMeasurements
from devlib.trace.ftrace import FtraceCollector
diff --git a/devlib/derived/__init__.py b/devlib/derived/__init__.py
index 5689a58..24ac060 100644
--- a/devlib/derived/__init__.py
+++ b/devlib/derived/__init__.py
@@ -12,8 +12,49 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+
+from devlib.instrument import MeasurementType, MEASUREMENT_TYPES
+
+
+class DerivedMetric(object):
+
+ __slots__ = ['name', 'value', 'measurement_type']
+
+ @property
+ def units(self):
+ return self.measurement_type.units
+
+ def __init__(self, name, value, measurement_type):
+ self.name = name
+ self.value = value
+ if isinstance(measurement_type, MeasurementType):
+ self.measurement_type = measurement_type
+ else:
+ try:
+ self.measurement_type = MEASUREMENT_TYPES[measurement_type]
+ except KeyError:
+ msg = 'Unknown measurement type: {}'
+ raise ValueError(msg.format(measurement_type))
+
+ def __cmp__(self, other):
+ if hasattr(other, 'value'):
+ return cmp(self.value, other.value)
+ else:
+ return cmp(self.value, other)
+
+ def __str__(self):
+ if self.units:
+ return '{}: {} {}'.format(self.name, self.value, self.units)
+ else:
+ return '{}: {}'.format(self.name, self.value)
+
+ __repr__ = __str__
+
+
class DerivedMeasurements(object):
- @staticmethod
- def process(measurements_csv):
- raise NotImplementedError()
+ def process(self, measurements_csv):
+ return []
+
+ def process_raw(self, *args):
+ return []
diff --git a/devlib/derived/derived_measurements.py b/devlib/derived/energy.py
similarity index 86%
rename from devlib/derived/derived_measurements.py
rename to devlib/derived/energy.py
index 770db88..84d3d7c 100644
--- a/devlib/derived/derived_measurements.py
+++ b/devlib/derived/energy.py
@@ -15,8 +15,8 @@
from __future__ import division
from collections import defaultdict
-from devlib import DerivedMeasurements
-from devlib.instrument import Measurement, MEASUREMENT_TYPES, InstrumentChannel
+from devlib import DerivedMeasurements, DerivedMetric
+from devlib.instrument import MEASUREMENT_TYPES, InstrumentChannel
class DerivedEnergyMeasurements(DerivedMeasurements):
@@ -56,7 +56,7 @@
power_results = defaultdict(float)
# Process data
- for count, row in enumerate(measurements_csv.itermeasurements()):
+ for count, row in enumerate(measurements_csv.iter_measurements()):
if use_timestamp:
last_ts = row_ts
row_ts = time_measurment.convert(float(row[ts_index].value), 'time')
@@ -86,12 +86,12 @@
derived_measurements = []
for site in energy_results:
total_energy = energy_results[site]['end'] - energy_results[site]['start']
- instChannel = InstrumentChannel('cum_energy', site, MEASUREMENT_TYPES['energy'])
- derived_measurements.append(Measurement(total_energy, instChannel))
+ name = '{}_total_energy'.format(site)
+ derived_measurements.append(DerivedMetric(name, total_energy, MEASUREMENT_TYPES['energy']))
for site in power_results:
power = power_results[site] / (count + 1) #pylint: disable=undefined-loop-variable
- instChannel = InstrumentChannel('avg_power', site, MEASUREMENT_TYPES['power'])
- derived_measurements.append(Measurement(power, instChannel))
+ name = '{}_average_power'.format(site)
+ derived_measurements.append(DerivedMetric(name, power, MEASUREMENT_TYPES['power']))
return derived_measurements
diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py
index 77ba1d3..0d2c1ed 100644
--- a/devlib/instrument/__init__.py
+++ b/devlib/instrument/__init__.py
@@ -72,38 +72,60 @@
return text.format(self.name, self.units)
-# Standard measures
+# Standard measures. In order to make sure that downstream data processing is not tied
+# to particular insturments (e.g. a particular method of mearuing power), instruments
+# must, where possible, resport their measurments formatted as on of the standard types
+# defined here.
_measurement_types = [
+ # For whatever reason, the type of measurement could not be established.
MeasurementType('unknown', None),
- MeasurementType('time', 'seconds',
+
+ # Generic measurements
+ MeasurementType('count', 'count'),
+ MeasurementType('percent', 'percent'),
+
+ # Time measurement. While there is typically a single "canonical" unit
+ # used for each type of measurmenent, time may be measured to a wide variety
+ # of events occuring at a wide range of scales. Forcing everying into a
+ # single scale will lead to inefficient and awkward to work with result tables.
+ # Coversion functions between the formats are specified, so that downstream
+ # processors that expect all times time be at a particular scale can automatically
+ # covert without being familar with individual instruments.
+ MeasurementType('time', 'seconds', 'time',
conversions={
'time_us': lambda x: x * 1000000,
'time_ms': lambda x: x * 1000,
}
),
- MeasurementType('time_us', 'microseconds',
+ MeasurementType('time_us', 'microseconds', 'time',
conversions={
'time': lambda x: x / 1000000,
'time_ms': lambda x: x / 1000,
}
),
- MeasurementType('time_ms', 'milliseconds',
+ MeasurementType('time_ms', 'milliseconds', 'time',
conversions={
'time': lambda x: x / 1000,
'time_us': lambda x: x * 1000,
}
),
- MeasurementType('temperature', 'degrees'),
+ # Measurements related to thermals.
+ MeasurementType('temperature', 'degrees', 'thermal'),
+
+ # Measurements related to power end energy consumption.
MeasurementType('power', 'watts', 'power/energy'),
MeasurementType('voltage', 'volts', 'power/energy'),
MeasurementType('current', 'amps', 'power/energy'),
MeasurementType('energy', 'joules', 'power/energy'),
+ # Measurments realted to data transfer, e.g. neworking,
+ # memory, or backing storage.
MeasurementType('tx', 'bytes', 'data transfer'),
MeasurementType('rx', 'bytes', 'data transfer'),
MeasurementType('tx/rx', 'bytes', 'data transfer'),
+ MeasurementType('fps', 'fps', 'ui render'),
MeasurementType('frames', 'frames', 'ui render'),
]
for m in _measurement_types:
@@ -127,7 +149,7 @@
self.channel = channel
def __cmp__(self, other):
- if isinstance(other, Measurement):
+ if hasattr(other, 'value'):
return cmp(self.value, other.value)
else:
return cmp(self.value, other)
@@ -147,26 +169,32 @@
self.path = path
self.channels = channels
self.sample_rate_hz = sample_rate_hz
- self._fh = open(path, 'rb')
if self.channels is None:
self._load_channels()
+ headings = [chan.label for chan in self.channels]
+ self.data_tuple = collections.namedtuple('csv_entry', headings)
def measurements(self):
- return list(self.itermeasurements())
+ return list(self.iter_measurements())
- def itermeasurements(self):
- self._fh.seek(0)
- reader = csv.reader(self._fh)
- reader.next() # headings
- for row in reader:
+ def iter_measurements(self):
+ for row in self._iter_rows():
values = map(numeric, row)
yield [Measurement(v, c) for (v, c) in zip(values, self.channels)]
+ def values(self):
+ return list(self.iter_values())
+
+ def iter_values(self):
+ for row in self._iter_rows():
+ values = map(numeric, row)
+ yield self.data_tuple(*values)
+
def _load_channels(self):
- self._fh.seek(0)
- reader = csv.reader(self._fh)
- header = reader.next()
- self._fh.seek(0)
+ header = []
+ with open(self.path, 'rb') as fh:
+ reader = csv.reader(fh)
+ header = reader.next()
self.channels = []
for entry in header:
@@ -175,22 +203,35 @@
if entry.endswith(suffix):
site = entry[:-len(suffix)]
measure = mt
- name = '{}_{}'.format(site, measure)
break
else:
- site = entry
- measure = 'unknown'
- name = entry
+ if entry in MEASUREMENT_TYPES:
+ site = None
+ measure = entry
+ else:
+ site = entry
+ measure = 'unknown'
- chan = InstrumentChannel(name, site, measure)
+ chan = InstrumentChannel(site, measure)
self.channels.append(chan)
+ def _iter_rows(self):
+ with open(self.path, 'rb') as fh:
+ reader = csv.reader(fh)
+ reader.next() # headings
+ for row in reader:
+ yield row
+
class InstrumentChannel(object):
@property
def label(self):
- return '{}_{}'.format(self.site, self.kind)
+ if self.site is not None:
+ return '{}_{}'.format(self.site, self.kind)
+ return self.kind
+
+ name = label
@property
def kind(self):
@@ -200,8 +241,7 @@
def units(self):
return self.measurement_type.units
- def __init__(self, name, site, measurement_type, **attrs):
- self.name = name
+ def __init__(self, site, measurement_type, **attrs):
self.site = site
if isinstance(measurement_type, MeasurementType):
self.measurement_type = measurement_type
@@ -243,10 +283,8 @@
measure = measure.name
return [c for c in self.list_channels() if c.kind == measure]
- def add_channel(self, site, measure, name=None, **attrs):
- if name is None:
- name = '{}_{}'.format(site, measure)
- chan = InstrumentChannel(name, site, measure, **attrs)
+ def add_channel(self, site, measure, **attrs):
+ chan = InstrumentChannel(site, measure, **attrs)
self.channels[chan.label] = chan
# initialization and teardown
@@ -297,3 +335,6 @@
def get_data(self, outfile):
pass
+
+ def get_raw(self):
+ return []
diff --git a/devlib/instrument/acmecape.py b/devlib/instrument/acmecape.py
index e1bb6c1..1053c9d 100644
--- a/devlib/instrument/acmecape.py
+++ b/devlib/instrument/acmecape.py
@@ -121,3 +121,6 @@
output_row.append(float(row[i])/1000)
writer.writerow(output_row)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
+
+ def get_raw(self):
+ return [self.raw_data_file]
diff --git a/devlib/instrument/daq.py b/devlib/instrument/daq.py
index 75e854d..d497151 100644
--- a/devlib/instrument/daq.py
+++ b/devlib/instrument/daq.py
@@ -33,6 +33,7 @@
# pylint: disable=no-member
super(DaqInstrument, self).__init__(target)
self._need_reset = True
+ self._raw_files = []
if execute_command is None:
raise HostError('Could not import "daqpower": {}'.format(import_error_mesg))
if labels is None:
@@ -68,6 +69,7 @@
if not result.status == Status.OK: # pylint: disable=no-member
raise HostError(result.message)
self._need_reset = False
+ self._raw_files = []
def start(self):
if self._need_reset:
@@ -86,6 +88,7 @@
site = os.path.splitext(entry)[0]
path = os.path.join(tempdir, entry)
raw_file_map[site] = path
+ self._raw_files.append(path)
active_sites = unique([c.site for c in self.active_channels])
file_handles = []
@@ -131,6 +134,9 @@
for fh in file_handles:
fh.close()
+ def get_raw(self):
+ return self._raw_files
+
def teardown(self):
self.execute('close')
diff --git a/devlib/instrument/energy_probe.py b/devlib/instrument/energy_probe.py
index 5f47430..c8f179e 100644
--- a/devlib/instrument/energy_probe.py
+++ b/devlib/instrument/energy_probe.py
@@ -52,6 +52,7 @@
self.raw_output_directory = None
self.process = None
self.sample_rate_hz = 10000 # Determined empirically
+ self.raw_data_file = None
for label in self.labels:
for kind in self.attributes:
@@ -64,6 +65,7 @@
for i, rval in enumerate(self.resistor_values)]
rstring = ''.join(parts)
self.command = '{} -d {} -l {} {}'.format(self.caiman, self.device_entry, rstring, self.raw_output_directory)
+ self.raw_data_file = None
def start(self):
self.logger.debug(self.command)
@@ -92,10 +94,10 @@
num_of_ports = len(self.resistor_values)
struct_format = '{}I'.format(num_of_ports * self.attributes_per_sample)
not_a_full_row_seen = False
- raw_data_file = os.path.join(self.raw_output_directory, '0000000000')
+ self.raw_data_file = os.path.join(self.raw_output_directory, '0000000000')
- self.logger.debug('Parsing raw data file: {}'.format(raw_data_file))
- with open(raw_data_file, 'rb') as bfile:
+ self.logger.debug('Parsing raw data file: {}'.format(self.raw_data_file))
+ with open(self.raw_data_file, 'rb') as bfile:
with open(outfile, 'wb') as wfh:
writer = csv.writer(wfh)
writer.writerow(active_channels)
@@ -114,3 +116,6 @@
else:
not_a_full_row_seen = True
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
+
+ def get_raw(self):
+ return [self.raw_data_file]
diff --git a/devlib/instrument/frames.py b/devlib/instrument/frames.py
index d1899fb..54869c1 100644
--- a/devlib/instrument/frames.py
+++ b/devlib/instrument/frames.py
@@ -20,6 +20,7 @@
self.collector = None
self.header = None
self._need_reset = True
+ self._raw_file = None
self._init_channels()
def reset(self, sites=None, kinds=None, channels=None):
@@ -27,6 +28,7 @@
self.collector = self.collector_cls(self.target, self.period,
self.collector_target, self.header)
self._need_reset = False
+ self._raw_file = None
def start(self):
if self._need_reset:
@@ -38,14 +40,16 @@
self._need_reset = True
def get_data(self, outfile):
- raw_outfile = None
if self.keep_raw:
- raw_outfile = outfile + '.raw'
- self.collector.process_frames(raw_outfile)
+ self._raw_file = outfile + '.raw'
+ self.collector.process_frames(self._raw_file)
active_sites = [chan.label for chan in self.active_channels]
self.collector.write_frames(outfile, columns=active_sites)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
+ def get_raw(self):
+ return [self._raw_file] if self._raw_file else []
+
def _init_channels(self):
raise NotImplementedError()
diff --git a/devlib/instrument/hwmon.py b/devlib/instrument/hwmon.py
index ae49f40..5a9d8af 100644
--- a/devlib/instrument/hwmon.py
+++ b/devlib/instrument/hwmon.py
@@ -45,7 +45,7 @@
measure = self.measure_map.get(ts.kind)[0]
if measure:
self.logger.debug('\tAdding sensor {}'.format(ts.name))
- self.add_channel(_guess_site(ts), measure, name=ts.name, sensor=ts)
+ self.add_channel(_guess_site(ts), measure, sensor=ts)
else:
self.logger.debug('\tSkipping sensor {} (unknown kind "{}")'.format(ts.name, ts.kind))
except ValueError:
diff --git a/devlib/platform/arm.py b/devlib/platform/arm.py
index e760eaf..76b58a4 100644
--- a/devlib/platform/arm.py
+++ b/devlib/platform/arm.py
@@ -210,22 +210,22 @@
mode = CONTINUOUS | INSTANTANEOUS
_channels = [
- InstrumentChannel('sys_curr', 'sys', 'current'),
- InstrumentChannel('a57_curr', 'a57', 'current'),
- InstrumentChannel('a53_curr', 'a53', 'current'),
- InstrumentChannel('gpu_curr', 'gpu', 'current'),
- InstrumentChannel('sys_volt', 'sys', 'voltage'),
- InstrumentChannel('a57_volt', 'a57', 'voltage'),
- InstrumentChannel('a53_volt', 'a53', 'voltage'),
- InstrumentChannel('gpu_volt', 'gpu', 'voltage'),
- InstrumentChannel('sys_pow', 'sys', 'power'),
- InstrumentChannel('a57_pow', 'a57', 'power'),
- InstrumentChannel('a53_pow', 'a53', 'power'),
- InstrumentChannel('gpu_pow', 'gpu', 'power'),
- InstrumentChannel('sys_cenr', 'sys', 'energy'),
- InstrumentChannel('a57_cenr', 'a57', 'energy'),
- InstrumentChannel('a53_cenr', 'a53', 'energy'),
- InstrumentChannel('gpu_cenr', 'gpu', 'energy'),
+ InstrumentChannel('sys', 'current'),
+ InstrumentChannel('a57', 'current'),
+ InstrumentChannel('a53', 'current'),
+ InstrumentChannel('gpu', 'current'),
+ InstrumentChannel('sys', 'voltage'),
+ InstrumentChannel('a57', 'voltage'),
+ InstrumentChannel('a53', 'voltage'),
+ InstrumentChannel('gpu', 'voltage'),
+ InstrumentChannel('sys', 'power'),
+ InstrumentChannel('a57', 'power'),
+ InstrumentChannel('a53', 'power'),
+ InstrumentChannel('gpu', 'power'),
+ InstrumentChannel('sys', 'energy'),
+ InstrumentChannel('a57', 'energy'),
+ InstrumentChannel('a53', 'energy'),
+ InstrumentChannel('gpu', 'energy'),
]
def __init__(self, target):
diff --git a/devlib/target.py b/devlib/target.py
index 51826fb..4b2da42 100644
--- a/devlib/target.py
+++ b/devlib/target.py
@@ -1011,11 +1011,12 @@
self.uninstall_executable(name)
def get_pids_of(self, process_name):
- result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
- if result and 'not found' not in result:
- return [int(x.split()[1]) for x in result.split('\n')[1:]]
- else:
- return []
+ result = []
+ search_term = process_name[-15:]
+ for entry in self.ps():
+ if search_term in entry.name:
+ result.append(entry.pid)
+ return result
def ps(self, **kwargs):
lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
@@ -1023,8 +1024,12 @@
result = []
for line in lines:
parts = line.split(None, 8)
- if parts:
- result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
+ if not parts:
+ continue
+ if len(parts) == 8:
+ # wchan was blank; insert an empty field where it should be.
+ parts.insert(5, '')
+ result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
if not kwargs:
return result
else:
diff --git a/devlib/utils/misc.py b/devlib/utils/misc.py
index f601686..8cfd59f 100644
--- a/devlib/utils/misc.py
+++ b/devlib/utils/misc.py
@@ -103,6 +103,9 @@
0x211: {0x1: 'KryoGold'},
0x800: {None: 'Falkor'},
},
+ 0x53: { # Samsung LSI
+ 0x001: {0x1: 'MongooseM1'},
+ },
0x56: { # Marvell
0x131: {
0x2: 'Feroceon 88F6281',
diff --git a/devlib/utils/rendering.py b/devlib/utils/rendering.py
index 3b7b6c4..665135a 100644
--- a/devlib/utils/rendering.py
+++ b/devlib/utils/rendering.py
@@ -83,9 +83,14 @@
header = self.header
frames = self.frames
else:
- header = [c for c in self.header if c in columns]
- indexes = [self.header.index(c) for c in header]
+ indexes = []
+ for c in columns:
+ if c not in self.header:
+ msg = 'Invalid column "{}"; must be in {}'
+ raise ValueError(msg.format(c, self.header))
+ indexes.append(self.header.index(c))
frames = [[f[i] for i in indexes] for f in self.frames]
+ header = columns
with open(outfile, 'w') as wfh:
writer = csv.writer(wfh)
if header:
@@ -122,7 +127,8 @@
return self.target.execute(cmd.format(activity))
def list(self):
- return self.target.execute('dumpsys SurfaceFlinger --list').split('\r\n')
+ text = self.target.execute('dumpsys SurfaceFlinger --list')
+ return text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
def _process_raw_file(self, fh):
text = fh.read().replace('\r\n', '\n').replace('\r', '\n')
@@ -203,3 +209,43 @@
if not found:
logger.warning('Could not find frames data in gfxinfo output')
return
+
+
+def _file_reverse_iter(fh, buf_size=1024):
+ fh.seek(0, os.SEEK_END)
+ offset = 0
+ file_size = remaining_size = fh.tell()
+ while remaining_size > 0:
+ offset = min(file_size, offset + buf_size)
+ fh.seek(file_size - offset)
+ buf = fh.read(min(remaining_size, buf_size))
+ remaining_size -= buf_size
+ yield buf
+
+
+def gfxinfo_get_last_dump(filepath):
+ """
+ Return the last gfxinfo dump from the frame collector's raw output.
+
+ """
+ record = ''
+ with open(filepath, 'r') as fh:
+ fh_iter = _file_reverse_iter(fh)
+ try:
+ while True:
+ buf = fh_iter.next()
+ ix = buf.find('** Graphics')
+ if ix >= 0:
+ return buf[ix:] + record
+
+ ix = buf.find(' **\n')
+ if ix >= 0:
+ buf = fh_iter.next() + buf
+ ix = buf.find('** Graphics')
+ if ix < 0:
+ msg = '"{}" appears to be corrupted'
+ raise RuntimeError(msg.format(filepath))
+ return buf[ix:] + record
+ record = buf + record
+ except StopIteration:
+ pass
diff --git a/doc/derived_measurements.rst b/doc/derived_measurements.rst
index fcd497c..285bce6 100644
--- a/doc/derived_measurements.rst
+++ b/doc/derived_measurements.rst
@@ -9,7 +9,7 @@
-------
The following example shows how to use an implementation of a
-:class:`DerivedMeasurement` to obtain a list of calculated ``Measurements``.
+:class:`DerivedMeasurement` to obtain a list of calculated ``DerivedMetric``'s.
.. code-block:: ipython
@@ -35,35 +35,92 @@
Derived Measurements
~~~~~~~~~~~~~~~~~~~~
-.. class:: DerivedMeasurements()
+.. class:: DerivedMeasurements
- The ``DerivedMeasurements`` class is an abstract base for implementing
- additional classes to calculate various metrics.
+ The ``DerivedMeasurements`` class provides an API for post-processing
+ instrument output offline (i.e. without a connection to the target device) to
+ generate additional metrics.
.. method:: DerivedMeasurements.process(measurement_csv)
- Returns a list of :class:`Measurement` objects that have been calculated.
+ Process a :class:`MeasurementsCsv`, returning a list of
+ :class:`DerivedMetric` and/or :class:`MeasurementsCsv` objects that have been
+ derived from the input. The exact nature and ordering of the list memebers
+ is specific to indivial 'class'`DerivedMeasurements` implementations.
+.. method:: DerivedMeasurements.process_raw(\*args)
+
+ Process raw output from an instrument, returnin a list :class:`DerivedMetric`
+ and/or :class:`MeasurementsCsv` objects that have been derived from the
+ input. The exact nature and ordering of the list memebers is specific to
+ indivial 'class'`DerivedMeasurements` implewmentations.
+
+ The arguents to this method should be paths to raw output files generated by
+ an instrument. The number and order of expected arguments is specific to
+ particular implmentations.
+
+
+Derived Metric
+~~~~~~~~~~~~~~
+
+.. class:: DerivedMetric
+
+ Represents a metric derived from previously collected ``Measurement``s.
+ Unlike, a ``Measurement``, this was not measured directly from the target.
+
+
+.. attribute:: DerivedMetric.name
+
+ The name of the derived metric. This uniquely defines a metric -- two
+ ``DerivedMetric`` objects with the same ``name`` represent to instances of
+ the same metric (e.g. computed from two different inputs).
+
+.. attribute:: DerivedMetric.value
+
+ The ``numeric`` value of the metric that has been computed for a particular
+ input.
+
+.. attribute:: DerivedMetric.measurement_type
+
+ The ``MeasurementType`` of the metric. This indicates which conceptual
+ category the metric falls into, its units, and conversions to other
+ measurement types.
+
+.. attribute:: DerivedMetric.units
+
+ The units in which the metric's value is expressed.
Available Derived Measurements
-------------------------------
-.. class:: DerivedEnergyMeasurements()
- The ``DerivedEnergyMeasurements`` class is used to calculate average power and
- cumulative energy for each site if the required data is present.
+.. note:: If a method of the API is not documented for a particular
+ implementation, that means that it s not overriden by that
+ implementation. It is still safe to call it -- an empty list will be
+ returned.
- The calculation of cumulative energy can occur in 3 ways. If a
- ``site`` contains ``energy`` results, the first and last measurements are extracted
- and the delta calculated. If not, a ``timestamp`` channel will be used to calculate
- the energy from the power channel, failing back to using the sample rate attribute
- of the :class:`MeasurementCsv` file if timestamps are not available. If neither
- timestamps or a sample rate are available then an error will be raised.
+Energy
+~~~~~~
+
+.. class:: DerivedEnergyMeasurements
+
+ The ``DerivedEnergyMeasurements`` class is used to calculate average power and
+ cumulative energy for each site if the required data is present.
+
+ The calculation of cumulative energy can occur in 3 ways. If a
+ ``site`` contains ``energy`` results, the first and last measurements are extracted
+ and the delta calculated. If not, a ``timestamp`` channel will be used to calculate
+ the energy from the power channel, failing back to using the sample rate attribute
+ of the :class:`MeasurementCsv` file if timestamps are not available. If neither
+ timestamps or a sample rate are available then an error will be raised.
.. method:: DerivedEnergyMeasurements.process(measurement_csv)
- Returns a list of :class:`Measurement` objects that have been calculated for
- the average power and cumulative energy for each site.
-
+ This will return total cumulative energy for each energy channel, and the
+ average power for each power channel in the input CSV. The output will contain
+ all energy metrics followed by power metrics. The ordering of both will match
+ the ordering of channels in the input. The metrics will by named based on the
+ sites of the coresponding channels according to the following patters:
+ ``"<site>_total_energy"`` and ``"<site>_average_power"``.
diff --git a/doc/instrumentation.rst b/doc/instrumentation.rst
index 8aee1ce..0d4a6ce 100644
--- a/doc/instrumentation.rst
+++ b/doc/instrumentation.rst
@@ -65,8 +65,8 @@
:INSTANTANEOUS: The instrument supports taking a single sample via
``take_measurement()``.
:CONTINUOUS: The instrument supports collecting measurements over a
- period of time via ``start()``, ``stop()``, and
- ``get_data()`` methods.
+ period of time via ``start()``, ``stop()``, ``get_data()``,
+ and (optionally) ``get_raw`` methods.
.. note:: It's possible for one instrument to support more than a single
mode.
@@ -161,6 +161,13 @@
.. note:: This method is only implemented by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurement.
+.. method:: Instrument.get_raw()
+
+ Returns a list of paths to files containing raw output from the underlying
+ source(s) that is used to produce the data CSV. If now raw output is
+ generated or saved, an empty list will be returned. The format of the
+ contents of the raw files is entirely source-dependent.
+
.. attribute:: Instrument.sample_rate_hz
Sample rate of the instrument in Hz. Assumed to be the same for all channels.
@@ -229,13 +236,15 @@
+-------------+-------------+---------------+
| name | units | category |
+=============+=============+===============+
-| time | seconds | |
+| count | count | |
+-------------+-------------+---------------+
-| time | microseconds| |
+| percent | percent | |
+-------------+-------------+---------------+
-| time | milliseconds| |
+| time_us | microseconds| time |
+-------------+-------------+---------------+
-| temperature | degrees | |
+| time_ms | milliseconds| time |
++-------------+-------------+---------------+
+| temperature | degrees | thermal |
+-------------+-------------+---------------+
| power | watts | power/energy |
+-------------+-------------+---------------+