Exemple #1
0
 def _apply_labels_ROOT(self, hist: Hist) -> None:
     if self.title is not None:
         hist.SetTitle(labels.use_label_with_root(self.title))
     if self.x_label is not None:
         hist.GetXaxis().SetTitle(labels.use_label_with_root(self.x_label))
     if self.y_label is not None:
         hist.GetYaxis().SetTitle(labels.use_label_with_root(self.y_label))
def rho_centrality(rho_hist: Hist,
                   output_info: analysis_objects.PlottingOutputWrapper,
                   includes_constituent_cut: bool = True) -> None:
    """ Plot rho as a function of centrality vs jet pt.

    Args:
        rho_hist: Rho centrality dependent hist.
        output_info: Output information.
        includes_constituent_cut: True if the plot was produced using the constituent cut.
    Returns:
        None. The figure is plotted.
    """
    # Setup
    import ROOT
    canvas = ROOT.TCanvas("c", "c")
    canvas.SetRightMargin(0.15)
    # Set labels
    rho_hist.SetTitle("")
    rho_hist.Draw("colz")
    # Keep the range more meaningful.
    rho_hist.GetYaxis().SetRangeUser(0, 50)
    # Draw a profile of the mean
    rho_hist_profile = rho_hist.ProfileX(f"{rho_hist.GetName()}_profile")
    rho_hist_profile.SetMarkerStyle(ROOT.kFullCircle)
    rho_hist_profile.SetMarkerColor(ROOT.kRed)
    rho_hist_profile.SetMarkerSize(1.4)
    rho_hist_profile.Draw("same")

    # Finally, save and cleanup
    output_name = "rho_background"
    if includes_constituent_cut:
        output_name += "_3GeVConstituents"
    plot_base.save_plot(output_info, canvas, output_name)
Exemple #3
0
def _scale_set_of_bins(hist: Hist, bin_of_interest: int,
                       response_normalization: ResponseNormalization,
                       scale_factor: float) -> np.ndarray:
    """ Scale a set of bins associated with a particular bin value in the other axis.

    For further information on how the bins are selected, etc, see
    ``_access_set_of_values_associated_with_a_bin(...)``.

    Args:
        hist: The histogram whose bins should be accessed. This must be a 2D hist.
        bin_of_interest: Bin which we would like to access.
        response_normalization: Response normalization convention, which dictates which axis to retrieve the bins from.
        scale_factor: Every bin will be = bin_content / scale_factor .
    Returns:
        None. The histogram is updated in place.
    """
    # Initial setup
    axis, get_bin = _setup_access_bins(
        response_normalization=response_normalization)

    # Get the relevant bins.
    set_of_bins_content, set_of_bins_errors = _access_set_of_values_associated_with_a_bin(
        hist=hist,
        bin_of_interest=bin_of_interest,
        response_normalization=response_normalization)

    # Set the scaled bin values in the histogram.
    for bin_index, (bin_content,
                    bin_error) in enumerate(zip(set_of_bins_content,
                                                set_of_bins_errors),
                                            start=1):
        hist.SetBinContent(get_bin(hist, bin_of_interest, bin_index),
                           bin_content / scale_factor)
        hist.SetBinError(get_bin(hist, bin_of_interest, bin_index),
                         bin_error / scale_factor)
Exemple #4
0
def post_projection_processing_for_2d_correlation(
        hist: Hist,
        normalization_factor: float,
        title_label: str,
        jet_pt: analysis_objects.JetPtBin,
        track_pt: analysis_objects.TrackPtBin,
        rebin_factors: Optional[Tuple[int, int]] = None) -> None:
    """ Basic post processing tasks for a new 2D correlation observable.

    Args:
        hist: Histogram to be post processed.
        normalization_factor: Factor by which the hist should be scaled.
        title_label: Histogram title label.
        jet_pt: Jet pt bin.
        track_pt: Track pt bin.
        rebin_factors: (x rebin factor, y rebin factor). Both values must be specified (can set to 1 if you
            don't want to rebin a particular axis). Default: None.
    Returns:
        None. The histogram is modified in place.
    """
    # If we specify a rebin factor, then rebin.
    if rebin_factors is not None:
        hist.Rebin2D(*rebin_factors)

    # Scale
    hist.Scale(1.0 / normalization_factor)

    # Set title, axis labels
    jet_pt_bins_title = labels.jet_pt_range_string(jet_pt)
    track_pt_bins_title = labels.track_pt_range_string(track_pt)
    hist.SetTitle(
        rf"{title_label}\:\mathrm{{with}}\:{jet_pt_bins_title} \mathrm{{,}} {track_pt_bins_title}"
    )
    hist.GetXaxis().SetTitle(r"$\Delta\varphi$")
    hist.GetYaxis().SetTitle(r"$\Delta\eta$")
