NextStatNextStat
← Все статьи

Где ROOT ошибается: строгая численная проверка реализаций HistFactory

Строгое численное сравнение реализаций HistFactory: ROOT/RooFit, pyhf и NextStat на сканах профильного правдоподобия.

/22 мин. чтения
ROOTpyhfHistFactoryВалидацияMinuit2L-BFGS-B

Аннотация. Мы проводим систематическое 3-стороннее численное сравнение реализаций HistFactory: ROOT/RooFit (Minuit2), pyhf (SLSQP) и NextStat (L-BFGS-B). NextStat и pyhf согласуются по q(μ) лучше чем до 1e-5 на всех фикстурах. В ROOT наблюдаются три режима отказа: несходимость оптимизатора, катастрофическая дивергенция свободного фита (μ̂ = 4.9e23) и систематическая переоценка q(μ). На крупных моделях (>100 параметров) L-BFGS-B устойчиво находит более глубокие минимумы. Все результаты полностью воспроизводимы.

Ключевые выводы

  • ShapeSys: ROOT, pyhf и NextStat согласуются до < 1e-6, если фиты сходятся
  • OverallSys: ROOT завышает q(μ) до 0.6% при больших μ (условные фиты Minuit2)
  • Связанный HistoSys: ROOT расходится на Δq(μ) = 22.5 при μ = 3.0 и сообщает status_free = -1. Наблюдаемая неконстантность оффсета NLL между найденными минимумами указывает на тяжёлую деградацию условных фитов; атрибуцию «оптимизатор vs модель» следует делать через cross-evaluation NLL в одних и тех же параметрах (см. §3.3)
  • Крупные модели: L-BFGS-B достигает лучшей стационарной точки (‖grad‖ = 0.020 против 4.63 на воркспейсе с 184 параметрами)
  • Производительность: NextStat в 37×–880× быстрее; +6.4× с GPU для тоев на NVIDIA

1.Введение

Модель HistFactory [1] это стандартная статистическая модель для бинированных анализов на Большом адронном коллайдере (LHC). Её математическая спецификация описывает, как систематические неопределённости (мешающие параметры) модифицируют ожидаемые выходы событий через мультипликативные факторы, морфинг форм и побиновые статистические неопределённости. Выведенная из этой модели тестовая статистика отношения профильных правдоподобий q̃(μ) [2] лежит в основе пределов исключения и значимостей открытия в ATLAS и CMS.

На практике широко используются три независимые реализации этой модели:

  • ROOT/RooFit — исходная C++ реализация. Использует hist2workspace для построения модели и Minuit2 [4] для оптимизации. Де-факто стандарт для I/O и визуализации в CERN.
  • pyhf — чистый Python, SLSQP. Разработан физиками ATLAS как формальный эталон спецификации HistFactory и используется в задачах реинтерпретации/комбинаций (ATL-PHYS-PUB-2019-029).
  • NextStat — Rust, L-BFGS-B. AD в обратном режиме, опциональное GPU-ускорение (CUDA f64, Metal f32). Валидируется относительно pyhf как эталона спецификации.

Естественный вопрос: дают ли эти три реализации одни и те же физические результаты? Если нет, какая из них корректна и почему? Мы отвечаем на него строгим 3-сторонним сравнением, покрывающим полный профильный скан (31 точка по μ) на канонических фикстурах, которые задействуют все основные типы модификаторов: мультипликативные (OverallSys), побиновые (ShapeSys/StatError) и морфинг форм (HistoSys).

2.Методология

2.1 Пайплайн валидации

text
HistFactory XML + ROOT histograms
        |
        +---> hist2workspace -> RooFit    -> ROOT профильный скан   (C++/PyROOT)
        +---> pyhf.readxml   -> pyhf      -> pyhf профильный скан   (Python)
        +---> NextStat import -> Rust     -> NextStat профильный скан (Rust)

Профильный скан вычисляет q̃(μ) в 31 равномерно распределённой точке на отрезке μ = [0, 3]. Во всех трёх пайплайнах используется оптимизация с ограничениями, причём параметр интереса (POI) ограничен как μ ≥ 0.

2.2 Тестовая статистика

              ⎧ 2 · [ NLL(μ, θ̂_μ) − NLL(μ̂, θ̂) ]   if μ̂ ≤ μ
q̃(μ)   =     ⎨
              ⎩ 0                                      if μ̂ > μ

Как определено в Cowan и др. [2]; реализовано одинаково во всех трёх инструментах.

