def write_fit_report(result_entries: List[AnalysisResultData]) -> str:
    """A function that generates fit reports documentation from list of data.

    Args:
        result_entries: List of data entries.

    Returns:
        Documentation of fit reports.
    """
    analysis_description = ""

    def format_val(float_val: float) -> str:
        if np.abs(float_val) < 1e-3 or np.abs(float_val) > 1e3:
            return f"{float_val: .4e}"
        return f"{float_val: .4g}"

    for res in result_entries:
        if isinstance(res.value, uncertainties.UFloat):
            fitval = res.value
            unit = res.extra.get("unit", None)
            if unit:
                # unit is defined. do detaching prefix, i.e. 1000 Hz -> 1 kHz
                val, val_prefix = detach_prefix(fitval.nominal_value,
                                                decimal=3)
                val_unit = val_prefix + unit
                value_repr = f"{val: .3g}"

                # write error bar if it is finite value
                if fitval.std_dev is not None and np.isfinite(fitval.std_dev):
                    # with stderr
                    err, err_prefix = detach_prefix(fitval.std_dev, decimal=3)
                    err_unit = err_prefix + unit
                    if val_unit == err_unit:
                        # same value scaling, same prefix
                        value_repr += f" \u00B1 {err: .2f} {val_unit}"
                    else:
                        # different value scaling, different prefix
                        value_repr += f" {val_unit} \u00B1 {err: .2f} {err_unit}"
                else:
                    # without stderr, just append unit
                    value_repr += f" {val_unit}"
            else:
                # unit is not defined. raw value formatting is performed.
                value_repr = format_val(fitval.nominal_value)
                if np.isfinite(fitval.std_dev):
                    # with stderr
                    value_repr += f" \u00B1 {format_val(fitval.std_dev)}"

            analysis_description += f"{res.name} = {value_repr}\n"

    return analysis_description
Beispiel #2
0
    def test_detach_prefix(self):
        """Test detach prefix from the value."""
        ref_values = [
            (1e12, (1.0, "T")),
            (1e11, (100.0, "G")),
            (1e10, (10.0, "G")),
            (1e9, (1.0, "G")),
            (1e8, (100.0, "M")),
            (1e7, (10.0, "M")),
            (1e6, (1.0, "M")),
            (1e5, (100.0, "k")),
            (1e4, (10.0, "k")),
            (1e3, (1.0, "k")),
            (100, (100.0, "")),
            (10, (10.0, "")),
            (1.0, (1.0, "")),
            (0.1, (100.0, "m")),
            (0.01, (10.0, "m")),
            (1e-3, (1.0, "m")),
            (1e-4, (100.0, "µ")),
            (1e-5, (10.0, "µ")),
            (1e-6, (1.0, "µ")),
            (1e-7, (100.0, "n")),
            (1e-8, (10.0, "n")),
            (1e-9, (1.0, "n")),
            (1e-10, (100.0, "p")),
            (1e-11, (10.0, "p")),
            (1e-12, (1.0, "p")),
        ]

        for arg, ref_rets in ref_values:
            self.assertTupleEqual(detach_prefix(arg), ref_rets)
Beispiel #3
0
    def test_get_same_value_after_attach_detach(self, value: float):
        """Test if same value can be obtained."""
        unit = "Hz"

        for prefix in ["P", "T", "G", "k", "m", "µ", "n", "p", "f"]:
            scaled_val = apply_prefix(value, prefix + unit)
            test_val, ret_prefix = detach_prefix(scaled_val)
            self.assertAlmostEqual(test_val, value)
            self.assertEqual(prefix, ret_prefix)
Beispiel #4
0
 def _format_val(value):
     # Return value with unit with prefix, i.e. 1000 Hz -> 1 kHz.
     if unit:
         try:
             val, val_prefix = detach_prefix(value, decimal=3)
         except ValueError:
             val = value
             val_prefix = ""
         return f"{val: .3g}", f" {val_prefix}{unit}"
     if np.abs(value) < 1e-3 or np.abs(value) > 1e3:
         return f"{value: .4e}", ""
     return f"{value: .4g}", ""
