ctools  2.0.0
 All Classes Namespaces Files Functions Variables Macros Pages
csfootprint.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 # ==========================================================================
3 # Carbon footprint report script
4 #
5 # Copyright (C) 2022 Juergen Knoedlseder
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #
20 # ==========================================================================
21 import os
22 import pwd
23 import sys
24 import inspect
25 import gammalib
26 import ctools
27 
28 
29 # ================= #
30 # csfootprint class #
31 # ================= #
32 class csfootprint(ctools.cscript):
33  """
34  Carbon footprint report script
35  """
36 
37  # Constructor
38  def __init__(self, *argv):
39  """
40  Constructor
41 
42  Parameters
43  ----------
44  argv : list of str
45  List of IRAF command line parameter strings of the form
46  ``parameter=3``.
47  """
48  # Initialise application by calling the base class constructor
49  self._init_cscript(self.__class__.__name__, ctools.__version__, argv)
50 
51  # Initialise members
52  self._statistics = {}
53 
54  # Return
55  return
56 
57  # State methods for pickling
58  def __getstate__(self):
59  """
60  Extend ctools.cscript __getstate__ method
61 
62  Returns
63  -------
64  state : dict
65  Pickled instance
66  """
67  # Set pickled dictionary
68  state = {'base' : ctools.cscript.__getstate__(self),
69  'statistics' : self._statistics}
70 
71  # Return pickled dictionary
72  return state
73 
74  def __setstate__(self, state):
75  """
76  Extend ctools.cscript __setstate__ method
77 
78  Parameters
79  ----------
80  state : dict
81  Pickled instance
82  """
83  # Set state
84  ctools.cscript.__setstate__(self, state['base'])
85  self._statistics = state['statistics']
86 
87  # Return
88  return
89 
90 
91  # Private methods
92  def _get_parameters(self):
93  """
94  Get parameters from parfile
95  """
96  # Query input parameters
97  self['infile'].filename()
98  if self['tmin'].is_valid():
99  self['tmin'].time()
100  if self['tmax'].is_valid():
101  self['tmax'].time()
102 
103  # Query ahead output model filename
104  if self._read_ahead():
105  if self['outfile'].is_valid():
106  self['outfile'].filename()
107 
108  # Write input parameters into logger
109  self._log_parameters(gammalib.TERSE)
110 
111  # Return
112  return
113 
114  # Load statistics
115  def _load_statistics(self):
116  """
117  Load statistics
118  """
119  # Initialise statistics
120  statistics = {}
121 
122  # Get filename
123  infile = self['infile'].filename()
124 
125  # Load XML file
126  xml = gammalib.GXml()
127  xml.load(infile)
128 
129  # Get useful nodes
130  header = xml.element('statistics > header')
131  data = xml.element('statistics > data')
132  dates = header.element('dates')
133  countries = data.element('countries')
134  versions = data.element('versions')
135  daily = data.element('daily')
136 
137  # Extract header dates information
138  statistics['creation'] = dates.element('creation').string()
139  statistics['modified'] = dates.element('modified').string()
140  statistics['start'] = dates.element('start').string()
141  statistics['stop'] = dates.element('stop').string()
142 
143  # Set used time interval [start,stop]
144  if self['tmin'].is_valid():
145  start = self['tmin'].time().utc()
146  if start < statistics['start']:
147  start = statistics['start']
148  else:
149  start = statistics['start']
150  if self['tmax'].is_valid():
151  stop = self['tmax'].time().utc()
152  if stop > statistics['stop']:
153  stop = statistics['stop']
154  else:
155  stop = statistics['stop']
156 
157  # Set used time interval
158  statistics['use_start'] = start
159  statistics['use_stop'] = stop
160 
161  # Extract list of countries
162  statistics['countries'] = []
163  num = countries.elements()
164  for i in range(num):
165  element = countries.element(i)
166  country = element.name()
167  calls = element.attribute('calls')
168  wall = element.attribute('wall')
169  cpu = element.attribute('cpu')
170  gCO2e = element.attribute('gCO2e')
171  entry = {'country': country, 'calls': calls, 'wall': wall,
172  'cpu': cpu, 'gCO2e': gCO2e}
173  statistics['countries'].append(entry)
174 
175  # Extract list of versions
176  statistics['versions'] = []
177  num = versions.elements()
178  for i in range(num):
179  element = versions.element(i)
180  version = element.name()
181  calls = element.attribute('calls')
182  wall = element.attribute('wall')
183  cpu = element.attribute('cpu')
184  gCO2e = element.attribute('gCO2e')
185  entry = {'version': version, 'calls': calls, 'wall': wall,
186  'cpu': cpu, 'gCO2e': gCO2e}
187  statistics['versions'].append(entry)
188 
189  # Extract daily and tools statistics
190  statistics['daily'] = []
191  statistics['tools'] = []
192  num = daily.elements()
193  total_calls = 0
194  total_wall = 0.0
195  total_cpu = 0.0
196  total_gCO2e = 0.0
197  for i in range(num):
198 
199  # Get date
200  element = daily.element(i)
201  date = element.element('value',0).string()
202 
203  # Continue if date is out of range
204  if date < start[0:10] or date > stop[0:10]:
205  continue
206 
207  # Collect information
208  tools = element.element('tools',0)
209  ntools = tools.elements()
210  sum_calls = 0
211  sum_wall = 0.0
212  sum_cpu = 0.0
213  sum_gCO2e = 0.0
214  for k in range(ntools):
215 
216  # Extract information
217  tool = tools.element(k)
218  name = tool.name()
219  calls = int(tool.attribute('calls'))
220  wall = float(tool.attribute('wall'))
221  cpu = float(tool.attribute('cpu'))
222  gCO2e = float(tool.attribute('gCO2e'))
223 
224  # Update sums
225  sum_calls += calls
226  sum_wall += wall
227  sum_cpu += cpu
228  sum_gCO2e += gCO2e
229 
230  # Update tools data
231  exists = False
232  for s in statistics['tools']:
233  if name == s['name']:
234  s['calls'] += calls
235  s['wall'] += wall
236  s['cpu'] += cpu
237  s['gCO2e'] += gCO2e
238  exists = True
239  break
240  if not exists:
241  entry = {'name': name, 'calls': calls, 'wall': wall,
242  'cpu': cpu, 'gCO2e': gCO2e}
243  statistics['tools'].append(entry)
244 
245  # Update totals
246  total_calls += sum_calls
247  total_wall += sum_wall
248  total_cpu += sum_cpu
249  total_gCO2e += sum_gCO2e
250 
251  # Set entry
252  entry = {'date': date, 'calls': sum_calls, 'wall': sum_wall,
253  'cpu': sum_cpu, 'gCO2e': sum_gCO2e}
254  statistics['daily'].append(entry)
255 
256  # Update total statistics
257  statistics['calls'] = total_calls
258  statistics['wall'] = total_wall
259  statistics['cpu'] = total_cpu
260  statistics['gCO2e'] = total_gCO2e
261 
262  # Return statistics
263  return statistics
264 
265  # Format time
266  def _format_time(self, seconds):
267  """
268  Format time
269 
270  Parameters
271  ----------
272  seconds : float
273  Time in seconds
274  """
275  # Format according to precision
276  if seconds < 60.0:
277  format = '%.1f seconds' % (seconds)
278  elif seconds < 3600.0:
279  format = '%.1f minutes' % (seconds/60.0)
280  else:
281  format = '%.1f hours' % (seconds/3600.0)
282 
283  # Return format
284  return format
285 
286  # Format carbon footprint
287  def _format_footprint(self, gCO2e, latex=False):
288  """
289  Format carbon footprint
290 
291  Parameters
292  ----------
293  gCO2e : float
294  Carbon footprint in gCO2e
295  latex : bool, optional
296  Use LaTeX formatting
297  """
298  # Set unit
299  if latex:
300  unit = r'CO$_2$e'
301  else:
302  unit = 'CO2e'
303 
304  # Format according to precision
305  if gCO2e < 1000.0:
306  format = '%.1f g %s' % (gCO2e, unit)
307  elif gCO2e < 1.0e6:
308  format = '%.1f kg %s' % (gCO2e/1000.0, unit)
309  else:
310  format = '%.1f t %s' % (gCO2e/1.0e6, unit)
311 
312  # Return format
313  return format
314 
315  # Get global information
316  def _get_global_information(self, statistics, latex=False):
317  """
318  Get global information from statistics
319 
320  Parameters
321  ----------
322  statistics : dict
323  Statistics dictionary
324  latex : bool, optional
325  Use LaTeX formatting
326  """
327  # Derive information
328  tstart = gammalib.GTime(statistics['use_start'])
329  tstop = gammalib.GTime(statistics['use_stop'])
330  duration = tstop - tstart
331  cpu_hours = statistics['cpu']/3600.0
332  if statistics['cpu'] != 0.0:
333  ci_cpu = statistics['gCO2e']/cpu_hours
334  else:
335  ci_cpu = 0.0
336  if duration != 0.0:
337  fp_dur = statistics['gCO2e']/(duration * gammalib.sec2day)
338  else:
339  fp_dur = 0.0
340  fp_yr = fp_dur * 365.25
341  dur_str = self._format_time(duration)
342  if statistics['wall'] != 0.0:
343  load_str = '%.1f %%' % (statistics['cpu']/statistics['wall']*100.0)
344  else:
345  load_str = 'undefined'
346  ci_cpu_str = self._format_footprint(ci_cpu, latex) + ' / CPU hour'
347  fp_dur_str = self._format_footprint(fp_dur, latex) + ' / day'
348  fp_yr_str = self._format_footprint(fp_yr, latex) + ' / year'
349  wall_str = self._format_time(statistics['wall'])
350  cpu_str = self._format_time(statistics['cpu'])
351  gCO2e_str = self._format_footprint(statistics['gCO2e'], latex)
352  date_str = '%s - %s' % (statistics['start'], statistics['stop'])
353  use_str = '%s - %s' % (statistics['use_start'], statistics['use_stop'])
354 
355  # Split footprint into infrastructure and electricity use footprint
356  if statistics['cpu'] != 0.0:
357  ef_kWh = 108.0
358  ef_total = 4.68
359  ef_electricity = 2.43
360  ef_other = ef_total - ef_electricity
361  val_ef = (ci_cpu - ef_other) * ef_kWh / ef_electricity
362  gCO2e_infra = ef_other * cpu_hours
363  gCO2e_elect = ef_electricity * val_ef / ef_kWh * cpu_hours
364  else:
365  val_ef = 0.0
366  gCO2e_infra = 0.0
367  gCO2e_elect = 0.0
368  gCO2e_kWh_str = self._format_footprint(val_ef, latex) + ' / kWh'
369  gCO2e_infra_str = self._format_footprint(gCO2e_infra, latex)
370  gCO2e_elect_str = self._format_footprint(gCO2e_elect, latex)
371 
372  # Build electricity footprint string
373  elect_str = '%s (%s)' % (gCO2e_elect_str, gCO2e_kWh_str)
374 
375  # Build result directory
376  result = {'ci_cpu': ci_cpu_str,
377  'fp_dur': fp_dur_str,
378  'fp_yr': fp_yr_str,
379  'wall': wall_str,
380  'cpu': cpu_str,
381  'gCO2e': gCO2e_str,
382  'date': date_str,
383  'use': use_str,
384  'dur': dur_str,
385  'load': load_str,
386  'gCO2e_kWh': gCO2e_kWh_str,
387  'gCO2e_infra': gCO2e_infra_str,
388  'gCO2e_elect': gCO2e_elect_str,
389  'elect': elect_str}
390 
391  # Return result
392  return result
393 
394  # Log global statistics
395  def _global_statistics(self, statistics):
396  """
397  Log global statistics
398 
399  Parameters
400  ----------
401  statistics : dict
402  Statistics dictionary
403  """
404  # Log header
405  self._log_header1(gammalib.TERSE, 'Global statistics')
406 
407  # Derive information from statistics
408  info = self._get_global_information(statistics)
409 
410  # Log report information
411  self._log_value(gammalib.NORMAL, 'Creation date', statistics['creation'])
412  self._log_value(gammalib.NORMAL, 'Last statistics update', statistics['modified'])
413  self._log_value(gammalib.NORMAL, 'Statistics date interval', info['date'])
414  self._log_value(gammalib.NORMAL, 'Used date interval', info['use'])
415  self._log_value(gammalib.NORMAL, 'Duration of used interval', info['dur'])
416  self._log_value(gammalib.NORMAL, 'Total number of ctool runs', statistics['calls'])
417  self._log_value(gammalib.NORMAL, 'Total wall clock time', info['wall'])
418  self._log_value(gammalib.NORMAL, 'Total CPU time', info['cpu'])
419  self._log_value(gammalib.NORMAL, 'Average CPU load', info['load'])
420  self._log_value(gammalib.NORMAL, 'Total carbon footprint', info['gCO2e'])
421  self._log_value(gammalib.NORMAL, ' due to power consumption', info['elect'])
422  self._log_value(gammalib.NORMAL, ' due to infrastructure', info['gCO2e_infra'])
423  self._log_value(gammalib.NORMAL, 'Average carbon intensity', info['ci_cpu'])
424  self._log_value(gammalib.NORMAL, 'Average daily footprint', info['fp_dur'])
425  self._log_value(gammalib.NORMAL, 'Expected annual footprint', info['fp_yr'])
426 
427  # Return
428  return
429 
430  # Log daily statistics
431  def _daily_statistics(self, statistics):
432  """
433  Log daily statistics
434 
435  Parameters
436  ----------
437  statistics : dict
438  Statistics dictionary
439  """
440  # Log header
441  self._log_header1(gammalib.TERSE, 'Daily statistics')
442 
443  # Log daily carbon footprint statistics
444  self._log_header3(gammalib.NORMAL, 'Carbon footprint')
445 
446  # Loop over daily entries
447  for entry in statistics['daily']:
448  self._log_value(gammalib.NORMAL, entry['date'], self._format_footprint(entry['gCO2e']))
449 
450  # Log daily run statistics
451  self._log_header3(gammalib.NORMAL, 'ctools or cscript calls')
452 
453  # Loop over daily entries
454  for entry in statistics['daily']:
455  self._log_value(gammalib.NORMAL, entry['date'], entry['calls'])
456 
457  # Log daily carbon footprint statistics
458  self._log_header3(gammalib.NORMAL, 'Used wall clock time')
459 
460  # Loop over daily entries
461  for entry in statistics['daily']:
462  self._log_value(gammalib.NORMAL, entry['date'], self._format_time(entry['wall']))
463 
464  # Log daily carbon footprint statistics
465  self._log_header3(gammalib.NORMAL, 'Used CPU time')
466 
467  # Loop over daily entries
468  for entry in statistics['daily']:
469  self._log_value(gammalib.NORMAL, entry['date'], self._format_time(entry['cpu']))
470 
471  # Return
472  return
473 
474  # Log tools statistics
475  def _tools_statistics(self, statistics):
476  """
477  Log tools statistics
478 
479  Parameters
480  ----------
481  statistics : dict
482  Statistics dictionary
483  """
484  # Get Python version information
485  req_version = (2,4)
486  cur_version = sys.version_info
487 
488  # Log header
489  self._log_header1(gammalib.TERSE, 'ctools and cscripts statistics')
490 
491  # Log daily carbon footprint statistics
492  self._log_header3(gammalib.NORMAL, 'Carbon footprint')
493 
494  # Optionally sort list
495  if cur_version > req_version:
496  sorted_entries = sorted(statistics['tools'], key=lambda d: d['gCO2e'], reverse=True)
497  else:
498  sorted_entries = statistics['tools']
499 
500  # Loop over entries
501  for i, entry in enumerate(sorted_entries):
502  if i < 10:
503  level = gammalib.NORMAL
504  else:
505  level = gammalib.EXPLICIT
506  self._log_value(level, entry['name'], self._format_footprint(entry['gCO2e']))
507  if len(sorted_entries) > 9 and self['chatter'].integer() < 3:
508  self._log_string(gammalib.NORMAL, ' ... (list truncated after 10 entries) ...')
509 
510  # Log daily run statistics
511  self._log_header3(gammalib.NORMAL, 'ctools or cscript calls')
512 
513  # Optionally sort list
514  if cur_version > req_version:
515  sorted_entries = sorted(statistics['tools'], key=lambda d: d['calls'], reverse=True)
516  else:
517  sorted_entries = statistics['tools']
518 
519  # Loop over entries
520  for i, entry in enumerate(sorted_entries):
521  if i < 10:
522  level = gammalib.NORMAL
523  else:
524  level = gammalib.EXPLICIT
525  self._log_value(level, entry['name'], entry['calls'])
526  if len(sorted_entries) > 9 and self['chatter'].integer() < 3:
527  self._log_string(gammalib.NORMAL, ' ... (list truncated after 10 entries) ...')
528 
529  # Log daily carbon footprint statistics
530  self._log_header3(gammalib.NORMAL, 'Used wall clock time')
531 
532  # Optionally sort list
533  if cur_version > req_version:
534  sorted_entries = sorted(statistics['tools'], key=lambda d: d['wall'], reverse=True)
535  else:
536  sorted_entries = statistics['tools']
537 
538  # Loop over entries
539  for i, entry in enumerate(sorted_entries):
540  if i < 10:
541  level = gammalib.NORMAL
542  else:
543  level = gammalib.EXPLICIT
544  self._log_value(level, entry['name'], self._format_time(entry['wall']))
545  if len(sorted_entries) > 9 and self['chatter'].integer() < 3:
546  self._log_string(gammalib.NORMAL, ' ... (list truncated after 10 entries) ...')
547 
548  # Log daily carbon footprint statistics
549  self._log_header3(gammalib.NORMAL, 'Used CPU time')
550 
551  # Optionally sort list
552  if cur_version > req_version:
553  sorted_entries = sorted(statistics['tools'], key=lambda d: d['cpu'], reverse=True)
554  else:
555  sorted_entries = statistics['tools']
556 
557  # Loop over entries
558  for i, entry in enumerate(sorted_entries):
559  if i < 10:
560  level = gammalib.NORMAL
561  else:
562  level = gammalib.EXPLICIT
563  self._log_value(level, entry['name'], self._format_time(entry['cpu']))
564  if len(sorted_entries) > 9 and self['chatter'].integer() < 3:
565  self._log_string(gammalib.NORMAL, ' ... (list truncated after 10 entries) ...')
566 
567  # Return
568  return
569 
570  # Create figure
571  def _create_figure(self, outfile, statistics):
572  """
573  Create figure
574 
575  Parameters
576  ----------
577  outfile : str
578  Figure file name
579  statistics : dict
580  Statistics dictionary
581  """
582  # Optionally use matplotlib to create a figure
583  try:
584 
585  # Import matplotlib
586  import matplotlib.pyplot as plt
587  import matplotlib.gridspec as gridspec
588 
589  # Create figure
590  ysize = 7.0
591  xsize = 1.4142 * ysize
592  fig = plt.figure(figsize=(xsize, ysize))
593 
594  # Create title and subtitle
595  user = pwd.getpwuid(os.getuid())[0]
596  title = 'ctools carbon footprint report for user "%s"' % (user)
597  subtitle = r'Dates: %s - %s' % \
598  (statistics['use_start'], statistics['use_stop'])
599  fig.suptitle(title, fontsize=16)
600  fig.text(0.5, 0.925, subtitle, fontsize=11, ha='center')
601 
602  # Set plot margins
603  fig.subplots_adjust(left=0.08, bottom=0.07, right=0.97, top=0.88,
604  wspace=0.1, hspace=0.2)
605 
606  # Divide figure
607  gs1 = gridspec.GridSpec(4,3) # (rows,cols)
608  gs1.update(hspace=0.8, wspace=0.8)
609  ax1 = fig.add_subplot(gs1[0,0:2])
610  ax2 = fig.add_subplot(gs1[1,0:2])
611  ax3 = fig.add_subplot(gs1[2,0:2])
612  ax4 = fig.add_subplot(gs1[3,0:2])
613  #
614  gs2 = gridspec.GridSpec(8,3) # (rows,cols)
615  gs2.update(hspace=0.3, bottom=0.0, top=0.95, right=0.96, wspace=0.0)
616  ax10 = fig.add_subplot(gs2[2:5,2])
617  ax11 = fig.add_subplot(gs2[5:8,2])
618 
619  # Plot daily figures
620  self._plot_daily(ax1, statistics, quantity='gCO2e', title='Footprint',
621  ylabel=r'g CO$_2$e')
622  self._plot_daily(ax2, statistics, quantity='cpu', title='CPU hours',
623  ylabel='hours', yscale=1.0/3600.0)
624  self._plot_daily(ax3, statistics, quantity='wall', title='Wall clock hours',
625  ylabel='hours', yscale=1.0/3600.0)
626  self._plot_daily(ax4, statistics, quantity='calls',
627  title='ctools calls', ylabel='calls')
628 
629  # Kludge to adapt pie plotting to matplotlib version
630  args, _, _, _ = inspect.getargspec(plt.pie)
631 
632  # Plot pie figures
633  self._plot_pie(ax10, statistics, quantity='gCO2e', title='Footprint', args=args)
634  self._plot_pie(ax11, statistics, quantity='calls', title='ctools calls', args=args)
635 
636  # Derive summary information from statistics
637  info = self._get_global_information(statistics, latex=True)
638 
639  # Write summary information
640  x0 = 0.63
641  y0 = 0.89
642  dx = 0.17
643  dy = 0.022
644  fontsize = 8
645  fig.text(x0, y0, 'Total carbon footprint: ', fontsize=fontsize, ha='left')
646  fig.text(x0+dx, y0, info['gCO2e'], fontsize=fontsize, ha='left')
647  y0 -= dy
648  fig.text(x0, y0, ' due to power consumption: ', fontsize=fontsize, ha='left')
649  fig.text(x0+dx, y0, info['elect'], fontsize=fontsize, ha='left')
650  y0 -= dy
651  fig.text(x0, y0, ' due to infrastructure: ', fontsize=fontsize, ha='left')
652  fig.text(x0+dx, y0, info['gCO2e_infra'], fontsize=fontsize, ha='left')
653  y0 -= dy
654  fig.text(x0, y0, 'Total CPU time: ', fontsize=fontsize, ha='left')
655  fig.text(x0+dx, y0, info['cpu'], fontsize=fontsize, ha='left')
656  y0 -= dy
657  fig.text(x0, y0, 'Average carbon intensity: ', fontsize=fontsize, ha='left')
658  fig.text(x0+dx, y0, info['ci_cpu'], fontsize=fontsize, ha='left')
659  y0 -= dy
660  fig.text(x0, y0, 'Average daily footprint: ', fontsize=fontsize, ha='left')
661  fig.text(x0+dx, y0, info['fp_dur'], fontsize=fontsize, ha='left')
662  y0 -= dy
663  fig.text(x0, y0, 'Expected annual footprint: ', fontsize=fontsize, ha='left')
664  fig.text(x0+dx, y0, info['fp_yr'], fontsize=fontsize, ha='left')
665 
666  # Optionally display figure for debugging
667  if self._logDebug():
668  plt.show()
669 
670  # Save figure
671  fig.savefig(outfile, dpi=300)
672 
673  # Log file creation
674  self._log_value(gammalib.NORMAL, 'Graphics file', outfile)
675 
676  # Catch exceptions
677  except (ImportError, RuntimeError):
678 
679  # Log file creation
680  self._log_value(gammalib.NORMAL, 'Graphics file', 'matplotlib not available')
681 
682  # Return
683  return
684 
685  # Plot daily figure
686  def _plot_daily(self, ax, statistics, quantity='gCO2e', title='Footprint',
687  ylabel=r'g CO$_2$e', yscale=1.0):
688  """
689  Plot daily figure
690 
691  Parameters
692  ----------
693  ax : pyplot
694  Plotting frame
695  statistics : dict
696  Statistics dictionary
697  quantity : str, optional
698  Quantity to plot
699  title : str, optional
700  Plot title
701  ylabel : str, optional
702  Y axis label
703  yscale : float, optional
704  Y axis scale
705  """
706  # Create bar data
707  days = [i for i, _ in enumerate(statistics['daily'])]
708  data = [entry[quantity]*yscale for entry in statistics['daily']]
709 
710  # Plot bar data
711  ax.bar(days, data, 1.0, bottom=0.0, color='red')
712 
713  # Set labels
714  ax.set_title(title)
715  ax.set_xlabel('Days since %s' % statistics['use_start'][0:10])
716  ax.set_ylabel(ylabel)
717 
718  # Return
719  return
720 
721  # Plot pie figure
722  def _plot_pie(self, ax, statistics, quantity='gCO2e', title='Footprint', num=5, args=None):
723  """
724  Plot pie figure
725 
726  Parameters
727  ----------
728  ax : pyplot
729  Plotting frame
730  statistics : dict
731  Statistics dictionary
732  quantity : str, optional
733  Quantity to plot
734  title : str, optional
735  Plot title
736  num : integer, optional
737  Number of pies displayed explicitly
738  arg : list or str, optional
739  List of pie method keyword arguments
740  """
741  # Get Python version information
742  req_version = (2,4)
743  cur_version = sys.version_info
744 
745  # Optionally sort list
746  if cur_version > req_version:
747  sorted_entries = sorted(statistics['tools'], key=lambda d: d[quantity], reverse=True)
748  else:
749  sorted_entries = statistics['tools']
750 
751  # Create pie data
752  labels = []
753  sizes = []
754  others = 0.0
755  for i, entry in enumerate(sorted_entries):
756  if i < num:
757  labels.append(entry['name'])
758  sizes.append(entry[quantity])
759  else:
760  others += entry[quantity]
761  labels.append('others')
762  sizes.append(others)
763 
764  # Plot pie chart
765  if 'wedgeprops' in args:
766  wedges, _, _ = ax.pie(sizes, autopct='%1.0f%%', pctdistance=0.8,
767  startangle=90, radius=1.0,
768  wedgeprops=dict(width=0.4, edgecolor='w'))
769  else:
770  wedges, _, _ = ax.pie(sizes, autopct='%1.0f%%', pctdistance=0.8,
771  startangle=90, radius=1.0)
772  ax.axis('equal')
773  ax.legend(wedges, labels, loc='center', fontsize=8, frameon=False)
774 
775  # Set labels
776  ax.set_title(title)
777 
778  # Return
779  return
780 
781 
782  # Public methods
783  def process(self):
784  """
785  Process the script
786  """
787  # Get parameters
788  self._get_parameters()
789 
790  # Log header
791  self._log_header1(gammalib.TERSE, 'Load statistics data')
792 
793  # Load statistics
794  self._statistics = self._load_statistics()
795 
796  # Log statistics
798  self._daily_statistics(self._statistics)
799  self._tools_statistics(self._statistics)
800 
801  # Return
802  return
803 
804  def save(self):
805  """
806  Save something
807  """
808  # Write header
809  self._log_header1(gammalib.TERSE, 'Save graphics')
810 
811  # Continue only if filename is valid
812  if self['outfile'].is_valid():
813 
814  # Get outfile
815  outfile = self['outfile'].filename()
816 
817  # Create figure
818  self._create_figure(outfile.url(), self._statistics)
819 
820  # ... signal that no graphics file was specified
821  else:
822  self._log_value(gammalib.NORMAL, 'Graphics file', 'not specified')
823 
824  # Return
825  return
826 
827 
828 # ======================== #
829 # Main routine entry point #
830 # ======================== #
831 if __name__ == '__main__':
832 
833  # Create instance of application
834  app = csfootprint(sys.argv)
835 
836  # Execute application
837  app.execute()