Skip to content

Implement integrated CMS Combine backend - #520

Open
captainvogon wants to merge 28 commits into
HEP-FCC:masterfrom
captainvogon:feature/combine-integration
Open

Implement integrated CMS Combine backend#520
captainvogon wants to merge 28 commits into
HEP-FCC:masterfrom
captainvogon:feature/combine-integration

Conversation

@captainvogon

@captainvogon captainvogon commented Jun 25, 2026

Copy link
Copy Markdown

Overview

This PR transitions the Combine statistical backend integration from a standalone YAML parser to a fully integrated, object-oriented Python framework.

Key Changes

  • Object-Oriented Configs: Replaced YAML-based configs with a Pythonic Fit class (see examples/fcc_ee_zh_mumu_bb.py). Users now define channels, processes, and systematics natively in Python.
  • Backend Integration (python/combine.py): Built the wrapper to dynamically load the user's fit script, instantiate the Fit class, and validate inputs.
  • Datacard Generation: Implemented the logic to translate the Fit object into a production-ready Combine datacard.txt.
  • Automated Testing: Added robust integration tests (test_combine_backend.py) that verify config sanitization, datacard generation, and isolated combine tool execution while gracefully handling CI environment limits.

Status

All framework integration tests and CI pipelines are passing successfully.

Comment thread examples/fcc_ee_zh_mumu_bb.py Outdated
@captainvogon
captainvogon marked this pull request as ready for review July 17, 2026 09:28
Copilot AI review requested due to automatic review settings July 17, 2026 09:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an object-oriented Python interface for defining Combine fits, adds a new fccanalysis fit subcommand to generate datacards (and optionally execute Combine), and wires an integration test into CTest to validate end-to-end behavior.

Changes:

  • Added python/combine.py for loading user Fit scripts, validating configs, and writing Combine text datacards.
  • Added python/fit.py and CLI plumbing (python/parsers.py, bin/fccanalysis) for the new fit subcommand with optional backend execution.
  • Added an example OOP fit configuration and a CTest-driven Python integration test.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/python/test_combine_backend.py Adds a Python integration test that runs fccanalysis fit and validates datacard generation / execution behavior.
tests/CMakeLists.txt Registers the new Python integration test with CTest.
python/parsers.py Adds a new fit subcommand parser and arguments.
python/fit.py Implements the fccanalysis fit subcommand entry point and Combine execution wrapper.
python/combine.py Implements Fit script loading, config sanitization/validation, and datacard writing.
examples/fcc_ee_zh_mumu_bb.py Provides an example Fit class config for the new backend.
bin/fccanalysis Wires the new fit command into the main CLI dispatcher.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread python/combine.py
Comment thread python/combine.py
Comment on lines +223 to +247
for ch_name, ch_info in channels.items():
# Determine what the shape mapping rule is for this channel
# Handles wildcard '*' or specific process rules
mapping_rule = shapes_config.get('*', {}).get(ch_name) or shapes_config.get(proc_name, {}).get(ch_name)

if not mapping_rule:
continue

# Extract the ROOT file path (assumes space-separated path and inner structure mapping)
root_file_path = mapping_rule.split()[0]

if not os.path.isfile(root_file_path):
LOGGER.warning("Shape file '%s' not created yet or using relative build path. Skipping deep object check.", root_file_path)
continue

# Open the file via PyROOT to peek inside
if root_file_path in failed_to_open_files:
raise ValueError(f"Validation Error: Failed to open shape ROOT file: {root_file_path}")

for proc_name in ch_info.get('processes', {}).keys():
if proc_name in apply_to and str(apply_to[proc_name]) != '-':

# Resolve the inner path template (e.g., $CHANNEL/$PROCESS -> mumu_bjets_channel/ZH_signal)
base_path = mapping_rule.split()[1].replace('$CHANNEL', ch_name).replace('$PROCESS', proc_name)

Comment thread python/combine.py
Comment thread tests/CMakeLists.txt Outdated
)