Beispiel #5
0
    def draw(
        cls,
        series_defs: List[SeriesDef],
        raw_samples: List[CurveData],
        fit_samples: List[CurveData],
        tick_labels: Dict[str, str],
        fit_data: FitData,
        result_entries: List[AnalysisResultData],
        style: Optional[PlotterStyle] = None,
        axis: Optional["matplotlib.axes.Axes"] = None,
    ) -> "pyplot.Figure":
        """Create a fit result of all curves in the single canvas.

        Args:
            series_defs: List of definition for each curve.
            raw_samples: List of raw sample data for each curve.
            fit_samples: List of formatted sample data for each curve.
            tick_labels: Dictionary of axis label information. Axis units and label for x and y
                value should be explained.
            fit_data: fit data generated by the analysis.
            result_entries: List of analysis result data entries.
            style: Optional. A configuration object to modify the appearance of the figure.
            axis: Optional. A matplotlib Axis object.

        Returns:
            A matplotlib figure of the curve fit result.
        """
        if axis is None:
            axis = get_non_gui_ax()

            # update image size to experiment default
            figure = axis.get_figure()
            figure.set_size_inches(*style.figsize)
        else:
            figure = axis.get_figure()

        # draw all curves on the same canvas
        for series_def, raw_samp, fit_samp in zip(series_defs, raw_samples,
                                                  fit_samples):
            draw_single_curve_mpl(
                axis=axis,
                series_def=series_def,
                raw_sample=raw_samp,
                fit_sample=fit_samp,
                fit_data=fit_data,
                style=style,
            )

        # add legend
        if len(series_defs) > 1:
            axis.legend(loc=style.legend_loc)

        # get axis scaling factor
        for this_axis in ("x", "y"):
            sub_axis = getattr(axis, this_axis + "axis")
            unit = tick_labels[this_axis + "val_unit"]
            label = tick_labels[this_axis + "label"]
            if unit:
                maxv = np.max(np.abs(sub_axis.get_data_interval()))
                scaled_maxv, prefix = detach_prefix(maxv, decimal=3)
                prefactor = scaled_maxv / maxv
                # pylint: disable=cell-var-from-loop
                sub_axis.set_major_formatter(
                    FuncFormatter(lambda x, p: f"{x * prefactor: .3g}"))
                sub_axis.set_label_text(f"{label} [{prefix}{unit}]",
                                        fontsize=style.axis_label_size)
            else:
                sub_axis.set_label_text(label, fontsize=style.axis_label_size)
                axis.ticklabel_format(axis=this_axis,
                                      style="sci",
                                      scilimits=(-3, 3))

        if tick_labels["xlim"]:
            axis.set_xlim(tick_labels["xlim"])

        if tick_labels["ylim"]:
            axis.set_ylim(tick_labels["ylim"])

        # write analysis report
        if fit_data:
            report_str = write_fit_report(result_entries)
            report_str += r"Fit $\chi^2$ = " + f"{fit_data.reduced_chisq: .4g}"

            report_handler = axis.text(
                *style.fit_report_rpos,
                report_str,
                ha="center",
                va="top",
                size=style.fit_report_text_size,
                transform=axis.transAxes,
            )

            bbox_props = dict(boxstyle="square, pad=0.3",
                              fc="white",
                              ec="black",
                              lw=1,
                              alpha=0.8)
            report_handler.set_bbox(bbox_props)

        axis.tick_params(labelsize=style.tick_label_size)
        axis.grid(True)

        return figure
