Skip to content

RooFormulaVar @N parameter indices silently shift after workspace serialization #21371

@adeiana

Description

@adeiana

Check duplicate issues.

  • Checked for duplicates

Description

Summary

When a RooFormulaVar is created with unused parameters in its parameter list, writing the workspace to a ROOT file and reading it back causes the @N positional indices to silently map to wrong variables. This produces incorrect numerical results without any error or warning.

Description

RooFormulaVar (via expr:: factory syntax) accepts a formula string with positional references (@0, @1, ...) and a list of parameters. Since ROOT 6.30, only the parameters that the formula actually depends on are kept in actualVars (unused ones are pruned). However, the formula string retains the original @N indices. After a write/read cycle, the pruned actualVars list is shorter, so the @N indices map to different positions — silently producing wrong results.

Example

// Create: 4 parameters, formula uses @0, @2, and 'd' by name. @1 (b) is unused.
w.factory("a[2.0, -10, 10]");
w.factory("b[99.0, -10, 10]");  // unused in formula
w.factory("c[3.0, -10, 10]");
w.factory("d[4.0, -10, 10]");
w.factory("expr::f('@0 * @2 + d', a, b, c, d)");
// Before serialization: @0=a, @2=c → f = a*c + d = 2*3 + 4 = 10 ✓

After writeToFile() and reading back:

  • actualVars = {a, c, d} (b was pruned)
  • @2 now maps to d (position 2 in pruned list) instead of c
  • ROOT's own interpretation confirms: '[a]*[d]+d'

Mechanism

  1. Construction: expr::f('@0*@2+d', a, b, c, d)@0=a, @1=b, @2=c, @3=d
  2. Pruning (ROOT 6.30+): only used variables kept → actualVars = {a, c, d}
  3. Serialization: actualVars (3 entries) and formula string '@0*@2+d' written to file
  4. Deserialization: actualVars = {a(0), c(1), d(2)}. Formula @0*@2+d is re-parsed: @0→a (correct), @2→d (WRONG — should be c)

Relation to existing issues

This is the binary (TFile) serialization counterpart of #17291, which reported the same class of parameter ordering bug in the JSON/HS3 serialization path and was fixed in ROOT 6.34.04 via PR #17292.

Suggested fix

After deserialization, when getFormula() reconstructs the RooFormula from _formExpr and the pruned _actualVars, the @N indices in the formula string should be re-mapped to match the new positions in actualVars. Alternatively, the formula string could be stored in a serialization-stable form (e.g. using named references instead of positional indices).

Reproducer

A self-contained macro is attached: reproduce_formulavar_bug.C

root -l -b -q reproduce_formulavar_bug.C

Key output:

=== Before serialization ===
Formula: @0 * @2 + d   (expect a*c + d = 2*3 + 4 = 10)
f = 10

=== After deserialization ===
Formula: @0 * @2 + d   (still expect a*c + d = 2*3 + 4 = 10)
f = 10       ← cached value, appears correct

Formula details:
   Formula:        '@0*@2+d'
   Interpretation: '[a]*[d]+d'     ← ROOT confirms @2 maps to d, not c
    In use : (a,d)                 ← c is no longer used!

--- Diagnostic: change c from 3 to 100 ---
If @2=c: expect 204
If @2=d: expect 12 (no change from c)
f = 12       ← confirms @2 ≠ c

--- Diagnostic: change d from 4 to 100 ---
If @2=c: expect 106
If @2=d: expect 300
f = 30       ← d dominates the result

reproduce_formulavar_bug.C

ROOT version

Tested on ROOT 6.36.06, 6.38 and 6.30.02. Based on the release notes, the behavior was introduced in ROOT 6.30.

Installation method

conda-forge

Operating system

MacOS, Sonoma 14.6.1

Additional context

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions