Commit 5f703f7f2b477dd1e7e502963bdd3fda97e32d93

Authored by Jürgen Knödlseder
1 parent bd0a89b5

Complement csfootprint script.

Showing 1 changed file with 239 additions and 55 deletions
cscripts/csfootprint.py
... ... @@ -18,6 +18,8 @@
18 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19 #
20 20 # ==========================================================================
  21 +import os
  22 +import pwd
21 23 import sys
22 24 import gammalib
23 25 import ctools
... ... @@ -217,7 +219,7 @@ class csfootprint(ctools.cscript):
217 219 wall = float(tool.attribute('wall'))
218 220 cpu = float(tool.attribute('cpu'))
219 221 gCO2e = float(tool.attribute('gCO2e'))
220   -
  222 +
221 223 # Update sums
222 224 sum_calls += calls
223 225 sum_wall += wall
... ... @@ -271,17 +273,17 @@ class csfootprint(ctools.cscript):
271 273 """
272 274 # Format according to precision
273 275 if seconds < 60.0:
274   - format = '%.3f seconds' % (seconds)
  276 + format = '%.1f seconds' % (seconds)
275 277 elif seconds < 3600.0:
276   - format = '%.3f minutes' % (seconds/60.0)
  278 + format = '%.1f minutes' % (seconds/60.0)
277 279 else:
278   - format = '%.3f hours' % (seconds/3600.0)
  280 + format = '%.1f hours' % (seconds/3600.0)
279 281  
280 282 # Return format
281 283 return format
282 284  
283 285 # Format carbon footprint
284   - def _format_footprint(self, gCO2e):
  286 + def _format_footprint(self, gCO2e, latex=False):
285 287 """
286 288 Format carbon footprint
287 289  
... ... @@ -289,72 +291,137 @@ class csfootprint(ctools.cscript):
289 291 ----------
290 292 gCO2e : float
291 293 Carbon footprint in gCO2e
  294 + latex : bool, optional
  295 + Use LaTeX formatting
292 296 """
  297 + # Set unit
  298 + if latex:
  299 + unit = r'CO$_2$e'
  300 + else:
  301 + unit = 'CO2e'
  302 +
293 303 # Format according to precision
294 304 if gCO2e < 1000.0:
295   - format = '%.3f g CO2e' % (gCO2e)
  305 + format = '%.1f g %s' % (gCO2e, unit)
296 306 elif gCO2e < 1.0e6:
297   - format = '%.3f kg CO2e' % (gCO2e/1000.0)
  307 + format = '%.1f kg %s' % (gCO2e/1000.0, unit)
298 308 else:
299   - format = '%.3f t CO2e' % (gCO2e/1.0e6)
  309 + format = '%.1f t %s' % (gCO2e/1.0e6, unit)
300 310  
301 311 # Return format
302 312 return format
303 313  
304   - # Log global statistics
305   - def _global_statistics(self, statistics):
  314 + # Get global information
  315 + def _get_global_information(self, statistics, latex=False):
306 316 """
307   - Log global statistics
308   -
  317 + Get global information from statistics
  318 +
309 319 Parameters
310 320 ----------
311 321 statistics : dict
312 322 Statistics dictionary
  323 + latex : bool, optional
  324 + Use LaTeX formatting
