From 2de2b36387ed55ec620767c6fd1583a4baa43d50 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Tue, 25 Jul 2017 16:19:08 +0100
Subject: [PATCH 01/12] Instrumentation: Fix conversion between microseconds
 and seconds

---
 devlib/instrument/__init__.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py
index 044c7d4..dd5b1f5 100644
--- a/devlib/instrument/__init__.py
+++ b/devlib/instrument/__init__.py
@@ -75,12 +75,12 @@ _measurement_types = [
     MeasurementType('unknown', None),
     MeasurementType('time', 'seconds',
         conversions={
-            'time_us': lambda x: x * 1000,
+            'time_us': lambda x: x * 1000000,
         }
     ),
     MeasurementType('time_us', 'microseconds',
         conversions={
-            'time': lambda x: x / 1000,
+            'time': lambda x: x / 1000000,
         }
     ),
     MeasurementType('temperature', 'degrees'),

From 9b465c27662a8cdf68a52d02b823fc7d51cae051 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Fri, 18 Aug 2017 09:59:23 +0100
Subject: [PATCH 02/12] Instruments: Add millisecond MeasurementType and
 conversion

Allows for reporting times in milliseconds as used with the acmecape
instrument.
---
 devlib/instrument/__init__.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py
index dd5b1f5..f1a89e0 100644
--- a/devlib/instrument/__init__.py
+++ b/devlib/instrument/__init__.py
@@ -76,11 +76,19 @@ _measurement_types = [
     MeasurementType('time', 'seconds',
         conversions={
             'time_us': lambda x: x * 1000000,
+            'time_ms': lambda x: x * 1000,
         }
     ),
     MeasurementType('time_us', 'microseconds',
         conversions={
             'time': lambda x: x / 1000000,
+            'time_ms': lambda x: x / 1000,
+        }
+    ),
+    MeasurementType('time_ms', 'milliseconds',
+        conversions={
+            'time': lambda x: x / 1000,
+            'time_us': lambda x: x * 1000,
         }
     ),
     MeasurementType('temperature', 'degrees'),

From 5ef99f2cff14f00da46d9ca011acdee8b5596779 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Wed, 2 Aug 2017 16:59:10 +0100
Subject: [PATCH 03/12] Instrument/MeasurementType: Allow for converting to the
 same type

When trying to convert measurments to a standarised type some inputs
may already be of the correct type and will now return the same value
unchanged.
---
 devlib/instrument/__init__.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py
index f1a89e0..46f9c3e 100644
--- a/devlib/instrument/__init__.py
+++ b/devlib/instrument/__init__.py
@@ -48,6 +48,8 @@ class MeasurementType(object):
         if not isinstance(to, MeasurementType):
             msg = 'Unexpected conversion target: "{}"'
             raise ValueError(msg.format(to))
+        if to.name == self.name:
+            return value
         if not to.name in self.conversions:
             msg = 'No conversion from {} to {} available'
             raise ValueError(msg.format(self.name, to.name))

From d3c3015fc82efe11521e41dc4b696ae396474e1d Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Thu, 3 Aug 2017 12:13:48 +0100
Subject: [PATCH 04/12] Instrument/MeasurementCSV: Add support for recording
 sample rate.

If performing post processing on a MeasurementCsv file, if a timestamp
is not available then the recorded sample rate can be used as a
substitute.
---
 devlib/instrument/__init__.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py
index 46f9c3e..9f8ac00 100644
--- a/devlib/instrument/__init__.py
+++ b/devlib/instrument/__init__.py
@@ -143,9 +143,10 @@ class Measurement(object):
 
 class MeasurementsCsv(object):
 
-    def __init__(self, path, channels=None):
+    def __init__(self, path, channels=None, sample_rate_hz=None):
         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()

From 30fdfc23d37d136ce66cea427f396653a340743f Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Mon, 24 Jul 2017 15:52:34 +0100
Subject: [PATCH 05/12] Instrument/Acmecape: Add support for acmecape

---
 devlib/instrument/acmecape.py | 122 ++++++++++++++++++++++++++++++++++
 1 file changed, 122 insertions(+)
 create mode 100644 devlib/instrument/acmecape.py

diff --git a/devlib/instrument/acmecape.py b/devlib/instrument/acmecape.py
new file mode 100644
index 0000000..abf6a19
--- /dev/null
+++ b/devlib/instrument/acmecape.py
@@ -0,0 +1,122 @@
+#pylint: disable=attribute-defined-outside-init
+from __future__ import division
+import csv
+import os
+import time
+import tempfile
+from fcntl import fcntl, F_GETFL, F_SETFL
+from string import Template
+from subprocess import Popen, PIPE, STDOUT
+
+from devlib import Instrument, CONTINUOUS, MeasurementsCsv
+from devlib.exception import HostError
+from devlib.utils.misc import which
+
+OUTPUT_CAPTURE_FILE = 'acme-cape.csv'
+IIOCAP_CMD_TEMPLATE = Template("""
+${iio_capture} -n ${host} -b ${buffer_size} -c -f ${outfile} ${iio_device}
+""")
+
+def _read_nonblock(pipe, size=1024):
+    fd = pipe.fileno()
+    flags = fcntl(fd, F_GETFL)
+    flags |= os.O_NONBLOCK
+    fcntl(fd, F_SETFL, flags)
+
+    output = ''
+    try:
+        while True:
+            output += pipe.read(size)
+    except IOError:
+        pass
+    return output
+
+
+class AcmeCapeInstrument(Instrument):
+
+    mode = CONTINUOUS
+
+    def __init__(self, target,
+                 iio_capture=which('iio_capture'),
+                 host='baylibre-acme.local',
+                 iio_device='iio:device0',
+                 buffer_size=256):
+        super(AcmeCapeInstrument, self).__init__(target)
+        self.iio_capture = iio_capture
+        self.host = host
+        self.iio_device = iio_device
+        self.buffer_size = buffer_size
+        if self.iio_capture is None:
+            raise HostError('Missing iio-capture binary')
+        self.command = None
+        self.process = None
+
+        self.add_channel('shunt', 'voltage')
+        self.add_channel('bus', 'voltage')
+        self.add_channel('device', 'power')
+        self.add_channel('device', 'current')
+        self.add_channel('timestamp', 'time_ms')
+
+    def reset(self, sites=None, kinds=None, channels=None):
+        super(AcmeCapeInstrument, self).reset(sites, kinds, channels)
+        self.raw_data_file = tempfile.mkstemp('.csv')[1]
+        params = dict(
+            iio_capture=self.iio_capture,
+            host=self.host,
+            buffer_size=self.buffer_size,
+            iio_device=self.iio_device,
+            outfile=self.raw_data_file
+        )
+        self.command = IIOCAP_CMD_TEMPLATE.substitute(**params)
+        self.logger.debug('ACME cape command: {}'.format(self.command))
+
+    def start(self):
+        self.process = Popen(self.command.split(), stdout=PIPE, stderr=STDOUT)
+
+    def stop(self):
+        self.process.terminate()
+        timeout_secs = 10
+        for _ in xrange(timeout_secs):
+            if self.process.poll() is not None:
+                break
+            time.sleep(1)
+        else:
+            output = _read_nonblock(self.process.stdout)
+            self.process.kill()
+            self.logger.error('iio-capture did not terminate gracefully')
+            if self.process.poll() is None:
+                msg = 'Could not terminate iio-capture:\n{}'
+                raise HostError(msg.format(output))
+        if not os.path.isfile(self.raw_data_file):
+            raise HostError('Output CSV not generated.')
+
+    def get_data(self, outfile):
+        if os.stat(self.raw_data_file).st_size == 0:
+            self.logger.warning('"{}" appears to be empty'.format(self.raw_data_file))
+            return
+
+        all_channels = [c.label for c in self.list_channels()]
+        active_channels = [c.label for c in self.active_channels]
+        active_indexes = [all_channels.index(ac) for ac in active_channels]
+
+        with open(self.raw_data_file, 'rb') as fh:
+            with open(outfile, 'wb') as wfh:
+                writer = csv.writer(wfh)
+                writer.writerow(active_channels)
+
+                reader = csv.reader(fh, skipinitialspace=True)
+                header = reader.next()
+                ts_index = header.index('timestamp ms')
+
+
+                for row in reader:
+                    output_row = []
+                    for i in active_indexes:
+                        if i == ts_index:
+                            # Leave time in ms
+                            output_row.append(float(row[i]))
+                        else:
+                            # Convert rest into standard units.
+                            output_row.append(float(row[i])/1000)
+                    writer.writerow(output_row)
+        return MeasurementsCsv(outfile, self.active_channels)

From 7dd934a5d8a9902d54385ea775558e2e8551570d Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Thu, 3 Aug 2017 16:41:10 +0100
Subject: [PATCH 06/12] Instrumentation: Update to populate missing
 'sample_rate_hz` attributes

To conform with the Instrumentation API each instrument should populate
the `sample_rate_hz` attribute with the relevant information.
---
 devlib/instrument/acmecape.py  |  1 +
 devlib/instrument/frames.py    |  1 +
 devlib/instrument/gem5power.py | 13 ++++++++-----
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/devlib/instrument/acmecape.py b/devlib/instrument/acmecape.py
index abf6a19..103a9f8 100644
--- a/devlib/instrument/acmecape.py
+++ b/devlib/instrument/acmecape.py
@@ -46,6 +46,7 @@ class AcmeCapeInstrument(Instrument):
         self.host = host
         self.iio_device = iio_device
         self.buffer_size = buffer_size
+        self.sample_rate_hz = 100
         if self.iio_capture is None:
             raise HostError('Missing iio-capture binary')
         self.command = None
diff --git a/devlib/instrument/frames.py b/devlib/instrument/frames.py
index d5a2147..d43977f 100644
--- a/devlib/instrument/frames.py
+++ b/devlib/instrument/frames.py
@@ -16,6 +16,7 @@ class FramesInstrument(Instrument):
         self.collector_target = collector_target
         self.period = period
         self.keep_raw = keep_raw
+        self.sample_rate_hz = 1 / self.period
         self.collector = None
         self.header = None
         self._need_reset = True
diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py
index 7715ec1..534046e 100644
--- a/devlib/instrument/gem5power.py
+++ b/devlib/instrument/gem5power.py
@@ -28,10 +28,10 @@ class Gem5PowerInstrument(Instrument):
 
     mode = CONTINUOUS
     roi_label = 'power_instrument'
-    
+
     def __init__(self, target, power_sites):
         '''
-        Parameter power_sites is a list of gem5 identifiers for power values. 
+        Parameter power_sites is a list of gem5 identifiers for power values.
         One example of such a field:
             system.cluster0.cores0.power_model.static_power
         '''
@@ -51,6 +51,9 @@ class Gem5PowerInstrument(Instrument):
             self.add_channel(field, 'power')
         self.target.gem5stats.book_roi(self.roi_label)
         self.sample_period_ns = 10000000
+        # Sample rate must remain unset as gem5 does not provide samples
+        # at regular intervals therefore the reported timestamp should be used.
+        self.sample_rate_hz = None
         self.target.gem5stats.start_periodic_dump(0, self.sample_period_ns)
         self._base_stats_dump = 0
 
@@ -59,17 +62,17 @@ class Gem5PowerInstrument(Instrument):
 
     def stop(self):
         self.target.gem5stats.roi_end(self.roi_label)
-       
+
     def get_data(self, outfile):
         active_sites = [c.site for c in self.active_channels]
         with open(outfile, 'wb') as wfh:
             writer = csv.writer(wfh)
             writer.writerow([c.label for c in self.active_channels]) # headers
-            for rec, rois in self.target.gem5stats.match_iter(active_sites, 
+            for rec, rois in self.target.gem5stats.match_iter(active_sites,
                     [self.roi_label], self._base_stats_dump):
                 writer.writerow([float(rec[s]) for s in active_sites])
         return MeasurementsCsv(outfile, self.active_channels)
-    
+
     def reset(self, sites=None, kinds=None, channels=None):
         super(Gem5PowerInstrument, self).reset(sites, kinds, channels)
         self._base_stats_dump = self.target.gem5stats.next_dump_no()

From 411719d58d14250ef7fd9a992a1b3602b3856c88 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Fri, 18 Aug 2017 17:06:28 +0100
Subject: [PATCH 07/12] Instrument/gem5power: Rename timestamp channel to match
 new API

To conform with the new DerivedMeasuements API the "sim_seconds" channel
has been renamed to "timestamp" so that it can be identified in post
processing. As "sim_seconds" needs to be extracted from the gem5
statistics file, an addional mapping has been added to support this.
---
 devlib/instrument/gem5power.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py
index 534046e..1e392a8 100644
--- a/devlib/instrument/gem5power.py
+++ b/devlib/instrument/gem5power.py
@@ -28,6 +28,7 @@ class Gem5PowerInstrument(Instrument):
 
     mode = CONTINUOUS
     roi_label = 'power_instrument'
+    site_mapping = {'timestamp': 'sim_seconds'}
 
     def __init__(self, target, power_sites):
         '''
@@ -46,7 +47,7 @@ class Gem5PowerInstrument(Instrument):
             self.power_sites = power_sites
         else:
             self.power_sites = [power_sites]
-        self.add_channel('sim_seconds', 'time')
+        self.add_channel('timestamp', 'time')
         for field in self.power_sites:
             self.add_channel(field, 'power')
         self.target.gem5stats.book_roi(self.roi_label)
@@ -68,7 +69,8 @@ class Gem5PowerInstrument(Instrument):
         with open(outfile, 'wb') as wfh:
             writer = csv.writer(wfh)
             writer.writerow([c.label for c in self.active_channels]) # headers
-            for rec, rois in self.target.gem5stats.match_iter(active_sites,
+            sites_to_match = [self.site_mapping.get(s, s) for s in active_sites]
+            for rec, rois in self.target.gem5stats.match_iter(sites_to_match,
                     [self.roi_label], self._base_stats_dump):
                 writer.writerow([float(rec[s]) for s in active_sites])
         return MeasurementsCsv(outfile, self.active_channels)

From 049b275665e961f8d27c3f54347842e4560f8f68 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Thu, 3 Aug 2017 16:43:32 +0100
Subject: [PATCH 08/12] Instrumentation: Update to store sample rate in
 MeasurementCSV file.

This commit updates existing instruments to store their sample rates
when creating the respective MeasurementCsv files.
---
 devlib/instrument/acmecape.py     | 2 +-
 devlib/instrument/daq.py          | 2 +-
 devlib/instrument/energy_probe.py | 2 +-
 devlib/instrument/frames.py       | 2 +-
 devlib/instrument/gem5power.py    | 2 +-
 devlib/instrument/monsoon.py      | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/devlib/instrument/acmecape.py b/devlib/instrument/acmecape.py
index 103a9f8..e1bb6c1 100644
--- a/devlib/instrument/acmecape.py
+++ b/devlib/instrument/acmecape.py
@@ -120,4 +120,4 @@ class AcmeCapeInstrument(Instrument):
                             # Convert rest into standard units.
                             output_row.append(float(row[i])/1000)
                     writer.writerow(output_row)
-        return MeasurementsCsv(outfile, self.active_channels)
+        return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
diff --git a/devlib/instrument/daq.py b/devlib/instrument/daq.py
index 58d2f3e..75e854d 100644
--- a/devlib/instrument/daq.py
+++ b/devlib/instrument/daq.py
@@ -126,7 +126,7 @@ class DaqInstrument(Instrument):
                     writer.writerow(row)
                     raw_row = _read_next_rows()
 
-            return MeasurementsCsv(outfile, self.active_channels)
+            return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
         finally:
             for fh in file_handles:
                 fh.close()
diff --git a/devlib/instrument/energy_probe.py b/devlib/instrument/energy_probe.py
index ed502f5..5f47430 100644
--- a/devlib/instrument/energy_probe.py
+++ b/devlib/instrument/energy_probe.py
@@ -113,4 +113,4 @@ class EnergyProbeInstrument(Instrument):
                             continue
                         else:
                             not_a_full_row_seen = True
-        return MeasurementsCsv(outfile, self.active_channels)
+        return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
diff --git a/devlib/instrument/frames.py b/devlib/instrument/frames.py
index d43977f..d1899fb 100644
--- a/devlib/instrument/frames.py
+++ b/devlib/instrument/frames.py
@@ -44,7 +44,7 @@ class FramesInstrument(Instrument):
         self.collector.process_frames(raw_outfile)
         active_sites = [chan.label for chan in self.active_channels]
         self.collector.write_frames(outfile, columns=active_sites)
-        return MeasurementsCsv(outfile, self.active_channels)
+        return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
 
     def _init_channels(self):
         raise NotImplementedError()
diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py
index 1e392a8..d265440 100644
--- a/devlib/instrument/gem5power.py
+++ b/devlib/instrument/gem5power.py
@@ -73,7 +73,7 @@ class Gem5PowerInstrument(Instrument):
             for rec, rois in self.target.gem5stats.match_iter(sites_to_match,
                     [self.roi_label], self._base_stats_dump):
                 writer.writerow([float(rec[s]) for s in active_sites])
-        return MeasurementsCsv(outfile, self.active_channels)
+        return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
 
     def reset(self, sites=None, kinds=None, channels=None):
         super(Gem5PowerInstrument, self).reset(sites, kinds, channels)
diff --git a/devlib/instrument/monsoon.py b/devlib/instrument/monsoon.py
index e373d68..3103618 100644
--- a/devlib/instrument/monsoon.py
+++ b/devlib/instrument/monsoon.py
@@ -129,4 +129,4 @@ class MonsoonInstrument(Instrument):
                     row.append(usb)
                 writer.writerow(row)
 
-        return MeasurementsCsv(outfile, self.active_channels)
+        return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)

From c093d567542460e398662b76e0577fbb49ae89f7 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Mon, 7 Aug 2017 14:29:40 +0100
Subject: [PATCH 09/12] DerivedMeasurements: Add DerivedEnergyMeasurments

Adds `DerivedMeasurements` which are designed to perform post processing on
a provided MeasurementCsv.
Currently only a `DerivedEnergyMeasurements` class has been added which
has 2 purposes:
- Calculate energy from power results if not present using recorded timestamps,
  falling back to a provided sample rate
- Calculate cumulative energy and average power from a specified MeasurementCSV
  file.
---
 devlib/__init__.py                     |  3 +
 devlib/derived/__init__.py             | 19 +++++
 devlib/derived/derived_measurements.py | 97 ++++++++++++++++++++++++++
 3 files changed, 119 insertions(+)
 create mode 100644 devlib/derived/__init__.py
 create mode 100644 devlib/derived/derived_measurements.py

diff --git a/devlib/__init__.py b/devlib/__init__.py
index 2f50632..b1b4fa3 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -19,6 +19,9 @@ from devlib.instrument.monsoon import MonsoonInstrument
 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.trace.ftrace import FtraceCollector
 
 from devlib.host import LocalConnection
diff --git a/devlib/derived/__init__.py b/devlib/derived/__init__.py
new file mode 100644
index 0000000..5689a58
--- /dev/null
+++ b/devlib/derived/__init__.py
@@ -0,0 +1,19 @@
+#    Copyright 2015 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.
+#
+class DerivedMeasurements(object):
+
+    @staticmethod
+    def process(measurements_csv):
+        raise NotImplementedError()
diff --git a/devlib/derived/derived_measurements.py b/devlib/derived/derived_measurements.py
new file mode 100644
index 0000000..770db88
--- /dev/null
+++ b/devlib/derived/derived_measurements.py
@@ -0,0 +1,97 @@
+#    Copyright 2013-2015 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.
+#
+from __future__ import division
+from collections import defaultdict
+
+from devlib import DerivedMeasurements
+from devlib.instrument import Measurement, MEASUREMENT_TYPES, InstrumentChannel
+
+
+class DerivedEnergyMeasurements(DerivedMeasurements):
+
+    @staticmethod
+    def process(measurements_csv):
+
+        should_calculate_energy = []
+        use_timestamp = False
+
+        # Determine sites to calculate energy for
+        channel_map = defaultdict(list)
+        for channel in measurements_csv.channels:
+            channel_map[channel].append(channel.kind)
+        for channel, kinds in channel_map.iteritems():
+            if 'power' in kinds and not 'energy' in kinds:
+                should_calculate_energy.append(channel.site)
+            if channel.site == 'timestamp':
+                use_timestamp = True
+                time_measurment = channel.measurement_type
+
+        if measurements_csv.sample_rate_hz is None and not use_timestamp:
+            msg = 'Timestamp data is unavailable, please provide a sample rate'
+            raise ValueError(msg)
+
+        if use_timestamp:
+            # Find index of timestamp column
+            ts_index = [i for i, chan in enumerate(measurements_csv.channels)
+                        if chan.site == 'timestamp']
+            if len(ts_index) > 1:
+                raise ValueError('Multiple timestamps detected')
+            ts_index = ts_index[0]
+
+        row_ts = 0
+        last_ts = 0
+        energy_results = defaultdict(dict)
+        power_results = defaultdict(float)
+
+        # Process data
+        for count, row in enumerate(measurements_csv.itermeasurements()):
+            if use_timestamp:
+                last_ts = row_ts
+                row_ts = time_measurment.convert(float(row[ts_index].value), 'time')
+            for entry in row:
+                channel = entry.channel
+                site = channel.site
+                if channel.kind == 'energy':
+                    if count == 0:
+                        energy_results[site]['start'] = entry.value
+                    else:
+                        energy_results[site]['end'] = entry.value
+
+                if channel.kind == 'power':
+                    power_results[site] += entry.value
+
+                    if site in should_calculate_energy:
+                        if count == 0:
+                            energy_results[site]['start'] = 0
+                            energy_results[site]['end'] = 0
+                        elif use_timestamp:
+                            energy_results[site]['end'] += entry.value * (row_ts - last_ts)
+                        else:
+                            energy_results[site]['end'] += entry.value * (1 /
+                                                           measurements_csv.sample_rate_hz)
+
+        # Calculate final measurements
+        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))
+
+        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))
+
+        return derived_measurements

From eeb5e93e6f30887e6e0835f9d033e64c233ff885 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Mon, 7 Aug 2017 15:39:38 +0100
Subject: [PATCH 10/12] Documentation/Instrumentation: Fix typos

---
 doc/instrumentation.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/instrumentation.rst b/doc/instrumentation.rst
index 3c777ac..91a2e64 100644
--- a/doc/instrumentation.rst
+++ b/doc/instrumentation.rst
@@ -151,7 +151,7 @@ Instrument
    Sample rate of the instrument in Hz. Assumed to be the same for all channels.
 
    .. note:: This attribute is only provided by :class:`Instrument`\ s that
-             support ``CONTINUOUS`` measurment.
+             support ``CONTINUOUS`` measurement.
 
 Instrument Channel
 ~~~~~~~~~~~~~~~~~~

From c62905cfdc03938803e3a2dd261953599fb0416f Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Fri, 18 Aug 2017 17:22:47 +0100
Subject: [PATCH 11/12] Instrumentation/Instrumentation: Update `timestamp`
 documentation

---
 doc/instrumentation.rst | 54 +++++++++++++++++++++++++----------------
 1 file changed, 33 insertions(+), 21 deletions(-)

diff --git a/doc/instrumentation.rst b/doc/instrumentation.rst
index 91a2e64..76adf39 100644
--- a/doc/instrumentation.rst
+++ b/doc/instrumentation.rst
@@ -139,6 +139,14 @@ Instrument
    ``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the columns
    will be the same as the order of channels in ``Instrument.active_channels``.
 
+   If reporting timestamps, one channel must have a ``site`` named ``"timestamp"``
+   and a ``kind`` of a :class:`MeasurmentType` of an appropriate time unit which will
+   be used, if appropriate, during any post processing.
+
+   .. note:: Currently supported time units are seconds, milliseconds and
+             microseconds, other units can also be used if an appropriate
+             conversion is provided.
+
    This returns a :class:`MeasurementCsv` instance associated with the outfile
    that can be used to stream :class:`Measurement`\ s lists (similar to what is
    returned by ``take_measurement()``.
@@ -211,27 +219,31 @@ be reported as "power" in Watts, and never as "pwr" in milliWatts. Currently
 defined measurement types are
 
 
-+-------------+---------+---------------+
-| name        | units   | category      |
-+=============+=========+===============+
-| time        | seconds |               |
-+-------------+---------+---------------+
-| temperature | degrees |               |
-+-------------+---------+---------------+
-| power       | watts   | power/energy  |
-+-------------+---------+---------------+
-| voltage     | volts   | power/energy  |
-+-------------+---------+---------------+
-| current     | amps    | power/energy  |
-+-------------+---------+---------------+
-| energy      | joules  | power/energy  |
-+-------------+---------+---------------+
-| tx          | bytes   | data transfer |
-+-------------+---------+---------------+
-| rx          | bytes   | data transfer |
-+-------------+---------+---------------+
-| tx/rx       | bytes   | data transfer |
-+-------------+---------+---------------+
++-------------+-------------+---------------+
+| name        | units       | category      |
++=============+=============+===============+
+| time        | seconds     |               |
++-------------+-------------+---------------+
+| time        | microseconds|               |
++-------------+-------------+---------------+
+| time        | milliseconds|               |
++-------------+-------------+---------------+
+| temperature | degrees     |               |
++-------------+-------------+---------------+
+| power       | watts       | power/energy  |
++-------------+-------------+---------------+
+| voltage     | volts       | power/energy  |
++-------------+-------------+---------------+
+| current     | amps        | power/energy  |
++-------------+-------------+---------------+
+| energy      | joules      | power/energy  |
++-------------+-------------+---------------+
+| tx          | bytes       | data transfer |
++-------------+-------------+---------------+
+| rx          | bytes       | data transfer |
++-------------+-------------+---------------+
+| tx/rx       | bytes       | data transfer |
++-------------+-------------+---------------+
 
 
 .. instruments:

From 60e69fc4e89cdf17649848729fd8b9add0e21a11 Mon Sep 17 00:00:00 2001
From: Marc Bonnici <marc.bonnici@arm.com>
Date: Fri, 18 Aug 2017 17:23:18 +0100
Subject: [PATCH 12/12] Documentation/DerivedMeasurements: Added documentation
 for new API

---
 doc/derived_measurements.rst | 69 ++++++++++++++++++++++++++++++++++++
 doc/index.rst                |  1 +
 2 files changed, 70 insertions(+)
 create mode 100644 doc/derived_measurements.rst

diff --git a/doc/derived_measurements.rst b/doc/derived_measurements.rst
new file mode 100644
index 0000000..fcd497c
--- /dev/null
+++ b/doc/derived_measurements.rst
@@ -0,0 +1,69 @@
+Derived Measurements
+=====================
+
+
+The ``DerivedMeasurements`` API provides a consistent way of performing post
+processing on a provided :class:`MeasurementCsv` file.
+
+Example
+-------
+
+The following example shows how to use an implementation of a
+:class:`DerivedMeasurement` to obtain a list of calculated ``Measurements``.
+
+.. code-block:: ipython
+
+    # Import the relevant derived measurement module
+    # in this example the derived energy module is used.
+    In [1]: from devlib import DerivedEnergyMeasurements
+
+    # Obtain a MeasurementCsv file from an instrument or create from
+    # existing .csv file. In this example an existing csv file is used which was
+    # created with a sampling rate of 100Hz
+    In [2]: from devlib import MeasurementsCsv
+    In [3]: measurement_csv = MeasurementsCsv('/example/measurements.csv', sample_rate_hz=100)
+
+    # Process the file and obtain a list of the derived measurements
+    In [4]: derived_measurements = DerivedEnergyMeasurements.process(measurement_csv)
+
+    In [5]: derived_measurements
+    Out[5]: [device_energy: 239.1854075 joules, device_power: 5.5494089227 watts]
+
+API
+---
+
+Derived Measurements
+~~~~~~~~~~~~~~~~~~~~
+
+.. class:: DerivedMeasurements()
+
+   The ``DerivedMeasurements`` class is an abstract base for implementing
+   additional classes to calculate various metrics.
+
+.. method:: DerivedMeasurements.process(measurement_csv)
+
+   Returns a list of :class:`Measurement` objects that have been calculated.
+
+
+
+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.
+
+  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.
+
+
diff --git a/doc/index.rst b/doc/index.rst
index 2c6d72f..5f4dda5 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -19,6 +19,7 @@ Contents:
    target
    modules
    instrumentation
+   derived_measurements
    platform
    connection