def calculate_residual_2D(efficiency_data: Hist, efficiency_function: Callable[..., float],
                          efficiency_period: Any, centrality_bin: int) -> Tuple[np.ndarray, List[float], List[float]]:
    """ Calculate residual for 2D tracking efficiency.

    There is a separate 1D and 2D function for convenience. If there is no entries for a particular
    bin, we set the value to NaN so that it can be ignored later when plotting.

    Args:
        efficiency_data: 2D efficiency data.
        efficiency_function: Efficiency function.
        efficiency_period: Efficiency period.
        centrality_bin: Centrality bin.
    Returns:
        Calculated residual, pt values where it was evaluated, eta values where it was evaluated.
    """
    pts = [efficiency_data.GetXaxis().GetBinCenter(x) for x in range(1, efficiency_data.GetXaxis().GetNbins() + 1)]
    etas = [efficiency_data.GetYaxis().GetBinCenter(y) for y in range(1, efficiency_data.GetYaxis().GetNbins() + 1)]
    residual = np.zeros(shape = (efficiency_data.GetXaxis().GetNbins(),
                                 efficiency_data.GetYaxis().GetNbins()))
    # Loop over all of the bins in the data histogram.
    chi_2 = []
    for pt_index, pt in enumerate(pts):
        for eta_index, eta in enumerate(etas):
            x = pt_index + 1
            y = eta_index + 1
            # Calculate the efficiency. It's calculated again here to ensure that it's evaluated at exactly
            # the same location as in the data histogram.
            efficiency_at_value = efficiency_function(pt, eta, centrality_bin, efficiency_period, "task_name")

            # Determine the histogram value, setting it to NaN if there's no entries.
            if np.abs(efficiency_data.GetBinContent(x, y)) < epsilon:
                value = np.nan
            else:
                value = (efficiency_data.GetBinContent(x, y) - efficiency_at_value) / efficiency_at_value * 100.
                # The points around the edges aren't super reliable for calcuating chi squared
                if pt > 1 and np.abs(eta) < 0.8:
                    chi_2.append(np.power(efficiency_data.GetBinContent(x, y) - efficiency_at_value, 2) / np.power(efficiency_data.GetBinError(x, y), 2))

            residual[pt_index, eta_index] = value

    # Check max values
    logger.debug(f"min efficiency_data: {efficiency_data.GetMinimum()}, "
                 f"max efficiency_data: {efficiency_data.GetMaximum()}")
    logger.debug(f"min residual: {np.nanmin(residual)}, max residual: {np.nanmax(residual)}")
    logger.debug(f"standard mean: {np.nanmean(residual)}")
    logger.debug(f"restricted mean: {np.nanmean(residual[:,np.abs(etas) < 0.8])}")
    logger.debug(f"len(pts): {len(pts)}, len(etas): {len(etas)}")

    # Check chi squared
    chi_squared = np.sum(chi_2)
    # 23 is the number of parameters (10 + 13) at any given point
    ndf = len(chi_2) - 23
    logger.warning("NOTE: The restricted chi squared value calculated here may not be super reliable.")
    logger.info(f"Chi squared: {chi_squared}")
    logger.info(f"NDF: {ndf}")
    logger.info(f"chi2/ndf: {chi_squared / ndf}")

    return residual, pts, etas
Exemple #6
0
def _check_normalization(
        hist: Hist, response_normalization: ResponseNormalization) -> bool:
    """ Check each bin to ensure that the normalization was successful.

    Args:
        hist: Response matrix to check. This must be a 2D histogram.
        response_normalization: Normalization convention for the response matrix.
    Returns:
        True if the normalization is fine.
    Raises:
        ValueError: If the normalization fails for a particular bin.
    """
    for index in range(1, hist.GetXaxis().GetNbins() + 1):
        # Access bins
        bins_content, _ = _access_set_of_values_associated_with_a_bin(
            hist=hist,
            bin_of_interest=index,
            response_normalization=response_normalization,
        )
        # Get norm
        norm = np.sum(bins_content)

        # Somewhat arbitrarily comparison limit selected. It should be sufficiently small.
        comparison_limit = 1e-9
        if not np.isclose(norm, 0, atol=comparison_limit) and not np.isclose(
                norm, 1, atol=comparison_limit):
            raise ValueError(
                f"Normalization not successful for bin {index}. Norm: {norm}")

    return True
Exemple #7
0
def fit_2d_mixed_event_normalization(
        hist: Hist, delta_phi_limits: Sequence[float],
        delta_eta_limits: Sequence[float]) -> ROOT.TF2:
    """ Alternative to determine the mixed event normalization.

    A linear function is fit to the dPhi-dEta mixed event normalization for some predefined range.

    Args:
        hist: 2D mixed event histogram to be fit.
        delta_phi_limits: Min and max fit limits in delta phi.
        delta_eta_limits: Min and max fit limits in delta eta.
    Returns:
        The ROOT fit function.
    """
    fit_func = ROOT.TF2("mixedEventNormalization2D", "[0] + 0.0*x + 0.0*y",
                        delta_phi_limits[0], delta_phi_limits[1],
                        delta_eta_limits[0], delta_eta_limits[1])

    # Fit to the given histogram
    # R uses the range defined in the fit function
    # 0 ensures that the fit isn't drawn
    # Q ensures minimum printing
    # + adds the fit to function list to ensure that it is not deleted on the creation of a new fit
    hist.Fit(fit_func, "RIB0")

    # And return the fit
    return fit_func