313 325 """
314   - # Log header
315   - self._log_header1(gammalib.TERSE, 'Global statistics')
316   -
317 326 # Derive information
318   - tstart = gammalib.GTime(statistics['use_start'])
319   - tstop = gammalib.GTime(statistics['use_stop'])
320   - duration = tstop - tstart
  327 + tstart = gammalib.GTime(statistics['use_start'])
  328 + tstop = gammalib.GTime(statistics['use_stop'])
  329 + duration = tstop - tstart
  330 + cpu_hours = statistics['cpu']/3600.0
321 331 if statistics['cpu'] != 0.0:
322   - ci_cpu = statistics['gCO2e']/(statistics['cpu']/3600.0)
  332 + ci_cpu = statistics['gCO2e']/cpu_hours
323 333 else:
324 334 ci_cpu = 0.0
325 335 if duration != 0.0:
326 336 fp_dur = statistics['gCO2e']/(duration * gammalib.sec2day)
327 337 else:
328 338 fp_dur = 0.0
329   - fp_yr = fp_dur * 365.25
330   - dur_str = self._format_time(duration)
  339 + fp_yr = fp_dur * 365.25
  340 + dur_str = self._format_time(duration)
331 341 if statistics['wall'] != 0.0:
332 342 load_str = '%.1f %%' % (statistics['cpu']/statistics['wall']*100.0)
333 343 else:
334 344 load_str = 'undefined'
335   - ci_cpu_str = self._format_footprint(ci_cpu) + ' / CPU hour'
336   - fp_dur_str = self._format_footprint(fp_dur) + ' / day'
337   - fp_yr_str = self._format_footprint(fp_yr) + ' / year'
  345 + ci_cpu_str = self._format_footprint(ci_cpu, latex) + ' / CPU hour'
  346 + fp_dur_str = self._format_footprint(fp_dur, latex) + ' / day'
  347 + fp_yr_str = self._format_footprint(fp_yr, latex) + ' / year'
338 348 wall_str = self._format_time(statistics['wall'])
339 349 cpu_str = self._format_time(statistics['cpu'])
340   - gCO2e_str = self._format_footprint(statistics['gCO2e'])
  350 + gCO2e_str = self._format_footprint(statistics['gCO2e'], latex)
341 351 date_str = '%s - %s' % (statistics['start'], statistics['stop'])
342 352 use_str = '%s - %s' % (statistics['use_start'], statistics['use_stop'])
343 353  
  354 + # Split footprint into infrastructure and electricity use footprint
  355 + if statistics['cpu'] != 0.0:
  356 + ef_kWh = 108.0
  357 + ef_total = 4.68
  358 + ef_electricity = 2.43
  359 + ef_other = ef_total - ef_electricity
  360 + val_ef = (ci_cpu - ef_other) * ef_kWh / ef_electricity
  361 + gCO2e_infra = ef_other * cpu_hours
  362 + gCO2e_elect = ef_electricity * val_ef / ef_kWh * cpu_hours
  363 + else:
  364 + val_ef = 0.0
  365 + gCO2e_infra = 0.0
  366 + gCO2e_elect = 0.0
  367 + gCO2e_kWh_str = self._format_footprint(val_ef, latex) + ' / kWh'
  368 + gCO2e_infra_str = self._format_footprint(gCO2e_infra, latex)
  369 + gCO2e_elect_str = self._format_footprint(gCO2e_elect, latex)
  370 +
  371 + # Build electricity footprint string
  372 + elect_str = '%s (%s)' % (gCO2e_elect_str, gCO2e_kWh_str)
  373 +
  374 + # Build result directory
  375 + result = {'ci_cpu': ci_cpu_str,
  376 + 'fp_dur': fp_dur_str,
  377 + 'fp_yr': fp_yr_str,
  378 + 'wall': wall_str,
  379 + 'cpu': cpu_str,
  380 + 'gCO2e': gCO2e_str,
  381 + 'date': date_str,
  382 + 'use': use_str,
  383 + 'dur': dur_str,
  384 + 'load': load_str,
  385 + 'gCO2e_kWh': gCO2e_kWh_str,
  386 + 'gCO2e_infra': gCO2e_infra_str,
  387 + 'gCO2e_elect': gCO2e_elect_str,
  388 + 'elect': elect_str}
  389 +
  390 + # Return result
  391 + return result
  392 +
  393 + # Log global statistics
  394 + def _global_statistics(self, statistics):
  395 + """
  396 + Log global statistics
  397 +
  398 + Parameters
  399 + ----------
  400 + statistics : dict
  401 + Statistics dictionary
  402 + """
  403 + # Log header
  404 + self._log_header1(gammalib.TERSE, 'Global statistics')
  405 +
  406 + # Derive information from statistics
  407 + info = self._get_global_information(statistics)
  408 +
344 409 # Log report information
345 410 self._log_value(gammalib.NORMAL, 'Creation date', statistics['creation'])
346 411 self._log_value(gammalib.NORMAL, 'Last statistics update', statistics['modified'])
347   - self._log_value(gammalib.NORMAL, 'Statistics date interval', date_str)
348   - self._log_value(gammalib.NORMAL, 'Used date interval', use_str)
349   - self._log_value(gammalib.NORMAL, 'Duration of used interval', dur_str)
  412 + self._log_value(gammalib.NORMAL, 'Statistics date interval', info['date'])
  413 + self._log_value(gammalib.NORMAL, 'Used date interval', info['use'])
  414 + self._log_value(gammalib.NORMAL, 'Duration of used interval', info['dur'])
350 415 self._log_value(gammalib.NORMAL, 'Total number of ctool runs', statistics['calls'])
351   - self._log_value(gammalib.NORMAL, 'Total wall clock time', wall_str)
352   - self._log_value(gammalib.NORMAL, 'Total CPU time', cpu_str)
353   - self._log_value(gammalib.NORMAL, 'Average CPU load', load_str)
354   - self._log_value(gammalib.NORMAL, 'Total carbon footprint', gCO2e_str)
355   - self._log_value(gammalib.NORMAL, 'Average carbon intensity', ci_cpu_str)
356   - self._log_value(gammalib.NORMAL, 'Average daily footprint', fp_dur_str)
357   - self._log_value(gammalib.NORMAL, 'Expected annual footprint', fp_yr_str)
  416 + self._log_value(gammalib.NORMAL, 'Total wall clock time', info['wall'])
  417 + self._log_value(gammalib.NORMAL, 'Total CPU time', info['cpu'])
  418 + self._log_value(gammalib.NORMAL, 'Average CPU load', info['load'])
  419 + self._log_value(gammalib.NORMAL, 'Total carbon footprint', info['gCO2e'])
  420 + self._log_value(gammalib.NORMAL, ' due to power consumption', info['elect'])
  421 + self._log_value(gammalib.NORMAL, ' due to infrastructure', info['gCO2e_infra'])
  422 + self._log_value(gammalib.NORMAL, 'Average carbon intensity', info['ci_cpu'])
  423 + self._log_value(gammalib.NORMAL, 'Average daily footprint', info['fp_dur'])
  424 + self._log_value(gammalib.NORMAL, 'Expected annual footprint', info['fp_yr'])
358 425  
359 426 # Return
360 427 return
... ... @@ -513,33 +580,86 @@ class csfootprint(ctools.cscript):
513 580 """
514 581 # Optionally use matplotlib to create a figure
515 582 try:
516   -
  583 +
