""" ReducePyMatplotlibHistogram is a base class for classes that create histograms using matplotlib. """ # This file is part of MAUS: http://micewww.pp.rl.ac.uk:8080/projects/maus # # MAUS is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # MAUS is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with MAUS. If not, see . import base64 import json from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from matplotlib.figure import Figure import StringIO import ErrorHandler class ReducePyMatplotlibHistogram: # pylint: disable=R0903 """ @class ReducePyMatplotlibHistogram.PyMatplotlibHistogram is a base class for classes that create histograms using matplotlib. Histograms are output as JSON documents of form: @verbatim {"image": {"keywords": [...list of image keywords...], "description":"...a description of the image...", "tag": TAG, "image_type": "eps", "data": "...base 64 encoded image..."}} @endverbatim "TAG" is specified by the sub-class. If "histogram_auto_number" (see below) is "true" then the TAG will have a number N appended where N means that the histogram was produced as a consequence of the (N + 1)th spill processed by the worker. The number will be zero-padded to form a six digit string e.g. "00000N". If "histogram_auto_number" is false then no such number is appended. In cases where a spill is input that contains errors (e.g. is badly formatted or is missing the data needed to update a histogram) then a spill is output which is just the input spill with an "errors" field containing the error e.g. @verbatim {"errors": {..., "bad_json_document": "unable to do json.loads on input"}} {"errors": {..., "...": "..."}} @endverbatim The caller can configure the worker and specify: -Image type ("histogram_image_type"). Must be one of those supported by matplot lib (currently "svg", "ps", "emf", "rgba", "raw", "svgz", "pdf", "eps", "png"). Default: "eps". -Auto-number ("histogram_auto_number"). Default: false. Flag that determines if the image tag (see above) has the spill count appended to it or not. -Sub-classes may support additional configuration parameter Sub-classes must override: -_configure_at_birth - to extract any additional sub-class-specific configuration from data cards. -_update_histograms. This checks that a spill has the data necessary to update any histograms then creates JSON documents in the format described above. -_cleanup_at_death - to do any sub-class-specific cleanup. """ def __init__(self): """ Set initial attribute values. @param self Object reference. """ # matplotlib histogram - for validation. figure = Figure(figsize=(6, 6)) self.__histogram = FigureCanvas(figure) self.spill_count = 0 # Number of spills processed to date. self.image_type = "eps" self.auto_number = False def birth(self, config_json): """ Configure worker from data cards. If "image_type" is not in those supported then a ValueError is thrown. @param self Object reference. @param config_json JSON document string. @returns True if configuration succeeded. """ config_doc = json.loads(config_json) key = "histogram_auto_number" if key in config_doc: self.auto_number = config_doc[key] key = "histogram_image_type" if key in config_doc: self.image_type = config_doc[key] else: self.image_type = "eps" if self.image_type not in \ self.__histogram.get_supported_filetypes().keys(): error = "Unsupported histogram image type: %s Expect one of %s" \ % (self.image_type, self.__histogram.get_supported_filetypes().keys()) raise ValueError(error) self.spill_count = 0 # Do sub-class-specific configuration. return self._configure_at_birth(config_doc) def _configure_at_birth(self, config_doc): """ Perform sub-class-specific configuration from data cards. Sub-classes must define this function. @param self Object reference. @param config_json JSON document. @returns True if configuration succeeded. """ def process(self, json_string): """ Update the histogram with data from the current spill and output the histogram. @param self Object reference. @param json_string String with current JSON document. @returns JSON document containing current histogram. """ # Load and validate the JSON document. try: json_doc = json.loads(json_string.rstrip()) except Exception: # pylint:disable=W0703 json_doc = {} ErrorHandler.HandleException(json_doc, self) return unicode(json.dumps(json_doc)) self.spill_count = self.spill_count + 1 # Process spill and update histograms. try: result = self._update_histograms(json_doc) except Exception: # pylint:disable=W0703 ErrorHandler.HandleException(json_doc, self) return unicode(json.dumps(json_doc)) image_list = [image['image'] for image in result] # Convert results to strings. return json.dumps({"maus_event_type":"Image", "image_list":image_list}) def _update_histograms(self, spill): """ Check that the spill has the data necessary to update the histograms then creates JSON documents in the format described above. Sub-classes must define this function. @param self Object reference. @param spill Current spill. @returns list of JSON documents. If the sub-class only updates histograms every N spills then this list can just contain the input spill. Otherwise it should consist of 1 or more JSON documents containing image data in the form described above. @throws Exception if various sub-class specific errors arise. """ def death(self): #pylint: disable=R0201 """ Invokes _cleanup_at_death(). @returns True """ return self._cleanup_at_death() def _cleanup_at_death(self): #pylint: disable=R0201 """ A no-op. Sub-classes can override this function to do sub-class-specific clean-up at death time. @param self Object reference. @returns True """ return True def _create_histogram(self): #pylint: disable=R0201 """ Create a histogram using matplotlib. @param self Object reference. @returns matplotlib FigureCanvas representing the histogram. """ figure = Figure(figsize=(6, 6)) histogram = FigureCanvas(figure) axes = figure.add_subplot(111) axes.grid(True, linestyle="-", color="0.75") return histogram def _rescale_axes(self, histogram, xmin, xmax, ymin, ymax, xfudge = 0.5, yfudge = 0.5): #pylint: disable=C0301, R0913, R0201 """ Rescale the X and Y axes of the histogram to show the given axis ranges. Fudge factors are used avoid matplotlib warning about "Attempting to set identical bottom==top" which arises if the axes are set to be exactly the maximum of the data. @param self Object reference. @param histogram FigureCanvas representing a histogram. @param xmin Minimum X value. @param xmax Maximum X value. @param ymin Minimum Y value. @param ymin Maximum Y value. @param xfudge X fudge factor. @param yfudge Y fudge factor. """ # Fudge factors are used histogram.figure.get_axes()[0].set_xlim( \ [xmin, xmax + xfudge]) histogram.figure.get_axes()[0].set_ylim( \ [ymin, ymax + yfudge]) def _get_image_doc(self, keywords, description, tag, canvas): #pylint: disable=C0301 """ Build a JSON document holding image data. @param self Object reference. @param keywords List of image keywords. @param description String describing the image. @param tag Image tag. @param histogram FigureCanvas representing a histogram. @returns JSON document. """ json_doc = {} json_doc["maus_event_type"] = "Image" json_doc["image"] = {} if (self.auto_number): image_tag = "%s%06d" % (tag, self.spill_count) else: image_tag = tag data = self.__convert_to_binary(canvas) json_doc["image"]["keywords"] = keywords json_doc["image"]["description"] = description json_doc["image"]["tag"] = image_tag json_doc["image"]["image_type"] = self.image_type json_doc["image"]["data"] = data return json_doc def __convert_to_binary(self, canvas): #pylint: disable=R0201 """ Convert histogram to binary format. @param self Object reference. @param canvas matplotlib FigureCanvas representing a histogram. @returns representation of histogram in base 64-encoded image type format. """ data_file = StringIO.StringIO() canvas.print_figure(data_file, dpi=500, format=self.image_type) data_file.seek(0) data = data_file.read() return base64.b64encode(data)