1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3
4###############################################################################
5# Copyright (c) 2012-7 Bryce Adelstein Lelbach aka wash <brycelelbach@gmail.com>
6#
7# Distributed under the Boost Software License, Version 1.0. (See accompanying
8# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
9###############################################################################
10
11###############################################################################
12# Copyright (c) 2018 NVIDIA Corporation
13#
14# Licensed under the Apache License, Version 2.0 (the "License");
15# you may not use this file except in compliance with the License.
16# You may obtain a copy of the License at
17#
18#     http://www.apache.org/licenses/LICENSE-2.0
19#
20# Unless required by applicable law or agreed to in writing, software
21# distributed under the License is distributed on an "AS IS" BASIS,
22# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23# See the License for the specific language governing permissions and
24# limitations under the License.
25###############################################################################
26
27# XXX Put code shared with `compare_benchmark_results.py` in a common place.
28
29# XXX Relative uncertainty.
30
31from sys import exit, stdout
32
33from os.path import splitext
34
35from itertools import imap # Lazy map.
36
37from math import sqrt, log10, floor
38
39from collections import deque
40
41from argparse import ArgumentParser as argument_parser
42
43from csv import DictReader as csv_dict_reader
44from csv import DictWriter as csv_dict_writer
45
46from re import compile as regex_compile
47
48###############################################################################
49
50def unpack_tuple(f):
51  """Return a unary function that calls `f` with its argument unpacked."""
52  return lambda args: f(*iter(args))
53
54def strip_dict(d):
55  """Strip leading and trailing whitespace from all keys and values in `d`."""
56  d.update({key: value.strip() for (key, value) in d.items()})
57
58def merge_dicts(d0, d1):
59  """Create a new `dict` that is the union of `dict`s `d0` and `d1`."""
60  d = d0.copy()
61  d.update(d1)
62  return d
63
64def strip_list(l):
65  """Strip leading and trailing whitespace from all values in `l`."""
66  for i, value in enumerate(l): l[i] = value.strip()
67
68###############################################################################
69
70def int_or_float(x):
71  """Convert `x` to either `int` or `float`, preferring `int`.
72
73  Raises:
74    ValueError : If `x` is not convertible to either `int` or `float`
75  """
76  try:
77    return int(x)
78  except ValueError:
79    return float(x)
80
81def try_int_or_float(x):
82  """Try to convert `x` to either `int` or `float`, preferring `int`. `x` is
83  returned unmodified if conversion fails.
84  """
85  try:
86    return int_or_float(x)
87  except ValueError:
88    return x
89
90###############################################################################
91
92def find_significant_digit(x):
93  """Return the significant digit of the number x. The result is the number of
94  digits after the decimal place to round to (negative numbers indicate rounding
95  before the decimal place)."""
96  if x == 0: return 0
97  return -int(floor(log10(abs(x))))
98
99def round_with_int_conversion(x, ndigits = None):
100  """Rounds `x` to `ndigits` after the the decimal place. If `ndigits` is less
101  than 1, convert the result to `int`. If `ndigits` is `None`, the significant
102  digit of `x` is used."""
103  if ndigits is None: ndigits = find_significant_digit(x)
104  x_rounded = round(x, ndigits)
105  return int(x_rounded) if ndigits < 1 else x_rounded
106
107###############################################################################
108
109class measured_variable(object):
110  """A meta-variable representing measured data. It is composed of three raw
111  variables plus units meta-data.
112
113  Attributes:
114    quantity (`str`) :
115      Name of the quantity variable of this object.
116    uncertainty (`str`) :
117      Name of the uncertainty variable of this object.
118    sample_size (`str`) :
119      Name of the sample size variable of this object.
120    units (units class or `None`) :
121      The units the value is measured in.
122  """
123
124  def __init__(self, quantity, uncertainty, sample_size, units = None):
125    self.quantity    = quantity
126    self.uncertainty = uncertainty
127    self.sample_size = sample_size
128    self.units       = units
129
130  def as_tuple(self):
131    return (self.quantity, self.uncertainty, self.sample_size, self.units)
132
133  def __iter__(self):
134    return iter(self.as_tuple())
135
136  def __str__(self):
137    return str(self.as_tuple())
138
139  def __repr__(self):
140    return str(self)
141
142class measured_value(object):
143  """An object that represents a value determined by multiple measurements.
144
145  Attributes:
146    quantity (scalar) :
147      The quantity of the value, e.g. the arithmetic mean.
148    uncertainty (scalar) :
149      The measurement uncertainty, e.g. the sample standard deviation.
150    sample_size (`int`) :
151      The number of observations contributing to the value.
152    units (units class or `None`) :
153      The units the value is measured in.
154  """
155
156  def __init__(self, quantity, uncertainty, sample_size = 1, units = None):
157    self.quantity    = quantity
158    self.uncertainty = uncertainty
159    self.sample_size = sample_size
160    self.units       = units
161
162  def as_tuple(self):
163    return (self.quantity, self.uncertainty, self.sample_size, self.units)
164
165  def __iter__(self):
166    return iter(self.as_tuple())
167
168  def __str__(self):
169    return str(self.as_tuple())
170
171  def __repr__(self):
172    return str(self)
173
174###############################################################################
175
176def arithmetic_mean(X):
177  """Computes the arithmetic mean of the sequence `X`.
178
179  Let:
180
181    * `n = len(X)`.
182    * `u` denote the arithmetic mean of `X`.
183
184  .. math::
185
186    u = \frac{\sum_{i = 0}^{n - 1} X_i}{n}
187  """
188  return sum(X) / len(X)
189
190def sample_variance(X, u = None):
191  """Computes the sample variance of the sequence `X`.
192
193  Let:
194
195    * `n = len(X)`.
196    * `u` denote the arithmetic mean of `X`.
197    * `s` denote the sample standard deviation of `X`.
198
199  .. math::
200
201    v = \frac{\sum_{i = 0}^{n - 1} (X_i - u)^2}{n - 1}
202
203  Args:
204    X (`Iterable`) : The sequence of values.
205    u (number)     : The arithmetic mean of `X`.
206  """
207  if u is None: u = arithmetic_mean(X)
208  return sum(imap(lambda X_i: (X_i - u) ** 2, X)) / (len(X) - 1)
209
210def sample_standard_deviation(X, u = None, v = None):
211  """Computes the sample standard deviation of the sequence `X`.
212
213  Let:
214
215    * `n = len(X)`.
216    * `u` denote the arithmetic mean of `X`.
217    * `v` denote the sample variance of `X`.
218    * `s` denote the sample standard deviation of `X`.
219
220  .. math::
221
222    s &= \sqrt{v}
223      &= \sqrt{\frac{\sum_{i = 0}^{n - 1} (X_i - u)^2}{n - 1}}
224
225  Args:
226    X (`Iterable`) : The sequence of values.
227    u (number)     : The arithmetic mean of `X`.
228    v (number)     : The sample variance of `X`.
229  """
230  if u is None: u = arithmetic_mean(X)
231  if v is None: v = sample_variance(X, u)
232  return sqrt(v)
233
234def combine_sample_size(As):
235  """Computes the combined sample variance of a group of `measured_value`s.
236
237  Let:
238
239    * `g = len(As)`.
240    * `n_i = As[i].samples`.
241    * `n` denote the combined sample size of `As`.
242
243  .. math::
244
245    n = \sum{i = 0}^{g - 1} n_i
246  """
247  return sum(imap(unpack_tuple(lambda u_i, s_i, n_i, t_i: n_i), As))
248
249def combine_arithmetic_mean(As, n = None):
250  """Computes the combined arithmetic mean of a group of `measured_value`s.
251
252  Let:
253
254    * `g = len(As)`.
255    * `u_i = As[i].quantity`.
256    * `n_i = As[i].samples`.
257    * `n` denote the combined sample size of `As`.
258    * `u` denote the arithmetic mean of the quantities of `As`.
259
260  .. math::
261
262    u = \frac{\sum{i = 0}^{g - 1} n_i u_i}{n}
263  """
264  if n is None: n = combine_sample_size(As)
265  return sum(imap(unpack_tuple(lambda u_i, s_i, n_i, t_i: n_i * u_i), As)) / n
266
267def combine_sample_variance(As, n = None, u = None):
268  """Computes the combined sample variance of a group of `measured_value`s.
269
270  Let:
271
272    * `g = len(As)`.
273    * `u_i = As[i].quantity`.
274    * `s_i = As[i].uncertainty`.
275    * `n_i = As[i].samples`.
276    * `n` denote the combined sample size of `As`.
277    * `u` denote the arithmetic mean of the quantities of `As`.
278    * `v` denote the sample variance of `X`.
279
280  .. math::
281
282    v = \frac{(\sum_{i = 0}^{g - 1} n_i (u_i - u)^2 + s_i^2 (n_i - 1))}{n - 1}
283
284  Args:
285    As (`Iterable` of `measured_value`s) : The sequence of values.
286    n (number)                           : The combined sample sizes of `As`.
287    u (number)                           : The combined arithmetic mean of `As`.
288  """
289  if n <= 1: return 0
290  if n is None: n = combine_sample_size(As)
291  if u is None: u = combine_arithmetic_mean(As, n)
292  return sum(imap(unpack_tuple(
293    lambda u_i, s_i, n_i, t_i: n_i * (u_i - u) ** 2 + (s_i ** 2) * (n_i - 1)
294  ), As)) / (n - 1)
295
296def combine_sample_standard_deviation(As, n = None, u = None, v = None):
297  """Computes the combined sample standard deviation of a group of
298  `measured_value`s.
299
300  Let:
301
302    * `g = len(As)`.
303    * `u_i = As[i].quantity`.
304    * `s_i = As[i].uncertainty`.
305    * `n_i = As[i].samples`.
306    * `n` denote the combined sample size of `As`.
307    * `u` denote the arithmetic mean of the quantities of `As`.
308    * `v` denote the sample variance of `X`.
309    * `s` denote the sample standard deviation of `X`.
310
311  .. math::
312
313    s &= \sqrt{v}
314      &= \sqrt{\frac{(\sum_{i = 0}^{g - 1} n_i (u_i - u)^2 + s_i^2 (n_i - 1))}{n - 1}}
315
316  Args:
317    As (`Iterable` of `measured_value`s) : The sequence of values.
318    n (number)                           : The combined sample sizes of `As`.
319    u (number)                           : The combined arithmetic mean of `As`.
320    v (number)                           : The combined sample variance of `As`.
321  """
322  if n <= 1: return 0
323  if n is None: n = combine_sample_size(As)
324  if u is None: u = combine_arithmetic_mean(As, n)
325  if v is None: v = combine_sample_variance(As, n, u)
326  return sqrt(v)
327
328###############################################################################
329
330def process_program_arguments():
331  ap = argument_parser(
332    description = (
333      "Aggregates the results of multiple runs of benchmark results stored in "
334      "CSV format."
335    )
336  )
337
338  ap.add_argument(
339    "-d", "--dependent-variable",
340    help = ("Treat the specified three variables as a dependent variable. The "
341            "1st variable is the measured quantity, the 2nd is the uncertainty "
342            "of the measurement and the 3rd is the sample size. The defaults "
343            "are the dependent variables of Thrust's benchmark suite. May be "
344            "specified multiple times."),
345    action = "append", type = str, dest = "dependent_variables",
346    metavar = "QUANTITY,UNCERTAINTY,SAMPLES"
347  )
348
349  ap.add_argument(
350    "-p", "--preserve-whitespace",
351    help = ("Don't trim leading and trailing whitespace from each CSV cell."),
352    action = "store_true", default = False
353  )
354
355  ap.add_argument(
356    "-o", "--output-file",
357    help = ("The file that results are written to. If `-`, results are "
358            "written to stdout."),
359    action = "store", type = str, default = "-",
360    metavar = "OUTPUT"
361  )
362
363  ap.add_argument(
364    "input_files",
365    help = ("Input CSV files. The first two rows should be a header. The 1st "
366            "header row specifies the name of each variable, and the 2nd "
367            "header row specifies the units for that variable."),
368    type = str, nargs = "+",
369    metavar = "INPUTS"
370  )
371
372  return ap.parse_args()
373
374###############################################################################
375
376def filter_comments(f, s = "#"):
377  """Return an iterator to the file `f` which filters out all lines beginning
378  with `s`."""
379  return filter(lambda line: not line.startswith(s), f)
380
381###############################################################################
382
383class io_manager(object):
384  """Manages I/O operations and represents the input data as an `Iterable`
385  sequence of `dict`s.
386
387  It is `Iterable` and an `Iterator`. It can be used with `with`.
388
389  Attributes:
390    preserve_whitespace (`bool`) :
391      If `False`, leading and trailing whitespace is stripped from each CSV cell.
392    writer (`csv_dict_writer`) :
393      CSV writer object that the output is written to.
394    output_file (`file` or `stdout`) :
395      The output `file` object.
396    readers (`list` of `csv_dict_reader`s) :
397      List of input files as CSV reader objects.
398    input_files (list of `file`s) :
399      List of input `file` objects.
400    variable_names (`list` of `str`s) :
401      Names of the variables, in order.
402    variable_units (`list` of `str`s) :
403      Units of the variables, in order.
404  """
405
406  def __init__(self, input_files, output_file, preserve_whitespace = True):
407    """Read input files and open the output file and construct a new `io_manager`
408    object.
409
410    If `preserve_whitespace` is `False`, leading and trailing whitespace is
411    stripped from each CSV cell.
412
413    Raises
414      AssertionError :
415        If `len(input_files) <= 0` or `type(preserve_whitespace) != bool`.
416    """
417    assert len(input_files) > 0, "No input files provided."
418
419    assert type(preserve_whitespace) == bool
420
421    self.preserve_whitespace = preserve_whitespace
422
423    self.readers = deque()
424
425    self.variable_names = None
426    self.variable_units = None
427
428    self.input_files = deque()
429
430    for input_file in input_files:
431      input_file_object = open(input_file)
432      reader = csv_dict_reader(filter_comments(input_file_object))
433
434      if not self.preserve_whitespace:
435        strip_list(reader.fieldnames)
436
437      if self.variable_names is None:
438        self.variable_names = reader.fieldnames
439      else:
440        # Make sure all inputs have the same schema.
441        assert self.variable_names == reader.fieldnames,                      \
442          "Input file (`" + input_file + "`) variable schema `"             + \
443          str(reader.fieldnames) + "` does not match the variable schema `" + \
444          str(self.variable_names) + "`."
445
446      # Consume the next row, which should be the second line of the header.
447      variable_units = reader.next()
448
449      if not self.preserve_whitespace:
450        strip_dict(variable_units)
451
452      if self.variable_units is None:
453        self.variable_units = variable_units
454      else:
455        # Make sure all inputs have the same units schema.
456        assert self.variable_units == variable_units,                         \
457          "Input file (`" + input_file + "`) units schema `"                + \
458          str(variable_units) + "` does not match the units schema `"       + \
459          str(self.variable_units) + "`."
460
461      self.readers.append(reader)
462      self.input_files.append(input_file_object)
463
464    if   output_file == "-": # Output to stdout.
465      self.output_file = stdout
466    else:                    # Output to user-specified file.
467      self.output_file = open(output_file, "w")
468
469    self.writer = csv_dict_writer(
470      self.output_file, fieldnames = self.variable_names
471    )
472
473  def __enter__(self):
474    """Called upon entering a `with` statement."""
475    return self
476
477  def __exit__(self, *args):
478    """Called upon exiting a `with` statement."""
479    if   self.output_file is stdout:
480      self.output_file = None
481    elif self.output_file is not None:
482      self.output_file.__exit__(*args)
483
484    for input_file in self.input_files:
485      input_file.__exit__(*args)
486
487  #############################################################################
488  # Input Stream.
489
490  def __iter__(self):
491    """Return an iterator to the input sequence.
492
493    This is a requirement for the `Iterable` protocol.
494    """
495    return self
496
497  def next(self):
498    """Consume and return the next record (a `dict` representing a CSV row) in
499    the input.
500
501    This is a requirement for the `Iterator` protocol.
502
503    Raises:
504      StopIteration : If there is no more input.
505    """
506    if len(self.readers) == 0:
507      raise StopIteration()
508
509    try:
510      row = self.readers[0].next()
511      if not self.preserve_whitespace: strip_dict(row)
512      return row
513    except StopIteration:
514      # The current reader is empty, so pop it, pop it's input file, close the
515      # input file, and then call ourselves again.
516      self.readers.popleft()
517      self.input_files.popleft().close()
518      return self.next()
519
520  #############################################################################
521  # Output.
522
523  def write_header(self):
524    """Write the header for the output CSV file."""
525    # Write the first line of the header.
526    self.writer.writeheader()
527
528    # Write the second line of the header.
529    self.writer.writerow(self.variable_units)
530
531  def write(self, d):
532    """Write a record (a `dict`) to the output CSV file."""
533    self.writer.writerow(d)
534
535###############################################################################
536
537class dependent_variable_parser(object):
538  """Parses a `--dependent-variable=AVG,STDEV,TRIALS` command line argument."""
539
540  #############################################################################
541  # Grammar
542
543  # Parse a variable_name.
544  variable_name_rule = r'[^,]+'
545
546  # Parse a variable classification.
547  dependent_variable_rule = r'(' + variable_name_rule + r')'   \
548                          + r','                               \
549                          + r'(' + variable_name_rule + r')'   \
550                          + r','                               \
551                          + r'(' + variable_name_rule + r')'
552
553  engine = regex_compile(dependent_variable_rule)
554
555  #############################################################################
556
557  def __call__(self, s):
558    """Parses the string `s` with the form "AVG,STDEV,TRIALS".
559
560    Returns:
561      A `measured_variable`.
562
563    Raises:
564      AssertionError : If parsing fails.
565    """
566
567    match = self.engine.match(s)
568
569    assert match is not None,                                          \
570      "Dependent variable (-d) `" +s+ "` is invalid, the format is " + \
571      "`AVG,STDEV,TRIALS`."
572
573    return measured_variable(match.group(1), match.group(2), match.group(3))
574
575###############################################################################
576
577class record_aggregator(object):
578  """Consumes and combines records and represents the result as an `Iterable`
579  sequence of `dict`s.
580
581  It is `Iterable` and an `Iterator`.
582
583  Attributes:
584    dependent_variables (`list` of `measured_variable`s) :
585      A list of dependent variables provided on the command line.
586    dataset (`dict`) :
587      A mapping of distinguishing (e.g. control + independent) values (`tuple`s
588      of variable-quantity pairs) to `list`s of dependent values (`dict`s from
589      variables to lists of cells).
590    in_order_dataset_keys :
591      A list of unique dataset keys (e.g. distinguishing variables) in order of
592      appearance.
593  """
594
595  parse_dependent_variable = dependent_variable_parser()
596
597  def __init__(self, raw_dependent_variables):
598    """Parse dependent variables and construct a new `record_aggregator` object.
599
600    Raises:
601      AssertionError : If parsing of dependent variables fails.
602    """
603    self.dependent_variables = []
604
605    if raw_dependent_variables is not None:
606      for variable in raw_dependent_variables:
607        self.dependent_variables.append(self.parse_dependent_variable(variable))
608
609    self.dataset = {}
610
611    self.in_order_dataset_keys = deque()
612
613  #############################################################################
614  # Insertion.
615
616  def append(self, record):
617    """Add `record` to the dataset.
618
619    Raises:
620      ValueError : If any `str`-to-numeric conversions fail.
621    """
622    # The distinguishing variables are the control and independent variables.
623    # They form the key for each record in the dataset. Records with the same
624    # distinguishing variables are treated as observations of the same data
625    # point.
626    dependent_values = {}
627
628    # To allow the same sample size variable to be used for multiple dependent
629    # variables, we don't pop sample size variables until we're done processing
630    # all variables.
631    sample_size_variables = []
632
633    # Separate the dependent values from the distinguishing variables and
634    # perform `str`-to-numeric conversions.
635    for variable in self.dependent_variables:
636      quantity, uncertainty, sample_size, units = variable.as_tuple()
637
638      dependent_values[quantity]    = [int_or_float(record.pop(quantity))]
639      dependent_values[uncertainty] = [int_or_float(record.pop(uncertainty))]
640      dependent_values[sample_size] = [int(record[sample_size])]
641
642      sample_size_variables.append(sample_size)
643
644    # Pop sample size variables.
645    for sample_size_variable in sample_size_variables:
646      # Allowed to fail, as we may have duplicates.
647      record.pop(sample_size_variable, None)
648
649    # `dict`s aren't hashable, so create a tuple of key-value pairs.
650    distinguishing_values = tuple(record.items())
651
652    if distinguishing_values in self.dataset:
653      # These distinguishing values already exist, so get the `dict` they're
654      # mapped to, look up each key in `dependent_values` in the `dict`, and
655      # add the corresponding quantity in `dependent_values` to the list in the
656      # the `dict`.
657      for variable, columns in dependent_values.iteritems():
658        self.dataset[distinguishing_values][variable] += columns
659    else:
660      # These distinguishing values aren't in the dataset, so add them and
661      # record them in `in_order_dataset_keys`.
662      self.dataset[distinguishing_values] = dependent_values
663      self.in_order_dataset_keys.append(distinguishing_values)
664
665  #############################################################################
666  # Postprocessing.
667
668  def combine_dependent_values(self, dependent_values):
669    """Takes a mapping of dependent variables to lists of cells and returns
670    a new mapping with the cells combined.
671
672    Raises:
673      AssertionError : If class invariants were violated.
674    """
675    combined_dependent_values = dependent_values.copy()
676
677    for variable in self.dependent_variables:
678      quantity, uncertainty, sample_size, units = variable.as_tuple()
679
680      quantities    = dependent_values[quantity]
681      uncertainties = dependent_values[uncertainty]
682      sample_sizes  = dependent_values[sample_size]
683
684      if type(sample_size) is list:
685        # Sample size hasn't been combined yet.
686        assert len(quantities)    == len(uncertainties)                       \
687           and len(uncertainties) == len(sample_sizes),                       \
688          "Length of quantities list `(" + str(len(quantities)) + ")`, "    + \
689          "length of uncertainties list `(" + str(len(uncertainties))       + \
690          "),` and length of sample sizes list `(" + str(len(sample_sizes)) + \
691          ")` are not the same."
692      else:
693        # Another dependent variable that uses our sample size has combined it
694        # already.
695        assert len(quantities) == len(uncertainties),                         \
696          "Length of quantities list `(" + str(len(quantities)) + ")` and " + \
697          "length of uncertainties list `(" + str(len(uncertainties))       + \
698          ")` are not the same."
699
700      # Convert the three separate `list`s into one list of `measured_value`s.
701      measured_values = []
702
703      for i in range(len(quantities)):
704        mv = measured_value(
705          quantities[i], uncertainties[i], sample_sizes[i], units
706        )
707
708        measured_values.append(mv)
709
710      # Combine the `measured_value`s.
711      combined_sample_size = combine_sample_size(
712        measured_values
713      )
714
715      combined_arithmetic_mean = combine_arithmetic_mean(
716        measured_values, combined_sample_size
717      )
718
719      combined_sample_standard_deviation = combine_sample_standard_deviation(
720        measured_values, combined_sample_size, combined_arithmetic_mean
721      )
722
723      # Round the quantity and uncertainty to the significant digit of
724      # uncertainty and insert the combined values into the results.
725      sigdig = find_significant_digit(combined_sample_standard_deviation)
726
727#      combined_arithmetic_mean = round_with_int_conversion(
728#        combined_arithmetic_mean, sigdig
729#      )
730
731#      combined_sample_standard_deviation = round_with_int_conversion(
732#        combined_sample_standard_deviation, sigdig
733#      )
734
735      combined_dependent_values[quantity]    = combined_arithmetic_mean
736      combined_dependent_values[uncertainty] = combined_sample_standard_deviation
737      combined_dependent_values[sample_size] = combined_sample_size
738
739    return combined_dependent_values
740
741  #############################################################################
742  # Output Stream.
743
744  def __iter__(self):
745    """Return an iterator to the output sequence of separated distinguishing
746    variables and dependent variables (a tuple of two `dict`s).
747
748    This is a requirement for the `Iterable` protocol.
749    """
750    return self
751
752  def records(self):
753    """Return an iterator to the output sequence of CSV rows (`dict`s of
754    variables to values).
755    """
756    return imap(unpack_tuple(lambda dist, dep: merge_dicts(dist, dep)), self)
757
758  def next(self):
759    """Produce the components of the next output record - a tuple of two
760    `dict`s. The first `dict` is a mapping of distinguishing variables to
761    distinguishing values, the second `dict` is a mapping of dependent
762    variables to combined dependent values. Combining the two dicts forms a
763    CSV row suitable for output.
764
765    This is a requirement for the `Iterator` protocol.
766
767    Raises:
768      StopIteration  : If there is no more output.
769      AssertionError : If class invariants were violated.
770    """
771    assert len(self.dataset.keys()) == len(self.in_order_dataset_keys),      \
772      "Number of dataset keys (`" + str(len(self.dataset.keys()))          + \
773      "`) is not equal to the number of keys in the ordering list (`"      + \
774      str(len(self.in_order_dataset_keys)) + "`)."
775
776    if len(self.in_order_dataset_keys) == 0:
777      raise StopIteration()
778
779    # Get the next set of distinguishing values and convert them to a `dict`.
780    raw_distinguishing_values = self.in_order_dataset_keys.popleft()
781    distinguishing_values     = dict(raw_distinguishing_values)
782
783    dependent_values = self.dataset.pop(raw_distinguishing_values)
784
785    combined_dependent_values = self.combine_dependent_values(dependent_values)
786
787    return (distinguishing_values, combined_dependent_values)
788
789###############################################################################
790
791args = process_program_arguments()
792
793if args.dependent_variables is None:
794  args.dependent_variables = [
795    "STL Average Walltime,STL Walltime Uncertainty,STL Trials",
796    "STL Average Throughput,STL Throughput Uncertainty,STL Trials",
797    "Thrust Average Walltime,Thrust Walltime Uncertainty,Thrust Trials",
798    "Thrust Average Throughput,Thrust Throughput Uncertainty,Thrust Trials"
799  ]
800
801# Read input files and open the output file.
802with io_manager(args.input_files,
803                args.output_file,
804                args.preserve_whitespace) as iom:
805  # Parse dependent variable options.
806  ra = record_aggregator(args.dependent_variables)
807
808  # Add all input data to the `record_aggregator`.
809  for record in iom:
810    ra.append(record)
811
812  iom.write_header()
813
814  # Write combined results out.
815  for record in ra.records():
816    iom.write(record)
817
818