517 584 # Import matplotlib
518 585 import matplotlib.pyplot as plt
519 586 import matplotlib.gridspec as gridspec
520 587  
521 588 # Create figure
522   - fig = plt.figure(figsize=(15.0,7.8))
  589 + ysize = 7.0
  590 + xsize = 1.4142 * ysize
  591 + fig = plt.figure(figsize=(xsize, ysize))
523 592  
524 593 # Create title and subtitle
525   - title = 'ctools carbon footprint report'
  594 + user = pwd.getpwuid(os.getuid())[0]
  595 + title = 'ctools carbon footprint report for user "%s"' % (user)
526 596 subtitle = r'Dates: %s - %s' % \
527 597 (statistics['use_start'], statistics['use_stop'])
528 598 fig.suptitle(title, fontsize=16)
529   - fig.text(0.5, 0.93, subtitle, fontsize=12, ha='center')
  599 + fig.text(0.5, 0.925, subtitle, fontsize=11, ha='center')
530 600  
531 601 # Set plot margins
532   - fig.subplots_adjust(left=0.07, bottom=0.07, right=0.97, top=0.88, wspace=0.3, hspace=0.35)
  602 + fig.subplots_adjust(left=0.07, bottom=0.07, right=0.97, top=0.88,
  603 + wspace=0.1, hspace=0.2)