# Ensure test data assets are fully downloaded via CI before running this test
set_tests_properties(test_combine_fit_backend PROPERTIES DEPENDS get_test_inputs)
Comment thread tests/python/test_combine_backend.py Outdated
Comment on lines +15 to +16
test_output_dir = os.path.join(REPO_ROOT, "outputs", "test_integration", "mumu")
test_datacard = os.path.join(test_output_dir, "datacard.txt")
Comment thread python/fit.py
Comment on lines +76 to +78
except KeyboardInterrupt:
LOGGER.info('Fit execution interrupted by user (Ctrl+C). Terminating cleanly...')
sys.exit(0)
Comment thread python/combine.py Outdated
Comment on lines +106 to +114
for syst_name, syst_info in self.datacard.systematics.items():
syst_type = syst_info.get("type", "lnN")
apply_to = syst_info.get("apply_to", {})

# Start each row with the systematic name and its type
row_cells = [syst_name, syst_type]

# Loop over channels
for ch_name, ch_info in self.datacard.channels.items():
@captainvogon captainvogon changed the title Draft: Combine Backend Skeleton Implement integrated CMS Combine backend Jul 17, 2026
Comment thread examples/fcc_ee_zh_mumu_bb.py
@kjvbrt

kjvbrt commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Hi @captainvogon, looks good. One thing to think about is how possible next step (plotting) will interact with the combine products. In the current state of the framework, the following step always require input directory, which is produced as output of the previous step.

Shape Systematic proc_name Bug (part 1)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (4)

python/combine.py:28

  • jmax is computed from the first channel only, which can produce an incorrect header when different channels have different numbers of background processes. This can lead to invalid/inconsistent Combine datacards.
    def _get_metadata(self) -> Dict[str, int]:
        imax = len(self.channels)
        jmax = 0
        if self.channels:
            first_channel = list(self.channels.values())[0]

python/fit.py:28

  • The CLI allows --backend pyhf, but run_fit() only handles combine. For pyhf, the command exits successfully without generating output or reporting an error.
    if backend == 'combine':
        from combine import generate_datacard
        generate_datacard(anapath, output_path)

python/combine.py:234

  • The shape-systematics validation block references proc_name before it is defined when selecting mapping_rule, which raises NameError for any shape systematic. Additionally, the validation currently assumes variations are named by appending _{syst}{Up/Down} to the nominal template, ignoring the $SYSTEMATIC template token when provided.

                # Extract the ROOT file path (assumes space-separated path and inner structure mapping)
                root_file_path = mapping_rule.split()[0]

                if not os.path.isfile(root_file_path):

tests/CMakeLists.txt:36

  • set_tests_properties(... DEPENDS get_test_inputs) references a CTest test named get_test_inputs, but input downloading is performed at CMake configure time (top-level CMakeLists.txt) and there is no such CTest test. This dependency will be ignored and may emit CTest warnings.
    # Ensure test data assets are fully downloaded via CI before running this test
    set_tests_properties(test_combine_fit_backend PROPERTIES DEPENDS get_test_inputs)
endif()

Comment thread python/combine.py
Comment on lines +188 to +193
b_path = m_rule.split()[1].replace('$CHANNEL', c_name).replace('$PROCESS', p_name)
for var in ['Up', 'Down']:
t_path = f"{b_path}_{s_name}{var}"
if rf_path not in targets_to_load:
targets_to_load[rf_path] = set()
targets_to_load[rf_path].add(t_path)
Comment thread python/parsers.py
@captainvogon
captainvogon marked this pull request as draft July 21, 2026 18:20
@captainvogon
captainvogon marked this pull request as ready for review July 22, 2026 06:57
Comment thread python/fit.py

# 3. Add the --out flag
output_dir = os.path.dirname(os.path.abspath(output_path))
full_command.extend(['--out', output_dir])

@kjvbrt kjvbrt Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if user provides --out as well on the command line, who's --out wins?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants