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
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)
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)
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}", ""
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
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)
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, ""))
def test_detach_prefix_with_negative(self): """Test detach prefix by input negative values.""" self.assertTupleEqual(detach_prefix(-1.234e7), (-12.34, "M"))
def test_detach_prefix_with_zero(self): """Test detach prefix by input zero.""" self.assertTupleEqual(detach_prefix(0.0), (0.0, ""))
def test_get_symbol_mu(self): """Test if µ is returned rather than u.""" _, prefix = detach_prefix(3e-6) self.assertEqual(prefix, "µ")