# Copyright 2017 The TensorFlow Authors. All Rights Reserved. # # 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. # ============================================================================== """The TensorBoard Custom Scalars plugin. This plugin lets the user create scalars plots with custom run-tag combinations by specifying regular expressions. See `http_api.md` in this directory for specifications of the routes for this plugin. """ import re from google.protobuf import json_format from werkzeug import wrappers from tensorboard import plugin_util from tensorboard.backend import http_util from tensorboard.compat import tf from tensorboard.data import provider from tensorboard.plugins import base_plugin from tensorboard.plugins.custom_scalar import layout_pb2 from tensorboard.plugins.custom_scalar import metadata from tensorboard.plugins.scalar import metadata as scalars_metadata from tensorboard.plugins.scalar import scalars_plugin # The name of the property in the response for whether the regex is valid. _REGEX_VALID_PROPERTY = "regex_valid" # The name of the property in the response for the payload (tag to ScalarEvents # mapping). _TAG_TO_EVENTS_PROPERTY = "tag_to_events" # The number of seconds to wait in between checks for the config file specifying # layout. _CONFIG_FILE_CHECK_THROTTLE = 60 class CustomScalarsPlugin(base_plugin.TBPlugin): """CustomScalars Plugin for TensorBoard.""" plugin_name = metadata.PLUGIN_NAME def __init__(self, context): """Instantiates ScalarsPlugin via TensorBoard core. Args: context: A base_plugin.TBContext instance. """ self._logdir = context.logdir self._data_provider = context.data_provider self._plugin_name_to_instance = context.plugin_name_to_instance def _get_scalars_plugin(self): """Tries to get the scalars plugin. Returns: The scalars plugin. Or None if it is not yet registered. """ if scalars_metadata.PLUGIN_NAME in self._plugin_name_to_instance: # The plugin is registered. return self._plugin_name_to_instance[scalars_metadata.PLUGIN_NAME] # The plugin is not yet registered. return None def get_plugin_apps(self): return { "/download_data": self.download_data_route, "/layout": self.layout_route, "/scalars": self.scalars_route, } def is_active(self): """Plugin is active if there is a custom layout for the dashboard.""" return False # `list_plugins` as called by TB core suffices def frontend_metadata(self): return base_plugin.FrontendMetadata( element_name="tf-custom-scalar-dashboard", tab_name="Custom Scalars", ) @wrappers.Request.application def download_data_route(self, request): ctx = plugin_util.context(request.environ) run = request.args.get("run") tag = request.args.get("tag") experiment = plugin_util.experiment_id(request.environ) response_format = request.args.get("format") try: body, mime_type = self.download_data_impl( ctx, run, tag, experiment, response_format ) except ValueError as e: return http_util.Respond( request=request, content=str(e), content_type="text/plain", code=400, ) return http_util.Respond(request, body, mime_type) def download_data_impl(self, ctx, run, tag, experiment, response_format): """Provides a response for downloading scalars data for a data series. Args: ctx: A tensorboard.context.RequestContext value. run: The run. tag: The specific tag. experiment: An experiment ID, as a possibly-empty `str`. response_format: A string. One of the values of the OutputFormat enum of the scalar plugin. Raises: ValueError: If the scalars plugin is not registered. Returns: 2 entities: - A JSON object response body. - A mime type (string) for the response. """ scalars_plugin_instance = self._get_scalars_plugin() if not scalars_plugin_instance: raise ValueError( ( "Failed to respond to request for /download_data. " "The scalars plugin is oddly not registered." ) ) body, mime_type = scalars_plugin_instance.scalars_impl( ctx, tag, run, experiment, response_format ) return body, mime_type @wrappers.Request.application def scalars_route(self, request): """Given a tag regex and single run, return ScalarEvents. This route takes 2 GET params: run: A run string to find tags for. tag: A string that is a regex used to find matching tags. The response is a JSON object: { // Whether the regular expression is valid. Also false if empty. regexValid: boolean, // An object mapping tag name to a list of ScalarEvents. payload: Object, } """ ctx = plugin_util.context(request.environ) tag_regex_string = request.args.get("tag") run = request.args.get("run") experiment = plugin_util.experiment_id(request.environ) mime_type = "application/json" try: body = self.scalars_impl(ctx, run, tag_regex_string, experiment) except ValueError as e: return http_util.Respond( request=request, content=str(e), content_type="text/plain", code=400, ) # Produce the response. return http_util.Respond(request, body, mime_type) def scalars_impl(self, ctx, run, tag_regex_string, experiment): """Given a tag regex and single run, return ScalarEvents. Args: ctx: A tensorboard.context.RequestContext value. run: A run string. tag_regex_string: A regular expression that captures portions of tags. Raises: ValueError: if the scalars plugin is not registered. Returns: A dictionary that is the JSON-able response. """ if not tag_regex_string: # The user provided no regex. return { _REGEX_VALID_PROPERTY: False, _TAG_TO_EVENTS_PROPERTY: {}, } # Construct the regex. try: regex = re.compile(tag_regex_string) except re.error: return { _REGEX_VALID_PROPERTY: False, _TAG_TO_EVENTS_PROPERTY: {}, } # Fetch the tags for the run. Filter for tags that match the regex. run_to_data = self._data_provider.list_scalars( ctx, experiment_id=experiment, plugin_name=scalars_metadata.PLUGIN_NAME, run_tag_filter=provider.RunTagFilter(runs=[run]), ) tag_to_data = None try: tag_to_data = run_to_data[run] except KeyError: # The run could not be found. Perhaps a configuration specified a run that # TensorBoard has not read from disk yet. payload = {} if tag_to_data: scalars_plugin_instance = self._get_scalars_plugin() if not scalars_plugin_instance: raise ValueError( ( "Failed to respond to request for /scalars. " "The scalars plugin is oddly not registered." ) ) form = scalars_plugin.OutputFormat.JSON payload = { tag: scalars_plugin_instance.scalars_impl( ctx, tag, run, experiment, form )[0] for tag in tag_to_data.keys() if regex.match(tag) } return { _REGEX_VALID_PROPERTY: True, _TAG_TO_EVENTS_PROPERTY: payload, } @wrappers.Request.application def layout_route(self, request): """Fetches the custom layout specified by the config file in the logdir. If more than 1 run contains a layout, this method merges the layouts by merging charts within individual categories. If 2 categories with the same name are found, the charts within are merged. The merging is based on the order of the runs to which the layouts are written. The response is a JSON object mirroring properties of the Layout proto if a layout for any run is found. The response is an empty object if no layout could be found. """ ctx = plugin_util.context(request.environ) experiment = plugin_util.experiment_id(request.environ) body = self.layout_impl(ctx, experiment) return http_util.Respond(request, body, "application/json") def layout_impl(self, ctx, experiment): # Keep a mapping between and category so we do not create duplicate # categories. title_to_category = {} merged_layout = None data = self._data_provider.read_tensors( ctx, experiment_id=experiment, plugin_name=metadata.PLUGIN_NAME, run_tag_filter=provider.RunTagFilter( tags=[metadata.CONFIG_SUMMARY_TAG] ), downsample=1, ) for run in sorted(data): points = data[run][metadata.CONFIG_SUMMARY_TAG] content = points[0].numpy.item() layout_proto = layout_pb2.Layout() layout_proto.ParseFromString(tf.compat.as_bytes(content)) if merged_layout: # Append the categories within this layout to the merged layout. for category in layout_proto.category: if category.title in title_to_category: # A category with this name has been seen before. Do not create a # new one. Merge their charts, skipping any duplicates. title_to_category[category.title].chart.extend( [ c for c in category.chart if c not in title_to_category[category.title].chart ] ) else: # This category has not been seen before. merged_layout.category.add().MergeFrom(category) title_to_category[category.title] = category else: # This is the first layout encountered. merged_layout = layout_proto for category in layout_proto.category: title_to_category[category.title] = category if merged_layout: return json_format.MessageToJson( merged_layout, including_default_value_fields=True ) else: # No layout was found. return {}