Source code for reaction_plane_fit.three_orientations

#!/usr/bin/env python

""" Implements the three RP orientation fit.

.. code-author: Raymond Ehlers <raymond.ehlers@cern.ch>, Yale University
"""

import logging
import numpy as np
from numpy import sin, cos
from typing import Any, Callable, Dict, Union

from reaction_plane_fit import base
from reaction_plane_fit import fit
from reaction_plane_fit import functions

logger = logging.getLogger(__name__)

[docs]class SignalFitComponent(fit.SignalFitComponent): """ Signal fit component for three RP orientations. Args: inclusive_background_function: Background function for the inclusive RP orientation. By default, one should use ``fourier``, but when not fitting the inclusive orientation, one should use the constrained ``fourier`` which sets the background level. """ def __init__(self, inclusive_background_function: Callable[..., float], *args: Any, **kwargs: Any): self._inclusive_background_function = inclusive_background_function super().__init__(*args, **kwargs)
[docs] def determine_fit_function(self, resolution_parameters: fit.ResolutionParameters, reaction_plane_parameter: base.ReactionPlaneParameter) -> None: self.fit_function = functions.determine_signal_dominated_fit_function( rp_orientation = self.rp_orientation, resolution_parameters = resolution_parameters, reaction_plane_parameter = reaction_plane_parameter, rp_background_function = background, inclusive_background_function = self._inclusive_background_function, ) self.background_function = functions.determine_background_fit_function( rp_orientation = self.rp_orientation, resolution_parameters = resolution_parameters, reaction_plane_parameter = reaction_plane_parameter, rp_background_function = background, inclusive_background_function = self._inclusive_background_function, )
[docs]class BackgroundFitComponent(fit.BackgroundFitComponent): """ Background fit component for three RP orientations. """
[docs] def determine_fit_function(self, resolution_parameters: fit.ResolutionParameters, reaction_plane_parameter: base.ReactionPlaneParameter) -> None: self.fit_function = functions.determine_background_fit_function( rp_orientation = self.rp_orientation, resolution_parameters = resolution_parameters, reaction_plane_parameter = reaction_plane_parameter, rp_background_function = background, inclusive_background_function = unconstrained_inclusive_background, ) # This is identical to the fit function. self.background_function = self.fit_function
[docs]class ReactionPlaneFit(fit.ReactionPlaneFit): """ Base class for reaction plane fit for 3 reaction plane orientations. Attributes: _rp_orientations: List of the reaction plane orientations. reaction_plane_parameter: Reaction plane parameters, including the orientation, center, and width. """ _rp_orientations = ["in_plane", "mid_plane", "out_of_plane", "inclusive"] reaction_plane_parameters = { "in_plane": base.ReactionPlaneParameter(orientation = "in_plane", phiS = 0, c = np.pi / 6.), # NOTE: This c value is halved in the fit to account for the four non-continuous regions "mid_plane": base.ReactionPlaneParameter(orientation = "mid_plane", phiS = np.pi / 4., c = np.pi / 12.), "out_of_plane": base.ReactionPlaneParameter(orientation = "out_of_plane", phiS = np.pi / 2., c = np.pi / 6.), # This is really meaningful, so it should be ignored. # However, it is helpful for it to be defined in this dict for lookup purposes. "inclusive": base.ReactionPlaneParameter(orientation = "inclusive", phiS = 0, c = np.pi / 2), }
[docs]class BackgroundFit(ReactionPlaneFit): """ RPF for background region in 3 reaction plane orientations. This is a simple helper class to define the necessary fit component. Contains fit components for 3 background RP orientations. Args: Same as for ``ReactionPlaneFit``. """ def __init__(self, *args: Any, **kwargs: Any): # Create the base class first super().__init__(*args, **kwargs) # Setup the fit components for orientation in self.rp_orientations: fit_type = base.FitType(region = "background", orientation = orientation) self.components[fit_type] = BackgroundFitComponent(rp_orientation = fit_type.orientation, resolution_parameters = self.resolution_parameters, use_log_likelihood = self.use_log_likelihood) # Complete basic setup of the components by setting up the fit functions. self._setup_component_fit_functions()
[docs] def create_full_set_of_components(self, input_data: fit.Data) -> Dict[str, fit.FitComponent]: """ Create the full set of fit components. """ # Sanity check that the fit has actually been performed. if not hasattr(self, "fit_result"): raise RuntimeError("Must perform fit before attempt to retrieve all of the fit components.") # Determine the components to return components = {} # First copy the RP orientation components. for fit_type, c in self.components.items(): components[fit_type.orientation] = c # Create the inclusive component. We only want the background fit component. inclusive_component = BackgroundFitComponent( rp_orientation = "inclusive", resolution_parameters = self.resolution_parameters, use_log_likelihood = self.use_log_likelihood ) # Fully setup the component and retrieve the appropriate fit result values. inclusive_component.determine_fit_function( resolution_parameters = self.resolution_parameters, reaction_plane_parameter = self.reaction_plane_parameters["inclusive"], ) inclusive_component._setup_fit( input_hist = input_data[base.FitType(region = "background", orientation = "inclusive")] ) # Extract the relevant information into the component inclusive_component.fit_result = base.component_fit_result_from_rp_fit_result( fit_result = self.fit_result, component = inclusive_component, ) # We need to calculate the fit errors. x = self.fit_result.x inclusive_component.fit_result.errors = inclusive_component.calculate_fit_errors(x) # Store the newly created component components["inclusive"] = inclusive_component return components
[docs]class InclusiveSignalFit(ReactionPlaneFit): """ RPF for inclusive signal region, and background region in 3 reaction planes orientations. This is a simple helper class to define the necessary fit component. Contains an inclusive signal fit, and 3 background RP orientations. Args: Same as for ``ReactionPlaneFit``. """ def __init__(self, *args: Any, use_constrained_inclusive_background: bool = False, **kwargs: Any): # Create the base class first super().__init__(*args, **kwargs) # Determine the signal background function inclusive_background_function = constrained_inclusive_background if use_constrained_inclusive_background \ else functions.fourier # Setup the fit components fit_type = base.FitType(region = "signal", orientation = "inclusive") self.components[fit_type] = SignalFitComponent(inclusive_background_function = inclusive_background_function, rp_orientation = fit_type.orientation, resolution_parameters = self.resolution_parameters, use_log_likelihood = self.use_log_likelihood) for orientation in self.rp_orientations: fit_type = base.FitType(region = "background", orientation = orientation) self.components[fit_type] = BackgroundFitComponent(rp_orientation = fit_type.orientation, resolution_parameters = self.resolution_parameters, use_log_likelihood = self.use_log_likelihood) # Complete basic setup of the components by setting up the fit functions. self._setup_component_fit_functions()
[docs] def create_full_set_of_components(self, input_data: fit.Data) -> Dict[str, fit.FitComponent]: """ Create the full set of fit components. """ # Sanity check that the fit has actually been performed. if not hasattr(self, "fit_result"): raise RuntimeError("Must perform fit before attempt to retrieve all of the fit components.") # We just return a component of the existing components, but with a simplified key. components = {} for fit_type, c in self.components.items(): components[fit_type.orientation] = c return components
[docs]class SignalFit(ReactionPlaneFit): """ RPF for signal and background regions with 3 reaction plane orientations. This is a simple helper class to define the necessary fit component. Contains 3 signal orientations and 3 background RP orientations. Args: Same as for ``ReactionPlaneFit``. """ def __init__(self, *args: Any, **kwargs: Any): # Create the base class first super().__init__(*args, **kwargs) # Setup the fit components for region, fit_component in [("signal", SignalFitComponent), ("background", BackgroundFitComponent)]: for orientation in self.rp_orientations: fit_type = base.FitType(region = region, orientation = orientation) # Determine the proper arguments component_args: Dict[str, Any] = { "inclusive_background_function": functions.fourier, "rp_orientation": fit_type.orientation, "resolution_parameters": self.resolution_parameters, "use_log_likelihood": self.use_log_likelihood, } if region != "signal": # This isn't a valid argument for the background component, so remove it. component_args.pop("inclusive_background_function") # Finally, create the components. self.components[fit_type] = fit_component(**component_args) # Complete basic setup of the components by setting up the fit functions. self._setup_component_fit_functions()
[docs] def create_full_set_of_components(self, input_data: fit.Data) -> Dict[str, fit.FitComponent]: """ Create the full set of fit components. """ # Sanity check that the fit has actually been performed. if not hasattr(self, "fit_result"): raise RuntimeError("Must perform fit before attempt to retrieve all of the fit components.") # Determine the components to return components = {} # First copy the RP orientation components. for fit_type, c in self.components.items(): # We only want the signal components if fit_type.region == "background": continue components[fit_type.orientation] = c # We then return a background inclusive fit because extracting a signal inclusive fit isn't # straightforward to determine because gaussians don't add when they are dependent. # Create the inclusive component. We only want the background fit component. logger.warning("Creating background inclusive component. Be aware of the implications!") inclusive_component = BackgroundFitComponent( rp_orientation = "inclusive", resolution_parameters = self.resolution_parameters, use_log_likelihood = self.use_log_likelihood ) # Fully setup the component and retrieve the appropriate fit result values. inclusive_component.determine_fit_function( resolution_parameters = self.resolution_parameters, reaction_plane_parameter = self.reaction_plane_parameters["inclusive"], ) inclusive_component._setup_fit( input_hist = input_data[base.FitType(region = "background", orientation = "inclusive")] ) # Extract the relevant information into the component inclusive_component.fit_result = base.component_fit_result_from_rp_fit_result( fit_result = self.fit_result, component = inclusive_component, ) # We need to calculate the fit errors. x = self.fit_result.x inclusive_component.fit_result.errors = inclusive_component.calculate_fit_errors(x) # Store the newly created component components["inclusive"] = inclusive_component return components
[docs]def constrained_inclusive_background(x: Union[np.ndarray, float], B: float, v2_t: float, v2_a: float, v4_t: float, v4_a: float, v1: float, v3: float, **kwargs: float) -> float: """ Background function for inclusive signal component when performing the background fit. Includes the trivial scaling factor of ``B / 3`` because there are 3 RP orientations and that background level is set by the individual RP orientations. So when they are added together, they are (approximately) 3 times more. This constrain is not included by default because there is additional information in the relative scaling of the various EP orientations. Args: x (float): Delta phi value for which the background will be calculated. B (float): Overall multiplicative background level. v2_t (float): Trigger v_{2}. v2_a (float): Associated v_{2}. v4_t (float): Trigger v_{4}. v4_a (float): Associated v_{4} v1 (float): v1 parameter. v3 (float): v3 parameter. kwargs (dict): Used to absorbs extra possible parameters from Minuit (especially when used in conjunction with other functions). Returns: float: Values calculated by the function. """ return functions.fourier(x, B / 3, v2_t, v2_a, v4_t, v4_a, v1, v3)
[docs]def unconstrained_inclusive_background(x: Union[np.ndarray, float], B: float, v2_t: float, v2_a: float, v4_t: float, v4_a: float, v1: float, v3: float, **kwargs: float) -> float: """ Background function for inclusive signal component when performing the background fit. This basically just forwards the arguments onto the Fourier series, but it renames the background variable. It doesn't include the trivial scaling factor of approximately ``B / 3`` that occurs when adding the three event plane orientations to compare against the inclusive because that information can be useful to further constrain the fits. Args: x: Delta phi value for which the background will be calculated. B: Overall multiplicative background level. v2_t: Trigger v_{2}. v2_a: Associated v_{2}. v4_t: Trigger v_{4}. v4_a: Associated v_{4} v1: v1 parameter. v3: v3 parameter. kwargs: Used to absorbs extra possible parameters from Minuit (especially when used in conjunction with other functions). Returns: float: Values calculated by the function. """ return functions.fourier(x, B, v2_t, v2_a, v4_t, v4_a, v1, v3)
[docs]def background(x: float, phi: float, c: float, resolution_parameters: fit.ResolutionParameters, B: float, v2_t: float, v2_a: float, v4_t: float, v4_a: float, v1: float, v3: float, **kwargs: float) -> float: """ The background function is of the form specified in the RPF paper. Resolution parameters implemented include R{2,2} through R{8,2}, which denotes the resolution of an order m reaction plane with respect to n = 2 reaction plane. R{8,2} is the highest value which should contribute to v_4^{eff}. Args: x (float): Delta phi value for which the background will be calculated. phi (float): Center of the reaction plane bin. Matches up to phi_s in the RPF paper c (float): Width of the reaction plane bin. Matches up to c in the RPF paper resolution_parameters (dict): Contains the resolution parameters with respect to the n = 2 reaction plane. Note the information about the parameters above. The expected keys are "R22" - "R82". B (float): Overall multiplicative background level. v2_t (float): Trigger v_{2}. v2_a (float): Associated v_{2}. v4_t (float): Trigger v_{4}. v4_a (float): Associated v_{4} v1 (float): v1 parameter. v3 (float): v3 parameter. kwargs (dict): Used to absorbs extra possible parameters from Minuit (especially when used in conjunction with other functions). Returns: float: Values calculated by the function. """ # Define individual resolution variables to make the expressions more concise. R22 = resolution_parameters["R22"] R42 = resolution_parameters["R42"] R62 = resolution_parameters["R62"] R82 = resolution_parameters["R82"] num = v2_t + cos(2 * phi) * sin(2 * c) / (2 * c) * R22 \ + v4_t * cos(2 * phi) * sin(2 * c) / (2 * c) * R22 \ + v2_t * cos(4 * phi) * sin(4 * c) / (4 * c) * R42 \ + v4_t * cos(6 * phi) * sin(6 * c) / (6 * c) * R62 den = 1 \ + 2 * v2_t * cos(2 * phi) * sin(2 * c) / (2 * c) * R22 \ + 2 * v4_t * cos(4 * phi) * sin(4 * c) / (4 * c) * R42 v2R = num / den num2 = v4_t + cos(4 * phi) * sin(4 * c) / (4 * c) * R42 \ + v2_t * cos(2 * phi) * sin(2 * c) / (2 * c) * R22 \ + v2_t * cos(6 * phi) * sin(6 * c) / (6 * c) * R62 \ + v4_t * cos(8 * phi) * sin(8 * c) / (8 * c) * R82 v4R = num2 / den BR = B * den * c * 2 / np.pi factor = 1.0 # In the case of mid-plane, it has 4 regions instead of 2 if c == np.pi / 12.0: factor = 2.0 BR = BR * factor return functions.fourier(x, BR, v2R, v2_a, v4R, v4_a, v1, v3)