533 604  
534 605 # Divide figure
535   - gs1 = gridspec.GridSpec(3,3)
  606 + gs1 = gridspec.GridSpec(4,3) # (rows,cols)
  607 + gs1.update(hspace=0.8, wspace=0.8)
536 608 ax1 = fig.add_subplot(gs1[0,0:2])
537   - ax2 = fig.add_subplot(gs1[0,2])
538   -
539   - # Plot daily footprint
540   - self._plot_daily(ax1, statistics)
541   -
542   - #plt.show()
  609 + ax2 = fig.add_subplot(gs1[1,0:2])
  610 + ax3 = fig.add_subplot(gs1[2,0:2])
  611 + ax4 = fig.add_subplot(gs1[3,0:2])
  612 + #
  613 + gs2 = gridspec.GridSpec(8,3) # (rows,cols)
  614 + gs2.update(hspace=0.3, bottom=0.0, top=0.95, right=0.96, wspace=0.0)
  615 + ax10 = fig.add_subplot(gs2[2:5,2])
  616 + ax11 = fig.add_subplot(gs2[5:8,2])
  617 +
  618 + # Plot daily figures
  619 + self._plot_daily(ax1, statistics, quantity='gCO2e', title='Footprint',
  620 + ylabel=r'g CO$_2$e')
  621 + self._plot_daily(ax2, statistics, quantity='cpu', title='CPU hours',
  622 + ylabel='hours', yscale=1.0/3600.0)
  623 + self._plot_daily(ax3, statistics, quantity='wall', title='Wall clock hours',
  624 + ylabel='hours', yscale=1.0/3600.0)
  625 + self._plot_daily(ax4, statistics, quantity='calls',
  626 + title='ctools calls', ylabel='calls')
  627 +
  628 + # Plot pie figures
  629 + self._plot_pie(ax10, statistics, quantity='gCO2e', title='Footprint')
  630 + self._plot_pie(ax11, statistics, quantity='calls', title='ctools calls')
  631 +
  632 + # Derive summary information from statistics
  633 + info = self._get_global_information(statistics, latex=True)
  634 +
  635 + # Write summary information
  636 + x0 = 0.63
  637 + y0 = 0.89
  638 + dx = 0.17
  639 + dy = 0.022
  640 + fontsize = 8
  641 + fig.text(x0, y0, 'Total carbon footprint: ', fontsize=fontsize, ha='left')
  642 + fig.text(x0+dx, y0, info['gCO2e'], fontsize=fontsize, ha='left')
  643 + y0 -= dy
  644 + fig.text(x0, y0, ' due to power consumption: ', fontsize=fontsize, ha='left')
  645 + fig.text(x0+dx, y0, info['elect'], fontsize=fontsize, ha='left')
  646 + y0 -= dy
  647 + fig.text(x0, y0, ' due to infrastructure: ', fontsize=fontsize, ha='left')
  648 + fig.text(x0+dx, y0, info['gCO2e_infra'], fontsize=fontsize, ha='left')
  649 + y0 -= dy
  650 + fig.text(x0, y0, 'Total CPU time: ', fontsize=fontsize, ha='left')
  651 + fig.text(x0+dx, y0, info['cpu'], fontsize=fontsize, ha='left')
  652 + y0 -= dy
  653 + fig.text(x0, y0, 'Average carbon intensity: ', fontsize=fontsize, ha='left')
  654 + fig.text(x0+dx, y0, info['ci_cpu'], fontsize=fontsize, ha='left')
  655 + y0 -= dy
  656 + fig.text(x0, y0, 'Average daily footprint: ', fontsize=fontsize, ha='left')
  657 + fig.text(x0+dx, y0, info['fp_dur'], fontsize=fontsize, ha='left')
  658 + y0 -= dy
  659 + fig.text(x0, y0, 'Expected annual footprint: ', fontsize=fontsize, ha='left')
  660 + fig.text(x0+dx, y0, info['fp_yr'], fontsize=fontsize, ha='left')
  661 +
  662 + plt.show()
