示例#1
0
def test_hist_input_type_validation(logging_mixin: Any, bin_edges: np.ndarray, y: np.ndarray,
                                    errors_squared: np.ndarray, expect_corrected_types: str) -> None:
    """ Check histogram input type validation. """
    if expect_corrected_types:
        h = histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = errors_squared)
        for arr in [h.bin_edges, h.y, h.errors_squared]:
            assert isinstance(arr, np.ndarray)
    else:
        with pytest.raises(ValueError) as exception_info:
            h = histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = errors_squared)
        assert "Arrays must be numpy arrays" in exception_info.value.args[0]
示例#2
0
def setup_basic_hist(logging_mixin: Any) -> Tuple[histogram.Histogram1D, np.ndarray, np.ndarray, np.ndarray]:
    """ Setup a basic `Histogram1D` for basic tests.

    This histogram contains 4 bins, with edges of [0, 1, 2, 3, 5], values of [2, 2, 3, 0], with
    errors of [4, 2, 3, 0], simulating the first bin being filled once with a weight of 2, and the
    rest being filled normally. It could be reproduced in ROOT with:

    >>> bins = np.array([0, 1, 2, 3, 5], dtype = np.float64)
    >>> hist = ROOT.TH1F("test", "test", 4, binning)
    >>> hist.Fill(0, 2)
    >>> hist.Fill(1)
    >>> hist.Fill(1)
    >>> hist.Fill(2)
    >>> hist.Fill(2)
    >>> hist.Fill(2)

    Args:
        None.
    Returns:
        hist, bin_edges, y, errors_squared
    """
    bin_edges = np.array([0, 1, 2, 3, 5])
    y = np.array([2, 2, 3, 0])
    # As if the first bin was filled with weight of 2.
    errors_squared = np.array([4, 2, 3, 0])

    h = histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = errors_squared)

    return h, bin_edges, y, errors_squared
示例#3
0
 def convert_to_histogram_1D(self, bin_edges: np.array) -> histogram.Histogram1D:
     """ Convert these stored values into a ``Histogram1D``. """
     return histogram.Histogram1D(
         bin_edges = bin_edges,
         y = self.y,
         errors_squared = self.errors_squared,
     )
示例#4
0
    def _calculate_resolution(self) -> histogram.Histogram1D:
        """ Calculate the event plane resolution.

        Args:
            None.
        Returns:
            Resolutions and errors calculated for each centrality bin.
        """
        # R = sqrt(multiply all other detectors/main detector)
        # NOTE: main_detector actually contains the cosine of the difference of the other detectors. The terminology
        #       is a bit confusing. As an example, for VZERO resolution, sqrt((TPC_Pos * TPC_Neg)/VZERO)
        # NOTE: The output of reduce is an element-wise multiplication of all other_detector arrays. For
        #       the example of two other detectors, this is the same as just other[0].data * other[1].data
        resolution_squared = reduce(
            lambda x, y: x * y,
            [other.data
             for other in self.other_detectors]) / self.main_detector.data
        # We sometimes end up with a few very small negative values. This is a problem for sqrt, so we
        # explicitly set them to 0.
        resolution_squared.y[resolution_squared.y < 0] = 0
        resolutions = np.sqrt(resolution_squared.y)
        # Sometimes the resolution is 0, so we carefully divide to avoid NaNs
        errors = np.divide(
            1. / 2 * resolution_squared.errors,
            resolutions,
            out=np.zeros_like(resolution_squared.errors),
            where=resolutions != 0,
        )

        # Return the values in a histogram for convenience
        return histogram.Histogram1D(
            bin_edges=self.main_detector.data.bin_edges,
            y=resolutions,
            errors_squared=errors**2)
示例#5
0
def test_hist_identical_arrays(logging_mixin: Any) -> None:
    """ Test handling receiving identical numpy arrays. """
    bin_edges = np.array([1, 2, 3])
    y = np.array([1, 2])

    h = histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = y)
    # Even through we passed the same array, they should be copied by the validation.
    assert np.may_share_memory(h.y, h.errors_squared) is False