Exemple #8
0
def determine_number_of_triggers(hist: Hist,
                                 jet_pt: analysis_objects.JetPtBin) -> int:
    """ Determine the number of triggers for the specific analysis parameters.

    When retrieving the number of triggers, carefully note the information below:

    .. code-block:: python

        >>> hist = ROOT.TH1D("test", "test", 10, 0, 10)
        >>> x = 2, y = 5
        >>> hist.FindBin(x)
        2
        >>> hist.FindBin(x+epsilon)
        2
        >>> hist.FindBin(y)
        6
        >>> hist.FindBin(y-epsilon)
        5

    Note:
        The bin + epsilon on the lower bin is not strictly necessary, but it is used for consistency.

    Note:
        This bin + epsilon technique is frequently used when determining projector bin ends.

    Args:
        hist: Histogram containing the number of triggers.
        jet_pt: Jet pt for which we want to know the number of triggers.
    Returns:
        Number of triggers for the selected analysis parameters.
    """
    #logger.debug(
    #    f"Find bin({jet_pt.range.min} + epsilon): "
    #    f"{hist.FindBin(jet_pt.range.min + epsilon)} to "
    #    f"Find bin({jet_pt.range.max} - epsilon): "
    #    f"{hist.FindBin(jet_pt.range.max - epsilon)}"
    #)
    number_of_triggers: int = hist.Integral(
        hist.FindBin(jet_pt.range.min + epsilon),
        hist.FindBin(jet_pt.range.max - epsilon))
    logger.info(
        f"n_trig for [{jet_pt.range.min}, {jet_pt.range.max}): {number_of_triggers}"
    )

    return number_of_triggers
Exemple #9
0
def _peak_finding_objects_from_mixed_event(
        mixed_event: Hist,
        eta_limits: Tuple[float, float]) -> Tuple[Hist, np.ndarray]:
    """ Get the peak finding hist and array from the mixed event.

    Used for studying and determining the mixed event normalization.

    We need to project over a range of constant eta to be able to use the extracted max in the 2D
    mixed event. Joel uses [-0.4, 0.4], but it really seems to drop in the 0.4 bin, so instead I'll
    use 0.3 This value also depends on the max track eta. For 0.9, it should be 0.4 (0.9-0.5), but
    for 0.8, it should be 0.3 (0.8-0.5)

    Note:
        This assumes that the delta eta axis is the y axis. This is fairly standard for mixed events,
        so it should be fine.

    Args:
        mixed_event: The mixed event hist.
        eta_limits: Eta limits of which the mixed event should be projection into 1D.
    Returns:
        1D peak finding histogram, numpy array of 1D y values.
    """
    # Scale the 1D norm by the eta range.
    eta_limit_bins = [
        mixed_event.GetYaxis().FindBin(eta_limits[0] + epsilon),
        mixed_event.GetYaxis().FindBin(eta_limits[1] - epsilon),
    ]
    # This is basically just a sanity check that the selected values align with the binning
    projection_length = mixed_event.GetYaxis().GetBinUpEdge(
        eta_limit_bins[1]) - mixed_event.GetYaxis().GetBinLowEdge(
            eta_limit_bins[0])
    logger.debug(
        f"Mixed event scale factor from 1D to 2D: {mixed_event.GetYaxis().GetBinWidth(1) / projection_length}"
    )
    peak_finding_hist = mixed_event.ProjectionX(
        f"{mixed_event.GetName()}_peak_finding_hist", eta_limit_bins[0],
        eta_limit_bins[1])
    peak_finding_hist.Scale(mixed_event.GetYaxis().GetBinWidth(1) /
                            projection_length)
    peak_finding_hist_array = histogram.Histogram1D.from_existing_hist(
        peak_finding_hist).y
    #logger.debug("peak_finding_hist_array: {}".format(peak_finding_hist_array))

    return peak_finding_hist, peak_finding_hist_array
Exemple #10
0
def scale_by_bin_width(hist: Hist) -> None:
    """ Scale a histogram by it's bin width.

    Args:
        hist: Hist to be scaled.
    Returns:
        None. The histogram is scaled in place.
    """
    scale_factor = _calculate_bin_width_scale_factor(hist)
    hist.Scale(scale_factor)
def plot_particle_level_spectra_agreement(
        difference: Hist, absolute_value_of_difference: Hist,
        output_info: analysis_objects.PlottingOutputWrapper) -> None:
    """ Plot the agreement of the particle level spectra between the inclusive and sum of all EP orientations.

    Args:
        difference: Hist of the sum of the EP orientations spectra minus the inclusive spectra.
        absolute_value_of_difference: Same as the difference hist, but having taken the absolute value.
            This allows us to plot the difference on a log scale (which is useful if the differences
            are small).
        output_info: Output information.
    Returns:
        None.
    """
    # Setup
    output_name = "difference_of_sum_EP_orientations_vs_inclusive"
    canvas = ROOT.TCanvas("canvas", "canvas")
    # Labeling
    x_label = labels.use_label_with_root(
        fr"{labels.jet_pt_display_label(upper_label = 'part')}\:({labels.momentum_units_label_gev()})"
    )
    y_label = r"\mathrm{d}N/\mathrm{d}p_{\mathrm{T}}"

    # Apply settings to hists
    for h in [difference, absolute_value_of_difference]:
        # Labeling
        h.GetXaxis().SetTitle(x_label)
        h.GetYaxis().SetTitle(y_label)
        # Center axis title
        h.GetXaxis().CenterTitle(True)
        h.GetYaxis().CenterTitle(True)

    # Draw and save the difference histogram.
    difference.Draw()
    plot_base.save_plot(output_info, canvas, output_name)

    # Draw and save the absolute value of the difference histogram.
    absolute_value_of_difference.Draw()
    canvas.SetLogy(True)
    output_name += "_abs"
    plot_base.save_plot(output_info, canvas, output_name)
