Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions runcards/analyze/test_runcard_closure_lines.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: "Report_test_closure_lines"
title: "Closure Line Test"

result_IDs: [SM_G23]
fit_labels: [SM G23]

report_path: /data/theorie/wtelman/reports/benchmark
result_path: /data/theorie/wtelman/results/benchmark

summary: False

coefficients_plots:
posterior_histograms:
nrows: -1
disjointed_lists: False
bins: 40
show_closure_truth: True
closure_line_style: "--"
closure_line_color: red
show_closure_legend: True
closure_line_label: "Closure truth"
logo: False

coeff_info:
default:
- [OpBox, "$O_{Box}$"]

data_info:
default:
- [FCCee_ww_161GeV, "WW @ 161 GeV"]
80 changes: 79 additions & 1 deletion src/smefit/analyze/coefficients_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,32 @@ def compute_rows_and_columns(self, nrows=-1, ncols=-1):

return nrows, ncols

@staticmethod
def _extract_point_value(point_values, coeff_name):
"""Return per-coefficient value from common container types."""
if point_values is None:
return None

try:
if isinstance(point_values, pd.DataFrame):
if coeff_name in point_values.columns:
return float(point_values[coeff_name].iloc[0])
return None

if isinstance(point_values, pd.Series):
if coeff_name in point_values.index:
return float(point_values[coeff_name])
return None

if isinstance(point_values, dict):
if coeff_name in point_values:
return float(point_values[coeff_name])
return None
except (TypeError, ValueError):
return None

return None

def plot_posteriors(self, posteriors, labels, **kwargs):
"""Plot posteriors histograms.

Expand All @@ -691,8 +717,25 @@ def plot_posteriors(self, posteriors, labels, **kwargs):
list of fit names
kwargs: dict
keyword arguments for the plot
Supported keys:
- show_closure_truth: bool
- closure_truth_points: list[dict | pd.Series | pd.DataFrame]
- closure_line_color: str
- closure_line_style: str
- closure_line_width: float
- closure_line_alpha: float
- show_closure_legend: bool
- closure_line_label: str
"""
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
show_closure_truth = kwargs.get("show_closure_truth", False)
closure_truth_points = kwargs.get("closure_truth_points", None)
closure_line_color = kwargs.get("closure_line_color", "black")
closure_line_style = kwargs.get("closure_line_style", "--")
closure_line_width = kwargs.get("closure_line_width", 1.6)
closure_line_alpha = kwargs.get("closure_line_alpha", 0.9)
show_closure_legend = kwargs.get("show_closure_legend", False)
closure_line_label = kwargs.get("closure_line_label", "Closure truth")

nrows, ncols = self.compute_rows_and_columns(
kwargs.get("nrows", -1), kwargs.get("ncols", -1)
Expand Down Expand Up @@ -739,6 +782,22 @@ def plot_posteriors(self, posteriors, labels, **kwargs):
alpha=0.3,
label=labels[clr_idx],
)

if show_closure_truth and closure_truth_points is not None:
if clr_idx < len(closure_truth_points):
closure_value = self._extract_point_value(
closure_truth_points[clr_idx], l
)
if closure_value is not None:
ax.axvline(
closure_value,
color=closure_line_color,
linestyle=closure_line_style,
linewidth=closure_line_width,
alpha=closure_line_alpha,
zorder=11,
)

