diff --git a/trains/backend_interface/metrics/reporter.py b/trains/backend_interface/metrics/reporter.py index 72911d8d..7afb6d16 100644 --- a/trains/backend_interface/metrics/reporter.py +++ b/trains/backend_interface/metrics/reporter.py @@ -142,7 +142,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param iter: Iteration number :type value: int """ - ev = ScalarEvent(metric=self._normalize_name(title), variant=self._normalize_name(series), value=value, iter=iter) + ev = ScalarEvent(metric=self._normalize_name(title), variant=self._normalize_name(series), value=value, + iter=iter) self._report(ev) def report_vector(self, title, series, values, iter): @@ -159,7 +160,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan """ if not isinstance(values, Iterable): raise ValueError('values: expected an iterable') - ev = VectorEvent(metric=self._normalize_name(title), variant=self._normalize_name(series), values=values, iter=iter) + ev = VectorEvent(metric=self._normalize_name(title), variant=self._normalize_name(series), values=values, + iter=iter) self._report(ev) def report_plot(self, title, series, plot, iter): @@ -185,7 +187,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan plot = json.dumps(plot, default=default) elif not isinstance(plot, six.string_types): raise ValueError('Plot should be a string or a dict') - ev = PlotEvent(metric=self._normalize_name(title), variant=self._normalize_name(series), plot_str=plot, iter=iter) + ev = PlotEvent(metric=self._normalize_name(title), variant=self._normalize_name(series), plot_str=plot, + iter=iter) self._report(ev) def report_image(self, title, series, src, iter): @@ -201,7 +204,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param iter: Iteration number :type value: int """ - ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, src=src) + ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, + src=src) self._report(ev) def report_media(self, title, series, src, iter): @@ -217,7 +221,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param iter: Iteration number :type value: int """ - ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, src=src) + ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, + src=src) self._report(ev) def report_image_and_upload(self, title, series, iter, path=None, image=None, upload_uri=None, @@ -245,7 +250,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan raise ValueError('Upload configuration is required (use setup_upload())') if len([x for x in (path, image) if x is not None]) != 1: raise ValueError('Expected only one of [filename, image]') - kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, file_history_size=max_image_history) + kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, + file_history_size=max_image_history) ev = ImageEvent(image_data=image, upload_uri=upload_uri, local_image_path=path, delete_after_upload=delete_after_upload, **kwargs) self._report(ev) @@ -284,7 +290,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan self._report(ev) def report_histogram(self, title, series, histogram, iter, labels=None, xlabels=None, - xtitle=None, ytitle=None, comment=None, mode='group'): + xtitle=None, ytitle=None, comment=None, mode='group', layout_config=None): """ Report an histogram bar plot :param title: Title (AKA metric) @@ -306,6 +312,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :type comment: str :param mode: multiple histograms mode. valid options are: stack / group / relative. Default is 'group'. :type mode: str + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ assert mode in ('stack', 'group', 'relative') @@ -319,6 +327,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan xlabels=xlabels, comment=comment, mode=mode, + layout_config=layout_config, ) return self.report_plot( @@ -328,7 +337,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan iter=iter, ) - def report_table(self, title, series, table, iteration): + def report_table(self, title, series, table, iteration, layout_config=None): """ Report a table plot. @@ -340,8 +349,10 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :type table: pandas.DataFrame :param iteration: Iteration number :type iteration: int + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ - table_output = create_plotly_table(table, title, series) + table_output = create_plotly_table(table, title, series, layout_config=layout_config) return self.report_plot( title=self._normalize_name(title), series=self._normalize_name(series), @@ -349,7 +360,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan iter=iteration, ) - def report_line_plot(self, title, series, iter, xtitle, ytitle, mode='lines', reverse_xaxis=False, comment=None): + def report_line_plot(self, title, series, iter, xtitle, ytitle, mode='lines', reverse_xaxis=False, + comment=None, layout_config=None): """ Report a (possibly multiple) line plot. @@ -369,6 +381,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :type reverse_xaxis: bool :param comment: comment underneath the title :type comment: str + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ plotly_dict = create_line_plot( @@ -379,6 +393,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan mode=mode, reverse_xaxis=reverse_xaxis, comment=comment, + layout_config=layout_config, ) return self.report_plot( @@ -389,7 +404,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan ) def report_2d_scatter(self, title, series, data, iter, mode='lines', xtitle=None, ytitle=None, labels=None, - comment=None): + comment=None, layout_config=None): """ Report a 2d scatter graph (with lines) @@ -407,6 +422,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param labels: label (text) per point in the scatter (in the same order) :param comment: comment underneath the title :type comment: str + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ plotly_dict = create_2d_scatter_series( np_row_wise=data, @@ -417,6 +434,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan ytitle=ytitle, labels=labels, comment=comment, + layout_config=layout_config, ) return self.report_plot( @@ -428,7 +446,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan def report_3d_scatter(self, title, series, data, iter, labels=None, mode='lines', color=((217, 217, 217, 0.14),), marker_size=5, line_width=0.8, xtitle=None, ytitle=None, ztitle=None, fill=None, - comment=None): + comment=None, layout_config=None): """ Report a 3d scatter graph (with markers) @@ -450,6 +468,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param ytitle: optional y-axis title :param ztitle: optional z-axis title :param comment: comment underneath the title + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ data_series = data if isinstance(data, list) else [data] @@ -469,6 +489,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan yaxis_title=ytitle, zaxis_title=ztitle, comment=comment, + layout_config=layout_config, ) for i, values in enumerate(data_series): @@ -492,7 +513,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan iter=iter, ) - def report_value_matrix(self, title, series, data, iter, xtitle=None, ytitle=None, xlabels=None, ylabels=None, comment=None): + def report_value_matrix(self, title, series, data, iter, xtitle=None, ytitle=None, xlabels=None, ylabels=None, + comment=None, layout_config=None): """ Report a heat-map matrix @@ -509,6 +531,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param xlabels: optional label per column of the matrix :param ylabels: optional label per row of the matrix :param comment: comment underneath the title + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ plotly_dict = create_value_matrix( @@ -520,6 +544,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan comment=comment, xtitle=xtitle, ytitle=ytitle, + layout_config=layout_config, ) return self.report_plot( @@ -530,7 +555,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan ) def report_value_surface(self, title, series, data, iter, xlabels=None, ylabels=None, - xtitle=None, ytitle=None, ztitle=None, camera=None, comment=None): + xtitle=None, ytitle=None, ztitle=None, camera=None, comment=None, layout_config=None): """ Report a 3d surface (same data as heat-map matrix, only presented differently) @@ -549,6 +574,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :param ztitle: optional z-axis title :param camera: X,Y,Z camera position. def: (1,1,1) :param comment: comment underneath the title + :param layout_config: optional dictionary for layout configuration, passed directly to plotly + :type layout_config: dict or None """ plotly_dict = create_3d_surface( @@ -562,6 +589,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan ztitle=ztitle, camera=camera, comment=comment, + layout_config=layout_config, ) return self.report_plot( @@ -588,6 +616,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan :type path: str :param matrix: A 3D numpy.ndarray object containing image data (RGB). Required unless filename is provided. :type matrix: str + :param upload_uri: upload image destination (str) + :type upload_uri: str :param max_image_history: maximum number of image to store per metric/variant combination use negative value for unlimited. default is set in global configuration (default=5) :param delete_after_upload: if True, one the file was uploaded the local copy will be deleted @@ -597,7 +627,8 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan raise ValueError('Upload configuration is required (use setup_upload())') if len([x for x in (path, matrix) if x is not None]) != 1: raise ValueError('Expected only one of [filename, matrix]') - kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, file_history_size=max_image_history) + kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, + file_history_size=max_image_history) ev = UploadEvent(image_data=matrix, upload_uri=upload_uri, local_image_path=path, delete_after_upload=delete_after_upload, **kwargs) _, url = ev.get_target_full_upload_uri(upload_uri or self._storage_uri, self._metrics.storage_key_prefix) diff --git a/trains/logger.py b/trains/logger.py index b835dde6..1419419b 100644 --- a/trains/logger.py +++ b/trains/logger.py @@ -13,7 +13,6 @@ except ImportError: from PIL import Image from pathlib2 import Path -from .backend_api.services import tasks from .backend_interface.logger import StdStreamPatch, LogFlusher from .backend_interface.task import Task as _Task from .backend_interface.task.development.worker import DevWorker @@ -144,16 +143,17 @@ class Logger(object): return self._task.reporter.report_scalar(title=title, series=series, value=float(value), iter=iteration) def report_vector( - self, - title, # type: str - series, # type: str - values, # type: Sequence[Union[int, float]] - iteration, # type: int - labels=None, # type: Optional[List[str]] - xlabels=None, # type: Optional[List[str]] - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - mode=None # type: Optional[str] + self, + title, # type: str + series, # type: str + values, # type: Sequence[Union[int, float]] + iteration, # type: int + labels=None, # type: Optional[List[str]] + xlabels=None, # type: Optional[List[str]] + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + mode=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, plot a vector as (default stacked) histogram. @@ -180,22 +180,25 @@ class Logger(object): :param str xaxis: The x-axis title. (Optional) :param str yaxis: The y-axis title. (Optional) :param str mode: Multiple histograms mode, stack / group / relative. Default is 'group'. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ self._touch_title_series(title, series) return self.report_histogram(title, series, values, iteration, labels=labels, xlabels=xlabels, - xaxis=xaxis, yaxis=yaxis, mode=mode) + xaxis=xaxis, yaxis=yaxis, mode=mode, extra_layout=extra_layout) def report_histogram( - self, - title, # type: str - series, # type: str - values, # type: Sequence[Union[int, float]] - iteration, # type: int - labels=None, # type: Optional[List[str]] - xlabels=None, # type: Optional[List[str]] - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - mode=None # type: Optional[str] + self, + title, # type: str + series, # type: str + values, # type: Sequence[Union[int, float]] + iteration, # type: int + labels=None, # type: Optional[List[str]] + xlabels=None, # type: Optional[List[str]] + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + mode=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, plot a (default grouped) histogram. @@ -207,8 +210,8 @@ class Logger(object): .. code-block:: py vector_series = np.random.randint(10, size=10).reshape(2,5) - logger.report_histogram(title='histogram example', series='histogram series', values=vector_series, iteration=0, - labels=['A','B'], xaxis='X axis label', yaxis='Y axis label') + logger.report_histogram(title='histogram example', series='histogram series', + values=vector_series, iteration=0, labels=['A','B'], xaxis='X axis label', yaxis='Y axis label') You can view the reported histograms in the **Trains Web-App (UI)**, **RESULTS** tab, **PLOTS** sub-tab. @@ -224,6 +227,8 @@ class Logger(object): :param str xaxis: The x-axis title. (Optional) :param str yaxis: The y-axis title. (Optional) :param str mode: Multiple histograms mode, stack / group / relative. Default is 'group'. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ if not isinstance(values, np.ndarray): @@ -241,17 +246,19 @@ class Logger(object): xlabels=xlabels, xtitle=xaxis, ytitle=yaxis, - mode=mode or 'group' + mode=mode or 'group', + layout_config=extra_layout, ) def report_table( - self, - title, # type: str - series, # type: str - iteration, # type: int - table_plot=None, # type: Optional[pd.DataFrame] - csv=None, # type: Optional[str] - url=None # type: Optional[str] + self, + title, # type: str + series, # type: str + iteration, # type: int + table_plot=None, # type: Optional[pd.DataFrame] + csv=None, # type: Optional[str] + url=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit report, report a table plot. @@ -284,6 +291,9 @@ class Logger(object): :type csv: str :param url: A URL to the location of csv file. :type url: str + :param extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} + :type extra_layout: dict """ mutually_exclusive( UsageError, _check_none=True, @@ -293,7 +303,8 @@ class Logger(object): if url or csv: if not pd: raise UsageError( - "pandas is required in order to support reporting tables using CSV or a URL, please install the pandas python package" + "pandas is required in order to support reporting tables using CSV or a URL, " + "please install the pandas python package" ) if url: table = pd.read_csv(url) @@ -313,19 +324,21 @@ class Logger(object): title=title, series=series, table=reporter_table, - iteration=iteration + iteration=iteration, + layout_config=extra_layout, ) def report_line_plot( - self, - title, # type: str - series, # type: str - iteration, # type: int - xaxis, # type: str - yaxis, # type: str - mode='lines', # type: str - reverse_xaxis=False, # type: bool - comment=None # type: Optional[str] + self, + title, # type: str + series, # type: str + iteration, # type: int + xaxis, # type: str + yaxis, # type: str + mode='lines', # type: str + reverse_xaxis=False, # type: bool + comment=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, plot one or more series as lines. @@ -352,6 +365,8 @@ class Logger(object): - ``False`` - The x-axis is low to high (not reversed). (Default) :param str comment: A comment displayed with the plot, underneath the title. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ series = [self.SeriesInfo(**s) if isinstance(s, dict) else s for s in series] @@ -368,19 +383,21 @@ class Logger(object): mode=mode, reverse_xaxis=reverse_xaxis, comment=comment, + layout_config=extra_layout, ) def report_scatter2d( - self, - title, # type: str - series, # type: str - scatter, # type: Union[Sequence[Tuple[float, float]], np.ndarray] - iteration, # type: int - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - labels=None, # type: Optional[List[str]] - mode='lines', # type: str - comment=None # type: Optional[str] + self, + title, # type: str + series, # type: str + scatter, # type: Union[Sequence[Tuple[float, float]], np.ndarray] + iteration, # type: int + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + labels=None, # type: Optional[List[str]] + mode='lines', # type: str + comment=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, report a 2d scatter plot. @@ -408,8 +425,7 @@ class Logger(object): :param str title: The title of the plot. :param str series: The title of the series. - :param scatter: The scatter data. - :type numpy.ndarray, list of (pairs of x,y) scatter: + :param list scatter: The scatter data. numpy.ndarray or list of (pairs of x,y) scatter: :param int iteration: The iteration number. To set an initial iteration, for example to continue a previously :param str xaxis: The x-axis title. (Optional) :param str yaxis: The y-axis title. (Optional) @@ -424,6 +440,8 @@ class Logger(object): - ``lines+markers`` :param str comment: A comment displayed with the plot, underneath the title. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ if not isinstance(scatter, np.ndarray): @@ -444,21 +462,23 @@ class Logger(object): ytitle=yaxis, labels=labels, comment=comment, + layout_config=extra_layout, ) def report_scatter3d( - self, - title, # type: str - series, # type: str - scatter, # type: Union[Sequence[Tuple[float, float, float]], np.ndarray] - iteration, # type: int - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - zaxis=None, # type: Optional[str] - labels=None, # type: Optional[List[str]] - mode='markers', # type: str - fill=False, # type: bool - comment=None # type: Optional[str] + self, + title, # type: str + series, # type: str + scatter, # type: Union[Sequence[Tuple[float, float, float]], np.ndarray] + iteration, # type: int + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + zaxis=None, # type: Optional[str] + labels=None, # type: Optional[List[str]] + mode='markers', # type: str + fill=False, # type: bool + comment=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, plot a 3d scatter graph (with markers). @@ -466,7 +486,7 @@ class Logger(object): :param str title: The title of the plot. :param str series: The title of the series. :param Union[numpy.ndarray, list] scatter: The scatter data. - :type scatter: list of (pairs of x,y,z), list of series [[(x1,y1,z1)...]], or numpy.ndarray + list of (pairs of x,y,z), list of series [[(x1,y1,z1)...]], or numpy.ndarray :param int iteration: The iteration number. :param str xaxis: The x-axis title. (Optional) :param str yaxis: The y-axis title. (Optional) @@ -497,18 +517,20 @@ class Logger(object): - ``False`` - Do not fill (Default) :param str comment: A comment displayed with the plot, underneath the title. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ # check if multiple series multi_series = ( - isinstance(scatter, list) - and ( - isinstance(scatter[0], np.ndarray) - or ( - scatter[0] - and isinstance(scatter[0], list) - and isinstance(scatter[0][0], list) + isinstance(scatter, list) + and ( + isinstance(scatter[0], np.ndarray) + or ( + scatter[0] + and isinstance(scatter[0], list) + and isinstance(scatter[0][0], list) + ) ) - ) ) if not multi_series: @@ -536,19 +558,21 @@ class Logger(object): xtitle=xaxis, ytitle=yaxis, ztitle=zaxis, + layout_config=extra_layout, ) def report_confusion_matrix( - self, - title, # type: str - series, # type: str - matrix, # type: np.ndarray - iteration, # type: int - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - xlabels=None, # type: Optional[List[str]] - ylabels=None, # type: Optional[List[str]] - comment=None # type: Optional[str] + self, + title, # type: str + series, # type: str + matrix, # type: np.ndarray + iteration, # type: int + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + xlabels=None, # type: Optional[List[str]] + ylabels=None, # type: Optional[List[str]] + comment=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, plot a heat-map matrix. @@ -570,6 +594,8 @@ class Logger(object): :param list(str) xlabels: Labels for each column of the matrix. (Optional) :param list(str) ylabels: Labels for each row of the matrix. (Optional) :param str comment: A comment displayed with the plot, underneath the title. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ if not isinstance(matrix, np.ndarray): @@ -588,18 +614,20 @@ class Logger(object): xlabels=xlabels, ylabels=ylabels, comment=comment, + layout_config=extra_layout, ) def report_matrix( - self, - title, # type: str - series, # type: str - matrix, # type: np.ndarray - iteration, # type: int - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - xlabels=None, # type: Optional[List[str]] - ylabels=None # type: Optional[List[str]] + self, + title, # type: str + series, # type: str + matrix, # type: np.ndarray + iteration, # type: int + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + xlabels=None, # type: Optional[List[str]] + ylabels=None, # type: Optional[List[str]] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, plot a confusion matrix. @@ -615,24 +643,28 @@ class Logger(object): :param str yaxis: The y-axis title. (Optional) :param list(str) xlabels: Labels for each column of the matrix. (Optional) :param list(str) ylabels: Labels for each row of the matrix. (Optional) + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ self._touch_title_series(title, series) return self.report_confusion_matrix(title, series, matrix, iteration, - xaxis=xaxis, yaxis=yaxis, xlabels=xlabels, ylabels=ylabels) + xaxis=xaxis, yaxis=yaxis, xlabels=xlabels, ylabels=ylabels, + extra_layout=extra_layout) def report_surface( - self, - title, # type: str - series, # type: str - matrix, # type: np.ndarray - iteration, # type: int - xaxis=None, # type: Optional[str] - yaxis=None, # type: Optional[str] - zaxis=None, # type: Optional[str] - xlabels=None, # type: Optional[List[str]] - ylabels=None, # type: Optional[List[str]] - camera=None, # type: Optional[Sequence[float]] - comment=None # type: Optional[str] + self, + title, # type: str + series, # type: str + matrix, # type: np.ndarray + iteration, # type: int + xaxis=None, # type: Optional[str] + yaxis=None, # type: Optional[str] + zaxis=None, # type: Optional[str] + xlabels=None, # type: Optional[List[str]] + ylabels=None, # type: Optional[List[str]] + camera=None, # type: Optional[Sequence[float]] + comment=None, # type: Optional[str] + extra_layout=None, # type: Optional[dict] ): """ For explicit reporting, report a 3d surface plot. @@ -658,6 +690,8 @@ class Logger(object): :param list(str) ylabels: Labels for each row of the matrix. (Optional) :param list(float) camera: X,Y,Z coordinates indicating the camera position. The default value is ``(1,1,1)``. :param str comment: A comment displayed with the plot, underneath the title. + :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ if not isinstance(matrix, np.ndarray): @@ -678,19 +712,20 @@ class Logger(object): ztitle=zaxis, camera=camera, comment=comment, + layout_config=extra_layout, ) def report_image( - self, - title, # type: str - series, # type: str - iteration, # type: int - local_path=None, # type: Optional[str] - image=None, # type: Optional[Union[np.ndarray, Image.Image]] - matrix=None, # type: Optional[np.ndarray] - max_image_history=None, # type: Optional[int] - delete_after_upload=False, # type: bool - url=None # type: Optional[str] + self, + title, # type: str + series, # type: str + iteration, # type: int + local_path=None, # type: Optional[str] + image=None, # type: Optional[Union[np.ndarray, Image.Image]] + matrix=None, # type: Optional[np.ndarray] + max_image_history=None, # type: Optional[int] + delete_after_upload=False, # type: bool + url=None # type: Optional[str] ): """ For explicit reporting, report an image and upload its contents. @@ -787,16 +822,16 @@ class Logger(object): ) def report_media( - self, - title, # type: str - series, # type: str - iteration, # type: int - local_path=None, # type: Optional[str] - stream=None, # type: Optional[six.BytesIO] - file_extension=None, # type: Optional[str] - max_history=None, # type: Optional[int] - delete_after_upload=False, # type: bool - url=None # type: Optional[str] + self, + title, # type: str + series, # type: str + iteration, # type: int + local_path=None, # type: Optional[str] + stream=None, # type: Optional[six.BytesIO] + file_extension=None, # type: Optional[str] + max_history=None, # type: Optional[int] + delete_after_upload=False, # type: bool + url=None # type: Optional[str] ): """ Report an image and upload its contents. @@ -805,7 +840,8 @@ class Logger(object): describing the task ID, title, series and iteration. .. note:: - :paramref:`~.Logger.report_image.local_path`, :paramref:`~.Logger.report_image.url`, :paramref:`~.Logger.report_image.image` and :paramref:`~.Logger.report_image.matrix` + :paramref:`~.Logger.report_image.local_path`, :paramref:`~.Logger.report_image.url`, + :paramref:`~.Logger.report_image.image` and :paramref:`~.Logger.report_image.matrix` are mutually exclusive, and at least one must be provided. :param str title: Title (AKA metric) @@ -953,14 +989,14 @@ class Logger(object): self._flusher.start() def report_image_and_upload( - self, - title, # type: str - series, # type: str - iteration, # type: int - path=None, # type: Optional[str] - matrix=None, # type: # type: Optional[Union[np.ndarray, Image.Image]] - max_image_history=None, # type: Optional[int] - delete_after_upload=False # type: bool + self, + title, # type: str + series, # type: str + iteration, # type: int + path=None, # type: Optional[str] + matrix=None, # type: # type: Optional[Union[np.ndarray, Image.Image]] + max_image_history=None, # type: Optional[int] + delete_after_upload=False # type: bool ): """ .. deprecated:: 0.13.0 @@ -1054,7 +1090,7 @@ class Logger(object): # noinspection PyBroadException try: # make sure we are writing to the original stdout - StdStreamPatch.stdout_original_write(str(msg)+'\n') + StdStreamPatch.stdout_original_write(str(msg) + '\n') except Exception: pass else: @@ -1064,14 +1100,14 @@ class Logger(object): self._start_task_if_needed() def _report_image_plot_and_upload( - self, - title, # type: str - series, # type: str - iteration, # type: int - path=None, # type: Optional[str] - matrix=None, # type: Optional[np.ndarray] - max_image_history=None, # type: Optional[int] - delete_after_upload=False # type: bool + self, + title, # type: str + series, # type: str + iteration, # type: int + path=None, # type: Optional[str] + matrix=None, # type: Optional[np.ndarray] + max_image_history=None, # type: Optional[int] + delete_after_upload=False # type: bool ): """ Report an image, upload its contents, and present in plots section using plotly @@ -1119,13 +1155,13 @@ class Logger(object): ) def _report_file_and_upload( - self, - title, # type: str - series, # type: str - iteration, # type: int - path=None, # type: Optional[str] - max_file_history=None, # type: Optional[int] - delete_after_upload=False # type: bool + self, + title, # type: str + series, # type: str + iteration, # type: int + path=None, # type: Optional[str] + max_file_history=None, # type: Optional[int] + delete_after_upload=False # type: bool ): """ Upload a file and report it as link in the debug images section. diff --git a/trains/utilities/dicts.py b/trains/utilities/dicts.py index fb0e2f15..ac9ca225 100644 --- a/trains/utilities/dicts.py +++ b/trains/utilities/dicts.py @@ -109,3 +109,15 @@ class NestedBlobsDict(BlobsDict): def keys(self): return self._keys(self, '') + + +def merge_dicts(dict1, dict2): + """ Recursively merges dict2 into dict1 """ + if not isinstance(dict1, dict) or not isinstance(dict2, dict): + return dict2 + for k in dict2: + if k in dict1: + dict1[k] = merge_dicts(dict1[k], dict2[k]) + else: + dict1[k] = dict2[k] + return dict1 diff --git a/trains/utilities/plotly_reporter.py b/trains/utilities/plotly_reporter.py index bf30d659..3514d36e 100644 --- a/trains/utilities/plotly_reporter.py +++ b/trains/utilities/plotly_reporter.py @@ -1,6 +1,7 @@ import numpy as np from ..errors import UsageError +from ..utilities.dicts import merge_dicts try: import pandas as pd @@ -10,7 +11,7 @@ from attr import attrs, attrib def create_2d_histogram_plot(np_row_wise, labels, title=None, xtitle=None, ytitle=None, series=None, xlabels=None, - comment=None, mode='group'): + comment=None, mode='group', layout_config=None): """ Create a 2D Plotly histogram chart from a 2D numpy array :param np_row_wise: 2D numpy data array @@ -20,12 +21,15 @@ def create_2d_histogram_plot(np_row_wise, labels, title=None, xtitle=None, ytitl :param ytitle: Y-Series title :param comment: comment underneath the title :param mode: multiple histograms mode. valid options are: stack / group / relative. Default is 'group'. + :param layout_config: optional extra layout configuration :return: Plotly chart dict """ assert mode in ('stack', 'group', 'relative') np_row_wise = np.atleast_2d(np_row_wise) assert len(np_row_wise.shape) == 2, "Expected a 2D numpy array" + use_series = bool(labels) + # using labels without xlabels leads to original behavior if labels is not None and xlabels is None: assert len(labels) == np_row_wise.shape[0], "Please provide a label for each data row" @@ -41,7 +45,9 @@ def create_2d_histogram_plot(np_row_wise, labels, title=None, xtitle=None, ytitl data = [_np_row_to_plotly_data_item(np_row=np_row_wise[i, :], label=labels[i] if labels else None, xlabels=xlabels) for i in range(np_row_wise.shape[0])] - return _plotly_hist_dict(title=title, xtitle=xtitle, ytitle=ytitle, mode=mode, data=data, comment=comment) + return _plotly_hist_dict(title=series if use_series else title, + xtitle=xtitle, ytitle=ytitle, mode=mode, data=data, comment=comment, + layout_config=layout_config) def _to_np_array(value): @@ -73,7 +79,8 @@ class SeriesInfo(object): ) -def create_line_plot(title, series, xtitle, ytitle, mode='lines', reverse_xaxis=False, comment=None, MAX_SIZE=None): +def create_line_plot(title, series, xtitle, ytitle, mode='lines', reverse_xaxis=False, + comment=None, MAX_SIZE=None, layout_config=None): plotly_obj = _plotly_scatter_layout_dict( title=title if not comment else (title + '
' + comment + ''), xaxis_title=xtitle, @@ -119,11 +126,14 @@ def create_line_plot(title, series, xtitle, ytitle, mode='lines', reverse_xaxis= "type": "scatter", } for s in series) + if layout_config: + plotly_obj["layout"] = merge_dicts(plotly_obj["layout"], layout_config) + return plotly_obj def create_2d_scatter_series(np_row_wise, title="Scatter", series_name="Series", xtitle="x", ytitle="y", mode="lines", - labels=None, comment=None): + labels=None, comment=None, layout_config=None): """ Create a 2D scatter Plotly graph from a 2 column numpy array :param np_row_wise: 2 column numpy data array [(x0,y0), (x1,y1) ...] @@ -134,6 +144,7 @@ def create_2d_scatter_series(np_row_wise, title="Scatter", series_name="Series", :param mode: scatter type mode ('lines' / 'markers' / 'lines+markers') :param labels: label (text) per point on the scatter graph :param comment: comment underneath the title + :param layout_config: optional dictionary for layout configuration, passed directly to plotly :return: Plotly chart dict :return: """ @@ -154,12 +165,12 @@ def create_2d_scatter_series(np_row_wise, title="Scatter", series_name="Series", series = SeriesInfo(name=series_name, data=np_row_wise, labels=labels) return create_line_plot(title=title, series=[series], xtitle=xtitle, ytitle=ytitle, mode=mode, - comment=comment, MAX_SIZE=100000) + comment=comment, MAX_SIZE=100000, layout_config=layout_config) def create_3d_scatter_series(np_row_wise, title="Scatter", series_name="Series", xtitle="x", ytitle="y", ztitle="z", mode="lines", color=((217, 217, 217, 0.14),), marker_size=5, line_width=0.8, - labels=None, fill_axis=-1, plotly_obj=None): + labels=None, fill_axis=-1, plotly_obj=None, layout_config=None): """ Create a 3D scatter Plotly graph from a 3 column numpy array :param np_row_wise: 3 column numpy data array [(x0,y0,z0), (x1,y1,z1) ...] @@ -170,6 +181,7 @@ def create_3d_scatter_series(np_row_wise, title="Scatter", series_name="Series", :param ztitle: Z-axis title :param labels: label (text) per point on the scatter graph :param fill_axis: fill area under the curve + :param layout_config: additional layout configuration :return: Plotly chart dict :return: """ @@ -198,11 +210,15 @@ def create_3d_scatter_series(np_row_wise, title="Scatter", series_name="Series", }, } plotly_obj["data"].append(this_scatter_data) + + if layout_config: + plotly_obj["layout"] = merge_dicts(plotly_obj["layout"], layout_config) + return plotly_obj def create_value_matrix(np_value_matrix, title="Heatmap Matrix", xlabels=None, ylabels=None, xtitle="X", ytitle="Y", - custom_colors=True, series=None, comment=None): + custom_colors=True, series=None, comment=None, layout_config=None): conf_matrix_plot = { "data": [ { @@ -231,12 +247,15 @@ def create_value_matrix(np_value_matrix, title="Heatmap Matrix", xlabels=None, y conf_matrix_plot["data"][0].update({"colorscale": scale}) conf_matrix_plot["data"][0].update({"colorbar": bar}) + if layout_config: + conf_matrix_plot["layout"] = merge_dicts(conf_matrix_plot["layout"], layout_config) + return conf_matrix_plot def create_3d_surface(np_value_matrix, title="3D Surface", xlabels=None, ylabels=None, xtitle="X", ytitle="Y", - ztitle="Z", custom_colors=True, series=None, camera=None, comment=None): - conf_matrix_plot = { + ztitle="Z", custom_colors=True, series=None, camera=None, comment=None, layout_config=None): + surface_plot = { "data": [ { "z": np_value_matrix.tolist(), @@ -278,17 +297,20 @@ def create_3d_surface(np_value_matrix, title="3D Surface", xlabels=None, ylabels } } if camera: - conf_matrix_plot['layout']['scene']['camera'] = {"eye": {"x": camera[0], "y": camera[1], "z": camera[2]}} + surface_plot['layout']['scene']['camera'] = {"eye": {"x": camera[0], "y": camera[1], "z": camera[2]}} if custom_colors: scale, bar = _get_z_colorbar_data() - conf_matrix_plot["data"][0].update({"colorscale": scale}) - conf_matrix_plot["data"][0].update({"colorbar": bar}) + surface_plot["data"][0].update({"colorscale": scale}) + surface_plot["data"][0].update({"colorbar": bar}) - return conf_matrix_plot + if layout_config: + surface_plot["layout"] = merge_dicts(surface_plot["layout"], layout_config) + + return surface_plot -def create_image_plot(image_src, title, width=640, height=480, series=None, comment=None): +def create_image_plot(image_src, title, width=640, height=480, series=None, comment=None, layout_config=None): image_plot = { "data": [], "layout": { @@ -315,6 +337,10 @@ def create_image_plot(image_src, title, width=640, height=480, series=None, comm "name": series, } } + + if layout_config: + image_plot["layout"] = merge_dicts(image_plot["layout"], layout_config) + return image_plot @@ -339,7 +365,7 @@ def _get_z_colorbar_data(z_data=None, values=None, colors=None): return colorscale, colorbar -def _plotly_hist_dict(title, xtitle, ytitle, mode='group', data=None, comment=None): +def _plotly_hist_dict(title, xtitle, ytitle, mode='group', data=None, comment=None, layout_config=None): """ Create a basic Plotly chart dictionary :param title: Chart title @@ -348,11 +374,12 @@ def _plotly_hist_dict(title, xtitle, ytitle, mode='group', data=None, comment=No :param mode: multiple histograms mode. optionals stack / group / relative. Default is 'group'. :param data: Data items :type data: list + :param layout_config: dict :return: Plotly chart dict """ assert mode in ('stack', 'group', 'relative') - return { + plotly_object = { "data": data or [], "layout": { "title": title if not comment else (title + '
' + comment + ''), @@ -367,6 +394,10 @@ def _plotly_hist_dict(title, xtitle, ytitle, mode='group', data=None, comment=No "bargroupgap": 0 } } + if layout_config: + plotly_object["layout"] = merge_dicts(plotly_object["layout"], layout_config) + + return plotly_object def _np_row_to_plotly_data_item(np_row, label, xlabels=None): @@ -413,8 +444,8 @@ def _plotly_scatter_layout_dict(title="Scatter", xaxis_title="X", yaxis_title="Y def plotly_scatter3d_layout_dict(title="Scatter", xaxis_title="X", yaxis_title="Y", zaxis_title="Z", - series=None, show_legend=True, comment=None): - return { + series=None, show_legend=True, comment=None, layout_config=None): + plotly_object = { "data": [], "layout": { "showlegend": show_legend, @@ -428,8 +459,13 @@ def plotly_scatter3d_layout_dict(title="Scatter", xaxis_title="X", yaxis_title=" } } + if layout_config: + plotly_object["layout"] = merge_dicts(plotly_object["layout"], layout_config) -def create_plotly_table(table_plot, title, series): + return plotly_object + + +def create_plotly_table(table_plot, title, series, layout_config=None): """ Create a basic Plotly table json style to be sent @@ -439,6 +475,7 @@ def create_plotly_table(table_plot, title, series): :type title: str :param series: Series (AKA variant) :type series: str + :param layout_config: additional configuration layout """ if not pd: raise UsageError( @@ -474,4 +511,7 @@ def create_plotly_table(table_plot, title, series): "name": series, } } + if layout_config: + ret["layout"] = merge_dicts(ret["layout"], layout_config) + return ret