2.3 Фикстуры

ФикстураКаналыКлючевые модификаторыПараметры
xmlimport1OverallSys, StatError~5
multichannel2ShapeSys (Poisson)~10
coupled_histosys2HistoSys (coupled)~5

Дополнительно мы тестируем реальные экспорты TRExFitter с числом мешающих параметров до 249.

2.4 Коды интерполяции

  • NormSys (OverallSys): Code 4 (полином 6-го порядка)
  • HistoSys: Code 4p (кусочно-полиномиальная)

В регрессионных тестах паритета NextStat↔pyhf эти коды фиксируются явно (pyhf: modifier_settings). В 3-стороннем сравнении ROOT↔pyhf↔NextStat мы строим все три тракта из одного и того же экспорта HistFactory (один combination.xml + те же ROOT-гистограммы), что минимизирует риск «расхождения из-за разных настроек», а не из-за численной/оптимизационной природы проблемы.

2.5 Версии и окружение

ROOT6.38.00 (hist2workspace + RooFit/Minuit2)
pyhf0.7.6 (SciPy 1.17.0, NumPy 2.4.2)
NextStat0.9.0 (git c98f36e)
ХостApple M5 (arm64), macOS 26.2

Сырые JSON-данные сканов фиксируются (snapshot) в docs/blog/assets/numerical-accuracy/data/ и рендерятся в SVG скриптом scripts/blog/generate_numerical_accuracy_plots.py. Для каждого фикстура-кейса директория данных содержит артефакты root_profile_scan.json, pyhf_profile_scan.json,nextstat_profile_scan.json и summary.json.

3.Результаты

3.1 Хорошо: ShapeSys и почти идеальное 3-стороннее согласие

Фикстура multichannel с модификаторами ShapeSys показывает «золотой стандарт»: все три реализации согласуются по q(μ) лучше чем 1e-6:

μROOT q(μ)pyhf q(μ)NextStat q(μ)NS−pyhfROOT−NS
1.03.1034753.1034753.103475-7.7e-8+3.4e-8
2.015.53340815.53340815.533408-4.1e-8+3.4e-8
3.036.23517436.23517436.235174-4.1e-8+3.4e-8

Когда все три оптимизатора сходятся, реализации дают идентичные физические результаты.

multichannel (ShapeSys): остатки Δq(μ)
Рис. 1 — multichannel (ShapeSys): остатки Δq(μ) на 31 точке по μ. Все три инструмента согласуются до < 1e-6.

3.2 Плохо: OverallSys — ROOT переоценивает при высоком μ

Фикстура xmlimport выявляет систематическое смещение в ROOT при больших значениях силы сигнала μ:

μROOT q(μ)pyhf q(μ)NextStat q(μ)ROOT−NS
1.20.01957410.01956470.0195648+9.4e-6
2.02.0727162.0666922.066692+6.0e-3
3.09.0578809.0067629.006762+5.11e-2

NextStat и pyhf согласуются на уровне нескольких микродолей по q(μ) на всём скане (max |Δq| ≈ 2.4e-6). ROOT завышает q(μ) до 0.051 при μ = 3.0 (0.6% по относительной величине), причём расхождение растёт примерно линейно с расстоянием от μ̂.

Диагноз: Смещение NLL между ROOT и NextStat почти постоянно (≈11.06 с дрейфом <0.04 по всему скану), что согласуется с различием в константной части ограничения, а не с расхождением в вычислении самой модели. Растущая Δq(μ) это чисто эффект оптимизатора: условный минимизатор Minuit2 сходится к немного большему NLL на экстремальных значениях μ. В результате ROOT сообщил бы слишком жёсткий предел исключения, не оправданный данными.

xmlimport (OverallSys): ROOT bias in q(μ)
Рис. 2 — xmlimport (OverallSys): завышение q(μ) в ROOT растёт с расстоянием от μ̂.

3.3 Ужасно: связанный HistoSys и полный отказ ROOT

Фикстура coupled_histosys даёт наиболее драматическое расхождение:

μROOT q(μ)pyhf q(μ)NextStat q(μ)ROOT−NS
0.50.00.00.00.0
1.00.9906180.4453800.445381+0.545
2.015.5261146.5433286.543328+8.98
3.041.56631419.04164519.041645+22.52
coupled_histosys: q(μ) профильный скан
Рис. 3 — coupled_histosys: ROOT катастрофически расходится при μ ≥ 1. pyhf и NextStat согласуются.

Свободный фит ROOT возвращает status_free = -1 (Minuit2 не смог определить положительно определённую ковариационную матрицу). Условные фиты при μ ≥ 1.0 затем расходятся катастрофически.

Ключевое наблюдение: оффсет NLL в coupled_histosys не ведёт себя как константа. Мы сравниваем значения NLL, достигнутые каждым инструментом в своих условных минимумах при фиксированном μ. Если бы ROOT вычислял ту же функцию правдоподобия, что pyhf/NextStat, и при этом надёжно находил те же условные минимумы, разность NLL отличалась бы на почти константный сдвиг (конвенции ограничения). На практике же ROOT сообщает status_free = -1, и оффсет между найденными минимумами заметно растёт с μ. Это согласуется с глубоким провалом сходимости условных фитов Minuit2; чтобы отличить «не тот минимум» от «другая функция», используйте диагностический режим --dump-root-params, который маппит параметры ROOT в порядок параметров NextStat и вычисляет NLL в одних и тех же параметрах (cross-evaluation).

Eval pointROOT NLLNextStat NLLOffset
Free fit (μ̂)434.75414.017420.74
μ = 0434.84114.103420.74
μ = 1435.25014.239421.01
μ = 2442.51717.288425.23
μ = 3455.53723.537432.00
coupled_histosys: NLL offset (ROOT − NextStat)
Рис. 4 — оффсет NLL(ROOT − NextStat) между найденными условными минимумами растёт с 420.74 до 432.00. Это признак деградации условных фитов ROOT; для отделения оптимизатора от возможной модельной разницы используйте cross-evaluation через --dump-root-params.

3.4 Реальные анализы

AnalysisParamsmax Δq NS vs pyhfmax Δq NS vs ROOTROOT issue
simple fixture~5< 1e-81.6e-10None
EWK (HEPData)medium< 1e-50.0μ̂ = 4.9e23
tttt-prod249< 1e-50.04Tail convergence

Анализ EWK показателен: безусловный фит ROOT разошёлся настолько, что Minuit2 вернул μ̂ = 4.9e23 и NLL = 1.8e23. NextStat сходится нормально и находит μ̂ = 6.57. Поскольку μ̂ > всех точек скана, все значения q̃ оказываются равными нулю, и катастрофический отказ легко «замаскировать».

4.Качество оптимизатора на больших моделях

Помимо 3-стороннего сравнения мы отдельно анализируем поведение оптимизаторов на моделях с >100 мешающими параметрами. NextStat (L-BFGS-B) и pyhf (SLSQP) сравниваются через cross-evaluation: каждый инструмент вычисляет NLL в точке параметров, найденной другим инструментом.

4.1 Результаты cross-evaluation

ModelParamsΔNLL (NS−pyhf)‖grad‖ pyhf‖grad‖ NS
simple30.0< 1e-6< 1e-6
complex80.0< 1e-6< 1e-6
tHu184-0.0814.630.020
tttt249-0.0101.440.008

На моделях с 184+ параметрами NextStat находит значения NLL на 0.01–0.08 ниже. Cross-evaluation подтверждает паритет целевой функции на уровне ~1e-13: различие обусловлено только качеством оптимизатора.

4.2 Анализ нормы градиента

Норма проецированного градиента в точке наилучшего фита это прямой индикатор качества оптимизатора. В оптимуме pyhf на воркспейсе tHu: ‖grad‖ = 4.63 (точка не стационарна). В оптимуме NextStat: ‖grad‖ = 0.020 (стационарна в пределах численной точности).

4.3 Может ли pyhf найти лучший минимум?

При тёплом старте из параметров NextStat, pyhf восстанавливает NLL = 179.404, подтверждая, что минимум реален. Однако multi-start поиск pyhf (20 случайных инициализаций) сходится лишь в 5/20 запусков, а лучший результат остаётся на 0.004 выше минимума NextStat.

4.4 Техническое объяснение

L-BFGS-B поддерживает аппроксимацию обратного гессиана ограниченной памяти, используя последние m = 10 пар (s, y), что даёт O(mn) на итерацию при наличии информативной кривизны. SLSQP может испытывать трудности на высокоразмерных правдоподобиях HistFactory с жёсткими ограничениями и коррелированными мешающими параметрами, где поверхность NLL имеет узкие «долины».

5.Производительность

Тайминги профильного скана (31 точка по μ, включая свободный фит):