Exemple #12
0
def post_creation_processing_for_1d_correlations(
        hist: Hist, normalization_factor: float, rebin_factor: int,
        title_label: str, axis_label: str, jet_pt: analysis_objects.JetPtBin,
        track_pt: analysis_objects.TrackPtBin) -> None:
    """ Basic post processing tasks for a new 1D correlation observable. """
    # Rebin to decrease the fluctuations in the correlations
    # We don't scale by the rebin factor here because we will scale by bin width later.
    # Since we will handle it later, it doesn't make sense to try to preserve normalization here.
    hist.Rebin(rebin_factor)

    # Scale
    hist.Scale(1.0 / normalization_factor)

    # Set title, labels
    jet_pt_bins_title = labels.jet_pt_range_string(jet_pt)
    track_pt_bins_title = labels.track_pt_range_string(track_pt)
    # This won't look so good in ROOT, but that's just because their latex rendering is absolutely atrocious...
    hist.SetTitle(
        rf"{title_label} with {jet_pt_bins_title}, {track_pt_bins_title}")
    hist.GetXaxis().SetTitle(axis_label)
    hist.GetYaxis().SetTitle(fr"$\mathrm{{dN}}/\mathrm{{d}}{axis_label}$")
def scale_CPU_time(hist: Hist) -> None:
    """ Rebin the CPU time for improved presentation.

    Time is only reported in increments of 10 ms. So we rebin by those 10 bins (since each bin is 1 ms)
    and then scale them down to be on the same scale as the real time hist. We can perform this scaling
    in place.

    Note:
        This scaling appears to be the same as one would usually do for a rebin, but it is slightly more
        subtle, because it is as if the data was already binned. That being said, the end result is
        effectively the same.

    Args:
        hist: CPU time histogram to be scaled.
    Returns:
        None.
    """
    logger.debug("Performing CPU time hist scaling.")
    timeIncrement = 10
    hist.Rebin(timeIncrement)
    hist.Scale(1.0 / timeIncrement)
Exemple #14
0
def _access_set_of_values_associated_with_a_bin(
    hist: Hist, bin_of_interest: int,
    response_normalization: ResponseNormalization
) -> Tuple[np.ndarray, np.ndarray]:
    """ Access a set of bins associated with a particular bin value in the other axis.

    For example, if the hist looks like this graphically:

    a b c
    d e f
    g h i <- values (here and above)
    1 2 3 <- bin number

    then in the case of accessing a set of y bins associated with an x bin, for example, x bin 2,
    it would return values h, e, and b.

    Args:
        hist: The histogram whose bins should be accessed. This must be a 2D hist.
        bin_of_interest: Bin which we would like to access.
        response_normalization: Response normalization convention, which dictates which axis to retrieve the bins from.
    Returns:
        Array of the bin contents, Array of bin errors
    """
    # Initial setup
    axis, get_bin = _setup_access_bins(
        response_normalization=response_normalization)

    #logger.debug(f"Axis: {axis(hist)}, getBin: {get_bin}")
    set_of_bins_content = np.zeros(axis(hist).GetNbins())
    set_of_bins_errors = np.zeros(axis(hist).GetNbins())
    for array_index, bin_index in enumerate(range(1,
                                                  axis(hist).GetNbins() + 1)):
        set_of_bins_content[array_index] = hist.GetBinContent(
            get_bin(hist, bin_of_interest, bin_index))
        set_of_bins_errors[array_index] = hist.GetBinError(
            get_bin(hist, bin_of_interest, bin_index))

    return set_of_bins_content, set_of_bins_errors
Exemple #15
0
def _calculate_bin_width_scale_factor(hist: Hist,
                                      additional_scale_factor: float = 1.0
                                      ) -> float:
    """ Calculate the bin width scale factor of a histogram.

    Args:
        hist: Hist to use for calculating the scale factor.
        additional_scale_factor: An additional scale factor to include in the calculation.
    Returns:
        The bin width scale factor for the hist.
    """
    # The first bin should always exist!
    bin_width_scale_factor: float = hist.GetXaxis().GetBinWidth(1)
    # Because of a ROOT quirk, even a TH1* hist has a Y and Z axis, with 1 bin
    # each. This bin has bin width 1, so it doesn't change anything if we multiply
    # by that bin width. So we just do it for all histograms.
    # This has the benefit that we don't need explicit dependence on an imported
    # ROOT package.
    bin_width_scale_factor *= hist.GetYaxis().GetBinWidth(1)
    bin_width_scale_factor *= hist.GetZaxis().GetBinWidth(1)

    final_scale_factor = additional_scale_factor / bin_width_scale_factor

    return final_scale_factor