示例#6
0
def test_histogram1D_equality(logging_mixin, setup_basic_hist, test_equality, access_attributes_which_are_stored):
    """ Test for Histogram1D equality. """
    h, bin_edges, y, errors_squared = setup_basic_hist

    h1 = histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = errors_squared)
    h2 = histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = errors_squared)

    if access_attributes_which_are_stored:
        # This attribute will be stored (but under "_x"), so we want to make sure that it
        # doesn't disrupt the equality comparison.
        h1.x

    if not test_equality:
        h1.bin_edges = np.array([5, 6, 7, 8, 9])

    if test_equality:
        assert h1 == h2
    else:
        assert h1 != h2
示例#7
0
def test_chi_squared(logging_mixin: Any) -> None:
    """ Test the chi squared calculation. """
    # Setup
    h = histogram.Histogram1D(
        bin_edges=np.array(np.arange(-0.5, 5.5)),
        y=np.array(np.ones(5)),
        errors_squared=np.ones(5),
    )
    chi_squared = cost_function.ChiSquared(f=func_1, data=h)

    # Check that it's set up properly
    assert chi_squared.func_code.co_varnames == ["a", "b"]

    # Calculate the chi_squared for the given parameters.
    result = chi_squared(np.array(range(-1, -6, -1)), np.zeros(5))
    # Each term is (1 - -1)^2 / 1^2 = 4
    assert result == 4 * 5
示例#8
0
def signal_dominated_with_background_function(
        analysis: "correlations.Correlations") -> None:
    """ Plot the signal dominated hist with the background function. """
    # Setup
    fig, ax = plt.subplots(figsize=(8, 6))

    # Plot signal and background dominated hists
    plot_correlations.plot_and_label_1d_signal_and_background_with_matplotlib_on_axis(
        ax=ax, jet_hadron=analysis)

    # Plot background function
    # First we retrieve the signal dominated histogram to get reference x values and bin edges.
    h = histogram.Histogram1D.from_existing_hist(
        analysis.correlation_hists_delta_phi.signal_dominated.hist)
    background = histogram.Histogram1D(
        bin_edges=h.bin_edges,
        y=analysis.fit_object.evaluate_background(h.x),
        errors_squared=analysis.fit_object.
        calculate_background_function_errors(h.x)**2,
    )
    background_plot = ax.plot(background.x,
                              background.y,
                              label="Background function")
    ax.fill_between(
        background.x,
        background.y - background.errors,
        background.y + background.errors,
        facecolor=background_plot[0].get_color(),
        alpha=0.9,
    )

    # Labeling
    ax.legend(loc="upper right")

    # Final adjustments
    fig.tight_layout()
    # Save plot and cleanup
    plot_base.save_plot(
        analysis.output_info, fig,
        f"jetH_delta_phi_{analysis.identifier}_signal_background_function_comparison"
    )
    plt.close(fig)
示例#9
0
def test_Histogram1D_with_yaml(logging_mixin: Any) -> None:
    """ Test writing and then reading a Histogram1D via YAML.

    This ensures that writing a histogram1D can be done successfully.
    """
    # Setup
    # YAML object
    y = yaml.yaml(classes_to_register=[histogram.Histogram1D])
    # Test hist
    input_hist = histogram.Histogram1D(bin_edges=np.linspace(0, 10, 11),
                                       y=np.linspace(2, 20, 10),
                                       errors_squared=np.linspace(2, 20, 10))
    # Access "x" since it is generated but then stored in the class. This could disrupt YAML, so
    # we should explicitly test it.
    input_hist.x

    # Dump and load (ie round trip)
    output_hist = dump_to_string_and_retrieve(input_hist, y=y)

    # Check the result
    assert input_hist == output_hist
示例#10
0
    def to_histogram1D(self) -> Any:
        """Convert to a Histogram 1D.

        This is entirely a convenience function. Generally, it's best to stay with BinnedData, but
        a Histogram1D is required in some cases, such as for fitting.

        Returns:
            Histogram1D containing the data.
        """
        # Validation
        if len(self.axes) > 1:
            raise ValueError(
                f"Can only convert to 1D histogram. Given {len(self.axes)} axes"
            )

        from pachyderm import histogram

        return histogram.Histogram1D(
            bin_edges=self.axes[0].bin_edges,
            y=self.values,
            errors_squared=self.variances,
        )
