Source code for vibe.analysis_validation_modes.svd.SVDPerformance_validation_mode
import basf2
try:
import rawdata
import tracking
import svd
except ImportError:
pass
import modularAnalysis as ma
from typing import List
import numpy as np
from vibe.core.validation_mode import ValidationModeBaseClass
from vibe.core.helper.histogram_tools import Histogram, HistComponent, HistVariable
from vibe.core.utils.misc import fancy_validation_mode_header
from vibe.core.helper.root_helper import makeROOTCompatible
__all__ = [
"SVDPerformance",
]
[docs]
@fancy_validation_mode_header
class SVDPerformance(ValidationModeBaseClass):
name = "SVDPerformance"
# Configuration: Set to True to generate per-ladder histograms (A LOT of histograms!)
# Set to False to generate only phi-integrated histograms
# TODO: This could be made into a user-configurable option layer
generate_per_ladder_histograms = False
[docs]
def create_basf2_path(self):
main_path = basf2.Path()
main_path.add_module("Gearbox")
main_path.add_module("Geometry", useDB=True)
rawdata.add_unpackers(main_path)
# now do reconstruction
tracking.add_tracking_reconstruction(
main_path,
mcTrackFinding=False,
trackFitHypotheses=[211],
append_full_grid_cdc_eventt0=True,
skip_full_grid_cdc_eventt0_if_svd_time_present=False,
)
# Reconstruct strips
svd.add_svd_create_recodigits(main_path)
ma.fillParticleLists(decayStringsWithCuts=[("pi+:all", "")], path=main_path)
svd_variables = [
# Cluster properties
"SVDClusterCharge",
"SVDClusterChargeNormTrkLength",
"SVDClusterSNR",
"SVDClusterSize",
"SVDClusterTime",
# For residuals and resolution
"SVDResidual",
"SVDTrackPrime",
"SVDTrackPositionErrorUnbiased",
"SVDTruePosition",
# Geometric coordinates of a cluster
"SVDLayer",
"SVDLadder",
"SVDSensor",
"SVDSide",
]
self.variables_to_validation_ntuple(
decay_str="pi+:all",
variables=svd_variables,
path=main_path,
)
return main_path
@property
def analysis_validation_histograms(self) -> List[Histogram]:
"""Generate histograms for SVD performance validation.
Generates two sets of histograms:
1. Phi-integrated (all ladders): L{layer}_S{sensor}_{side}_{variable}
2. Per-ladder: L{layer}_L{ladder}_S{sensor}_{side}_{variable}
"""
histograms = []
# Define layer-sensor-ladder mapping (from ROOT macro)
layer_config = {
3: {"sensors": 2, "ladders": 7}, # Layer 3: 2 sensors, 7 ladders
4: {"sensors": 3, "ladders": 10}, # Layer 4: 3 sensors, 10 ladders
5: {"sensors": 4, "ladders": 12}, # Layer 5: 4 sensors, 12 ladders
6: {"sensors": 5, "ladders": 16}, # Layer 6: 5 sensors, 16 ladders
}
# Common track selection (matching ROOT macro)
# TODO: Add these to the ntuple to activate the selection
# common_selection = "abs(svdTrkz0) < 0.5 and abs(svdTrkd0) < 0.2"
common_selection = ""
# Define histogram configurations for each variable type
histogram_configs = [
{
"var_name": "ClCharge",
"title": "Cluster Charge",
"label": r"Cluster Charge",
"unit": r"ke",
"scope": (0, 120),
"bins": 60,
"base_variable": "SVDClusterCharge",
},
{
"var_name": "ClChargeNorm",
"title": "Cluster Charge Normalized",
"label": r"Cluster Charge Normalized",
"unit": r"ke",
"scope": (0, 120),
"bins": 60,
"base_variable": "SVDClusterChargeNormTrkLength",
},
{
"var_name": "ClSNR",
"title": "Cluster SNR",
"label": r"Cluster SNR",
"unit": "",
"scope": (0, 140),
"bins": 50,
"base_variable": "SVDClusterSNR",
},
{
"var_name": "ClSize",
"title": "Cluster Size",
"label": r"Cluster Size",
"unit": "",
"scope": (0, 15),
"bins": 15,
"base_variable": "SVDClusterSize",
},
{
"var_name": "ClTime",
"title": "Cluster Time",
"label": r"Cluster Time",
"unit": "ns",
"scope": (-100, 100),
"bins": 200,
"base_variable": "SVDClusterTime",
},
]
# Loop over layers
for layer in [3, 4, 5, 6]:
max_sensor = layer_config[layer]["sensors"]
max_ladder = layer_config[layer]["ladders"]
# Loop over sensors
for sensor in range(1, max_sensor + 1):
# Loop over sides (0=V/N, 1=U/P)
for side_idx, side_name in [(0, "V"), (1, "U")]:
# 1. Phi-integrated histograms (all ladders)
layer_sensor_side_sel = f"SVDLayer == {layer} and SVDSensor == {sensor} and SVDSide == {side_idx}"
# Build full selection
if common_selection:
full_selection_phi_int = f"{common_selection} and {layer_sensor_side_sel}"
else:
full_selection_phi_int = layer_sensor_side_sel
# Create phi-integrated histograms for each variable
for config in histogram_configs:
var_name = config["var_name"]
formula = config["base_variable"]
# Create histogram variable
hist_var = HistVariable(
df_label=makeROOTCompatible(variable=config["base_variable"]),
label=config["label"],
unit=config["unit"],
scope=config["scope"],
bins=config["bins"],
)
# Create histogram component
component = HistComponent(
label=f"L{layer}-S{sensor}-{side_name}",
additional_cut_str=full_selection_phi_int,
variable=formula,
)
# Create histogram
histogram = Histogram(
name=f"L{layer}_S{sensor}_{side_name}_{var_name}",
title=f"Layer {layer}, Sensor {sensor}, {side_name} side",
hist_variable=hist_var,
hist_components=[component],
particle_list="pi+:all",
)
histograms.append(histogram)
# 2. per-ladder histograms (optional)
if self.generate_per_ladder_histograms:
for ladder in range(1, max_ladder + 1):
layer_ladder_sensor_side_sel = (
f"SVDLayer == {layer} and SVDLadder == {ladder} and "
f"SVDSensor == {sensor} and SVDSide == {side_idx}"
)
# Build full selection
if common_selection:
full_selection_per_ladder = f"{common_selection} and {layer_ladder_sensor_side_sel}"
else:
full_selection_per_ladder = layer_ladder_sensor_side_sel
# Create per-ladder histograms for each variable
for config in histogram_configs:
var_name = config["var_name"]
formula = config["base_variable"]
# Create histogram variable
hist_var = HistVariable(
df_label=makeROOTCompatible(variable=config["base_variable"]),
label=config["label"],
unit=config["unit"],
scope=config["scope"],
bins=config["bins"],
)
# Create histogram component
component = HistComponent(
label=f"L{layer}-L{ladder}-S{sensor}-{side_name}",
additional_cut_str=full_selection_per_ladder,
variable=formula,
)
# Create histogram
histogram = Histogram(
name=f"L{layer}_L{ladder}_S{sensor}_{side_name}_{var_name}",
title=f"Layer {layer}, Ladder {ladder}, Sensor {sensor}, {side_name} side",
hist_variable=hist_var,
hist_components=[component],
particle_list="pi+:all",
)
histograms.append(histogram)
return histograms
[docs]
def variables_to_validation_ntuple(self, decay_str, variables, path):
path.add_module(
"SVDVariablesToStorage",
outputFileName=self.output_filename,
containerName=self.get_tree_name(decay_str),
particleListName=decay_str,
variablesToNtuple=variables,
)
last_module = path.modules()[-1]
last_module.set_name(f"{last_module.type()}_{self.name}")
[docs]
def get_ntuple_particle_lists(self, basf2_kwargs=None):
if basf2_kwargs is None:
basf2_kwargs = {}
basf2_path = self.create_basf2_path(**basf2_kwargs)
return {
p.values
for m in basf2_path.modules()
if m.type() == "SVDVariablesToStorage"
for p in m.available_params()
if p.name == "particleListName"
}
[docs]
def get_ntuple_trees(self, basf2_kwargs=None):
if basf2_kwargs is None:
basf2_kwargs = {}
basf2_path = self.create_basf2_path(**basf2_kwargs)
return {
p.values
for m in basf2_path.modules()
if m.type() == "SVDVariablesToStorage"
for p in m.available_params()
if p.name == "containerName"
}
[docs]
def offline_df_manipulation(self, df):
# Convert cluster charge from electrons to kilo-electrons
df["SVDClusterCharge"] = df["SVDClusterCharge"] / 1000.0
df["SVDClusterChargeNormTrkLength"] = df["SVDClusterChargeNormTrkLength"] / 1000.0
# Set this as they are not set by the SVDVariablesToStorage module and are required for plotting
df[["__experiment__", "__run__", "__event__"]] = np.nan
df["__weight__"] = 1.0
return df
@property
def plotting_strategies(self) -> List[str]:
return [
"per_dataset_root_renderer",
"overlay_datasets_root_renderer",
]