def measure_mixed_event_normalization(
        mixed_event: Hist,
        eta_limits: Tuple[float, float],
        delta_phi_rebin_factor: int = 1) -> float:
    """ Determine normalization of the mixed event.

    The normalization is determined by using the moving average of half of the histogram.

    We need to project over a range of constant eta to be able to use the extracted max in the 2D
    mixed event. Joel uses [-0.4, 0.4], but it really seems to drop in the 0.4 bin, so instead I'll
    use 0.3 This value also depends on the max track eta. For 0.9, it should be 0.4 (0.9-0.5), but
    for 0.8, it should be 0.3 (0.8-0.5)

    Note:
        This assumes that delta phi is on the x axis and delta eta is on the y axis.

    Args:
        mixed_event: Mixed event histogram.
        eta_limits: Min and max eta range limits.
        delta_phi_rebin_factor: Factor by which we will rebin the mixed event, and therefore by which we
            must scaled the mixed event normalization.
    Returns:
        Mixed event normalization value.
    """
    # Project to 1D delta phi so it can be used with the signal finder
    peak_finding_hist, peak_finding_hist_array = _peak_finding_objects_from_mixed_event(
        mixed_event=mixed_event, eta_limits=eta_limits)

    # Using moving average looking at window half of the size of the delta phi axis (ie looking 5 bins
    # ahead if there 10 bins in the axis).
    moving_avg = utils.moving_average(peak_finding_hist_array,
                                      n=mixed_event.GetXaxis().GetNbins() // 2)
    max_moving_avg: float = np.max(moving_avg)

    # Finally determine the mixed event normalziation.
    mixed_event_normalization = max_moving_avg
    # Watch out for a zero would could cause problems.
    if not mixed_event_normalization != 0:
        logger.warning(
            f"Could not normalize the mixed event hist \"{mixed_event.GetName()}\" due to no data at (0,0)!"
        )
        mixed_event_normalization = 1

    # Account for a rebin factor. For example, if we rebin by 2, then we need to scale up
    # the normalization factor by 2.
    mixed_event_normalization *= delta_phi_rebin_factor

    return mixed_event_normalization
def eta_phi_removed(hist_name: str, before_hist: Hist,
                    after_hist: Hist) -> Hist:
    """ Show the eta phi locations of clusters removed by the exotics cut.

    This is created by subtracting the after removal hist from the before removal hist.
    The before and after histograms are expected to be TH2 histograms, but in principle
    they could be anything.

    Args:
        hist_name: Name of the new hist showing the removed clusters
        before_hist: Eta-Phi histogram before exotic clusters removal
        after_hist: Eta-Phi histogram after exotic cluster removal
    Returns:
        A new hist showing the difference between the two input hists.
    """
    # Create a new hist and remove the after hist
    hist = before_hist.Clone(hist_name)
    hist.Add(after_hist, -1)

    return hist
def _plot_response_matrix_with_ROOT(
        name: str, x_label: str, y_label: str, output_name: str, hist: Hist,
        plot_errors_hist: bool,
        output_info: analysis_objects.PlottingOutputWrapper) -> None:
    """ Underlying function to actually plot a response matrix with ROOT.

    Args:
        name: Name of the histogram.
        x_label: X axis label.
        y_label: Y axis label.
        output_name: Output name of the histogram.
        hist: The response matrix related 2D hist.
        errors_hist: True if the hist is the response matrix errors hist.
        output_info: Output information.
    Returns:
        None
    """
    # Setup
    canvas = ROOT.TCanvas("canvas", "canvas")
    canvas.SetLogz(True)

    # Plot the histogram
    hist.SetTitle(name)
    hist.GetXaxis().SetTitle(labels.use_label_with_root(x_label))
    hist.GetYaxis().SetTitle(labels.use_label_with_root(y_label))
    hist.Draw("colz")

    # Set the final axis ranges.
    # Z axis
    min_val = ctypes.c_double(0)
    max_val = ctypes.c_double(0)
    hist.GetMinimumAndMaximum(min_val, max_val)
    # * 1.1 to put it slightly above the max value
    # min_val doesn't work here, because there are some entries at 0
    hist.GetZaxis().SetRangeUser(10e-7, max_val.value * 1.1)

    # Save
    output_name += "_ROOT"
    plot_base.save_plot(output_info, canvas, output_name)
Exemple #19
0
def calculate_systematic_2D(
        nominal: Hist, variation: Hist,
        signal_dominated: analysis_objects.AnalysisBin,
        background_dominated: analysis_objects.AnalysisBin) -> float:
    """ Calculate a systematic in 2D.

    Args:
        nominal: Nominal hist.
        variation: Systematically varied hist.
        signal_dominated: Signal dominated region.
        Background_dominated: Background dominated region.
    Returns:
        The systematic calculated over the specified ranges.
    """
    # Calculate the nominal integral in the signal region
    nominal_signal = nominal.Integral(
        1,
        nominal.GetXaxis().GetNbins(),
        nominal.GetYaxis().FindBin(-1.0 * signal_dominated.max + epsilon),
        nominal.GetYaxis().FindBin(signal_dominated.max - epsilon),
    )
    # And in the background dominated region
    nominal_background = nominal.Integral(
        1,
        nominal.GetXaxis().GetNbins(),
        nominal.GetYaxis().FindBin(-1.0 * background_dominated.max + epsilon),
        nominal.GetYaxis().FindBin(-1.0 * background_dominated.min - epsilon),
    ) + nominal.Integral(
        1,
        nominal.GetXaxis().GetNbins(),
        nominal.GetYaxis().FindBin(background_dominated.min + epsilon),
        nominal.GetYaxis().FindBin(background_dominated.max - epsilon),
    )

    # Calculate the variation integral in the signal region
    variation_signal = variation.Integral(
        1,
        variation.GetXaxis().GetNbins(),
        variation.GetYaxis().FindBin(-1.0 * signal_dominated.max + epsilon),
        variation.GetYaxis().FindBin(signal_dominated.max - epsilon),
    )
    # And in the background dominated region
    variation_background = variation.Integral(
        1,
        variation.GetXaxis().GetNbins(),
        variation.GetYaxis().FindBin(-1.0 * background_dominated.max +
                                     epsilon),
        variation.GetYaxis().FindBin(-1.0 * background_dominated.min -
                                     epsilon),
    ) + variation.Integral(
        1,
        variation.GetXaxis().GetNbins(),
        variation.GetYaxis().FindBin(background_dominated.min + epsilon),
        variation.GetYaxis().FindBin(background_dominated.max - epsilon),
    )

    # Basically, signal / variation
    # NOTE: There is no need to apply the correlation scale factor because it will cancel
    #       in the factor.
    factor = (nominal_signal / nominal_background) / (variation_signal /
                                                      variation_background)

    # Help out mypy because it doesn't understand ROOT
    return cast(float, factor)
Exemple #20
0
def normalize_response_matrix(
        hist: Hist, response_normalization: ResponseNormalization) -> None:
    """ Normalize response matrix.

    In the case of normalizing each detector pt bin (usually on the x axis), we take all associated truth level
    bins (usually the y axis), and normalize that array of truth bins to 1. In the case of normalizing the truth
    level bins, the case is reversed.

    Args:
        hist: The response matrix
        response_normalization: Response normalization convention, which dictates which axis to normalize.
    Returns:
        None. The response matrix is modified in place.
    """
    if response_normalization == ResponseNormalization.none:
        # Nothing to be done, so just return.
        return

    # Determine the relevant parameters for normalizing the response
    # Each entry is of the form (projection_function, max_bins)
    parameters_map = {
        ResponseNormalization.normalize_each_detector_bin: (
            ROOT.TH2.ProjectionY,
            hist.GetXaxis().GetNbins() + 1,
        ),
        ResponseNormalization.normalize_each_truth_bin: (
            ROOT.TH2.ProjectionX,
            hist.GetYaxis().GetNbins() + 1,
        ),
    }
    projection_function, max_bins = parameters_map[response_normalization]

    # We decided to ignore the overflow bins.
    for index in range(1, max_bins):
        # Access bins
        bins_content, _ = _access_set_of_values_associated_with_a_bin(
            hist=hist,
            bin_of_interest=index,
            response_normalization=response_normalization,
        )

        norm = np.sum(bins_content)
        # NOTE: The upper bound on integrals is inclusive!
        proj = projection_function(hist,
                                   f"{hist.GetName()}_projection_{index}",
                                   index, index)

        # Sanity checks
        # NOTE: The upper bound on integrals is inclusive!
        # NOTE: Integral() == Integral(1, proj.GetXaxis().GetNbins())
        if not np.isclose(norm, proj.Integral(1, proj.GetXaxis().GetNbins())):
            raise ValueError(
                f"Mismatch between sum and integral! norm: {norm},"
                f" integral: {proj.Integral(1, proj.GetXaxis().GetNbins())}")
        if not np.isclose(proj.Integral(),
                          proj.Integral(1,
                                        proj.GetXaxis().GetNbins())):
            raise ValueError(
                f"Integral mismatch! Full: {proj.Integral()} 1-nBins: {proj.Integral(1, proj.GetXaxis().GetNbins())}"
            )

        # Avoid scaling by 0
        if not norm > 0.0:
            continue

        # normalization by sum
        _scale_set_of_bins(
            hist=hist,
            bin_of_interest=index,
            response_normalization=response_normalization,
            scale_factor=norm,
        )

    # Final sanity check by checking that the normalization is correct in each bin.
    res = _check_normalization(hist=hist,
                               response_normalization=response_normalization)

    if not res:
        raise ValueError("Normalization check failed.")
Exemple #21
0
def measure_mixed_event_normalization(
    mixed_event: Hist,
    eta_limits: Tuple[float, float],
    delta_phi_rebin_factor: int = 1
) -> Tuple[float, float, histogram.Histogram1D]:
    """ Determine normalization of the mixed event.

    The normalization is determined by using the moving average of half of the histogram.

    We need to project over a range of constant eta to be able to use the extracted max in the 2D
    mixed event. Joel uses [-0.4, 0.4], but it really seems to drop in the 0.4 bin, so instead I'll
    use 0.3 This value also depends on the max track eta. For 0.9, it should be 0.4 (0.9-0.5), but
    for 0.8, it should be 0.3 (0.8-0.5)

    Note:
        This assumes that delta phi is on the x axis and delta eta is on the y axis.

    Args:
        mixed_event: Mixed event histogram.
        eta_limits: Min and max eta range limits.
        delta_phi_rebin_factor: Factor by which we will rebin the mixed event, and therefore by which we
            must scaled the mixed event normalization.
    Returns:
        Mixed event normalization value, max systematic, histogram used for determining the normalization
    """
    # Project to 1D delta phi so it can be used with the signal finder
    peak_finding_hist, peak_finding_hist_array = _peak_finding_objects_from_mixed_event(
        mixed_event=mixed_event, eta_limits=eta_limits)

    # Using moving average looking at window half of the size of the delta phi axis (ie looking 5 bins
    # ahead if there 10 bins in the axis).
    moving_avg = utils.moving_average(peak_finding_hist_array,
                                      n=mixed_event.GetXaxis().GetNbins() // 2)
    max_moving_avg: float = np.max(moving_avg)

    # Finally determine the mixed event normalziation.
    mixed_event_normalization = max_moving_avg
    # Watch out for a zero would could cause problems.
    if not mixed_event_normalization != 0:
        logger.warning(
            f"Could not normalize the mixed event hist \"{mixed_event.GetName()}\" due to no data at (0,0)!"
        )
        mixed_event_normalization = 1

    # Account for a rebin factor. For example, if we rebin by 2, then we need to scale up
    # the normalization factor by 2.
    mixed_event_normalization *= delta_phi_rebin_factor

    # Measure the systematic. Compare the moving average to the 1D and 2D fit. Take the max difference
    fit1D = fitting.fit_1d_mixed_event_normalization(
        peak_finding_hist, [1. / 2. * np.pi, 3. / 2. * np.pi])
    max_linear_fit_1D = fit1D.GetParameter(0)
    fit2D = fitting.fit_2d_mixed_event_normalization(
        mixed_event, [1. / 2. * np.pi, 3. / 2. * np.pi], eta_limits)
    max_linear_fit_2D = fit2D.GetParameter(0)

    # For the systematic, we'll just evaluate the moving average vs the fits.
    fit_systematic_1D = np.abs(max_moving_avg - max_linear_fit_1D)
    fit_systematic_2D = np.abs(max_moving_avg - max_linear_fit_2D)
    max_systematic = np.max([fit_systematic_1D, fit_systematic_2D])

    logger.debug(f"mixed_event_normalization_uncertainty: {max_systematic}")
    logger.debug("mixed_event_normalization_uncertainty fractional: "
                 f" {max_systematic / mixed_event_normalization}")

    return mixed_event_normalization, max_systematic, histogram.Histogram1D.from_existing_hist(
        peak_finding_hist)
Exemple #22
0
def compare_mixed_event_normalization_options(
        mixed_event: Hist, eta_limits: Tuple[float, float]
) -> Tuple[Hist,
           # Basic data
           np.ndarray, np.ndarray, np.ndarray, np.ndarray,
           # CWT
           np.ndarray, np.ndarray,
           # Moving Average
           float, float,
           # Smoothed gaussian
           np.ndarray, np.ndarray, float,
           # Linear fits
           float, float, float, float]:
    """ Compare mixed event normalization options.

    The large window over which the normalization is extracted seems to be important to avoid fluctatuions.

    Also allows for comparison of:
        - Continuous wave transform with width ~ pi
        - Smoothing data assuming the points are distributed as a gaussian with options of:
            - Max of smoothed function
            - Moving average over pi of smoothed function
        - Moving average over pi
        - Linear 1D fit
        - Linear 2D fit

    All of the above were also performed over a 2 bin rebin except for the gaussian smoothed function.

    Args:
        mixed_event: The 2D mixed event histogram.
        eta_limits: Eta limits of which the mixed event should be projection into 1D.
    Returns:
        A very complicated tuple containing all of the various compared options. See the code.
    """
    # Create projected histograms
    peak_finding_hist, peak_finding_hist_array = _peak_finding_objects_from_mixed_event(
        mixed_event=mixed_event, eta_limits=eta_limits)
    # Determine max via the moving average
    # This is what is implemented in the mixed event normalization, but in principle, this could change.
    # Since it's easy to calculate, we do it by hand again here.
    max_moving_avg = np.max(utils.moving_average(peak_finding_hist_array,
                                                 n=36))

    # Create rebinned hist
    # The rebinned hist may be less susceptible to noise, so it should be compared.
    # Only rebin the 2D in delta phi because otherwise the delta eta bins will not align with the limits
    # NOTE: By passing a new name, it will create a new rebinned histogram.
    mixed_event_rebin = mixed_event.Rebin2D(2, 1,
                                            f"{mixed_event.GetName()}Rebin")
    # Scale back down to account for the rebin.
    mixed_event_rebin.Scale(1. / 2.)
    peak_finding_hist_rebin = peak_finding_hist.Rebin(
        2,
        peak_finding_hist.GetName() + "Rebin")
    # Scale back down to account for the rebin.
    peak_finding_hist_rebin.Scale(1. / 2.)
    # Note that peak finding will only be performed on the 1D hist
    peak_finding_hist_array_rebin = histogram.Histogram1D.from_existing_hist(
        peak_finding_hist_rebin).y

    # Define points where the plots and functions can be evaluted
    lin_space = np.linspace(-0.5 * np.pi, 3. / 2 * np.pi,
                            len(peak_finding_hist_array))
    lin_space_rebin = np.linspace(-0.5 * np.pi, 3. / 2 * np.pi,
                                  len(peak_finding_hist_array_rebin))

    # Using CWT
    # See: https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.signal.find_peaks_cwt.html
    # and: https://stackoverflow.com/a/42285002
    peak_locations: np.ndarray = scipy.signal.find_peaks_cwt(
        peak_finding_hist_array, widths=np.arange(20, 50, .1))
    peak_locations_rebin: np.ndarray = scipy.signal.find_peaks_cwt(
        peak_finding_hist_array_rebin, widths=np.arange(10, 25, .05))
    logger.info(
        f"peak_locations: {peak_locations}, values: {peak_finding_hist_array[peak_locations]}"
    )

    # Using gaussian smoothing
    # See: https://stackoverflow.com/a/22291860
    f = scipy.interpolate.interp1d(lin_space, peak_finding_hist_array)
    # Resample for higher resolution
    lin_space_resample = np.linspace(-0.5 * np.pi, 3. / 2 * np.pi, 7200)
    f_resample = f(lin_space_resample)
    # Gaussian
    # std deviation is in x!
    window = scipy.signal.gaussian(1000, 300)
    smoothed_array = scipy.signal.convolve(f_resample,
                                           window / window.sum(),
                                           mode="same")
    #max_smoothed = np.amax(smoothed_array)
    #logger.debug("max_smoothed: {}".format(max_smoothed))
    # Moving average on smoothed curve
    smoothed_moving_avg = utils.moving_average(smoothed_array,
                                               n=int(len(smoothed_array) // 2))
    max_smoothed_moving_avg = np.max(smoothed_moving_avg)

    # Moving average with rebin
    moving_avg_rebin = utils.moving_average(peak_finding_hist_array_rebin,
                                            n=18)
    max_moving_avg_rebin = np.max(moving_avg_rebin)

    # Fit using TF1 over some range
    # Fit the deltaPhi away side
    fit1D = fitting.fit_1d_mixed_event_normalization(
        peak_finding_hist, [1. / 2. * np.pi, 3. / 2. * np.pi])
    max_linear_fit1D = fit1D.GetParameter(0)
    fit1D_rebin = fitting.fit_1d_mixed_event_normalization(
        peak_finding_hist_rebin, [1. / 2. * np.pi, 3. / 2. * np.pi])
    max_linear_fit1D_rebin = fit1D_rebin.GetParameter(0)
    fit2D = fitting.fit_2d_mixed_event_normalization(
        mixed_event, [1. / 2. * np.pi, 3. / 2. * np.pi], eta_limits)
    max_linear_fit2D = fit2D.GetParameter(0)
    fit2D_rebin = fitting.fit_2d_mixed_event_normalization(
        mixed_event_rebin, [1. / 2. * np.pi, 3. / 2. * np.pi], eta_limits)
    max_linear_fit2D_rebin = fit2D_rebin.GetParameter(0)

    logger.debug(
        f"linear1D: {max_linear_fit1D}, linear1D_rebin: {max_linear_fit1D_rebin}"
    )
    logger.debug(
        f"linear2D: {max_linear_fit2D}, linear2D_rebin: {max_linear_fit2D_rebin}"
    )

    return (
        peak_finding_hist,
        # Basic data
        lin_space,
        peak_finding_hist_array,
        lin_space_rebin,
        peak_finding_hist_array_rebin,
        # CWT
        peak_locations,
        peak_locations_rebin,
        # Moving Average
        max_moving_avg,
        max_moving_avg_rebin,
        # Smoothed gaussian
        lin_space_resample,
        smoothed_array,
        max_smoothed_moving_avg,
        # Linear fits
        max_linear_fit1D,
        max_linear_fit1D_rebin,
        max_linear_fit2D,
        max_linear_fit2D_rebin,
    )
Exemple #23
0
 def get_bin(hist: Hist, bin_of_interest: int, index: int) -> int:
     return cast(int, hist.GetBin(index, bin_of_interest))
Exemple #24
0
def track_eta_phi(hist: Hist, event_activity: params.EventActivity,
                  output_info: analysis_objects.PlottingOutputWrapper) -> None:
    """ Plot track eta phi.

    Also include an annotation of the EMCal eta, phi location.

    Args:
        rho_hist: Rho centrality dependent hist.
        output_info: Output information.
        includes_constituent_cut: True if the plot was produced using the constituent cut.
    Returns:
        None. The figure is plotted.
    """
    # Setup
    fig, ax = plt.subplots(figsize=(8, 6))
    x, y, hist_array = histogram.get_array_from_hist2D(
        hist,
        set_zero_to_NaN=True,
        return_bin_edges=True,
    )

    plot = ax.imshow(
        hist_array.T,
        extent=[np.amin(x), np.amax(x),
                np.amin(y), np.amax(y)],
        interpolation="nearest",
        aspect="auto",
        origin="lower",
        # Intentionally stretch the scale near the top of the range to emphasise the inefficiency.
        norm=matplotlib.colors.Normalize(vmin=np.nanmax(hist_array) * 0.8,
                                         vmax=np.nanmax(hist_array) * 0.9),
        cmap="viridis",
    )
    # Draw the colorbar based on the drawn axis above.
    # NOTE: This can cause the warning:
    #       '''
    #       matplotlib/colors.py:1031: RuntimeWarning: invalid value encountered in less_equal
    #           mask |= resdat <= 0"
    #       '''
    #       The warning is due to the nan we introduced above. It can safely be ignored
    #       See: https://stackoverflow.com/a/34955622
    #       (Could suppress, but I don't feel it's necessary at the moment)
    fig.colorbar(plot)

    # Draw EMCal boundaries
    r = matplotlib.patches.Rectangle(
        xy=(-0.7, 80 * np.pi / 180),
        width=0.7 + 0.7,
        height=(187 - 80) * np.pi / 180,
        facecolor="none",
        edgecolor="tab:red",
        linewidth=1.5,
        label="EMCal",
    )
    ax.add_patch(r)

    # Final presentation settings
    ax.set_xlabel(labels.make_valid_latex_string(r"\eta"))
    ax.set_ylabel(labels.make_valid_latex_string(r"\varphi"))
    ax.legend(loc="upper right")
    fig.tight_layout()

    # Finally, save and cleanup
    output_name = f"track_eta_phi_{str(event_activity)}"
    plot_base.save_plot(output_info, fig, output_name)
    plt.close(fig)

    # Check the phi 1D projection
    track_phi = hist.ProjectionY()
    # Make it easier to view
    track_phi.Rebin(8)
    import ROOT
    canvas = ROOT.TCanvas("c", "c")
    # Labeling
    track_phi.SetTitle("")
    track_phi.GetXaxis().SetTitle(
        labels.use_label_with_root(labels.make_valid_latex_string(r"\varphi")))
    track_phi.GetYaxis().SetTitle("Counts")
    track_phi.Draw()
    # Draw lines corresponding to the EMCal
    line_min = ROOT.TLine(80 * np.pi / 180, track_phi.GetMinimum(),
                          80 * np.pi / 180, track_phi.GetMaximum())
    line_max = ROOT.TLine(187 * np.pi / 180, track_phi.GetMinimum(),
                          187 * np.pi / 180, track_phi.GetMaximum())
    for l in [line_min, line_max]:
        l.SetLineColor(ROOT.kRed)
        l.Draw("same")

    # Save the plot
    plot_base.save_plot(output_info, canvas,
                        f"track_phi_{str(event_activity)}")