FixtureROOTpyhfNextStatvs ROOTvs pyhf
xmlimport0.91 s0.23 s0.003 s303×73×
multichannel1.98 s0.26 s0.007 s283×37×
coupled_histosys1.76 s0.15 s0.002 s880×75×

Скорость достигается за счёт: компилируемого Rust (вместо Python-накладных расходов), AD в обратном режиме (вместо конечных разностей) и «горячего пути» без выделений памяти (zero-allocation) с предкомпилированным вычислением модификаторов. CPU: Apple M5 (arm64). GPU-бенчмарк на псевдоэкспериментах: NVIDIA RTX 4000, дополнительное ускорение 6.4× относительно CPU.

6.Методология валидации

6.1 Семь уровней контракта допусков

УровеньМетрикаДопускСмысл
1Ожидаемые значения (по бинам)1e-12Чистая f64-арифметика
2Полный вектор ожидаемых значений1e-8Порядок суммирования
32·NLL (pyhf twice_nll ↔ 2×NextStat nll)atol=1e-8, rtol=1e-6Детерминированный паритет по значению функции
4Градиентatol=1e-6, rtol=1e-4AD против конечных разностей
5Параметры наилучшего фита2e-4Плоскость около минимума
6Неопределённости5e-4Чувствительность гессиана
7Статистика ансамбля тоев0.03–0.05MC-шум

Уровни 1–6 гейтятся в CI относительно pyhf (как эталона спецификации) и внутренних детерминированных режимов NextStat; сравнение с ROOT выделено в отдельные ROOT-зависимые проверки и обычно пропускается в средах без root/hist2workspace.

6.2 Почему pyhf — эталон, а не ROOT

  • Опубликовано в JOSS [5] после рецензирования
  • Используется для опубликованных результатов ATLAS
  • Поддерживается физиками ATLAS (Heinrich, Feickert, Stark)
  • Источник формата JSON для обмена воркспейсами
  • Тестируется против вывода ROOT в собственном CI

Спецификация HistFactory задаётся математической моделью [1], а не какой-либо программной реализацией. Валидация относительно pyhf это и есть валидация относительно спецификации.

6.3 Воспроизводимость

bash
# Команды ниже предполагают запуск из корня репозитория NextStat.

# 3-стороннее сравнение профильного скана (ROOT vs NextStat, опционально +pyhf)
PYTHONPATH=bindings/ns-py/python ./.venv/bin/python tests/validate_root_profile_scan.py \
  --histfactory-xml tests/fixtures/pyhf_xmlimport/config/example.xml \
  --rootdir tests/fixtures/pyhf_xmlimport \
  --start 0.0 --stop 3.0 --points 31 \
  --workdir tmp/root_parity \
  --include-pyhf --keep

# Если нужно отделить «проблему оптимизатора» от возможной «разницы модели»,
# добавьте --dump-root-params: скрипт пересчитает NLL NextStat в параметрах ROOT
# и запишет cross-evaluation диагностику в summary.json.

# Агрегатор для нескольких кейсов (Apex2): пишет единый JSON-репорт + per-case run dirs
PYTHONPATH=bindings/ns-py/python ./.venv/bin/python tests/apex2_root_suite_report.py \
  --cases tests/fixtures/trex_parity_pack/cases_realistic_fourtop.json \
  --dq-atol 2e-2 \
  --mu-hat-atol 5e-2 \
  --deterministic \
  --out tmp/apex2_root_suite_realistic_fourtop.json

# Cross-evaluation / диагностика оптимизатора
PYTHONPATH=bindings/ns-py/python ./.venv/bin/python tests/diagnose_optimizer.py \
  --workspace tests/fixtures/workspace_tHu.json \
  --workspace tests/fixtures/tttt-prod_workspace.json \
  --multi-start 20

# Перегенерировать фигуры (читает snapshot-данные)
python3 scripts/blog/generate_numerical_accuracy_plots.py

Скрипт validate_root_profile_scan.py создаёт директорию видаtmp/root_parity/run_<timestamp>/, где сохраняютсяroot_profile_scan.json, nextstat_profile_scan.json (и опционально pyhf_profile_scan.json), а также summary.json с метриками расхождения. Для быстрого объяснения, где именно расходится q(μ), используйте:tests/explain_root_vs_nextstat_profile_diff.py.

7.Обсуждение

7.1 Следствия для опубликованных результатов

  • OverallSys bias: Переоценка 0.6% при μ = 3.0 приводит к немного более жёсткому пределу исключения. Для большинства анализов незначительно, но это систематическое смещение, растущее с расстоянием от μ̂.
  • Отказ на связанном HistoSys: q(μ) = 41.6 против 19.0 при μ = 3.0. В асимптотическом приближении: √41.6 − √19.0 = 6.4σ − 4.4σ = 2.0σ завышения.