543 663  
544 664 # Save figure
545 665 fig.savefig(outfile, dpi=300)
... ... @@ -556,10 +676,11 @@ class csfootprint(ctools.cscript):
556 676 # Return
557 677 return
558 678  
559   - # Plot daily footprint
560   - def _plot_daily(self, ax, statistics):
  679 + # Plot daily figure
  680 + def _plot_daily(self, ax, statistics, quantity='gCO2e', title='Footprint',
  681 + ylabel=r'g CO$_2$e', yscale=1.0):
561 682 """
562   - Plot daily footprint
  683 + Plot daily figure
563 684  
564 685 Parameters
565 686 ----------
... ... @@ -567,22 +688,85 @@ class csfootprint(ctools.cscript):
567 688 Plotting frame
568 689 statistics : dict
569 690 Statistics dictionary
  691 + quantity : str, optional
  692 + Quantity to plot
  693 + title : str, optional
  694 + Plot title
  695 + ylabel : str, optional
  696 + Y axis label
  697 + yscale : float, optional
  698 + Y axis scale
570 699 """
571 700 # Create bar data
572   - days = [i for i, _ in enumerate(statistics['daily'])]
573   - data = [entry['gCO2e'] for entry in statistics['daily']]
  701 + days = [i for i, _ in enumerate(statistics['daily'])]
  702 + data = [entry[quantity]*yscale for entry in statistics['daily']]
574 703  
575 704 # Plot bar data
576 705 ax.bar(days, data, 1.0, bottom=0.0, color='red')
577 706  
578 707 # Set labels
579   - ax.set_title('Footprint')
  708 + ax.set_title(title)
580 709 ax.set_xlabel('Days since %s' % statistics['use_start'][0:10])
581   - ax.set_ylabel(r'g CO$_2$e')
582   -
  710 + ax.set_ylabel(ylabel)
  711 +
583 712 # Return
584 713 return
585 714  
  715 + # Plot pie figure
  716 + def _plot_pie(self, ax, statistics, quantity='gCO2e', title='Footprint', num=5):
  717 + """
  718 + Plot pie figure
  719 +
  720 + Parameters
  721 + ----------
  722 + ax : pyplot
  723 + Plotting frame
  724 + statistics : dict
  725 + Statistics dictionary
  726 + quantity : str, optional
  727 + Quantity to plot
  728 + title : str, optional
  729 + Plot title
  730 + num : integer, optional
  731 + Number of pies displayed explicitly
  732 + """
  733 + # Get Python version information
  734 + req_version = (2,4)
  735 + cur_version = sys.version_info
  736 +
  737 + # Optionally sort list
  738 + if cur_version > req_version:
  739 + sorted_entries = sorted(statistics['tools'], key=lambda d: d[quantity], reverse=True)
  740 + else:
  741 + sorted_entries = statistics['tools']
  742 +
  743 + # Create pie data
  744 + labels = []
  745 + sizes = []
  746 + others = 0.0
  747 + for i, entry in enumerate(sorted_entries):
  748 + if i < num:
  749 + labels.append(entry['name'])
  750 + sizes.append(entry[quantity])
  751 + else:
  752 + others += entry[quantity]
  753 + labels.append('others')
  754 + sizes.append(others)
  755 +
  756 + # Plot pie chart
  757 + wedges, _, _ = ax.pie(sizes, autopct='%1.0f%%', pctdistance=0.8,
  758 + startangle=90, radius=1.0,
  759 + wedgeprops=dict(width=0.4, edgecolor='w'))
  760 + ax.axis('equal')
  761 + ax.legend(wedges, labels, loc='center', fontsize=8, frameon=False)
  762 +
  763 + # Set labels
  764 + ax.set_title(title)
  765 +
  766 + # Return
  767 + return
  768 +
  769 +
586 770 # Public methods
587 771 def process(self):
588 772 """
... ...