def restrict_hist_range(hist: histogram.Histogram1D, min_x: float,
                        max_x: float) -> histogram.Histogram1D:
    """ Restrict the histogram to only be within a provided x range.

    Args:
        hist: Histogram to be restricted.
        min_x: Minimum x value.
        max_x: Maximum x value.
    Returns:
        Restricted histogram.
    """
    selected_range = slice(hist.find_bin(min_x + epsilon),
                           hist.find_bin(max_x - epsilon) + 1)
    # Need the +/- epsilons here to be extra safe, because apparently some of the <= and >= can fail
    # (as might be guessed with floats, but I hadn't observed until now). We don't do this above
    # because we don't want to be inclusive on the edges.
    bin_edges_selected_range = ((hist.bin_edges >= min_x - epsilon) &
                                (hist.bin_edges <= max_x + epsilon))
    return histogram.Histogram1D(
        bin_edges=hist.bin_edges[bin_edges_selected_range],
        y=hist.y[selected_range],
        errors_squared=hist.errors_squared[selected_range])
示例#12
0
    def _setup(
        self, h: histogram.Histogram1D
    ) -> Tuple[histogram.Histogram1D, FitArguments]:
        """ Setup the histogram and arguments for the fit.

        Args:
            h: Background subtracted histogram to be fit.
        Returns:
            Histogram to use for the fit, default arguments for the fit. Note that the histogram may be range
                restricted or otherwise modified here.
        """
        # Restrict the range so that we only fit within the desired input.
        restricted_range = (h.x > self.fit_range.min) & (h.x <
                                                         self.fit_range.max)
        restricted_hist = histogram.Histogram1D(
            # We need the bin edges to be inclusive.
            bin_edges=h.bin_edges[(h.bin_edges >= self.fit_range.min)
                                  & (h.bin_edges <= self.fit_range.max)],
            y=h.y[restricted_range],
            errors_squared=h.errors_squared[restricted_range])

        # Default arguments required for the fit
        arguments: FitArguments = {
            "pedestal": 0,
            "limit_pedestal": (-1000, 1000),
            "error_pedestal": 0.1,
            "amplitude": 1,
            "limit_amplitude": (0.05, 100),
            "error_amplitude": 0.1 * 1,
            "mean": 0,
            "limit_mean": (-0.5, 0.5),
            "error_mean": 0.05,
            "width": 0.15,
            "limit_width": (0.05, 0.8),
            "error_width": 0.1 * 0.15,
        }

        return restricted_hist, arguments
示例#13
0
def _load_JEWEL_predictions_from_file(path: Path) -> histogram.Histogram1D:
    """ Load JEWEL predictions from file.

    The file format is:

    ```
    pt_bin_center pt_bin_center_uncertainty value value_uncertainty
    ```

    Args:
        path: Path to the file containing the JEWEL predictions.
    Returns:
        The data loaded into a histogram.
    """
    bin_edges = np.array([0.5, 1, 1.5, 2, 3, 4, 5, 6, 10])
    bin_centers_of_interest = bin_edges[:-1] + (bin_edges[1:] -
                                                bin_edges[:-1]) / 2
    values = []
    errors = []
    with open(path, "r") as f:
        for line in f:
            # Remove remaining newline.
            line = line.rstrip("\n")
            # Convert to floats
            converted_values = [float(v) for v in line.split("\t")]
            bin_center, _, value, value_uncertainty = converted_values
            # Only store if the bin center is of interest.
            for c in bin_centers_of_interest:
                if np.isclose(bin_center, c):
                    break
            else:
                continue
            values.append(value)
            errors.append(value_uncertainty)

    return histogram.Histogram1D(bin_edges=bin_edges,
                                 y=np.array(values),
                                 errors_squared=np.array(errors)**2)
示例#14
0
    def _setup(
        self, h: histogram.Histogram1D
    ) -> Tuple[histogram.Histogram1D, FitArguments]:
        """ Setup the histogram and arguments for the fit.

        Args:
            h: Background subtracted histogram to be fit.
        Returns:
            Histogram to use for the fit, default arguments for the fit. Note that the histogram may be range
                restricted or otherwise modified here.
        """
        restricted_range = (
            # For example, -1.2 < h.x < -0.8
            (h.x < -1 * self.fit_range.min) & (h.x > -1 * self.fit_range.max)
            # For example, 0.8 < h.x < 1.2
            | (h.x > self.fit_range.min) & (h.x < self.fit_range.max))
        # Same conditions as above, but we need the bin edges to be inclusive.
        bin_edges_restricted_range = (
            (h.bin_edges <= -1 * self.fit_range.min) &
            (h.bin_edges >= -1 * self.fit_range.max)
            | (h.bin_edges >= self.fit_range.min) &
            (h.bin_edges <= self.fit_range.max))
        restricted_hist = histogram.Histogram1D(
            bin_edges=h.bin_edges[bin_edges_restricted_range],
            y=h.y[restricted_range],
            errors_squared=h.errors_squared[restricted_range])

        # Default arguments
        # Use the minimum of the histogram as the starting value.
        min_hist = np.min(restricted_hist.y)
        arguments: FitArguments = {
            "pedestal": min_hist,
            "error_pedestal": min_hist * 0.1,
            "limit_pedestal": (-1000, 1000),
        }

        return restricted_hist, arguments
示例#15
0
def test_integration(logging_mixin: Any) -> None:
    """ Test our implementation of the Simpson 3/8 rule, along with some other integration methods. """
    # Setup
    f = func_1
    h = histogram.Histogram1D(bin_edges=np.array([0, 1, 2]),
                              y=np.array([0, 1]),
                              errors_squared=np.array([1, 2]))
    args = [0, 0]

    integral = cost_function._simpson_38(f, h.bin_edges, *args)
    # Evaluate at the bin center
    expected = np.array([f(i, *args) for i in h.x])
    np.testing.assert_allclose(integral, expected)

    # Compare against our quad implementation
    integral_quad = cost_function._quad(f, h.bin_edges, *args)
    np.testing.assert_allclose(integral, integral_quad)

    # Also compare against probfit and scipy for good measure
    probfit = pytest.importorskip("probfit")
    expected_probfit = []
    expected_scipy = []
    expected_quad = []
    for i in h.bin_edges[1:]:
        # Assumes uniform bin width
        expected_probfit.append(
            probfit.integrate1d(f, (i - 1, i), 1, tuple(args)))
        scipy_x = np.linspace(i - 1, i, 5)
        expected_scipy.append(
            scipy.integrate.simps(y=f(scipy_x, *args), x=scipy_x))
        res, _ = scipy.integrate.quad(f, i - 1, i, args=tuple(args))
        expected_quad.append(res)

    np.testing.assert_allclose(integral, expected_probfit)
    np.testing.assert_allclose(integral, expected_scipy)
    np.testing.assert_allclose(integral, expected_quad)
示例#16
0
    def test_get_array_from_hist(self, logging_mixin: Any, test_root_hists: Any) -> None:
        """ Test getting numpy arrays from a 1D hist.

        Note:
            This test is from the legacy get_array_from_hist(...) function. This functionality is
            superseded by Histogram1D.from_existing_hist(...), but we leave this test for good measure.
        """
        hist = test_root_hists.hist1D
        hist_array = histogram.Histogram1D.from_existing_hist(hist)

        # Determine expected values
        x_bins = range(1, hist.GetXaxis().GetNbins() + 1)
        expected_bin_edges = np.empty(len(x_bins) + 1)
        expected_bin_edges[:-1] = [hist.GetXaxis().GetBinLowEdge(i) for i in x_bins]
        expected_bin_edges[-1] = hist.GetXaxis().GetBinUpEdge(hist.GetXaxis().GetNbins())
        expected_hist_array = histogram.Histogram1D(
            bin_edges = expected_bin_edges,
            y = np.array([hist.GetBinContent(i) for i in x_bins]),
            errors_squared = np.array([hist.GetBinError(i) for i in x_bins])**2,
        )

        logger.debug(f"sumw2: {len(hist.GetSumw2())}")
        logger.debug(f"sumw2: {hist.GetSumw2N()}")
        assert check_hist(hist_array, expected_hist_array) is True