Beispiel #6
0
    def draw(
        cls,
        series_defs: List[SeriesDef],
        raw_samples: List[CurveData],
        fit_samples: List[CurveData],
        tick_labels: Dict[str, str],
        fit_data: FitData,
        result_entries: List[AnalysisResultData],
        style: Optional[PlotterStyle] = None,
        axis: Optional["matplotlib.axes.Axes"] = None,
    ) -> "pyplot.Figure":
        """Create a fit result of all curves in the single canvas.

        Args:
            series_defs: List of definition for each curve.
            raw_samples: List of raw sample data for each curve.
            fit_samples: List of formatted sample data for each curve.
            tick_labels: Dictionary of axis label information. Axis units and label for x and y
                value should be explained.
            fit_data: fit data generated by the analysis.
            result_entries: List of analysis result data entries.
            style: Optional. A configuration object to modify the appearance of the figure.
            axis: Optional. A matplotlib Axis object.

        Returns:
            A matplotlib figure of the curve fit result.
        """
        if axis is None:
            axis = get_non_gui_ax()

            # update image size to experiment default
            figure = axis.get_figure()
            figure.set_size_inches(*style.figsize)
        else:
            figure = axis.get_figure()

        # get canvas number
        n_subplots = max(series_def.canvas for series_def in series_defs) + 1

        # use inset axis. this allows us to draw multiple canvases on a given single axis object
        inset_ax_h = (1 - (0.05 * (n_subplots - 1))) / n_subplots
        inset_axes = [
            axis.inset_axes(
                [
                    0, 1 -
                    (inset_ax_h + 0.05) * n_axis - inset_ax_h, 1, inset_ax_h
                ],
                transform=axis.transAxes,
                zorder=1,
            ) for n_axis in range(n_subplots)
        ]

        # show x label only in the bottom canvas
        for inset_axis in inset_axes[:-1]:
            inset_axis.set_xticklabels([])
        inset_axes[-1].get_shared_x_axes().join(*inset_axes)

        # remove original axis frames
        axis.spines.right.set_visible(False)
        axis.spines.left.set_visible(False)
        axis.spines.top.set_visible(False)
        axis.spines.bottom.set_visible(False)
        axis.set_xticks([])
        axis.set_yticks([])

        # collect data source per canvas
        plot_map = defaultdict(list)
        for curve_ind, series_def in enumerate(series_defs):
            plot_map[series_def.canvas].append(curve_ind)

        y_labels = tick_labels["ylabel"].split(",")
        if len(y_labels) == 1:
            y_labels = y_labels * n_subplots

        for ax_ind, curve_inds in plot_map.items():
            inset_axis = inset_axes[ax_ind]

            for curve_ind in curve_inds:
                draw_single_curve_mpl(
                    axis=inset_axis,
                    series_def=series_defs[curve_ind],
                    raw_sample=raw_samples[curve_ind],
                    fit_sample=fit_samples[curve_ind],
                    fit_data=fit_data,
                    style=style,
                )

            # add legend to each inset axis
            if len(curve_inds) > 1:
                inset_axis.legend(loc=style.legend_loc)

            # format y axis tick value of each inset axis
            yaxis = getattr(inset_axis, "yaxis")
            unit = tick_labels["yval_unit"]
            label = y_labels[ax_ind]
            if unit:
                maxv = np.max(np.abs(yaxis.get_data_interval()))
                scaled_maxv, prefix = detach_prefix(maxv, decimal=3)
                prefactor = scaled_maxv / maxv
                # pylint: disable=cell-var-from-loop
                yaxis.set_major_formatter(
                    FuncFormatter(lambda x, p: f"{x * prefactor: .3g}"))
                yaxis.set_label_text(f"{label} [{prefix}{unit}]",
                                     fontsize=style.axis_label_size)
            else:
                inset_axis.ticklabel_format(axis="y",
                                            style="sci",
                                            scilimits=(-3, 3))
                yaxis.set_label_text(label, fontsize=style.axis_label_size)

            if tick_labels["ylim"]:
                inset_axis.set_ylim(tick_labels["ylim"])

        # format x axis
        xaxis = getattr(inset_axes[-1], "xaxis")
        unit = tick_labels["xval_unit"]
        label = tick_labels["xlabel"]
        if unit:
            maxv = np.max(np.abs(xaxis.get_data_interval()))
            scaled_maxv, prefix = detach_prefix(maxv, decimal=3)
            prefactor = scaled_maxv / maxv
            # pylint: disable=cell-var-from-loop
            xaxis.set_major_formatter(
                FuncFormatter(lambda x, p: f"{x * prefactor: .3g}"))
            xaxis.set_label_text(f"{label} [{prefix}{unit}]",
                                 fontsize=style.axis_label_size)
        else:
            axis.ticklabel_format(axis="x", style="sci", scilimits=(-3, 3))
            xaxis.set_label_text(label, fontsize=style.axis_label_size)

        if tick_labels["xlim"]:
            inset_axes[-1].set_xlim(tick_labels["xlim"])

        # write analysis report
        if fit_data:
            report_str = write_fit_report(result_entries)
            report_str += r"Fit $\chi^2$ = " + f"{fit_data.reduced_chisq: .4g}"

            report_handler = axis.text(
                *style.fit_report_rpos,
                report_str,
                ha="center",
                va="top",
                size=style.fit_report_text_size,
                transform=axis.transAxes,
            )

            bbox_props = dict(boxstyle="square, pad=0.3",
                              fc="white",
                              ec="black",
                              lw=1,
                              alpha=0.8)
            report_handler.set_bbox(bbox_props)

        axis.tick_params(labelsize=style.tick_label_size)
        axis.grid(True)

        return figure
    def format_canvas(self):
        if self._axis.child_axes:
            # Multi canvas mode
            all_axes = self._axis.child_axes
        else:
            all_axes = [self._axis]

        # Add data labels if there are multiple labels registered per sub_ax.
        for sub_ax in all_axes:
            _, labels = sub_ax.get_legend_handles_labels()
            if len(labels) > 1:
                sub_ax.legend()

        # Format x and y axis
        for ax_type in ("x", "y"):
            # Get axis formatter from drawing options
            if ax_type == "x":
                lim = self.options.xlim
                unit = self.options.xval_unit
            else:
                lim = self.options.ylim
                unit = self.options.yval_unit

            # Compute data range from auto scale
            if not lim:
                v0 = np.nan
                v1 = np.nan
                for sub_ax in all_axes:
                    if ax_type == "x":
                        this_v0, this_v1 = sub_ax.get_xlim()
                    else:
                        this_v0, this_v1 = sub_ax.get_ylim()
                    v0 = np.nanmin([v0, this_v0])
                    v1 = np.nanmax([v1, this_v1])
                lim = (v0, v1)

            # Format axis number notation
            if unit:
                # If value is specified, automatically scale axis magnitude
                # and write prefix to axis label, i.e. 1e3 Hz -> 1 kHz
                maxv = max(np.abs(lim[0]), np.abs(lim[1]))
                try:
                    scaled_maxv, prefix = detach_prefix(maxv, decimal=3)
                    prefactor = scaled_maxv / maxv
                except ValueError:
                    prefix = ""
                    prefactor = 1

                formatter = MplCurveDrawer.PrefixFormatter(prefactor)
                units_str = f" [{prefix}{unit}]"
            else:
                # Use scientific notation with 3 digits, 1000 -> 1e3
                formatter = ScalarFormatter()
                formatter.set_scientific(True)
                formatter.set_powerlimits((-3, 3))

                units_str = ""

            for sub_ax in all_axes:
                if ax_type == "x":
                    ax = getattr(sub_ax, "xaxis")
                    tick_labels = sub_ax.get_xticklabels()
                else:
                    ax = getattr(sub_ax, "yaxis")
                    tick_labels = sub_ax.get_yticklabels()

                if tick_labels:
                    # Set formatter only when tick labels exist
                    ax.set_major_formatter(formatter)
                if units_str:
                    # Add units to label if both exist
                    label_txt_obj = ax.get_label()
                    label_str = label_txt_obj.get_text()
                    if label_str:
                        label_txt_obj.set_text(label_str + units_str)

            # Auto-scale all axes to the first sub axis
            if ax_type == "x":
                all_axes[0].get_shared_x_axes().join(*all_axes)
                all_axes[0].set_xlim(lim)
            else:
                all_axes[0].get_shared_y_axes().join(*all_axes)
                all_axes[0].set_ylim(lim)
Beispiel #8
0
 def test_detach_prefix_with_value_too_small(self):
     """Test detach prefix by input too small value."""
     with self.assertRaises(Exception):
         self.assertTupleEqual(detach_prefix(1e-20), (1e-20, ""))
Beispiel #9
0
 def test_detach_prefix_with_negative(self):
     """Test detach prefix by input negative values."""
     self.assertTupleEqual(detach_prefix(-1.234e7), (-12.34, "M"))
Beispiel #10
0
 def test_detach_prefix_with_zero(self):
     """Test detach prefix by input zero."""
     self.assertTupleEqual(detach_prefix(0.0), (0.0, ""))
Beispiel #11
0
 def test_get_symbol_mu(self):
     """Test if µ is returned rather than u."""
     _, prefix = detach_prefix(3e-6)
     self.assertEqual(prefix, "µ")