ax.text(
0.05,
0.85,
Expand All @@ -754,14 +813,33 @@ def plot_posteriors(self, posteriors, labels, **kwargs):
if len(axes.get_legend_handles_labels()[0]) > len(lines):
lines, labels = axes.get_legend_handles_labels()

lines = list(lines)
labels = list(labels)
if (
show_closure_truth
and show_closure_legend
and closure_line_label not in labels
):
lines.append(
mlines.Line2D(
[],
[],
color=closure_line_color,
linestyle=closure_line_style,
linewidth=closure_line_width,
alpha=closure_line_alpha,
)
)
labels.append(closure_line_label)

# fontsize is normalised to 25 for 5 columns and subplot size 4
legend_font_size = 25 * (ncols * subplot_size) / 20
legend_font_size_inch = legend_font_size / 72 # 72 pt = 1 inch

fig.legend(
lines,
labels,
ncol=len(posteriors),
ncol=len(labels),
prop={"size": legend_font_size},
bbox_to_anchor=(0.5, 1.0),
loc="upper center",
Expand Down
100 changes: 100 additions & 0 deletions src/smefit/analyze/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import numpy as np
import pandas as pd
import yaml

from ..coefficients import CoefficientManager
from ..fit_manager import FitManager
from ..log import logging
from .chi2_utils import Chi2tableCalculator
Expand Down Expand Up @@ -142,6 +144,80 @@ def _append_section(self, title, links=None, figs=None, tables=None):
title, links=links, figs=figs, dataFrame=tables
)

@staticmethod
def _compute_closure_truth_from_coeff_config(coeff_cfg, coeff_names):
"""Compute closure-truth Wilson coefficients from a coefficient config."""
fit_truth = {coeff_name: 0.0 for coeff_name in coeff_names}
if not coeff_cfg:
return fit_truth
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a clear warning to the user here that the default values 0 is being used

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what does coeff_cfg stand for? It is not immediately obvious to me


coefficients = CoefficientManager.from_dict(copy.deepcopy(coeff_cfg))
if not coefficients.free_parameters.empty:
# Free parameters have no closure-truth value by construction.
coefficients.set_free_parameters(
np.zeros(coefficients.free_parameters.shape[0], dtype=float)
)
coefficients.set_constraints()

coeff_set = set(coefficients.name)
for coeff_name in coeff_names:
if coeff_name not in coeff_set:
continue
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, maybe need a relevant warning here

coeff_entry = coeff_cfg.get(coeff_name, {})
if isinstance(coeff_entry, (int, float, np.floating)):
fit_truth[coeff_name] = float(coeff_entry)
elif isinstance(coeff_entry, dict) and (
"value" in coeff_entry or "constrain" in coeff_entry
):
fit_truth[coeff_name] = float(coefficients[coeff_name]["value"])
return fit_truth

def _load_closure_truth_points_from_runcards(
self, closure_truth_runcards, coeff_names
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you loading the truth from the report runcard or from the projection runcard? Or both?
In any case, the possible options need to be better documented, both in the source code and in the example runcards

):
"""Load closure-truth values from one or more projection/run runcards."""
if isinstance(closure_truth_runcards, dict):
runcard_paths = []
for fit in self.fits:
fit_runcard = closure_truth_runcards.get(fit.name, None)
if fit_runcard is None:
raise ValueError(
"closure_truth_runcards is missing an entry for "
f"result_ID '{fit.name}'."
)
runcard_paths.append(fit_runcard)
elif isinstance(closure_truth_runcards, (str, pathlib.Path)):
runcard_paths = [closure_truth_runcards for _ in self.fits]
else:
runcard_paths = list(closure_truth_runcards)
if len(runcard_paths) == 1 and len(self.fits) > 1:
runcard_paths = [runcard_paths[0] for _ in self.fits]
elif len(runcard_paths) != len(self.fits):
raise ValueError(
"closure_truth_runcards must have either 1 entry or one entry per "
f"fit ({len(self.fits)}). Got {len(runcard_paths)} entries."
)

closure_truth_points = []
for runcard in runcard_paths:
runcard_path = pathlib.Path(runcard).expanduser()
with open(runcard_path, encoding="utf-8") as f:
runcard_config = yaml.safe_load(f)
coeff_cfg = runcard_config.get("coefficients", {})
closure_truth_points.append(
self._compute_closure_truth_from_coeff_config(coeff_cfg, coeff_names)
)
return closure_truth_points

def _load_closure_truth_points_from_fit_configs(self, coeff_names):
closure_truth_points = []
for fit in self.fits:
coeff_cfg = fit.config.get("coefficients", {})
closure_truth_points.append(
self._compute_closure_truth_from_coeff_config(coeff_cfg, coeff_names)
)
return closure_truth_points

def summary(self):
"""Summary Table runner."""
summary = SummaryWriter(self.fits, self.data_info, self.coeff_info)
Expand Down Expand Up @@ -244,6 +320,11 @@ def coefficients(
kwarg scatter plot or None
posterior_histograms: bool
if True plot the posterior distribution for each coefficient
Additional supported keys in the `posterior_histograms` block:
- closure_truth_points: explicit list of dict values per fit.
- closure_truth_runcards: path/list/dict to projection (or run) runcard(s)
used to generate closure pseudo-data. Values are evaluated from the
`coefficients` block, including constrained relations.
table: None, dict
kwarg the latex confidence level table per coefficient or None
double_solution: dict
Expand Down Expand Up @@ -365,10 +446,29 @@ def coefficients(
for fit in self.fits
]
posterior_histograms["disjointed_lists"] = disjointed_lists
closure_truth_points = posterior_histograms.pop(
"closure_truth_points", None
)
closure_truth_runcards = posterior_histograms.pop(
"closure_truth_runcards", None
)
if closure_truth_points is None:
coeff_names = coeff_plt.coeff_info.index.get_level_values(1)
if closure_truth_runcards is not None:
closure_truth_points = (
self._load_closure_truth_points_from_runcards(
closure_truth_runcards, coeff_names
)
)
else:
closure_truth_points = (
self._load_closure_truth_points_from_fit_configs(coeff_names)
)

coeff_plt.plot_posteriors(
[fit.results["samples"] for fit in self.fits],
labels=[fit.label for fit in self.fits],
closure_truth_points=closure_truth_points,
**posterior_histograms,
)
figs_list.append("coefficient_histo")
Expand Down