示例#17
0
def setup_histogram_conversion() -> Tuple[str, str, histogram.Histogram1D]:
    """ Setup expected values for histogram conversion tests.

    This set of expected values corresponds to:

    >>> hist = ROOT.TH1F("test", "test", 10, 0, 10)
    >>> hist.Fill(3, 2)
    >>> hist.Fill(8)
    >>> hist.Fill(8)
    >>> hist.Fill(8)

    Note:
        The error on bin 9 (one-indexed) is just sqrt(counts), while the error on bin 4
        is sqrt(4) because we filled it with weight 2 (sumw2 squares this values).
    """
    expected = histogram.Histogram1D(bin_edges = np.linspace(0, 10, 11),
                                     y = np.array([0, 0, 0, 2, 0, 0, 0, 0, 3, 0]),
                                     errors_squared = np.array([0, 0, 0, 4, 0, 0, 0, 0, 3, 0]))

    hist_name = "rootHist"
    filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "testFiles", "convertHist.root")
    if not os.path.exists(filename):
        # Need to create the initial histogram.
        # This shouldn't happen very often, as the file is stored in the repository.
        import ROOT
        root_hist = ROOT.TH1F(hist_name, hist_name, 10, 0, 10)
        root_hist.Fill(3, 2)
        for _ in range(3):
            root_hist.Fill(8)

        # Write out with normal ROOT so we can avoid further dependencies
        fOut = ROOT.TFile(filename, "RECREATE")
        root_hist.Write()
        fOut.Close()

    return filename, hist_name, expected
示例#18
0
def _plot_rp_fit_residuals(rp_fit: reaction_plane_fit.fit.ReactionPlaneFit,
                           ep_analyses: List[Tuple[
                               Any, "correlations.Correlations"]],
                           axes: matplotlib.axes.Axes) -> None:
    """ Plot fit residuals on a given set of axes.

    Args:
        rp_fit: Reaction plane fit object.
        ep_analyses: Event plane dependent correlation analysis objects.
        axes: Axes on which the residual should be plotted. It must have an axis per component.
    Returns:
        None. The axes are modified in place.
    """
    # Validation
    if len(rp_fit.components) != len(axes):
        raise TypeError(
            f"Number of axes is not equal to the number of fit components."
            f" # of components: {len(rp_fit.components)}, # of axes: {len(axes)}"
        )
    if len(ep_analyses) != len(axes):
        raise TypeError(
            f"Number of axes is not equal to the number of EP analysis objects."
            f" # of analysis objects: {len(ep_analyses)}, # of axes: {len(axes)}"
        )

    x = rp_fit.fit_result.x
    for (key_index, analysis), ax in zip(ep_analyses, axes):
        # Setup
        # Get the relevant data
        if analysis.reaction_plane_orientation == params.ReactionPlaneOrientation.inclusive:
            h: Union["correlations.DeltaPhiSignalDominated", "correlations.DeltaPhiBackgroundDominated"] = \
                analysis.correlation_hists_delta_phi.signal_dominated
        else:
            h = analysis.correlation_hists_delta_phi.background_dominated
        hist = histogram.Histogram1D.from_existing_hist(h)

        # We create a histogram to represent the fit so that we can take advantage
        # of the error propagation in the Histogram1D object.
        fit_hist = histogram.Histogram1D(
            # Bin edges must be the same
            bin_edges=hist.bin_edges,
            y=analysis.fit_object.evaluate_fit(x=x),
            errors_squared=analysis.fit_object.fit_result.errors**2,
        )
        # NOTE: Residual = data - fit / fit, not just data-fit
        residual = (hist - fit_hist) / fit_hist

        # Plot the main values
        plot = ax.plot(x,
                       residual.y,
                       label="Residual",
                       color=plot_base.AnalysisColors.fit)
        # Plot the fit errors
        ax.fill_between(
            x,
            residual.y - residual.errors,
            residual.y + residual.errors,
            facecolor=plot[0].get_color(),
            alpha=0.9,
        )

        # Set the y-axis limit to be symmetric
        # Selected the value by looking at the data.
        ax.set_ylim(bottom=-0.1, top=0.1)
示例#19
0
def test_hist_input_length_validation(logging_mixin: Any, bin_edges: np.ndarray, y: np.ndarray, errors_squared: np.ndarray) -> None:
    """ Check histogram input length validation. """
    with pytest.raises(ValueError) as exception_info:
        histogram.Histogram1D(bin_edges = bin_edges, y = y, errors_squared = errors_squared)
    assert "Length of input arrays doesn't match!" in exception_info.value.args[0]
def new_combine_gaussians(label: str, widths: Sequence[float]) -> None:
    """ Use a new approach devised in July 2019. """
    # Validation
    if len(widths) != 3:
        raise ValueError(f"Must pass only three widths. Passed: {widths}")

    # Imagine with 3 EP angles + inclusive
    # First define the 3 EP angles
    gaussians = []
    #for width in range(1, 4):
    for width in widths:
        gaussians.append(np.random.normal(0, width, size=1000000))
    n_trigs = [375, 300, 325]
    # Add the inclusive to the start of the number of trigs
    n_trigs.insert(0, np.sum(n_trigs))

    # Create the inclusive, and then histogram it
    inclusive = np.array([(i, val) for i, gaussian in enumerate(gaussians, 1)
                          for val in gaussian])

    # Recall that [:, 0] contains 1-3, while [:, 1] contains the x values.
    h_inclusive, x_bin_edges, y_bin_edges = np.histogram2d(
        inclusive[:, 1],
        inclusive[:, 0],
        bins=[200, np.linspace(0.5, 3.5, 3 + 1)],
        range=[[-10, 10], [0.5, 3.5]])

    logger.debug(f"h_inclusive.shape: {h_inclusive.shape}")
    #logger.debug(f"x_bin_edges: {x_bin_edges}")
    #logger.debug(f"y_bin_edges: {y_bin_edges}")
    logger.debug(f"inclusive[:, 0]: {inclusive[:, 0]}")

    fig, ax = plt.subplots(figsize=(8, 6))

    X, Y = np.meshgrid(x_bin_edges, y_bin_edges)
    ax.pcolormesh(X, Y, h_inclusive.T)
    ax.set_xlabel("x")
    ax.set_ylabel("EP orientation proxy")

    fig.tight_layout()
    fig.savefig("gaussian_2d_histogram.pdf")

    # Basically, the first two arguments are h.x and h.y
    binned_mean, _, _ = scipy.stats.binned_statistic(inclusive[:, 0],
                                                     inclusive[:, 1],
                                                     "std",
                                                     bins=np.linspace(
                                                         0.5, 3.5, 3 + 1))
    logger.debug(f"Binned mean: {binned_mean}")
    inclusive_binned_mean, _, _ = scipy.stats.binned_statistic(inclusive[:, 0],
                                                               inclusive[:, 1],
                                                               "std",
                                                               bins=1)
    logger.debug(f"Inclusive binned mean: {inclusive_binned_mean}")

    hists = []
    # Inclusive
    hists.append(
        histogram.Histogram1D(
            bin_edges=x_bin_edges,
            y=np.sum(h_inclusive, axis=1),
            errors_squared=np.sum(h_inclusive, axis=1),
        ))
    # Width = 1
    hists.append(
        histogram.Histogram1D(
            bin_edges=x_bin_edges,
            y=h_inclusive[:, 0],
            errors_squared=h_inclusive[:, 0],
        ))
    # Width = 2
    hists.append(
        histogram.Histogram1D(
            bin_edges=x_bin_edges,
            y=h_inclusive[:, 1],
            errors_squared=h_inclusive[:, 1],
        ))
    # Width = 3
    hists.append(
        histogram.Histogram1D(
            bin_edges=x_bin_edges,
            y=h_inclusive[:, 2],
            errors_squared=h_inclusive[:, 2],
        ))

    # Scale by number of triggers.
    #for h, n_trig in zip(hists, n_trigs):
    #for i, n_trig in enumerate(n_trigs):
    #    logger.debug(f" pre scale {i}: {np.max(hists[i].y)}")
    #    logger.debug(f"scale by {1 / n_trig}")
    #    hists[i] *= 1.0 / n_trig
    #    #h = h * 1.0 / n_trig
    #    logger.debug(f"post scale {i}: {np.max(hists[i].y)}")

    # Quickly plot hists
    fig, ax = plt.subplots(figsize=(8, 6))
    for i, h in enumerate(hists):
        ax.errorbar(h.x,
                    h.y,
                    yerr=h.errors,
                    marker="o",
                    linestyle="",
                    label=f"Data {i}")
    # Final adjustments
    ax.legend()
    fig.tight_layout()
    fig.savefig(f"gaussian_data.pdf")

    # Fit to gaussians
    def scaled_gaussian(x: float, mean: float, sigma: float,
                        amplitude: float) -> float:
        return amplitude * fit.gaussian(x=x, mean=mean, sigma=sigma)

    fig, ax = plt.subplots(figsize=(8, 6))
    gaussian_fit_results = []
    for i, h in enumerate(hists):
        # First try just -3 to 3
        #cost_func = fit.BinnedLogLikelihood(f = scaled_gaussian, data = restrict_hist_range(h, -3, 3))
        cost_func = fit.BinnedLogLikelihood(f=unnormalized_gaussian,
                                            data=restrict_hist_range(h, -3, 3))
        minuit_args: Dict[str, Union[float, Tuple[float, float]]] = {
            "mean": 0,
            "fix_mean": True,
            "sigma": 1.0,
            "error_sigma": 0.1,
            "limit_sigma": (0, 10),
            "amplitude": 100.0,
            "error_amplitude": 0.1,
        }
        fit_result, _ = fit.fit_with_minuit(cost_func=cost_func,
                                            minuit_args=minuit_args,
                                            x=h.x)
        gaussian_fit_results.append(fit_result)

        # Plot for a sanity check
        plot_label: str
        if i > 0:
            plot_label = fr"$\sigma = {widths[i-1]:0.2f}$"
        else:
            plot_label = "inclusive"
        ax.errorbar(h.x,
                    h.y,
                    yerr=h.errors,
                    marker="o",
                    linestyle="",
                    label=f"Data {plot_label}")
        #ax.plot(h.x, scaled_gaussian(h.x, *list(fit_result.values_at_minimum.values())), label = f"Fit {plot_label}", zorder = 5)
        ax.plot(h.x,
                unnormalized_gaussian(
                    h.x, *list(fit_result.values_at_minimum.values())),
                label=f"Fit {plot_label}",
                zorder=5)

    values_at_zero_from_hist = []
    for h in hists:
        values_at_zero_from_hist.append(h.y[h.find_bin(0.0)])
    values_at_zero_from_fits = []
    for fit_result in gaussian_fit_results:
        #values_at_zero_from_fits.append(scaled_gaussian(0, *list(fit_result.values_at_minimum.values())))
        values_at_zero_from_fits.append(
            unnormalized_gaussian(0,
                                  *list(
                                      fit_result.values_at_minimum.values())))
    sum_of_last_3_from_hist = np.sum(values_at_zero_from_hist[1:])
    sum_of_last_3_from_fit = np.sum(values_at_zero_from_fits[1:])
    logger.debug(
        f"Values at 0 from hist: {values_at_zero_from_hist}, Sum of last 3: {sum_of_last_3_from_hist}, Diff: {_percent_diff(values_at_zero_from_hist[0], sum_of_last_3_from_hist):.3f}%"
    )
    logger.debug(
        f"Values at 0 from fit: {values_at_zero_from_fits}, Sum of last 3: {sum_of_last_3_from_fit}, Diff: {_percent_diff(values_at_zero_from_fits[0], sum_of_last_3_from_fit):.3f}%"
    )

    # Predict gaussian fit and plot based on previous fit
    calculate_variances(hists, gaussian_fit_results)

    import IPython
    IPython.embed()

    # Final adjustments
    ax.legend()
    ax.set_title(f"{label} widths")
    fig.tight_layout()
    fig.savefig(f"gaussian_fit_{label}.pdf")
    def process_result(
            self, selected_analysis_options: params.SelectedAnalysisOptions,
            glauber_version: str,
            output_info: analysis_objects.PlottingOutputWrapper) -> None:
        """ Perform final processing of the event-by-event results. """
        logger.info("Performing final processing.")

        # Setup
        main_font_size = 18
        general_labels = fr"TGlauberMC v{glauber_version}, ${selected_analysis_options.event_activity.display_str()}\:{selected_analysis_options.collision_system.display_str()}$"
        general_labels += "\n" + fr"$\sigma = {self.cross_section.value} \pm {self.cross_section.width}$ mb"
        general_labels += "\n" + fr"$b = {self.impact_parameter_range.min} - {self.impact_parameter_range.max}$ fm"

        # Create histograms of the two axes
        h_x, x_edges = np.histogram(
            self.max_x,
            bins=100,
            range=(0, 10),
        )
        hist_max_x = histogram.Histogram1D(
            bin_edges=x_edges,
            y=h_x,
            errors_squared=h_x,
        )

        h_y, y_edges = np.histogram(
            self.max_y,
            bins=100,
            range=(0, 10),
        )
        hist_max_y = histogram.Histogram1D(
            bin_edges=y_edges,
            y=h_y,
            errors_squared=h_y,
        )

        # Plot the distributions
        import matplotlib.pyplot as plt
        fig, ax = plt.subplots(figsize=(8, 6))
        ax.errorbar(
            hist_max_x.x,
            hist_max_x.y,
            yerr=hist_max_x.errors,
            marker="o",
            linestyle="",
            # Equivalent to semi-minor
            label=fr"{params.ReactionPlaneOrientation.in_plane.display_str()}",
        )
        ax.errorbar(
            hist_max_y.x,
            hist_max_y.y,
            yerr=hist_max_y.errors,
            marker="o",
            linestyle="",
            # Equivalent to semi-major
            label=
            fr"{params.ReactionPlaneOrientation.out_of_plane.display_str()}",
        )

        # Label and final adjustments
        ax.legend(
            loc="upper left",
            bbox_to_anchor=(0.01, 0.99),
            borderaxespad=0,
            frameon=False,
            fontsize=main_font_size,
        )
        ax.set_xlabel("Max length (fm)")
        ax.set_ylabel("Counts / 0.1")
        ax.tick_params(axis='both', which='major')
        ax.text(0.97,
                0.97,
                s=general_labels,
                horizontalalignment="right",
                verticalalignment="top",
                multialignment="right",
                fontsize=main_font_size,
                transform=ax.transAxes)
        fig.tight_layout()

        # Save plot and cleanup
        plot_base.save_plot(output_info, fig, "glauber_lengths")
        plt.close(fig)

        # Plot ratio
        fig, ax = plt.subplots(figsize=(8, 6))
        h, edges = np.histogram(self.ratio, bins=60, range=(-1, 2))
        h = histogram.Histogram1D(
            bin_edges=edges,
            y=h,
            errors_squared=h,
        )
        ax.errorbar(
            h.x,
            h.y,
            yerr=h.errors,
            marker="o",
            linestyle="",
            label="out-of-plane/in-plane",
        )

        # Label and final adjustments
        ax.legend(loc="upper right",
                  bbox_to_anchor=(0.99, 0.99),
                  borderaxespad=0,
                  frameon=False,
                  fontsize=main_font_size)
        ax.set_xlabel("Length ratio")
        ax.set_ylabel("Counts / 0.05")
        ax.tick_params(axis='both', which='major')
        ratio_label = general_labels
        ratio_label += "\n" + fr"Mean: ${np.mean(self.ratio):.2f} \pm {_calculate_array_RMS(self.ratio):.2f}$ (RMS)"
        ax.text(0.03,
                0.97,
                s=ratio_label,
                horizontalalignment="left",
                verticalalignment="top",
                multialignment="left",
                fontsize=main_font_size,
                transform=ax.transAxes)
        fig.tight_layout()

        # Plot and cleanup
        plot_base.save_plot(output_info, fig, "glauber_ratio")
        plt.close(fig)