Это влияет только на анализы, где используются связанные модификаторы HistoSys при экстремальных значениях параметров. В большинстве анализов используется OverallSys, и там смещение ROOT невелико.

7.2 Оптимизатор важен

Cross-evaluation (§4) однозначно показывает, что ландшафт NLL идентичен, а все различия возникают из-за оптимизатора. Для <50 параметров все три оптимизатора сходятся к одному и тому же минимуму. Для больших моделей выбор оптимизатора становится физически значимым решением.

7.3 Чем NextStat отличается

  • Точные градиенты: AD в обратном режиме даёт градиенты на уровне машинной точности, устраняя O(h²) ошибку усечения конечных разностей.
  • Оптимизация без выделений: горячий путь NLL не делает heap-аллоков, уменьшая давление на кэш.
  • Валидация по спецификации: Каждый CI-запуск проверяется против pyhf, не ROOT. Если ROOT не согласен, мы исследуем — но не ломаем CI ради багов ROOT.

7.4 Практические меры предосторожности

  • Считать ненулевой статус фита или провал ковариации жёстким фейлом
  • Сверяться с независимой реализацией (pyhf) или делать cross-evaluation NLL в точках наилучшего фита
  • Трекать метрику сходимости (норма проецированного градиента / KKT-остатки) на высокоразмерных моделях
  • Делать тёплый старт условных фитов вдоль μ-скана из соседних точек
  • Фиксировать версии инструментов и толерантности фита с каждым опубликованным пределом

8.Заключение

Мы продемонстрировали через систематическое сравнение, что:

  • NextStat и pyhf согласуются до < 1e-5 по q(μ) на всех канонических фикстурах и реальных анализах с числом параметров до 249.
  • ROOT/RooFit демонстрирует три режима отказа: несходимость оптимизатора (связанный HistoSys), катастрофическая дивергенция (μ̂ = 4.9e23) и систематическое завышение q(μ) (OverallSys, до 0.6%).
  • На больших моделях (>100 параметров), L-BFGS-B находит минимумы NLL на 0.01–0.08 глубже, чем SLSQP; это подтверждено паритетом целевой функции на уровне ~1e-13.
  • NextStat быстрее в 37×–880× относительно ROOT и в 37×–75× относительно pyhf, с дополнительным GPU-ускорением 6.4×.

ROOT остаётся стандартом для I/O и визуализации, но для статистического инференса независимые реализации с современными оптимизаторами и строгой валидацией относительно математической спецификации дают более надёжные результаты.

Версии, использованные в этом посте

ROOT6.38.00 (hist2workspace + RooFit/Minuit2)
pyhf0.7.6 (SciPy 1.17.0, NumPy 2.4.2)
NextStat0.9.0 (git c98f36e, 2026-02-08)
CPUApple M5 (arm64), macOS 26.2
GPU (псевдоэксперименты)NVIDIA RTX 4000

Тайминги измерялись на CPU M5. GPU-ускорение на псевдоэкспериментах измерялось на RTX 4000. Сырые данные фиксируются (snapshot) в docs/blog/assets/numerical-accuracy/data/.

Ссылки

  1. K. Cranmer и др. "HistFactory: инструмент для построения статистических моделей для RooFit и RooStats." CERN-OPEN-2012-016, 2012.
  2. G. Cowan и др. "Асимптотические формулы для тестов новой физики на основе правдоподобия." Eur. Phys. J. C 71 (2011) 1554. arXiv:1007.1727.
  3. R. Brun, F. Rademakers. "ROOT: объектно-ориентированный фреймворк для анализа данных." Nucl. Instrum. Meth. A 389 (1997) 81-86.
  4. F. James, M. Roos. "Minuit: система минимизации функций." Comput. Phys. Commun. 10 (1975) 343-367.
  5. L. Heinrich и др. "pyhf: чисто-Python реализация статистических моделей HistFactory." JOSS 6 (2021) 2823. doi:10.21105/joss.02823.

Скрипты валидации

Профильный скан: tests/validate_root_profile_scan.py

Диагностика оптимизатора: tests/diagnose_optimizer.py

Генератор графиков: scripts/blog/generate_numerical_accuracy_plots.py

Данные снимка: docs/blog/assets/numerical-accuracy/data/